Skip to content

Webhooks

The webhook_url field on the Chat endpoint receives the agent result after the original request has ended. It is a per-call callback, suitable when your application should not hold a connection open while the agent runs.

  1. Send webhook_url and do not use sync: true in POST /chat/{id}.
  2. The API records the input and returns 202 Accepted with status: "processing" or status: "debounced".
  3. The agent runs in the background.
  4. When there is a non-empty external response, SquadOS sends a POST to the supplied URL.
Terminal window
curl -X POST https://api.squados.io/v1/chat/API_INBOX_ID \
-H "Authorization: Bearer pk_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"message": "What is the status of order 1234?",
"sync": false,
"webhook_url": "https://integration.example.com/squados/callback",
"metadata": {
"correlation_id": "evt_01J7Y8Q3",
"ticket_id": "TKT-9981"
}
}'

An input that goes through the grouping queue returns:

{
"status": "debounced",
"group_id": "d4e5f6a7-b8c9-0123-def0-234567890123",
"conversation_id": "c3d4e5f6-a7b8-9012-cdef-123456789012"
}

Without grouping, the acknowledgment may contain only:

{
"status": "processing",
"conversation_id": "c3d4e5f6-a7b8-9012-cdef-123456789012"
}

The 202 does not contain the assistant response or its message_id. It confirms that the input was accepted, not that the callback will be delivered.

The callback uses Content-Type: application/json. A common response has this shape:

{
"event": "message.completed",
"agent_id": "550e8400-e29b-41d4-a716-446655440000",
"success": true,
"conversation_id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"message_id": "e5f6a7b8-c9d0-1234-ef01-345678901234",
"response": "Order 1234 is being picked and will ship tomorrow.",
"model": "provider/model",
"credits_used": 3,
"attachments_processed": 0,
"responding_agent_name": "Support",
"metadata": {
"correlation_id": "evt_01J7Y8Q3",
"ticket_id": "TKT-9981"
},
"timestamp": "2026-09-01T14:32:07.000Z"
}
FieldTypeRule
eventstringmessage.completed for a normal response.
agent_idUUIDAgent associated with the execution.
successbooleantrue on message.completed.
conversation_idUUIDUse it to fetch the conversation and reconcile the result.
message_idUUIDIdentifies the agent message. It may be absent on a path that could not persist an external message.
responsestringFinal text delivered to the external channel.
modelstringModel actually used; may be absent from follow-up callbacks.
credits_usednumberExecution credits; may be absent from follow-up callbacks.
attachments_processedintegerNumber of input attachments; may be absent from follow-up callbacks.
responding_agent_namestringName of the agent that produced this response.
metadataobjectCorrelation data preserved from the input; may be absent when none was supplied.
timestampdate-timeTime when the payload was assembled.

The runtime contains a message.error variant for an internal failure during the delivery stage, with agent_id, conversation_id, error, metadata, and timestamp. It is not a guaranteed receipt for every failure: if the same destination is unreachable, the error payload cannot reach it either. Reconcile missing callbacks through conversation history.

With sync: false, nearby messages in the same conversation may be grouped into one execution. Text and attachments are combined chronologically, but the callback preserves only the first available metadata object in the group (first wins).

Use a stable correlation identifier across inputs that may enter the same group. Do not depend on different metadata values from each grouped message.

An agent transfer may produce two message.completed callbacks, in this order:

  1. the source agent message, with pre_transfer: true;
  2. the destination agent response, without that marker.

Use message_id as the idempotency key and do not treat conversation_id as a unique callback identifier.

An automated follow-up from the same conversation may also reach the stored URL. It uses message.completed and adds:

FieldTypeMeaning
followupbooleanAlways true for this variant.
followup_attemptintegerLogical follow-up attempt number. It is not an HTTP delivery attempt.

The destination must be a public HTTP or HTTPS URL with resolvable DNS. SquadOS blocks loopback, private and reserved networks, metadata endpoints, and local or internal hostnames. Validation happens during asynchronous processing, so a blocked URL may still receive an initial 202 while never receiving a callback.

Use HTTPS. The callback sends only the Content-Type: application/json header: it has no signature, Authorization, or custom headers. If the receiver requires authentication, use an opaque high-entropy path and also validate an unpredictable correlation value in metadata. Do not place credentials reused by other systems in this URL.

Respond with any 2xx status. Non-2xx responses, network failures, and blocked URLs are recorded as failures; the receiver’s response body is recorded up to 5,000 characters.

The current callback makes at most one delivery attempt and has no automatic retry. Design the flow as at-most-once:

  • persist the payload quickly and respond with 2xx;
  • deduplicate by message_id;
  • do not use callback arrival as the only source of truth;
  • if it does not arrive, call GET /conversations/{conversationId}/messages to reconcile history.

See Chat for the complete request contract and Conversations for history retrieval.