Documentation Index
Fetch the complete documentation index at: https://mulerouter.ai/docs/llms.txt
Use this file to discover all available pages before exploring further.
Instead of polling GET endpoints for task status, register a webhook endpoint and MuleRouter will POST a signed callback to your URL the moment a task transitions.
Every delivery is signed with HMAC-SHA256 so you can verify it came from MuleRouter and the payload has not been tampered with.
Event types
Events follow the {resource}.{action} convention:
| Event | When it fires | Task status |
|---|
task.created | Asynchronous task created | pending |
task.succeeded | Task finished successfully | completed |
task.failed | Task failed during execution | failed |
You choose which events each endpoint subscribes to. The intermediate processing state does not emit an event.
Register an endpoint
Webhook endpoints are managed from the MuleRouter Console. Open the Webhooks tab, click Create endpoint, and provide:
- URL — an HTTPS URL that can accept
POST requests (max 2048 characters).
- Events — one or more of the event types above.
- Description — optional, up to 200 characters.
On creation, the console shows the full signing secret (whsec_...) exactly once. Save it to a secret store immediately — it is never displayed again.
Each user can register up to 5 webhook endpoints. If you need to deliver to more destinations, use a single endpoint that fans out on your side.
Delivery payload
Every delivery is a POST with Content-Type: application/json. All events share the same envelope:
interface WebhookPayload {
id: string // Event ID, use for idempotent deduplication
type: string // Event type, e.g. "task.succeeded"
created_at: string // Event creation time, ISO 8601
data: {
vendor: string // Model vendor
model_name: string // Model name
payload: Record<string, unknown> // Same shape as the matching GET task response
}
}
The inner data.payload is the same JSON you would receive from the synchronous GET /vendors/.../generation/{task_id} endpoint at that point in the task lifecycle. That means your delivery handler can share result-parsing code with your polling handler, if you have one.
task.created
Fired when the task is accepted. payload only contains task_info.
{
"id": "evt_01JSGP3X7K9M2N4Q5R6S7T8U9V",
"type": "task.created",
"created_at": "2026-04-23T10:00:00Z",
"data": {
"vendor": "google",
"model_name": "nano-banana-2",
"payload": {
"task_info": {
"id": "123e4567-e89b-12d3-a456-426614174000",
"status": "pending",
"created_at": "2026-04-23T10:00:00Z",
"updated_at": "2026-04-23T10:00:00Z"
}
}
}
}
task.succeeded
Fired when the task completes successfully. payload contains the full task response, including whatever fields the endpoint would normally return (e.g. images, videos, audios).
{
"id": "evt_01JSGP4Y8L0N3O5P6Q7R8S9T0U",
"type": "task.succeeded",
"created_at": "2026-04-23T10:01:00Z",
"data": {
"vendor": "google",
"model_name": "nano-banana-2",
"payload": {
"task_info": {
"id": "123e4567-e89b-12d3-a456-426614174000",
"status": "completed",
"created_at": "2026-04-23T10:00:00Z",
"updated_at": "2026-04-23T10:01:00Z"
},
"images": [
"https://mulerouter.muleusercontent.com/public/123e4567/image.png"
]
}
}
}
task.failed
Fired when the task fails. Error details live on payload.task_info.error, using the same shape as the MuleRouter error object.
{
"id": "evt_01JSGP5Z9M1O4P6Q7R8S9T0U1V",
"type": "task.failed",
"created_at": "2026-04-23T10:01:00Z",
"data": {
"vendor": "google",
"model_name": "nano-banana-2",
"payload": {
"task_info": {
"id": "123e4567-e89b-12d3-a456-426614174000",
"status": "failed",
"created_at": "2026-04-23T10:00:00Z",
"updated_at": "2026-04-23T10:01:00Z",
"error": {
"code": 3001,
"title": "Task Execution Error",
"detail": "The upstream provider returned an error during task execution."
}
}
}
}
}
Each delivery includes the following HTTP headers:
| Header | Description | Example |
|---|
Content-Type | Always application/json | application/json |
User-Agent | Identifies the sender | MuleRouter-Webhook/1.0 |
X-MuleRouter-Webhook-Id | Delivery ID, unique per (event, endpoint); stays the same across retries | whd_01JSGP3X7K9M2N4Q5R6S7T8U9V |
X-MuleRouter-Webhook-Timestamp | Unix timestamp (seconds) at signing time | 1745402460 |
X-MuleRouter-Webhook-Signature | HMAC-SHA256 signature, prefixed with the algorithm version | v1=5257a869e7... |
Verify the signature
The signed content is three parts joined with .:
signed_content = "{delivery_id}.{timestamp}.{raw_body}"
The signature is computed as:
signature = HMAC-SHA256(key=<your whsec_... secret>, message=signed_content)
The header value carries a version prefix so we can upgrade the algorithm in the future without breaking existing receivers:
X-MuleRouter-Webhook-Signature: v1={hex_encoded_signature}
Verify signatures against the raw request body, not a re-serialized JSON object. Re-encoding the payload will change byte-level whitespace and key ordering, and your computed signature will not match.
Here is a complete receiver in Python:
import hmac
import hashlib
import time
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = "whsec_..." # from the MuleRouter Console
MAX_SKEW_SECONDS = 300 # 5 minutes
@app.post("/webhooks/mulerouter")
def receive():
raw_body = request.get_data() # bytes — do not re-serialize
delivery_id = request.headers.get("X-MuleRouter-Webhook-Id", "")
timestamp = request.headers.get("X-MuleRouter-Webhook-Timestamp", "")
signature_header = request.headers.get("X-MuleRouter-Webhook-Signature", "")
# Reject stale deliveries (protects against replay attacks).
try:
sent_at = int(timestamp)
except ValueError:
abort(400, "invalid timestamp")
if abs(time.time() - sent_at) > MAX_SKEW_SECONDS:
abort(400, "timestamp outside of allowed window")
# Only v1 is currently defined; reject anything else so future algorithm
# upgrades fail loudly instead of being silently trusted.
if not signature_header.startswith("v1="):
abort(400, "unsupported signature version")
received_signature = signature_header[len("v1="):]
signed_content = f"{delivery_id}.{timestamp}.".encode() + raw_body
expected_signature = hmac.new(
WEBHOOK_SECRET.encode(),
signed_content,
hashlib.sha256,
).hexdigest()
# Constant-time comparison prevents timing attacks.
if not hmac.compare_digest(received_signature, expected_signature):
abort(401, "invalid signature")
# At this point the payload is trusted. Deduplicate by delivery_id before
# acting, since retries reuse the same X-MuleRouter-Webhook-Id.
...
return ("", 204)
Security
| Measure | Description |
|---|
| HTTPS only | http:// URLs are rejected when registering or updating an endpoint. |
| HMAC-SHA256 | Each delivery is signed with your endpoint’s secret. |
| Timestamp skew check | Reject requests whose X-MuleRouter-Webhook-Timestamp is more than 5 minutes from now to defend against replay attacks. |
| Constant-time comparison | Use hmac.compare_digest (or your language’s equivalent) to avoid leaking information through response timing. |
| Single-show secret | The full whsec_... secret is only returned on create and rotate. After that, only a masked preview (whsec_...abcd) is visible. Rotate through the Console if a secret is compromised. |
Delivery and retries
| Setting | Value |
|---|
| HTTP Method | POST |
Content-Type | application/json |
| Connection timeout | 30 seconds |
| Success criterion | HTTP 2xx response |
| Max payload size | 64 KB |
If the target URL does not return a 2xx within the timeout, MuleRouter retries with exponential backoff:
| Attempt | Delay | Total elapsed |
|---|
| Retry 1 | 15 seconds | ~15 seconds |
| Retry 2 | 1 minute | ~1 min 15 s |
| Retry 3 | 5 minutes | ~6 min 15 s |
| Retry 4 | 30 minutes | ~36 min 15 s |
| Retry 5 | 1 hour | ~1 h 36 min |
After 5 retries (6 total attempts), the delivery is marked failed and no further attempts are made for that event.
Every attempt of the same delivery reuses the same X-MuleRouter-Webhook-Id. The timestamp and signature are recomputed each time, so your receiver must always recompute the signature from the headers it actually received — do not cache.
Idempotency
Two IDs cooperate to make retries safe:
evt_... (event ID) — lives on the payload’s top-level id field, identifies “something happened in MuleRouter”.
whd_... (delivery ID) — lives on the X-MuleRouter-Webhook-Id header, identifies “an attempt to deliver that event to this endpoint”. It is unique per (event, endpoint) and is reused across retries of the same delivery.
Use whd_... as your deduplication key: store it when you first successfully process a delivery, and short-circuit if you see it again. This is the standard way to handle retries without double-processing.
Rate limits and quotas
| Limit | Value |
|---|
| Webhook endpoints per user | 5 |
| Deliveries per endpoint per minute | 600 |
| Webhook URL length | 2048 characters |
| Description length | 200 characters |
| Delivery log retention | 30 days |
Next steps
- Open the Webhooks tab in the Console to register your first endpoint.
- Use the Send test event button on any endpoint to trigger a synthetic
task.succeeded delivery (id prefixed with evt_test_) and verify your signature-verification logic end-to-end before sending real traffic.