Agent Webhooks
Receive agent response notifications via HTTP callbacks.
How It Works
When you send a message to an agent via POST /UserAgent/Message/Send, include a callbackurl parameter. Once the agent finishes processing on the bridge, Wiro sends a POST request to your URL with the result. Callbacks fire on agent_end (success), agent_error (failure during processing), and agent_cancel only when the abort hits the bridge mid-flight. Messages cancelled while still queued (status agent_queue, before the bridge picks them up) are marked agent_cancel in the database but do not fire a webhook — there was no processing attempt to report on. See the "When the cancel webhook fires" note below for the full decision table.
This lets you build fully asynchronous workflows: fire a message and let your backend handle the response whenever it arrives, without polling or maintaining a WebSocket connection.
Results that arrive later. When the agent moves longer work to the background, its turn ends with a short line saying the result will follow, and that line is what your
callbackurlreceives (status: "agent_end"). The result is added to the session later as a separate message and doesn't call thecallbackurl. Read it withMessage/History, or follow the session on the Agent WebSocket.
Setting a Callback URL
Include callbackurl in your message request body. POST /UserAgent/Message/Send:
{
"useragentguid": "your-useragent-guid",
"message": "What are today's trending topics?",
"sessionkey": "user-123",
"callbackurl": "https://your-server.com/webhooks/agent-response"
}
The callback URL is stored per-message. You can use different URLs for different messages, or omit it entirely if you prefer polling or WebSocket.
Callback Payload
When the agent finishes, Wiro sends a POST request to your callbackurl with Content-Type: application/json. The payload is deliberately answer-focused: it contains the final answer/error and structured answer metadata, but not the ordered turn timeline.
Successful Completion (agent_end)
{
"messageguid": "c3d4e5f6-a7b8-9012-cdef-345678901234",
"status": "agent_end",
"content": "What are today's trending topics?",
"response": "Here are today's trending topics in tech...",
"debugoutput": "Here are today's trending topics in tech...",
"metadata": {
"type": "progressGenerate",
"task": "Generate",
"speed": "14.2",
"speedType": "words/s",
"elapsedTime": "8.1s",
"tokenCount": 156,
"wordCount": 118,
"raw": "Here are today's trending topics in tech...",
"answer": ["Here are today's trending topics in tech..."]
},
"endedat": 1712050004
}
Error (agent_error)
{
"messageguid": "c3d4e5f6-a7b8-9012-cdef-345678901234",
"status": "agent_error",
"content": "What are today's trending topics?",
"response": "Could not resolve agent endpoint",
"debugoutput": "Could not resolve agent endpoint",
"metadata": {},
"endedat": 1712050004
}
Cancelled (agent_cancel)
{
"messageguid": "c3d4e5f6-a7b8-9012-cdef-345678901234",
"status": "agent_cancel",
"content": "What are today's trending topics?",
"response": "AbortError",
"debugoutput": "AbortError",
"metadata": {},
"endedat": 1712050004
}
The
responseanddebugoutputfields contain the raw abort reason from the runtime — typically"AbortError"or a short technical string. Do not rely on a specific fixed user-facing message; usestatus === "agent_cancel"as the signal.When the cancel webhook fires:
agent_cancelis delivered only when the agent bridge catches anAbortErrorduring active processing — i.e. the message had already started on the agent side and was aborted mid-flight (viaPOST /UserAgent/Message/Cancelor an upstream timeout). For messages cancelled before they reach the bridge (still queued, or an instantMessage/Cancelthat beats dispatching), the message is markedagent_cancelin the database and returned as such inPOST /UserAgent/Message/Detail, but no webhook is fired — there was no processing attempt to report on. UsePOST /UserAgent/Message/Detail(checkingstatus === "agent_cancel") as the canonical source of truth for cancellation; WebSocket subscribers receive theagent_cancelevent only on active-processing aborts (same condition as the webhook). Treat the webhook as a best-effort "processing was interrupted" signal.
Field Reference
| Field | Type | Description |
|---|---|---|
messageguid |
string | Unique identifier of the message. Use this to correlate with your records. |
status |
string | Final status: agent_end, agent_error, or agent_cancel. |
content |
string | The original user message you sent. |
response |
string | The agent's full response text on success. For errors, contains the error message. For cancellation, contains the abort reason. |
debugoutput |
string | Same as response — the full accumulated output text. Included for consistency with the polling API. |
metadata |
object | Structured answer data, performance metrics, and raw text. Timeline blocks are not embedded in webhooks. Empty object ({}) for error and cancel statuses. |
endedat |
number | Unix timestamp (UTC seconds) when processing finished. |
The metadata Object
On successful completion (agent_end), the metadata object contains the structured answer and final stream metrics:
| Field | Type | Description |
|---|---|---|
type |
string | Always "progressGenerate". |
task |
string | Always "Generate". |
raw |
string | The complete response text. |
answer |
array | Array of response segments — the content to show the user. |
speed |
string | Final generation speed (e.g. "14.2"). |
speedType |
string | Speed unit — "words/s". |
elapsedTime |
string | Total generation time (e.g. "8.1s"). |
tokenCount |
number | Number of answer chunks received by the bridge. Use the message's billing columns for authoritative model tokens. |
wordCount |
number | Total words in the response. |
For agent_error and agent_cancel, metadata is an empty object {}. Always check status before accessing metadata fields.
After receiving the webhook, call POST /UserAgent/Message/Detail with messageguid whenever you need the authoritative top-level timeline[]. It preserves the ordered reasoning, answer, and safe tool-label/status blocks for the completed turn.
Note: For
agent_error, the webhook'sresponse/debugoutputcontain the raw error from the agent runtime (useful for debugging). The same message may be sanitized to a user-facing string when you read it back viaPOST /UserAgent/Message/Detail, so the two can differ. Log the webhook payload if you need the original.
Status Values
| Status | Description | response contains |
metadata contains |
|---|---|---|---|
agent_end |
Agent completed successfully | Full response text | Structured answer and metrics |
agent_error |
An error occurred during processing | Error message string | Empty object {} |
agent_cancel |
Message was cancelled before completion | Cancellation reason | Empty object {} |
Retry Policy
Wiro attempts to deliver each webhook up to 3 times:
- First attempt is immediate when the agent finishes
- 2-second delay between retries
- Your endpoint must return HTTP 200 to acknowledge receipt
- Any non-200 response triggers a retry
- After 3 failed attempts, the webhook is abandoned and the failure is logged server-side
The message result is always persisted in the database regardless of webhook delivery. You can retrieve it at any time via POST /UserAgent/Message/Detail.
Security Considerations
- Webhook calls do not include authentication headers — verify incoming requests by checking the
messageguidagainst your own records - Always use HTTPS endpoints in production
- Use a public address. Wiro doesn't call a
callbackurlwhose host is, or resolves to, a private, loopback or link-local address; that message gets no webhook, and its result stays readable withMessage/Detail - Validate the payload structure before processing
- Consider returning 200 immediately and processing the payload asynchronously to avoid timeouts
Code Examples
Webhook Receiver — Node.js (Express)
const express = require('express');
const app = express();
app.use(express.json());
app.post('/webhooks/agent-response', (req, res) => {
const { messageguid, status, content, response, endedat } = req.body;
if (status === 'agent_end') {
console.log(`Agent completed: ${messageguid}`);
console.log(`Response: ${response}`);
} else if (status === 'agent_error') {
console.error(`Agent error for ${messageguid}: ${response}`);
} else if (status === 'agent_cancel') {
console.log(`Agent cancelled: ${messageguid}`);
}
res.sendStatus(200);
});
app.listen(3000);
Webhook Receiver — Python (Flask)
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/webhooks/agent-response', methods=['POST'])
def agent_webhook():
data = request.json
messageguid = data.get('messageguid')
status = data.get('status')
response_text = data.get('response')
if status == 'agent_end':
print(f"Agent completed: {messageguid}")
print(f"Response: {response_text}")
elif status == 'agent_error':
print(f"Agent error for {messageguid}: {response_text}")
elif status == 'agent_cancel':
print(f"Agent cancelled: {messageguid}")
return jsonify({"ok": True}), 200
if __name__ == '__main__':
app.run(port=3000)
Webhook Receiver — PHP
<?php
$payload = json_decode(file_get_contents('php://input'), true);
$messageguid = $payload['messageguid'] ?? '';
$status = $payload['status'] ?? '';
$response = $payload['response'] ?? '';
if ($status === 'agent_end') {
error_log("Agent completed: $messageguid");
error_log("Response: $response");
} elseif ($status === 'agent_error') {
error_log("Agent error for $messageguid: $response");
} elseif ($status === 'agent_cancel') {
error_log("Agent cancelled: $messageguid");
}
http_response_code(200);
echo json_encode(['ok' => true]);
Sending a Message with Callback — curl
curl -X POST "https://api.wiro.ai/v1/UserAgent/Message/Send" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"message": "Summarize today'\''s news",
"sessionkey": "user-123",
"callbackurl": "https://your-server.com/webhooks/agent-response"
}'
Response
{
"result": true,
"errors": [],
"messageguid": "c3d4e5f6-a7b8-9012-cdef-345678901234",
"agenttoken": "eDcCm5yyUfIvMFspTwww49OUfgXkQt",
"status": "agent_queue"
}
The agenttoken can be used to track the message via Agent WebSocket for real-time streaming, while the webhook delivers the final result to your server.