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 firesResource state
task.createdAsynchronous task createdTask pending
task.succeededTask finished successfullyTask completed
task.failedTask failed during executionTask failed
balance.lowAvailable balance crosses below the system threshold (default $10)Wallet ALERTED
You choose which events each endpoint subscribes to. The intermediate task processing state does not emit an event. balance.low follows a one-shot edge-trigger semantics — see Billing events for details.

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: Record<string, unknown>  // Event-specific payload, see below
}
The data shape depends on the event family:
Event familydata shape
task.*Flat: { vendor, model_name, payload }payload mirrors the corresponding GET /vendors/.../{task_id} response
balance.*Nested: { user_id, payload: { ... } } — fields under data.payload.*
If you write a single dispatcher that handles both families, branch on the type field’s prefix (task. vs balance.) before reading data fields. The two shapes are intentional — task.* keeps backward compatibility with earlier releases, while balance.* introduces a data.user_id + data.payload.* convention so future balance.depleted / balance.topped_up events can reuse the same outer locator.

Task events

Task events use the flat data shape. 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."
        }
      }
    }
  }
}

Billing events

Billing events use the nested data shape with data.user_id for routing and data.payload for business fields. They cover wallet-state changes that don’t belong to any specific task.

balance.low

Fired when your wallet’s available balance crosses below the system threshold (default $10) following a charge. This is an edge-trigger event — it fires once when the balance drops past the threshold and stays silent until your balance recovers above the threshold and drops below it again.
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "type": "balance.low",
  "created_at": "2026-05-13T10:00:00Z",
  "data": {
    "user_id": "9b9c1d20-...",
    "payload": {
      "available_balance": "3.80",
      "trigger_threshold": "1000.00",
      "currency": "credits"
    }
  }
}
FieldTypeDescription
data.user_idstring (UUID)The user whose balance dropped — the owner of this webhook endpoint
data.payload.available_balancestringSnapshot of available balance after the triggering charge, in credits with 2 decimals (e.g. "3.85"). Returned as a string to avoid floating-point precision issues on the receiver side
data.payload.trigger_thresholdstringThe threshold value that was just crossed, in credits with 2 decimals. Default "1000.00"
data.payload.currencystringAlways "credits" for now
Trigger semantics
  • Low balance detected: when your available_balance drops below trigger_threshold, you’ll receive a balance.low delivery shortly after.
  • Sustained low: while you remain below threshold without a top-up, deliveries are throttled — you won’t be spammed with repeated notifications.
  • Top-up then drop again: if you top up above the threshold and later drop below again, you’ll receive a fresh delivery without waiting out the throttle window.
  • Healthy recovery: once your balance stays above the threshold, no further deliveries fire until it drops again.
When balance.low does not fire
  • You don’t have an active webhook endpoint subscribed to balance.low.
  • Your available_balance is at or above the trigger_threshold.
  • A recent delivery for the same wallet was already sent and your balance hasn’t recovered above the threshold in between.
  • The triggering activity is only a hold (frozen reservation) for an asynchronous task — balance.low fires when funds are actually settled, not when they’re held. This avoids false positives if the task later fails and the hold is released.
Recommended client behavior
  • Use this event as a hint to surface a low-balance banner in your UI, send your user a “top up” reminder, or pause non-critical workloads — not as the sole source of truth for your wallet balance.
  • Always verify the current balance via your wallet’s source of truth before acting on financial decisions, since available_balance in the payload is a snapshot at trigger time.

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.