openapi: 3.1.0 info: # Do not change the title, if the title changes, the import paths will be broken title: Api version: 0.2.0 description: | nmbr API specification. The **Agent API** (`/agent/v1/*`, tag `agent`) is the external-agent surface of the nmbr Agent Platform: software you run anywhere gets an `800-xxx-xxx` nmbr and a bearer token, and talks to people inside nmbr as a contact. Every Agent API request is authenticated with an `agent:` token (`Authorization: Bearer agent:…`); human session/bearer credentials are rejected on this surface, and agent tokens are rejected everywhere else. Errors on the Agent API always use the envelope `{ "error": { "code", "message" } }` (see `AgentError`). Rate limits: 300 requests/min per agent, 60 messages/min per agent, 20 messages/min per agent per recipient (action proposals count as messages); a 429 carries `Retry-After` and standard `RateLimit-*` headers. Cross-field rules that OpenAPI cannot express are enforced server-side and described on the relevant schema (for example, a `voice` message requires `audioData` and `audioDuration`). ## Human-in-the-loop approvals An agent never does anything consequential on its own say-so: it **proposes**, the person decides on a card in the chat, and the agent acts only after `action.approved`. nmbr itself never executes the action. 1. `POST /agent/v1/actions` with `to` (the person's nmbr), a developer-defined `kind`, a one-line `title`, and any `payload` the agent needs back. 2. The person sees an approval card in the 1:1 chat (push + realtime, like a message). They can **approve**, **edit the payload and approve**, or **reject**. Only that person, from that conversation, can decide — a reply from anyone else, or from a group, is refused. 3. The decision arrives as an event over long-poll or the webhook: `action.approved` (`payload.action.editedPayload` is set when they edited it — use it instead of `payload`), `action.rejected`, or `action.expired`. 4. **Expiry is fail-closed.** Nobody decided before `expiresAt` (default 24 h, 60 s – 7 d) ⇒ `action.expired`. Treat it exactly like a rejection. Never act on a proposal you have not seen approved. In five lines (bash; `$TOKEN` is the agent token, `$SEQ` your last event cursor): curl -s -X POST https://nmbr.ai/api/agent/v1/actions -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"to":"123-456-789","kind":"deploy","title":"Deploy v2 to prod?","payload":{"ref":"abc123"}}' curl -s "https://nmbr.ai/api/agent/v1/updates?afterSeq=$SEQ&wait=25" -H "Authorization: Bearer $TOKEN" # → { "events": [ { "type": "action.approved", "payload": { "action": { "id": "…", "editedPayload": null, … } } } ], "nextSeq": … } Loop the second call until an `action.*` event names your action id; act only on `action.approved`; at most 10 proposals may be pending per conversation (`too_many_pending`). `GET /agent/v1/actions/{actionId}` returns the current state at any time (useful after a missed webhook). servers: - url: /api description: Base API path tags: - name: health description: Health operations - name: agent description: Agent API — external agents (Agent Platform) paths: /healthz: get: operationId: healthCheck tags: [health] summary: Health check description: Returns server health status responses: "200": description: Healthy content: application/json: schema: $ref: "#/components/schemas/HealthStatus" /agent/v1/messages: post: operationId: agentSendMessage tags: [agent] summary: Send a message description: | Sends a message as the agent to a user who has added it as a contact. Address the recipient with `to` (their nmbr) **or** an existing `conversationId` — exactly one. Only 1:1 conversations are supported. security: [{ agentToken: [] }] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/AgentSendMessageRequest" responses: "201": description: Message stored and delivered (push + realtime) to the recipient content: application/json: schema: $ref: "#/components/schemas/AgentSendMessageResponse" "400": { $ref: "#/components/responses/AgentBadRequest" } "401": { $ref: "#/components/responses/AgentUnauthorized" } "403": description: Recipient hasn't added the agent as a contact (`not_a_contact`), is suspended (`recipient_suspended`), or blocked it (`blocked`) content: application/json: schema: $ref: "#/components/schemas/AgentError" "404": { $ref: "#/components/responses/AgentNotFound" } "429": { $ref: "#/components/responses/AgentRateLimited" } /agent/v1/actions: post: operationId: agentProposeAction tags: [agent] summary: Propose an action for approval description: | Proposes an action to a user who has added the agent as a contact. The proposal appears as an approval card in the 1:1 chat (delivered like a message: push + realtime); the returned `message` is that card and carries `agentActionId`. Address the user with `to` (their nmbr) **or** an existing `conversationId` — exactly one. nmbr never executes the action. Only that user, from that conversation, can approve or reject it; the decision reaches the agent as an `action.approved` / `action.rejected` event (with `payload`, edited if the user changed it), and expiry produces `action.expired` — treat it as a rejection. At most 10 proposals may be pending per conversation (`too_many_pending`). Walkthrough + a five-line example: see "Human-in-the-loop approvals" in the API description. security: [{ agentToken: [] }] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/AgentProposeActionRequest" responses: "201": description: Proposal recorded and its card delivered to the user content: application/json: schema: $ref: "#/components/schemas/AgentProposeActionResponse" "400": description: Validation error, or `invalid_expiry` when `expiresAt` is under 60 s or over 7 days ahead content: application/json: schema: $ref: "#/components/schemas/AgentError" "401": { $ref: "#/components/responses/AgentUnauthorized" } "403": description: User hasn't added the agent as a contact (`not_a_contact`), is suspended (`recipient_suspended`), or blocked it (`blocked`) content: application/json: schema: $ref: "#/components/schemas/AgentError" "404": { $ref: "#/components/responses/AgentNotFound" } "409": description: Too many pending proposals in this conversation (`too_many_pending`) content: application/json: schema: $ref: "#/components/schemas/AgentError" "429": { $ref: "#/components/responses/AgentRateLimited" } get: operationId: agentListActions tags: [agent] summary: List the agent's proposals description: The agent's own proposals, newest first. Filter by conversation and/or state. security: [{ agentToken: [] }] parameters: - name: conversationId in: query required: false schema: { type: string } - name: state in: query required: false schema: { $ref: "#/components/schemas/AgentActionState" } - name: limit in: query required: false schema: { type: integer, minimum: 1, maximum: 100, default: 50 } responses: "200": description: Proposals content: application/json: schema: $ref: "#/components/schemas/AgentActionsResponse" "400": { $ref: "#/components/responses/AgentBadRequest" } "401": { $ref: "#/components/responses/AgentUnauthorized" } "429": { $ref: "#/components/responses/AgentRateLimited" } /agent/v1/actions/{actionId}: get: operationId: agentGetAction tags: [agent] summary: Get one proposal description: One of the agent's own proposals, including its current state. security: [{ agentToken: [] }] parameters: - $ref: "#/components/parameters/ActionId" responses: "200": description: The proposal content: application/json: schema: $ref: "#/components/schemas/AgentActionResponse" "401": { $ref: "#/components/responses/AgentUnauthorized" } "404": { $ref: "#/components/responses/AgentNotFound" } "429": { $ref: "#/components/responses/AgentRateLimited" } /agent/v1/updates: get: operationId: agentGetUpdates tags: [agent] summary: Long-poll for events description: | Returns events with `seq` greater than `afterSeq`, oldest first. If none exist and `wait` > 0, the request parks until an event arrives or the wait elapses (capped server-side at 25 seconds). Pass the returned `nextSeq` as `afterSeq` on the next call. Works from behind NAT — no public URL needed. security: [{ agentToken: [] }] parameters: - name: afterSeq in: query description: Last `seq` already seen. Omit or 0 to replay from the beginning. schema: { type: integer, minimum: 0, default: 0 } - name: limit in: query schema: { type: integer, minimum: 1, maximum: 100, default: 100 } - name: wait in: query description: Seconds to wait for an event when none are pending (0 = return immediately; server caps at 25). schema: { type: number, minimum: 0, default: 0 } responses: "200": description: Events (possibly empty) content: application/json: schema: $ref: "#/components/schemas/AgentUpdatesResponse" "400": { $ref: "#/components/responses/AgentBadRequest" } "401": { $ref: "#/components/responses/AgentUnauthorized" } "429": { $ref: "#/components/responses/AgentRateLimited" } /agent/v1/updates/cursor: get: operationId: agentGetUpdatesCursor tags: [agent] summary: Current event cursor description: The agent's latest event `seq` (0 if none) — start "from now" by passing it as `afterSeq`. security: [{ agentToken: [] }] responses: "200": description: Cursor content: application/json: schema: type: object required: [seq] properties: seq: { type: integer, description: "Latest event seq for this agent, 0 if none." } "401": { $ref: "#/components/responses/AgentUnauthorized" } /agent/v1/me: get: operationId: agentGetMe tags: [agent] summary: The agent's own profile security: [{ agentToken: [] }] responses: "200": description: Profile content: application/json: schema: $ref: "#/components/schemas/AgentMe" "401": { $ref: "#/components/responses/AgentUnauthorized" } patch: operationId: agentUpdateMe tags: [agent] summary: Update the agent's profile security: [{ agentToken: [] }] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/AgentUpdateMeRequest" responses: "200": description: Updated profile content: application/json: schema: $ref: "#/components/schemas/AgentMe" "400": { $ref: "#/components/responses/AgentBadRequest" } "401": { $ref: "#/components/responses/AgentUnauthorized" } /agent/v1/conversations: get: operationId: agentListConversations tags: [agent] summary: List conversations description: 1:1 conversations the agent is in, newest activity first. security: [{ agentToken: [] }] responses: "200": description: Conversations content: application/json: schema: type: object required: [conversations] properties: conversations: type: array items: { $ref: "#/components/schemas/AgentConversation" } "401": { $ref: "#/components/responses/AgentUnauthorized" } /agent/v1/conversations/{conversationId}/messages: get: operationId: agentListMessages tags: [agent] summary: Message history description: Messages in a conversation, oldest first within the page. Page backwards with `before`. security: [{ agentToken: [] }] parameters: - { $ref: "#/components/parameters/ConversationId" } - name: limit in: query schema: { type: integer, minimum: 1, maximum: 100, default: 50 } - name: before in: query description: Message id to page backwards from. schema: { type: string } responses: "200": description: A page of messages content: application/json: schema: type: object required: [messages, hasMore] properties: messages: type: array items: { $ref: "#/components/schemas/AgentMessage" } hasMore: { type: boolean } "400": { $ref: "#/components/responses/AgentBadRequest" } "401": { $ref: "#/components/responses/AgentUnauthorized" } "403": { $ref: "#/components/responses/AgentForbidden" } "404": { $ref: "#/components/responses/AgentNotFound" } /agent/v1/conversations/{conversationId}/read: post: operationId: agentMarkRead tags: [agent] summary: Mark messages as read security: [{ agentToken: [] }] parameters: - { $ref: "#/components/parameters/ConversationId" } requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/AgentMarkReadRequest" responses: "200": { $ref: "#/components/responses/AgentOk" } "400": { $ref: "#/components/responses/AgentBadRequest" } "401": { $ref: "#/components/responses/AgentUnauthorized" } "403": { $ref: "#/components/responses/AgentForbidden" } "404": { $ref: "#/components/responses/AgentNotFound" } /agent/v1/conversations/{conversationId}/typing: post: operationId: agentSetTyping tags: [agent] summary: Show or clear the typing indicator security: [{ agentToken: [] }] parameters: - { $ref: "#/components/parameters/ConversationId" } requestBody: required: false content: application/json: schema: $ref: "#/components/schemas/AgentSetTypingRequest" responses: "200": { $ref: "#/components/responses/AgentOk" } "401": { $ref: "#/components/responses/AgentUnauthorized" } "403": { $ref: "#/components/responses/AgentForbidden" } "404": { $ref: "#/components/responses/AgentNotFound" } /agent/v1/messages/{messageId}/reactions: post: operationId: agentAddReaction tags: [agent] summary: React to a message security: [{ agentToken: [] }] parameters: - { $ref: "#/components/parameters/MessageId" } requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/AgentAddReactionRequest" responses: "201": description: Reaction added content: application/json: schema: type: object required: [reaction] properties: reaction: type: object required: [messageId, emoji, createdAt] properties: messageId: { type: string } emoji: { type: string } createdAt: { type: string, format: date-time } "400": { $ref: "#/components/responses/AgentBadRequest" } "401": { $ref: "#/components/responses/AgentUnauthorized" } "403": { $ref: "#/components/responses/AgentForbidden" } "404": { $ref: "#/components/responses/AgentNotFound" } /agent/v1/messages/{messageId}/reactions/{emoji}: delete: operationId: agentRemoveReaction tags: [agent] summary: Remove a reaction security: [{ agentToken: [] }] parameters: - { $ref: "#/components/parameters/MessageId" } - name: emoji in: path required: true schema: { type: string } responses: "200": { $ref: "#/components/responses/AgentOk" } "401": { $ref: "#/components/responses/AgentUnauthorized" } "403": { $ref: "#/components/responses/AgentForbidden" } "404": { $ref: "#/components/responses/AgentNotFound" } /agent/v1/webhook: get: operationId: agentGetWebhook tags: [agent] summary: Current webhook security: [{ agentToken: [] }] responses: "200": description: Webhook (null if none configured) content: application/json: schema: type: object required: [webhook] properties: webhook: oneOf: - $ref: "#/components/schemas/AgentWebhook" - type: "null" "401": { $ref: "#/components/responses/AgentUnauthorized" } put: operationId: agentSetWebhook tags: [agent] summary: Set or replace the webhook URL description: | Registers an https endpoint that receives every event as a signed POST (see `AgentEvent` and the `X-Nmbr-Signature` header). The signing `secret` is returned **once**. Events that existed before the webhook was first configured are not replayed to it (they remain readable via long-poll). Private, loopback and link-local hosts are rejected. security: [{ agentToken: [] }] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/AgentSetWebhookRequest" responses: "200": description: Webhook configured; the secret is shown once content: application/json: schema: $ref: "#/components/schemas/AgentWebhookWithSecret" "400": { $ref: "#/components/responses/AgentBadRequest" } "401": { $ref: "#/components/responses/AgentUnauthorized" } delete: operationId: agentDeleteWebhook tags: [agent] summary: Remove the webhook security: [{ agentToken: [] }] responses: "200": description: Removed (or nothing to remove) content: application/json: schema: type: object required: [deleted] properties: deleted: { type: boolean } "401": { $ref: "#/components/responses/AgentUnauthorized" } /agent/v1/webhook/rotate: post: operationId: agentRotateWebhookSecret tags: [agent] summary: Rotate the webhook signing secret description: Issues a new secret (shown once). The previous secret keeps verifying for 24 hours; deliveries carry both signatures during that window. security: [{ agentToken: [] }] responses: "200": description: New secret content: application/json: schema: $ref: "#/components/schemas/AgentWebhookWithSecret" "401": { $ref: "#/components/responses/AgentUnauthorized" } "404": { $ref: "#/components/responses/AgentNotFound" } components: securitySchemes: agentToken: type: http scheme: bearer description: "An `agent:` token minted for the agent in the nmbr app. `Authorization: Bearer agent:…`" parameters: ConversationId: name: conversationId in: path required: true schema: { type: string } MessageId: name: messageId in: path required: true schema: { type: string } ActionId: name: actionId in: path required: true schema: { type: string } responses: AgentOk: description: OK content: application/json: schema: type: object required: [ok] properties: ok: { type: boolean, const: true } AgentBadRequest: description: Validation failed (`validation_error`, with `issues`) or a request rule was broken content: application/json: schema: { $ref: "#/components/schemas/AgentError" } AgentUnauthorized: description: Missing, invalid, revoked or expired agent token (`unauthorized`, `token_revoked`, `token_expired`), or the owner account is unavailable content: application/json: schema: { $ref: "#/components/schemas/AgentError" } AgentForbidden: description: The agent is not a participant (`forbidden`), or the account is suspended content: application/json: schema: { $ref: "#/components/schemas/AgentError" } AgentNotFound: description: Not found content: application/json: schema: { $ref: "#/components/schemas/AgentError" } AgentRateLimited: description: Rate limited (`rate_limited` or `conversation_rate_limited`); honor `Retry-After` headers: Retry-After: schema: { type: integer } description: Seconds to wait before retrying content: application/json: schema: { $ref: "#/components/schemas/AgentError" } schemas: HealthStatus: type: object properties: status: type: string required: - status AgentError: type: object required: [error] properties: error: type: object required: [code, message] properties: code: type: string description: Stable machine-readable code (e.g. `unauthorized`, `token_revoked`, `validation_error`, `not_a_contact`, `rate_limited`). message: { type: string } issues: type: array description: Present for `validation_error`. items: type: object required: [path, message] properties: path: { type: string } message: { type: string } retryAfterSeconds: { type: integer, description: "Present on 429." } AgentUser: type: object description: Public shape of a user as seen by an agent. required: [id, nmbr, userType] properties: id: { type: string } nmbr: { type: string, example: "123-456-789" } displayName: { type: [string, "null"] } avatarUrl: { type: [string, "null"] } userType: { type: string, enum: [person, business, ai, agent] } AgentMe: allOf: - $ref: "#/components/schemas/AgentUser" - type: object required: [ownerId, requestPrivacy, createdAt] properties: bio: { type: [string, "null"] } ownerId: { type: [string, "null"], description: "The developer account that owns this agent." } requestPrivacy: type: string enum: [everyone, nobody, contacts_of_contacts] description: Who may add this agent as a contact. createdAt: { type: string, format: date-time } AgentUpdateMeRequest: type: object description: At least one field is required. properties: displayName: { type: string, minLength: 1, maxLength: 50 } bio: { type: string, maxLength: 500 } avatarUrl: { type: string, format: uri, maxLength: 2000, description: "Public https image URL." } requestPrivacy: { type: string, enum: [everyone, nobody, contacts_of_contacts] } AgentMarkReadRequest: type: object required: [messageId] properties: messageId: { type: string, description: "Id of the newest message the agent has read." } AgentSetTypingRequest: type: object properties: typing: { type: boolean, default: true, description: "true = show, false = clear." } AgentAddReactionRequest: type: object required: [emoji] properties: emoji: { type: string, minLength: 1, maxLength: 10 } AgentSetWebhookRequest: type: object required: [url] properties: url: { type: string, format: uri, maxLength: 2000, description: "https URL on a public host." } AgentMessageType: type: string enum: [text, voice, image, video, location, contact, document, sticker] AgentSendMessageRequest: type: object required: [content] description: | Provide exactly one of `to` or `conversationId`. Per-type required fields (enforced server-side): voice → `audioData` + `audioDuration`; image/sticker → `imageData`; video → `videoData`; location → `latitude` + `longitude`; contact → `sharedContactId` + `sharedContactNmbr`; document → `documentData` + `documentName`; text → no media fields. properties: to: { type: string, pattern: "^\\d{3}-\\d{3}-\\d{3}$", description: "Recipient nmbr.", example: "123-456-789" } conversationId: { type: string, description: "Existing 1:1 conversation id (e.g. from a `message.received` event)." } type: { $ref: "#/components/schemas/AgentMessageType" } content: { type: string, minLength: 1, maxLength: 10000, description: "Text body, or caption for media." } replyToId: { type: string } audioData: { type: string, description: "voice: data URL or /objects/… path." } audioDuration: { type: string, maxLength: 10, description: "voice: seconds, as a string." } imageData: { type: string, description: "image/sticker: data URL, https URL, or /objects/… path." } videoData: { type: string } thumbnailData: { type: string, maxLength: 500 } latitude: { type: string } longitude: { type: string } sharedContactId: { type: string } sharedContactName: { type: string } sharedContactNmbr: { type: string } sharedContactAvatar: { type: string } sharedContactType: { type: string } documentData: { type: string } documentName: { type: string, maxLength: 255 } documentSize: { type: string, maxLength: 20 } documentMimeType: { type: string, maxLength: 100 } mediaWidth: { type: integer, minimum: 1 } mediaHeight: { type: integer, minimum: 1 } AgentMessage: type: object description: Public shape of a message on the Agent API. Media groups are present only for their type. required: [id, conversationId, senderId, type, content, createdAt] properties: id: { type: string } conversationId: { type: string } senderId: { type: string } senderNmbr: { type: string } type: { $ref: "#/components/schemas/AgentMessageType" } content: { type: string } replyToId: { type: [string, "null"] } agentActionId: { type: [string, "null"], description: "Set when this message is the approval card for an action proposal (Phase 1b); clients render it as a card." } createdAt: { type: string, format: date-time } transcript: { type: string, description: "voice: transcript, once available." } audio: type: object properties: { data: { type: string }, duration: { type: [string, "null"] } } image: type: object properties: { data: { type: string }, width: { type: [integer, "null"] }, height: { type: [integer, "null"] } } video: type: object properties: { data: { type: string }, thumbnail: { type: [string, "null"] }, width: { type: [integer, "null"] }, height: { type: [integer, "null"] } } location: type: object properties: { latitude: { type: string }, longitude: { type: string } } document: type: object properties: { data: { type: string }, name: { type: [string, "null"] }, size: { type: [string, "null"] }, mimeType: { type: [string, "null"] } } sharedContact: type: object properties: { id: { type: [string, "null"] }, nmbr: { type: string }, name: { type: [string, "null"] }, avatar: { type: [string, "null"] }, type: { type: [string, "null"] } } AgentSendMessageResponse: type: object required: [message, conversationId] properties: message: { $ref: "#/components/schemas/AgentMessage" } conversationId: { type: string } AgentConversation: type: object required: [id, participant, createdAt] properties: id: { type: string } participant: { $ref: "#/components/schemas/AgentUser" } lastMessageAt: { type: [string, "null"], format: date-time } createdAt: { type: string, format: date-time } AgentEvent: type: object description: | Event envelope — identical over long-poll and webhooks. Types so far: `message.received` (payload `{ conversationId, from: AgentUser, message: AgentMessage }`), `contact.added` (payload `{ user: AgentUser }`), `action.approved` / `action.rejected` / `action.expired` (payload `{ conversationId, action: AgentAction }` — the outcome of a proposal; on approval `action.editedPayload` is set when the user edited it first; `action.expired` means nobody decided before `expiresAt` — treat it exactly like a rejection). New types may be added; never renamed. Webhook deliveries POST this JSON with headers `X-Nmbr-Event-Id`, `X-Nmbr-Event-Type`, `X-Nmbr-Delivery-Attempt` and `X-Nmbr-Signature: t=,v1=.")>` (a second `v1` for the previous secret during rotation). Reply 2xx; anything else is retried (1m, 5m, 15m, 1h, 3h, 6h, 12h) and then dead-lettered. Delivery is at-least-once — deduplicate on `id` or `seq`. required: [id, seq, type, ts, agentId, payload] properties: id: { type: string } seq: { type: integer, description: "Monotonic per platform; the long-poll cursor." } type: { type: string, example: message.received } ts: { type: string, format: date-time } agentId: { type: string } payload: type: object additionalProperties: true AgentUpdatesResponse: type: object required: [events, nextSeq] properties: events: type: array items: { $ref: "#/components/schemas/AgentEvent" } nextSeq: { type: integer, description: "Pass as `afterSeq` next time. Equals `afterSeq` when `events` is empty." } AgentWebhook: type: object required: [url, state, consecutiveFailures, createdAt, updatedAt] properties: url: { type: string } state: { type: string, enum: [active, paused, disabled], description: "`disabled` after sustained failures — re-enable from the app." } consecutiveFailures: { type: integer } lastDeliveryAt: { type: [string, "null"], format: date-time } lastSuccessAt: { type: [string, "null"], format: date-time } lastFailureAt: { type: [string, "null"], format: date-time } lastError: { type: [string, "null"] } rotationGraceUntil: { type: [string, "null"], format: date-time, description: "While set, the previous secret still verifies." } createdAt: { type: string, format: date-time } updatedAt: { type: string, format: date-time } AgentWebhookWithSecret: type: object required: [webhook, secret] properties: webhook: { $ref: "#/components/schemas/AgentWebhook" } secret: { type: string, description: "Signing secret (`whsec_…`). Shown once; store it where your agent runs." } AgentActionState: type: string enum: [pending, approved, rejected, expired] description: "`pending` → `approved` | `rejected` | `expired` (terminal). Expiry is fail-closed — treat it as a rejection." AgentAction: type: object description: | An action the agent proposed to a user, awaiting (or past) that user's decision in the 1:1 conversation where it was proposed. nmbr never executes it: on `approved` the agent performs it on its own infrastructure. Only `userId` can decide, and only from `conversationId` — never from another chat, a group, or another user. `payload` is returned verbatim; `editedPayload` is set when the user changed it before approving. required: [id, conversationId, userId, kind, title, payload, state, expiresAt, createdAt] properties: id: { type: string } conversationId: { type: string } userId: { type: string, description: "The one user who may approve or reject." } kind: { type: string, example: send_email } title: { type: string } description: { type: [string, "null"] } payload: type: object additionalProperties: true editedPayload: type: [object, "null"] additionalProperties: true state: { $ref: "#/components/schemas/AgentActionState" } expiresAt: { type: string, format: date-time } createdAt: { type: string, format: date-time } resolvedAt: { type: [string, "null"], format: date-time } AgentProposeActionRequest: type: object description: | Exactly one of `to` or `conversationId` (server-side rule). `payload` must be at most 16 KB serialized. `expiresAt` defaults to 24 h ahead; minimum 60 s, maximum 7 days. required: [kind, title] properties: to: { type: string, pattern: "^\\d{3}-\\d{3}-\\d{3}$", description: "The user who must approve (their nmbr). Required unless `conversationId` is given.", example: 123-456-789 } conversationId: { type: string, description: "Existing 1:1 conversation id. Required unless `to` is given." } kind: { type: string, pattern: "^[a-z][a-z0-9_.-]{0,63}$", description: "Developer-defined action kind. Opaque to nmbr; shown on the card and echoed in events.", example: send_email } title: { type: string, minLength: 1, maxLength: 200, description: "What the user is approving, in one line. Shown on the card and used as the chat preview.", example: "Send the Q3 summary to Dana?" } description: { type: string, maxLength: 2000, description: "Optional details shown on the card." } payload: type: object additionalProperties: true description: "Opaque JSON the agent needs back on approval. Returned verbatim — or as edited by the user — in `action.approved`." expiresAt: { type: string, format: date-time, description: "Default 24 h from now; min 60 s, max 7 days ahead. On expiry the action becomes `expired`." } AgentProposeActionResponse: type: object required: [action, message, conversationId] properties: action: { $ref: "#/components/schemas/AgentAction" } message: allOf: [{ $ref: "#/components/schemas/AgentMessage" }] description: The in-chat approval card; its `agentActionId` equals `action.id`. conversationId: { type: string } AgentActionResponse: type: object required: [action] properties: action: { $ref: "#/components/schemas/AgentAction" } AgentActionsResponse: type: object required: [actions] properties: actions: type: array items: { $ref: "#/components/schemas/AgentAction" }