Skip to main content
Instead of polling, you can have BananaHub POST to a URL of yours when a job reaches a terminal state (done or failed). Every delivery is signed so you can confirm it came from us.

Configure a webhook

Register one or more webhooks in the web panel. Each registered webhook has its own URL and its own webhook secret (shown next to the URL). The secret signs every delivery sent to that URL — copy it into the matching receiver to verify signatures. Treat it like a password. A delivery is always signed with the secret of the registered webhook whose URL it is sent to. That is the secret you verify against — no global or shared secret is involved.

Choosing which webhook receives a job

You have two options:
If you register a single webhook and send no callback_url, every terminal job is delivered to it automatically. (When multiple webhooks are registered and no callback_url is given, the oldest one is used as the default.)
The callback_url must exactly match a URL you registered in the panel. Only then is the delivery signed with that webhook’s secret and verifiable. A callback_url pointing at an unregistered URL cannot be validated and should not be used.

Delivery

When a job finishes, we send a single POST with a JSON body and these headers: A delivery counts as successful when your endpoint responds with any 2xx status. Anything else (or a timeout) is retried — see Retries.

Payload

string
UUID of the job.
string
Terminal status: done or failed.
object | null
Present (non-null) only when status is done.
string | null
Failure reason. Present only when status is failed, otherwise null.
object | null
Echo of the metadata you supplied on the generation request. Keys beginning with an underscore (_) are stripped before sending. null if you supplied none.
The webhook payload is similar to GET /jobs/{job_id} but not identical: it omits the created_at / started_at / finished_at timestamps and adds your metadata. Verify against the fields documented here.

Verify the signature

We compute HMAC-SHA256("{X-Timestamp}.{body}", secret) and send it as X-Signature: sha256=<hex>, where body is the JSON payload serialized with sorted keys and no whitespace.
Do not HMAC the raw bytes you received. Re-serialize the parsed JSON with sorted keys and compact separators, exactly as below — otherwise key ordering will not match and verification fails.
1

Re-serialize the body

Parse the JSON and dump it with sorted keys and no spaces: json.dumps(payload, sort_keys=True, separators=(",", ":")).
2

Build the signed message

Concatenate the timestamp header and the serialized body: "{X-Timestamp}.{body}".
3

Compute and compare

Compute HMAC-SHA256(message, your_webhook_secret), hex-encode it, prefix with sha256=, and compare to X-Signature using a constant-time comparison.
4

Reject stale deliveries

Reject the request if |now - X-Timestamp| > 300 seconds to guard against replays.
The Node.js example sorts only top-level keys. If your payload contains nested objects, sort recursively to match our serialization.

Retries

If a delivery does not return 2xx, we retry up to 6 times with increasing delays: That is 7 delivery attempts over roughly 53 minutes before the webhook is marked failed. Each attempt is re-signed with a fresh X-Timestamp, so validate every delivery independently and make your handler idempotent on job_id.