# 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. ## Setting a Callback URL Include `callbackurl` in your message request body. `POST /UserAgent/Message/Send`: ```json { "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 contains the complete message result including structured metadata. ### Successful Completion (`agent_end`) ```json { "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...", "thinking": [], "answer": ["Here are today's trending topics in tech..."], "isThinking": false }, "endedat": 1712050004 } ``` ### Error (`agent_error`) ```json { "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`) ```json { "messageguid": "c3d4e5f6-a7b8-9012-cdef-345678901234", "status": "agent_cancel", "content": "What are today's trending topics?", "response": "AbortError", "debugoutput": "AbortError", "metadata": {}, "endedat": 1712050004 } ``` > The `response` and `debugoutput` fields 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; use `status === "agent_cancel"` as the signal. > **When the cancel webhook fires:** `agent_cancel` is delivered **only when the agent bridge catches an `AbortError`** during active processing — i.e. the message had already started on the agent side and was aborted mid-flight (via `POST /UserAgent/Message/Cancel` or an upstream timeout). For messages cancelled **before** they reach the bridge (still queued, or an instant `Message/Cancel` that beats dispatching), the message is marked `agent_cancel` in the database and returned as such in `POST /UserAgent/Message/Detail`, but **no webhook is fired** — there was no processing attempt to report on. Use `POST /UserAgent/Message/Detail` (checking `status === "agent_cancel"`) as the canonical source of truth for cancellation; WebSocket subscribers receive the `agent_cancel` event 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 response data. Contains thinking/answer separation, performance metrics, and raw text. 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 response with thinking/answer separation and real-time metrics: | Field | Type | Description | |-------|------|-------------| | `type` | string | Always `"progressGenerate"`. | | `task` | string | Always `"Generate"`. | | `raw` | string | The complete response text including any `` tags. | | `thinking` | array | Array of reasoning/chain-of-thought blocks extracted from `...` tags. Empty if the model doesn't use thinking. | | `answer` | array | Array of response segments — the content to show the user. | | `isThinking` | boolean | Always `false` in webhooks (streaming is complete). | | `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 | Total tokens generated. | | `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. > **Note:** For `agent_error`, the webhook's `response` / `debugoutput` contain 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 via `POST /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 data with thinking, answer, 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 `messageguid` against your own records - Always use **HTTPS** endpoints in production - 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) ```javascript 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) ```python 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 true]); ``` ### Sending a Message with Callback — curl ```bash 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 ```json { "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](/docs/agent-websocket) for real-time streaming, while the webhook delivers the final result to your server.