Add webhook_url to a run and Fusara sends the complete result to your endpoint. No endpoint registration or shared-secret setup is required. You can save the payload directly without another API request.
Request a webhook
curl 'https://fusara.ai/api/v3/run' \
-H "Authorization: Bearer $FUSARA_API_KEY" \
-H 'Content-Type: application/json' \
--data-raw '{
"model": "qwen-image21-create",
"prompt": "A white ceramic lamp on a pale lavender background, soft studio light.",
"webhook_url": "https://your-app.example/webhooks/fusara"
}'Use a public HTTPS URL with a valid certificate. Credentials, fragments, private network destinations and custom ports are not supported. Keep the URL within 2,048 characters. You can choose a different URL for each run.
Receive the result
Fusara sends an HTTP POST with a JSON body. The body is the run itself, with the same shape as GET /run/{id}; there is no event envelope to unwrap.
{
"id": 123456,
"status": "succeeded",
"cost": {
"unit": "common_fuz",
"expected": "40.0000",
"status": "settled",
"charged": "40.0000"
},
"outputs": [
{
"type": "image",
"media_id": 901,
"url": "https://media.example/result.png",
"url_expires_at": "2026-10-25T12:00:00Z"
}
]
}Your receiver should:
- Read the run's
idand thewebhook-idrequest header. - Save the result durably, using
webhook-idto recognize a repeated delivery. - Respond with a successful
2xxstatus promptly, then do any slow downstream work.
Use the run ID to match a result to your own submitted job. A webhook can arrive before your submission handler has finished saving the initial response; make the receiver tolerant of that ordering.
Signature verification is optional. A basic integration can parse and save the JSON without cryptographic setup. Fusara signs every webhook; verification lets you authenticate the sender. Without it, sender authenticity is unverified. The optional verification details are below, after the delivery behavior.
A minimal receiver
Save this as receiver.py and run python3 receiver.py. It uses only Python's standard library and writes each event to SQLite before returning 204. Retrying the same event cannot insert a second row, including after a restart.
import json
import sqlite3
from contextlib import closing
from http.server import BaseHTTPRequestHandler, HTTPServer
DATABASE = "webhooks.sqlite3"
MAX_BODY = 8 * 1024 * 1024 # This example's receiver limit, not an API limit.
with closing(sqlite3.connect(DATABASE)) as db, db:
db.execute("""CREATE TABLE IF NOT EXISTS events (
event_id TEXT PRIMARY KEY,
run_id INTEGER NOT NULL,
payload TEXT NOT NULL
)""")
class Receiver(BaseHTTPRequestHandler):
def do_POST(self):
if self.path != "/webhooks/fusara":
self.send_error(404)
return
try:
size = int(self.headers.get("Content-Length", "0"))
if not 0 < size <= MAX_BODY:
self.send_error(413)
return
raw = self.rfile.read(size).decode("utf-8")
run = json.loads(raw)
event_id = self.headers.get("webhook-id")
if (not event_id or not isinstance(run, dict)
or type(run.get("id")) is not int or run["id"] <= 0
or run.get("status") not in ("succeeded", "failed", "canceled")):
raise ValueError("Invalid webhook")
except (ValueError, UnicodeError):
self.send_error(400)
return
try:
with closing(sqlite3.connect(DATABASE)) as db, db:
db.execute("""INSERT INTO events VALUES (?, ?, ?)
ON CONFLICT(event_id) DO NOTHING""", (event_id, run["id"], raw))
except sqlite3.Error:
self.send_error(503) # No acknowledgement until storage succeeds.
return
self.send_response(204)
self.end_headers()
if __name__ == "__main__":
HTTPServer(("127.0.0.1", 8000), Receiver).serve_forever()This is a local receiving example. For deployment, use your application's HTTP server behind public HTTPS, keep the database on persistent storage, and set request size/time limits for your workload. Forward /webhooks/fusara to the handler; localhost itself cannot receive a Fusara callback.
The saved JSON already contains the result: no extra API read is needed. Process saved events in a worker, match run_id to your own job, and make downstream actions idempotent too. A later billing event gets its own row, so an out-of-order delivery cannot overwrite the earlier payload. The example intentionally leaves signature verification optional.
Failures and partial outputs
A failed generation is delivered to the same endpoint:
{
"id": 123456,
"status": "failed",
"cost": {
"unit": "common_fuz",
"expected": "40.0000",
"status": "settled",
"charged": "0.0000"
},
"error": {
"code": "execution_failed",
"message": "The run could not be completed."
}
}Inspect status, then error. A failure can still include usable partial outputs; do not discard them automatically. Treat canceled as terminal too.
Duplicates and billing updates
Delivery is at least once while retries remain. A retry preserves webhook-id; store that ID with the processed event so a repeated POST does not repeat your application action.
A later billing settlement can produce another webhook for the same run, with a different webhook-id and an updated cost. Do not deduplicate solely by run ID, or you will lose the update. Events can arrive out of order: a pending-cost payload must not overwrite a cost you already know is settled.
Useful delivery headers:
| Header | Meaning |
|---|---|
webhook-id |
Stable ID of this event across delivery retries. |
webhook-timestamp |
Unix timestamp in seconds for this delivery attempt. |
webhook-signature |
Signature over the headers and exact request body. |
Delivery retries
Fusara makes up to 10 attempts total, including the first delivery. Delivery also stops after three days. Each HTTP attempt has a 30-second timeout.
The nine retry delays use jitter within these ranges:
| Retry | Delay after the previous attempt |
|---|---|
| 1 | 15–30 seconds |
| 2 | 30–60 seconds |
| 3 | 1–2 minutes |
| 4 | 2–4 minutes |
| 5 | 4–8 minutes |
| 6 | 8–16 minutes |
| 7 | 16–32 minutes |
| 8 | 32–64 minutes |
| 9 | 64–128 minutes |
These are scheduling ranges, not exact arrival times. Request duration and worker scheduling add time. A valid Retry-After can postpone another attempt, within the delivery deadline.
- Any
2xxresponse acknowledges the delivery. - Network failures,
408,429and transient server errors are retried. - Redirects are not followed. Most other
4xxerrors, and501or505, stop delivery.
If delivery is exhausted, the run still exists. Retrieve it with Get a run when you need recovery. Your normal successful webhook flow does not require that extra read.
Optional signature verification
This is a digital signature, not encryption. HTTPS protects the connection; the signature proves that the exact payload was signed by Fusara.
Fusara uses Ed25519 with the Standard Webhooks asymmetric signature format. Public verification keys are available without authentication at:
https://fusara.ai/.well-known/jwks.jsonTo verify a delivery:
- Preserve the raw request-body bytes before parsing JSON.
- Read
webhook-id,webhook-timestampandwebhook-signature. Check that the timestamp is recent; a five-minute tolerance is a reasonable default when your clock is synchronized. - Construct the signed message from
webhook-id, a period,webhook-timestamp, a period, then the raw body bytes. Do not reserialize the JSON. - Decode the base64 signature following
v1a,and verify Ed25519 against the keys from Fusara's fixed JWKS URL. Accept a valid supported signature; reject the payload if none verify. - Match the run to your own submission and deduplicate the event before processing it.
Cache the keys, typically for one hour. Support key rotation by refreshing on a verification miss, with a refresh rate limit; do not download keys on every delivery. Use only the fixed Fusara key URL, never a URL supplied by an incoming webhook. There is no per-endpoint secret to exchange.
Verification does not replace duplicate handling, nor does it prove that a run belongs to your application. Keep the ID matching from the basic receiver. The Standard Webhooks specification describes the signing format.