Skip to main content

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:
EventWhen it firesTask status
task.createdAsynchronous task createdpending
task.succeededTask finished successfullycompleted
task.failedTask failed during executionfailed
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."
        }
      }
    }
  }
}

Request headers

Each delivery includes the following HTTP headers:
HeaderDescriptionExample
Content-TypeAlways application/jsonapplication/json
User-AgentIdentifies the senderMuleRouter-Webhook/1.0
X-MuleRouter-Webhook-IdDelivery ID, unique per (event, endpoint); stays the same across retrieswhd_01JSGP3X7K9M2N4Q5R6S7T8U9V
X-MuleRouter-Webhook-TimestampUnix timestamp (seconds) at signing time1745402460
X-MuleRouter-Webhook-SignatureHMAC-SHA256 signature, prefixed with the algorithm versionv1=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

MeasureDescription
HTTPS onlyhttp:// URLs are rejected when registering or updating an endpoint.
HMAC-SHA256Each delivery is signed with your endpoint’s secret.
Timestamp skew checkReject requests whose X-MuleRouter-Webhook-Timestamp is more than 5 minutes from now to defend against replay attacks.
Constant-time comparisonUse hmac.compare_digest (or your language’s equivalent) to avoid leaking information through response timing.
Single-show secretThe 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

SettingValue
HTTP MethodPOST
Content-Typeapplication/json
Connection timeout30 seconds
Success criterionHTTP 2xx response
Max payload size64 KB
If the target URL does not return a 2xx within the timeout, MuleRouter retries with exponential backoff:
AttemptDelayTotal elapsed
Retry 115 seconds~15 seconds
Retry 21 minute~1 min 15 s
Retry 35 minutes~6 min 15 s
Retry 430 minutes~36 min 15 s
Retry 51 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

LimitValue
Webhook endpoints per user5
Deliveries per endpoint per minute600
Webhook URL length2048 characters
Description length200 characters
Delivery log retention30 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.