App setup and API reference

Register an agent app, install it with OAuth, verify webhooks and use Pulse APIs.

This page contains the exact setup contract for a Pulse agent app: registration, OAuth installation, webhooks and authenticated API calls. If you are new to agent apps, follow getting started first.

Overview

An app has two identities: the developer workspace owns its registration and secrets, while each installation has its own app user and tokens. In the first version, the app can be installed only into its developer workspace.

Build and run it in this order:

  1. Register the app from a manifest and copy its secrets.
  2. Serve an install endpoint that runs the OAuth flow with actor=app.
  3. Serve a webhook endpoint that verifies each delivery and answers within 5 seconds.
  4. Answer sessions through the Agent Session API.
  5. Call the Pulse REST API or the MCP server with the app's token for everything else.

Keep the secrets on your server. Your install endpoint must verify OAuth state and should require an install secret before starting consent. Every API URL on this page is under https://api.trypulse.tech/api/v1.

Create the app

Any workspace member can register an app in Settings → API → Agent apps, next to OAuth applications. Enter a recognizable name, developer name, OAuth redirect and webhook URL. You can fill in the form or use Import manifest to paste a manifest. Pulse returns the client and webhook secrets once; store them before leaving the page.

Register with the API

If your setup is automated, send the same manifest with a signed-in person's session. An app token cannot create or change app registrations.

POST https://api.trypulse.tech/api/v1/agent-apps
Authorization: Bearer $PULSE_SESSION_TOKEN
X-Workspace-ID: $WORKSPACE_ID
Content-Type: application/json

The body is the manifest. The 201 response returns the app and its credentials:

{
  "app": {
    "id": "6ab5a1c04f48187cbd3ce901",
    "client_id": "pulse_app_6ab5a1c04f48",
    "developer_workspace_id": "6a34cfb024a3b4ed28806e3b",
    "owner_user_id": "68cab92a5020377746176588",
    "name": "Scout",
    "description": null,
    "icon_url": null,
    "developer": { "name": "Acme Labs", "url": null },
    "distribution": "private",
    "redirect_uris": ["https://scout.acme.example/oauth/callback"],
    "requested_scopes": ["read", "write", "app:assignable"],
    "webhook": { "enabled": true, "url": "https://scout.acme.example/pulse/webhook", "resource_types": [] },
    "created_at": "2026-09-25T09:00:00.000Z",
    "updated_at": "2026-09-25T09:00:00.000Z"
  },
  "client_id": "pulse_app_6ab5a1c04f48",
  "client_secret": "pulse_sk_2hQpM4c1Yt9sV0eXkR7bNw",
  "webhook_secret": "pwhsec_9fK2mQ7xL0vB3nT8",
  "install_url": "https://api.trypulse.tech/api/v1/oauth/authorize?actor=app&client_id=pulse_app_6ab5a1c04f48&response_type=code&scope=read,write,app:assignable&redirect_uri={redirect_uri}&state={state}&code_challenge={code_challenge}&code_challenge_method=S256"
}

client_secret (pulse_sk_…) and webhook_secret (pwhsec_…) are shown once. Store them in a secrets manager. If you lose one, rotate it: POST /api/v1/agent-apps/{app_id}/secret or POST /api/v1/agent-apps/{app_id}/webhook-secret. The old value stops working at once.

Owners and admins of the developer workspace can change the app with PUT /api/v1/agent-apps/{app_id} (the whole manifest) or delete it with DELETE /api/v1/agent-apps/{app_id}, which uninstalls it everywhere first. GET /api/v1/agent-apps lists the workspace's apps. These routes need a person's session: an OAuth or app token gets 403 SESSION_ONLY_ROUTE.

Manifest reference

The manifest is JSON, validated against https://api.trypulse.tech/.well-known/agent-app-manifest.schema.json. Unknown keys are refused, and an invalid manifest answers 422 VALIDATION_ERROR with one message per field in details.

FieldRequiredRules
$schemaNoThe schema URL above
schemaVersionYes"1.0.0"
nameYes2–80 characters; must not contain "Pulse". Becomes the app's user name.
descriptionNoUp to 1000 characters
icon_urlNohttps:// URL of a square icon; Pulse proxies it
developer.nameYes2–80 characters
developer.urlNoYour website
distributionNoOnly "private" (the default). "public" is refused in v1.
oauth.redirect_urisYes1–32 unique https:// URLs
oauth.scopesYesFrom read, write, issues:create, comments:create, app:assignable, app:mentionable. Never admin.
webhook.urlWith webhookPublic https:// URL on port 443 or 8443
webhook.enabledNoDefault true. While false, nothing is delivered and the app cannot be delegated to or mentioned.
webhook.resource_typesNoOpt-in data events: Issue, Comment, Project. Default [].

AgentSessionEvent, PermissionChange and OAuthApp are always delivered; listing them in resource_types is refused.

Minimal manifest:

{
  "schemaVersion": "1.0.0",
  "name": "Scout",
  "developer": { "name": "Acme Labs" },
  "oauth": {
    "redirect_uris": ["https://scout.acme.example/oauth/callback"],
    "scopes": ["read", "write", "app:assignable"]
  },
  "webhook": { "url": "https://scout.acme.example/pulse/webhook" }
}

Full manifest:

{
  "$schema": "https://api.trypulse.tech/.well-known/agent-app-manifest.schema.json",
  "schemaVersion": "1.0.0",
  "name": "Scout",
  "description": "Reads a delegated issue, plans the work and reports back in the session.",
  "icon_url": "https://scout.acme.example/icon.png",
  "developer": { "name": "Acme Labs", "url": "https://acme.example" },
  "distribution": "private",
  "oauth": {
    "redirect_uris": [
      "https://scout.acme.example/oauth/callback",
      "https://scout-staging.acme.example/oauth/callback"
    ],
    "scopes": ["read", "write", "app:assignable", "app:mentionable"]
  },
  "webhook": {
    "enabled": true,
    "url": "https://scout.acme.example/pulse/webhook",
    "resource_types": ["Issue", "Comment"]
  }
}

Install with OAuth

A workspace owner or admin installs the app through the authorization code flow with actor=app. The installation acts as the app itself, not as the admin who approved it.

1. Send the admin to the authorize URL

GET https://api.trypulse.tech/api/v1/oauth/authorize
  ?client_id=pulse_app_6ab5a1c04f48
  &redirect_uri=https://scout.acme.example/oauth/callback
  &response_type=code
  &scope=read,write,app:assignable,app:mentionable
  &state=8c1f2e0b7d
  &actor=app
  &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
  &code_challenge_method=S256
ParameterValue
client_idYour app's client id
redirect_uriOne of the manifest's oauth.redirect_uris, exactly
response_typecode
scopeScopes separated by commas or spaces
stateA random value you check on the callback
actorapp
code_challengeBase64url SHA-256 of your code verifier
code_challenge_methodS256

PKCE is required: a request without code_challenge, or with any other method, is refused with invalid_request. Other refusals redirect back with error=unsupported_actor (the client is not an agent app) or error=invalid_scope (admin requested, or an app:* scope without actor=app).

2. The admin approves

The consent screen shows the app, its developer and its scopes, and a team picker: All teams or Selected teams. Only owners and admins of the app's workspace can approve; anyone else sees a refusal and nothing is installed. Approving creates the installation and the app's user, then redirects to your redirect_uri with code and state.

3. Exchange the code

The token endpoint is form-encoded. Authenticate with HTTP Basic (client_secret_basic), or send client_id and client_secret in the form body (client_secret_post):

curl -X POST https://api.trypulse.tech/api/v1/oauth/token \
  -u "$PULSE_CLIENT_ID:$PULSE_CLIENT_SECRET" \
  -d grant_type=authorization_code \
  -d code="$CODE" \
  -d redirect_uri=https://scout.acme.example/oauth/callback \
  -d code_verifier="$CODE_VERIFIER"
{
  "access_token": "eyJhbGciOi...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "prt_4f1c...",
  "scope": "read write app:assignable app:mentionable",
  "actor": "app",
  "installation_id": "6ab5a2d134765b14d76a6b10",
  "app_user_id": "6ab5a2d134765b14d76a6b12",
  "workspace_id": "6a34cfb024a3b4ed28806e3b"
}

The access token lasts 1 hour. Refresh it with grant_type=refresh_token; every refresh returns a new refresh_token and the old one stops working, so always store the latest. Revoke a refresh token with POST /api/v1/oauth/revoke, authenticating the client with HTTP Basic or client_id in the form: it ends that grant, so no refresh works again and the app needs to be installed again to get tokens. An access token already issued keeps working until it expires.

4. Store tokens per installation

Key your token store on installation_id and keep workspace_id next to it. Send X-Workspace-ID: <workspace_id> on every API call. Webhooks carry the same installation_id, so each event finds its token.

Protect your install endpoint. Generate state per install and reject a callback whose state you did not issue, and put the endpoint that starts the flow behind an install secret of your own so strangers cannot start installs of your app.

Learn your identity

GET /api/v1/auth/me answers for any valid app token and needs no scope. It is where you read your own user id and name (which may carry a suffix such as Scout 1):

{
  "user": {
    "id": "6ab5a2d134765b14d76a6b12",
    "display_name": "Scout",
    "user_kind": "application",
    "agent_kind": "app"
  },
  "app": { "id": "6ab5a1c04f48187cbd3ce901", "name": "Scout" },
  "installation_id": "6ab5a2d134765b14d76a6b10"
}

GET /api/v1/workspaces/me returns the installation's workspace, and GET /api/v1/teams/select the teams it covers.

Webhooks

Pulse POSTs signed JSON to your manifest's webhook.url.

Event (type)actionWhen
AgentSessionEventcreated, promptedA session opened for your app; a person wrote in it or pressed Stop. See Developing the agent interaction.
PermissionChangeteamAccessChangedAn admin changed the teams your app covers
OAuthApprevokedYour app was uninstalled
Issue, Comment, Projectcreate, update, removeOpt-in through resource_types, only for your installation's teams. Same data as workspace webhooks.
PingcreateOnly from POST /api/v1/agent-apps/{app_id}/webhook/test

Delivery headers

HeaderDescription
Content-Typeapplication/json; charset=utf-8
User-AgentPulse-Webhook/1
Pulse-DeliveryStable id for this delivery, the same on every retry
Pulse-EventThe envelope type, for example AgentSessionEvent
Pulse-SignatureLowercase hex HMAC-SHA256 of the raw body with your webhook secret. No sha256= prefix.
Pulse-TimestampUnix milliseconds of this attempt. Not signed — use the body's webhook_timestamp.

Envelope

FieldDescription
actionSee the table above
typeThe event type; the same as Pulse-Event
actor{id, type, name}: the person who delegated, mentioned or prompted for AgentSessionEvent; type: system for PermissionChange and OAuthApp
created_atWhen it happened (UTC). The same on every retry.
dataThe event body. It always has event_id, stable across retries.
urlThe issue, comment or session in the Pulse app
workspace_idThe workspace your app is installed in
webhook_idYour app's webhook
webhook_timestampUnix milliseconds of this attempt. Covered by the signature.
installation_idThe installation. Look up the token with it.
app_user_idYour app's user id in this workspace

Verify every delivery

  1. Read the raw body bytes before any JSON parser runs.
  2. Compute the lowercase hex HMAC-SHA256 of those bytes with the webhook secret (including pwhsec_), and compare it with Pulse-Signature in constant time.
  3. Parse the body and reject it if webhook_timestamp is more than 60 seconds from your clock. Do not use Pulse-Timestamp for this: it is not signed.
  4. Drop duplicates by data.event_id.
import crypto from "node:crypto";

export function verifyPulseWebhook(rawBody: Buffer, signature: string | undefined, secret: string) {
  if (typeof signature !== "string") return null;
  const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  const a = Buffer.from(signature, "utf8");
  const b = Buffer.from(expected, "utf8");
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return null;

  const payload = JSON.parse(rawBody.toString("utf8"));
  if (Math.abs(Date.now() - payload.webhook_timestamp) > 60_000) return null;
  return payload;
}

Answer within 5 seconds

Return any 2xx within 5 seconds, then do the work asynchronously. Pulse does not follow redirects. A timeout, network error or non-2xx status is a failed attempt, retried after 1 minute, 1 hour and 6 hours. A webhook with no successful delivery and at least three failures in 24 hours is disabled; fix the receiver, send a test, and enable it again.

GET /api/v1/agent-apps/{app_id}/deliveries lists recent deliveries with their attempts, and POST /api/v1/agent-apps/{app_id}/webhook/test sends one signed Ping.

Permission and uninstall events

PermissionChange / teamAccessChanged:

{
  "action": "teamAccessChanged",
  "type": "PermissionChange",
  "actor": { "id": "", "type": "system", "name": "" },
  "created_at": "2026-09-25T10:00:00.000Z",
  "data": {
    "event_id": "pc_6ab5a2d134765b14d76a6b10_3",
    "installation_id": "6ab5a2d134765b14d76a6b10",
    "all_teams": false,
    "team_ids": ["691897efbdac2591ea059cc4"],
    "added_team_ids": [],
    "removed_team_ids": ["68da1fb8af973c6419b9b3f7"]
  },
  "url": "",
  "workspace_id": "6a34cfb024a3b4ed28806e3b",
  "webhook_id": "6ab5a1c04f48187cbd3ce903",
  "webhook_timestamp": 1790330400000,
  "installation_id": "6ab5a2d134765b14d76a6b10",
  "app_user_id": "6ab5a2d134765b14d76a6b12"
}

OAuthApp / revoked — drop the installation's tokens:

{
  "action": "revoked",
  "type": "OAuthApp",
  "actor": { "id": "", "type": "system", "name": "" },
  "created_at": "2026-09-25T11:00:00.000Z",
  "data": {
    "event_id": "rv_6ab5a2d134765b14d76a6b10",
    "installation_id": "6ab5a2d134765b14d76a6b10",
    "app_id": "6ab5a1c04f48187cbd3ce901"
  },
  "url": "",
  "workspace_id": "6a34cfb024a3b4ed28806e3b",
  "webhook_id": "6ab5a1c04f48187cbd3ce903",
  "webhook_timestamp": 1790334000000,
  "installation_id": "6ab5a2d134765b14d76a6b10",
  "app_user_id": "6ab5a2d134765b14d76a6b12"
}

Call Pulse as the app

Use the access token as a bearer token, with X-Workspace-ID, on the Pulse REST API and on the Pulse MCP server at https://mcp.trypulse.tech/mcp. Everything you do is attributed to your app's user. For example, move a delegated issue to in_progress when you start:

PUT https://api.trypulse.tech/api/v1/issues/6ab5b0024f48187cbd3ce990
Authorization: Bearer $APP_ACCESS_TOKEN
X-Workspace-ID: 6a34cfb024a3b4ed28806e3b
Content-Type: application/json

{ "status": "in_progress" }

Errors use one shape, {code, message, details}:

{
  "code": "APP_STATUS_FORBIDDEN",
  "message": "Agent apps cannot move an issue to done",
  "details": { "status": "done", "allowed": ["in_progress", "qa"] }
}
StatuscodeMeaning
401INSTALLATION_REVOKEDThe app was uninstalled. Drop the tokens.
403INSUFFICIENT_SCOPEThe token lacks a scope; details.required names it. Also sent as WWW-Authenticate: Bearer error="insufficient_scope". Reinstall with the scope.
403APP_STATUS_FORBIDDENApps create issues only in backlog, todo, in_progress or qa, and move them only to in_progress or qa
403SESSION_ONLY_ROUTEThe route needs a person's session (consent, app and webhook management, passwords)
400INVALID_DELEGATEWith details.reason: app_actor: apps cannot set, change or clear a delegate

The Agent Session API allows 10 activities per second per session and 600 requests per minute per installation; above that it answers 429 RATE_LIMIT_EXCEEDED with Retry-After.

Pulse agent model reference

Three kinds of agent

Kindagent_kindWho creates itWhere it runs
Pulse AgentdeploymentBuilt inInside Pulse
Personal agentpersonalAny member, for themselvesInside Pulse, on the member's own instructions
Agent appappA developer, installed by a workspace adminOn the developer's server

All three are application users (user_kind: application) and share one picker. agent_kind tells them apart on user objects, the issue delegate, /users/select results and GET /api/v1/agents (which lists installed apps only with ?include_apps=true, and Pulse Agent only with ?include_deployment=true):

{
  "id": "6ab5a2d134765b14d76a6b12",
  "display_name": "Scout",
  "user_kind": "application",
  "agent_kind": "app"
}

Delegate and assignee

Delegating an issue to an agent sets its delegate, not its assignee. A human assignee always stays accountable for the work:

  • If the issue has no assignee, the person who delegates becomes the assignee in the same write.
  • A person outside the issue's team cannot delegate an unassigned issue: the write fails with DELEGATION_REQUIRES_ASSIGNEE, and they must name a teammate as assignee.
  • Removing the delegate, or delegating to another agent, ends the app's session with end_reason: undelegated.

See Assign and delegate issues for how people do this.

An issue can be delegated to an agent app only when all of these hold. Otherwise the picker shows the app as unavailable, with the matching unavailable_reason:

Requirementunavailable_reason when missing
An active installation covering the issue's teamapp_not_installed_for_team
The app:assignable scopeapp_missing_scope
The app's webhook is enabledapp_webhook_disabled

Scope reference

An agent app asks for scopes in its manifest and at install. Pulse enforces them on every request made with the app's token.

ScopeGrants
readRead what the app's teams can read. A grant with no scope is read.
writeCreate, change and delete. Implies issues:create and comments:create.
issues:createCreate issues only (POST /api/v1/issues).
comments:createCreate comments and replies only (POST /api/v1/comments).
app:assignablePeople can delegate issues to the app.
app:mentionablePeople can @mention the app in comments.

admin is never granted to an agent app. Most apps ask for read,write,app:assignable,app:mentionable. A request without the scope a route needs is refused with 403 INSUFFICIENT_SCOPE, which names the scope in details.required. GET /api/v1/auth/me, GET /api/v1/workspaces/me and GET /api/v1/teams/select need no scope.

Team access

At install the admin chooses All teams or Selected teams. The app's user becomes a member of those teams, and its token reads and writes only there: an issue of another team answers 403, and so does a person's personal issue.

Admins can change the teams or uninstall the app at any time, from Installed agents. The app is told at once:

ChangeWebhook the app receivesWhat else happens
Teams changedPermissionChange / teamAccessChangedDelegations on removed teams are released; their sessions end with end_reason: team_removed
UninstalledOAuthApp / revokedThe token stops working (401 INSTALLATION_REVOKED); every delegation is released; sessions end with end_reason: uninstalled

The payloads are in App setup and API reference.

What agent apps cannot do

In v1 an agent app cannot:

  • Close issues. It may create an issue in backlog, todo, in_progress or qa, and move one only to in_progress or qa. Any other status answers 403 APP_STATUS_FORBIDDEN. Hand work back with a response; a person closes it.
  • Change delegation. Setting, changing or clearing delegate_id answers 400 INVALID_DELEGATE with details.reason: app_actor, even for its own delegation.
  • Act outside its teams. Issues, comments and sessions of other teams are out of reach.
  • Be mentioned in descriptions or documents. Only comment mentions open a session.
  • Be installed outside its own workspace. Apps are private in v1.

FAQ

Agent Session API reference

The following reference preserves full endpoint, event and error payloads for implementations.

Session APIs

MethodPathDoes
GET/agent-sessions/{session_id}Get one session
GET/agent-sessionsList your sessions: issue_id, state, cursor, limit (up to 100)
GET/agent-sessions/{session_id}/activitiesRead the conversation: after_seq, limit (up to 200)
POST/agent-sessions/{session_id}/activitiesPost an activity
PATCH/agent-sessions/{session_id}Update plan and external URLs
POST/agent-sessionsOpen a session yourself on an issue or a comment

All paths are under https://api.trypulse.tech/api/v1.

Read the conversation

Rebuild the conversation from activities, not from issue comments. The list includes people's messages with their author (author.kind: user). seq always grows but has gaps, so page with after_seq:

GET https://api.trypulse.tech/api/v1/agent-sessions/sess_01J9Z3K8T5N2/activities?after_seq=4&limit=100
{
  "data": [
    {
      "id": "act_01J9Z4B2Q7",
      "seq": 9,
      "type": "prompt",
      "content": { "type": "prompt", "body": "Also check the Go client, please." },
      "ephemeral": false,
      "superseded_by_seq": null,
      "signal": null,
      "signal_metadata": null,
      "author": { "kind": "user", "id": "68d91d3629c051fe043158f9", "name": "Sara Ahmadi", "avatar_url": null },
      "created_at": "2026-09-25T09:15:40.000Z"
    }
  ],
  "has_more": false,
  "last_seq": 9
}

Ask again with after_seq set to last_seq. GET /agent-sessions pages the other way: newest first, with next_cursor (null on the last page) passed back as cursor.

Agent plans

A plan is a checklist people see in the session header. Send the whole array every time (at most 50 items):

PATCH https://api.trypulse.tech/api/v1/agent-sessions/sess_01J9Z3K8T5N2
Content-Type: application/json

{
  "plan": [
    { "content": "Read the issue and its threads", "status": "completed" },
    { "content": "Draft the change", "status": "inProgress" },
    { "content": "Report back", "status": "pending" }
  ]
}

status is pending, inProgress, completed or canceled; content is up to 500 characters.

Link people to your own UI for the session (at most 10, each url unique and https://, label up to 60 characters). Replace the list with external_urls, or change it with added_external_urls and removed_external_urls (not both at once):

{ "added_external_urls": [{ "label": "Open in Scout", "url": "https://scout.acme.example/runs/42" }] }
{ "removed_external_urls": ["https://scout.acme.example/runs/42"] }

Setting an external URL within 10 seconds of created counts as responding.

Open a session yourself

POST /agent-sessions with an issue_id or a comment_id your installation covers starts work nobody delegated:

{ "issue_id": "6ab5b0024f48187cbd3ce990" }

The response is the new session (201), with your app user as creator. No created webhook is sent for it. On a comment there is one open session per app per thread: if yours exists it comes back with 200.

When Pulse ends a session

Some ends are not your app's choice:

end_reasonCauseHow it ends
stoppedA person pressed Stopstopping, then prompted with signal: "stop"; ends after your final activity or 60 seconds
undelegatedThe issue was undelegated, or delegated to another agentSame as stopped
uninstalledYour app was uninstalledEnds at once, no prompted. You receive OAuthApp / revoked.
team_removedYour installation no longer covers the issue's teamEnds at once, no prompted. You receive PermissionChange / teamAccessChanged.
issue_deletedThe issue was deletedEnds at once, no prompted. With Issue opted in, you receive its remove.

For stopped and undelegated, end_reason is set as soon as the session enters stopping, so you know why you are being stopped. Once the session has ended (ended_at is set), every write answers 409:

{ "code": "SESSION_ENDED", "message": "This session has ended", "details": { "end_reason": "stopped" } }

Event payload reference

The examples below show the complete webhook envelope and the fields your app receives. Verify each delivery before using the payload.

The created event

{
  "action": "created",
  "type": "AgentSessionEvent",
  "actor": { "id": "68d91d3629c051fe043158f9", "type": "user", "name": "Sara Ahmadi" },
  "created_at": "2026-09-25T09:12:03.120Z",
  "data": {
    "event_id": "ase_sess_01J9Z3K8T5N2_created",
    "agent_session": {
      "id": "sess_01J9Z3K8T5N2",
      "state": "pending",
      "unresponsive_since": null,
      "issue": {
        "id": "6ab5b0024f48187cbd3ce990",
        "identifier": "PUL-123",
        "title": "Webhook retries ignore Retry-After",
        "url": "https://app.trypulse.tech/pulse/issues/6ab5b0024f48187cbd3ce990",
        "team": { "id": "691897efbdac2591ea059cc4", "key": "PUL", "name": "Pulse" }
      },
      "comment": {
        "id": "6ab5b0414f48187cbd3ce9b2",
        "body": "@Scout can you look at this?",
        "url": "https://app.trypulse.tech/pulse/issues/6ab5b0024f48187cbd3ce990#comment-6ab5b0414f48187cbd3ce9b2"
      },
      "creator": { "id": "68d91d3629c051fe043158f9", "name": "Sara Ahmadi" },
      "app_user_id": "6ab5a2d134765b14d76a6b12",
      "plan": [],
      "external_urls": [],
      "created_at": "2026-09-25T09:12:03.120Z",
      "updated_at": "2026-09-25T09:12:03.120Z",
      "ended_at": null,
      "end_reason": null
    },
    "previous_comments": [
      {
        "id": "6ab5b0304f48187cbd3ce9a8",
        "body": "Seen on staging too.",
        "author": { "id": "68cab92a5020377746176588", "name": "Alireza Attari" },
        "created_at": "2026-09-25T08:50:00.000Z"
      }
    ],
    "guidance": [
      { "origin": "workspace", "body": "Write in English. Link every issue you mention." },
      { "origin": "team", "team_id": "691897efbdac2591ea059cc4", "body": "Never touch the billing module." }
    ],
    "prompt_context": "<issue identifier=\"PUL-123\" url=\"…\">…</issue>…"
  },
  "url": "https://app.trypulse.tech/pulse/issues/6ab5b0024f48187cbd3ce990",
  "workspace_id": "6a34cfb024a3b4ed28806e3b",
  "webhook_id": "6ab5a1c04f48187cbd3ce903",
  "webhook_timestamp": 1790327523120,
  "installation_id": "6ab5a2d134765b14d76a6b10",
  "app_user_id": "6ab5a2d134765b14d76a6b12"
}
  • issue is null for a mention on a project comment. comment is null for a delegation.
  • creator is the person who delegated or mentioned your app.
  • previous_comments are the thread's earlier comments, oldest first.
  • guidance is the workspace's rule, then the issue team's rule. The team rule wins on conflict.

prompt_context

prompt_context is the same context as one XML string, ready for a model prompt. It uses Pulse context tags:

<issue identifier="PUL-123" url="https://app.trypulse.tech/pulse/issues/6ab5b0024f48187cbd3ce990">
  <title>Webhook retries ignore Retry-After</title>
  <description>Deliveries that get a 429 retry on the fixed schedule.</description>
  <team>Pulse</team>
  <label>bug</label>
  <project>Integrations</project>
</issue>
<primary-directive-thread comment-id="6ab5b0414f48187cbd3ce9b2">
  <comment author="Sara Ahmadi" created-at="2026-09-25T09:12:03.120Z">@Scout can you look at this?</comment>
</primary-directive-thread>
<guidance>
  <guidance-rule origin="workspace">Write in English. Link every issue you mention.</guidance-rule>
  <guidance-rule origin="team" team="PUL">Never touch the billing module.</guidance-rule>
</guidance>
<repository-hint repository="pulse/pulse-api"/>
TagContent
<issue identifier url>The issue, with <title>, <description> (Markdown), <team>, <label> (repeated), <project> and <parent-issue> when set
<primary-directive-thread comment-id>The thread that mentioned your app. A delegation has none.
<other-thread comment-id>The issue's other threads
<comment author created-at>One comment inside a thread
<guidance><guidance-rule origin="workspace">, then <guidance-rule origin="team" team="KEY">
<repository-hint repository>A repository the issue's team routes work to. A hint only: bring your own git access.

Only workspace and team guidance is sent. Global platform guidance and people's personal guidance never are. prompt_context is at most 64 KB: the oldest <other-thread> blocks are dropped first, then the description is truncated.

The prompted event

A person wrote in the session, or pressed Stop. data carries the session and the message:

{
  "action": "prompted",
  "type": "AgentSessionEvent",
  "actor": { "id": "68d91d3629c051fe043158f9", "type": "user", "name": "Sara Ahmadi" },
  "created_at": "2026-09-25T09:15:40.000Z",
  "data": {
    "event_id": "ase_sess_01J9Z3K8T5N2_act_9",
    "agent_session": { "id": "sess_01J9Z3K8T5N2", "state": "pending" },
    "agent_activity": {
      "id": "act_01J9Z4B2Q7",
      "content": { "type": "prompt", "body": "Also check the Go client, please." },
      "author": { "id": "68d91d3629c051fe043158f9", "name": "Sara Ahmadi" },
      "created_at": "2026-09-25T09:15:40.000Z"
    }
  },
  "url": "https://app.trypulse.tech/pulse/issues/6ab5b0024f48187cbd3ce990",
  "workspace_id": "6a34cfb024a3b4ed28806e3b",
  "webhook_id": "6ab5a1c04f48187cbd3ce903",
  "webhook_timestamp": 1790327740000,
  "installation_id": "6ab5a2d134765b14d76a6b10",
  "app_user_id": "6ab5a2d134765b14d76a6b12"
}

agent_session is abbreviated here; it is the full session, as in created. The message is in agent_activity.content.body. When agent_activity.signal is "stop", the person pressed Stop — see Signals.

Errors

StatuscodeMeaning
401UNAUTHORIZEDMissing, expired or revoked token
403INSUFFICIENT_SCOPEread or write missing; see details.required
404RESOURCE_NOT_FOUNDNot your app's session, or an issue or comment you cannot see
409SESSION_ENDEDThe session was ended by a person or by Pulse
422PROMPT_IS_HUMAN_ONLYYou posted a prompt
422VALIDATION_ERRORThe body does not match, for example ephemeral on a response; see details
429RATE_LIMIT_EXCEEDEDMore than 10 activities per second in a session, or 600 requests per minute for the installation. Wait Retry-After seconds.

Last updated on