This section as Markdown: View Download

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 callbackurl receives (status: "agent_end"). The result is added to the session later as a separate message and doesn't call the callbackurl. Read it with Message/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 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 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'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 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 messageguid against your own records
  • Always use HTTPS endpoints in production
  • Use a public address. Wiro doesn't call a callbackurl whose host is, or resolves to, a private, loopback or link-local address; that message gets no webhook, and its result stays readable with Message/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.