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:
- Register the app from a manifest and copy its secrets.
- Serve an install endpoint that runs the OAuth flow with
actor=app. - Serve a webhook endpoint that verifies each delivery and answers within 5 seconds.
- Answer sessions through the Agent Session API.
- 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/jsonThe 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.
| Field | Required | Rules |
|---|---|---|
$schema | No | The schema URL above |
schemaVersion | Yes | "1.0.0" |
name | Yes | 2–80 characters; must not contain "Pulse". Becomes the app's user name. |
description | No | Up to 1000 characters |
icon_url | No | https:// URL of a square icon; Pulse proxies it |
developer.name | Yes | 2–80 characters |
developer.url | No | Your website |
distribution | No | Only "private" (the default). "public" is refused in v1. |
oauth.redirect_uris | Yes | 1–32 unique https:// URLs |
oauth.scopes | Yes | From read, write, issues:create, comments:create, app:assignable, app:mentionable. Never admin. |
webhook.url | With webhook | Public https:// URL on port 443 or 8443 |
webhook.enabled | No | Default true. While false, nothing is delivered and the app cannot be delegated to or mentioned. |
webhook.resource_types | No | Opt-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| Parameter | Value |
|---|---|
client_id | Your app's client id |
redirect_uri | One of the manifest's oauth.redirect_uris, exactly |
response_type | code |
scope | Scopes separated by commas or spaces |
state | A random value you check on the callback |
actor | app |
code_challenge | Base64url SHA-256 of your code verifier |
code_challenge_method | S256 |
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) | action | When |
|---|---|---|
AgentSessionEvent | created, prompted | A session opened for your app; a person wrote in it or pressed Stop. See Developing the agent interaction. |
PermissionChange | teamAccessChanged | An admin changed the teams your app covers |
OAuthApp | revoked | Your app was uninstalled |
Issue, Comment, Project | create, update, remove | Opt-in through resource_types, only for your installation's teams. Same data as workspace webhooks. |
Ping | create | Only from POST /api/v1/agent-apps/{app_id}/webhook/test |
Delivery headers
| Header | Description |
|---|---|
Content-Type | application/json; charset=utf-8 |
User-Agent | Pulse-Webhook/1 |
Pulse-Delivery | Stable id for this delivery, the same on every retry |
Pulse-Event | The envelope type, for example AgentSessionEvent |
Pulse-Signature | Lowercase hex HMAC-SHA256 of the raw body with your webhook secret. No sha256= prefix. |
Pulse-Timestamp | Unix milliseconds of this attempt. Not signed — use the body's webhook_timestamp. |
Envelope
| Field | Description |
|---|---|
action | See the table above |
type | The 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_at | When it happened (UTC). The same on every retry. |
data | The event body. It always has event_id, stable across retries. |
url | The issue, comment or session in the Pulse app |
workspace_id | The workspace your app is installed in |
webhook_id | Your app's webhook |
webhook_timestamp | Unix milliseconds of this attempt. Covered by the signature. |
installation_id | The installation. Look up the token with it. |
app_user_id | Your app's user id in this workspace |
Verify every delivery
- Read the raw body bytes before any JSON parser runs.
- Compute the lowercase hex HMAC-SHA256 of those bytes with the webhook secret (including
pwhsec_), and compare it withPulse-Signaturein constant time. - Parse the body and reject it if
webhook_timestampis more than 60 seconds from your clock. Do not usePulse-Timestampfor this: it is not signed. - 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"] }
}| Status | code | Meaning |
|---|---|---|
401 | INSTALLATION_REVOKED | The app was uninstalled. Drop the tokens. |
403 | INSUFFICIENT_SCOPE | The token lacks a scope; details.required names it. Also sent as WWW-Authenticate: Bearer error="insufficient_scope". Reinstall with the scope. |
403 | APP_STATUS_FORBIDDEN | Apps create issues only in backlog, todo, in_progress or qa, and move them only to in_progress or qa |
403 | SESSION_ONLY_ROUTE | The route needs a person's session (consent, app and webhook management, passwords) |
400 | INVALID_DELEGATE | With 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
| Kind | agent_kind | Who creates it | Where it runs |
|---|---|---|---|
| Pulse Agent | deployment | Built in | Inside Pulse |
| Personal agent | personal | Any member, for themselves | Inside Pulse, on the member's own instructions |
| Agent app | app | A developer, installed by a workspace admin | On 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:
| Requirement | unavailable_reason when missing |
|---|---|
| An active installation covering the issue's team | app_not_installed_for_team |
The app:assignable scope | app_missing_scope |
| The app's webhook is enabled | app_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.
| Scope | Grants |
|---|---|
read | Read what the app's teams can read. A grant with no scope is read. |
write | Create, change and delete. Implies issues:create and comments:create. |
issues:create | Create issues only (POST /api/v1/issues). |
comments:create | Create comments and replies only (POST /api/v1/comments). |
app:assignable | People can delegate issues to the app. |
app:mentionable | People 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:
| Change | Webhook the app receives | What else happens |
|---|---|---|
| Teams changed | PermissionChange / teamAccessChanged | Delegations on removed teams are released; their sessions end with end_reason: team_removed |
| Uninstalled | OAuthApp / revoked | The 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_progressorqa, and move one only toin_progressorqa. Any other status answers403 APP_STATUS_FORBIDDEN. Hand work back with aresponse; a person closes it. - Change delegation. Setting, changing or clearing
delegate_idanswers400 INVALID_DELEGATEwithdetails.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
| Method | Path | Does |
|---|---|---|
GET | /agent-sessions/{session_id} | Get one session |
GET | /agent-sessions | List your sessions: issue_id, state, cursor, limit (up to 100) |
GET | /agent-sessions/{session_id}/activities | Read the conversation: after_seq, limit (up to 200) |
POST | /agent-sessions/{session_id}/activities | Post an activity |
PATCH | /agent-sessions/{session_id} | Update plan and external URLs |
POST | /agent-sessions | Open 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.
Session external links
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_reason | Cause | How it ends |
|---|---|---|
stopped | A person pressed Stop | stopping, then prompted with signal: "stop"; ends after your final activity or 60 seconds |
undelegated | The issue was undelegated, or delegated to another agent | Same as stopped |
uninstalled | Your app was uninstalled | Ends at once, no prompted. You receive OAuthApp / revoked. |
team_removed | Your installation no longer covers the issue's team | Ends at once, no prompted. You receive PermissionChange / teamAccessChanged. |
issue_deleted | The issue was deleted | Ends 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"
}issueis null for a mention on a project comment.commentis null for a delegation.creatoris the person who delegated or mentioned your app.previous_commentsare the thread's earlier comments, oldest first.guidanceis 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"/>| Tag | Content |
|---|---|
<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
| Status | code | Meaning |
|---|---|---|
401 | UNAUTHORIZED | Missing, expired or revoked token |
403 | INSUFFICIENT_SCOPE | read or write missing; see details.required |
404 | RESOURCE_NOT_FOUND | Not your app's session, or an issue or comment you cannot see |
409 | SESSION_ENDED | The session was ended by a person or by Pulse |
422 | PROMPT_IS_HUMAN_ONLY | You posted a prompt |
422 | VALIDATION_ERROR | The body does not match, for example ephemeral on a response; see details |
429 | RATE_LIMIT_EXCEEDED | More than 10 activities per second in a session, or 600 requests per minute for the installation. Wait Retry-After seconds. |
Last updated on