Webhooks
Receive a POST when a task finishes instead of polling — request field, delivery contract, retries, and how to verify calls.
Every submission endpoint accepts an optional webhook_url. When the task
reaches a terminal state — completed or failed — reAPI sends one POST to
that URL with the same JSON you would get from GET /api/v1/tasks/{id}.
Polling keeps working exactly as before; the webhook is an addition, not a
replacement.
GET /api/v1/tasks/{id} is always the fallback. A webhook is a
notification, not the only copy of the result. If your endpoint is down,
slow, or returns a non-2xx, reAPI retries a few times and then stops — but
the task result is never lost: it stays available through
GET /api/v1/tasks/{id} at any time, and
?include=webhook tells you whether the delivery succeeded. Build your
integration so that a missed webhook degrades into a poll, never into a
missing result (see Retries and duplicates).
Requesting a webhook
Add webhook_url next to the model parameters on any of these endpoints:
/api/v1/images/generations, /api/v1/videos/generations,
/api/v1/audio/generations, /api/v1/detect, /api/v1/humanize,
/api/v1/essay, /api/v1/moderations.
POST /api/v1/images/generations
Authorization: Bearer rk_live_xxx
Content-Type: application/json
{
"model": "z-image",
"prompt": "A red fox running through fresh snow at dawn",
"webhook_url": "https://hooks.example.com/reapi"
}Everything except webhook_url is the model's normal request; the field is
simply added to whatever you already send.
Rules for the URL:
| Rule | Detail |
|---|---|
https only | http:// and any other scheme are rejected |
| Hostname only | The host must be a domain name; IP addresses (IPv4 or IPv6, in any notation) are rejected |
| Port 443 only | An explicit port is accepted only if it is 443 (https://hooks.example.com:8443/x is rejected) |
| No credentials | https://user:pass@host/ is rejected |
| Length | At most 2048 characters |
These are checked at submission: a URL that breaks one of them returns a
20003 parameter error and no task is created. Whether the endpoint is
reachable is not checked at submission.
One more rule applies at delivery, when the name is resolved: if it
resolves to a private, loopback, link-local or cloud-metadata address (any of
its records), the attempt is refused and recorded as
blocked_private_address. The task itself is created and runs normally —
only the notification is affected, and GET /api/v1/tasks/{id} still has
the result.
The delivery
POST https://hooks.example.com/reapi
Content-Type: application/json
User-Agent: reAPI-Webhook/1There are no reAPI-specific headers: the task is identified by the id
field in the body, and nothing else about your account travels with the
call. If you use several API keys and need to tell their tasks apart, give
each key its own webhook_url.
The body is exactly the default GET /api/v1/tasks/{id} response for
that task — same fields, same shapes, nothing added — with status always
completed or failed. Its id is the same task id the submission POST
returned, so you always know which task a call belongs to:
{
"id": "task_01a093d0162671be8280f65bc79becff",
"model": "z-image",
"status": "completed",
"created_at": 1789186286,
"output": {
"image_urls": ["https://cdn.reapi.ai/media/tasks/task_01a093d0162671be8280f65bc79becff/0.jpg"]
},
"usage": { "credits": 5 },
"error": null
}A failed task arrives the same way, with output: null, usage.credits at
the final charge (0 when the reservation was fully refunded), and the same
error object polling would return. The
body never includes the opt-in billing block; fetch it with
?include=billing if you need exact token billing. error.request_id
carries the delivery id (whd_…), useful when contacting support about a
specific call.
Your endpoint should respond with any 2xx status within 10 seconds and do the real work afterwards. Redirects are not followed and the response body is never read.
Retries and duplicates
| Attempt | When |
|---|---|
| 1 | as soon as the task finishes |
| 2 | about 40 seconds later, if attempt 1 failed |
| 3 | about 5 minutes after the task finished, if attempt 2 failed |
A non-2xx status, a timeout, or a connection failure counts as a failed
attempt. After the third failure reAPI stops. Nothing is lost when that
happens: the task result stays available through GET /api/v1/tasks/{id}
indefinitely, and GET /api/v1/tasks/{id}?include=webhook shows
"status": "failed" with the reason. The recommended pattern:
- Record the task
idwhen you submit. - Handle the webhook when it arrives (deduplicate on the task id).
- If no webhook has arrived within the time you expect the task to take
(plus the ~5 minute retry window), poll
GET /api/v1/tasks/{id}for that id and continue exactly as if the webhook had come in.
Treat the webhook as the fast path and the GET as the guaranteed path.
Delivery is at-least-once. If your server processed a call but the 2xx
response was lost on the network, reAPI cannot tell the difference from a
failure and will send it again. Every attempt for a task carries the same
body id and an otherwise identical body (apart from error.request_id),
so deduplicate on the task id: if you have already handled that id,
respond 2xx and ignore the payload.
Verifying that a call is from reAPI
reAPI does not sign webhooks and sends no credentials of its own. The
robust pattern needs no secret at all: treat the call as a signal, not as
the source of truth. Read the body's id, then fetch
GET /api/v1/tasks/{id} with your API key and act on that response. The
GET is authenticated, so a forged call cannot forge a task result.
A forged call can still make you do a lookup, so keep the lookups cheap:
only act on task ids you recorded as pending when you submitted them and
drop anything else without a request; treat a repeated id as one event;
and do not let a burst of calls fan out into a burst of GETs — the
polling endpoint is rate-limited per user, and a flood of lookups could
throttle your own legitimate requests.
If you would rather trust the payload directly, put a secret of your own
into the URL — a query parameter or a path segment, for example
https://hooks.example.com/reapi?token=<random value you generate> — and
check it on your side. The URL is called back verbatim, so whatever you
embed arrives unchanged; you generate, store, and rotate that value.
Checking a delivery
GET /api/v1/tasks/{id}?include=webhook
Authorization: Bearer rk_live_xxxAdds a webhook object to the task response:
{
"webhook": {
"status": "failed",
"attempts": 2,
"response_status": 503,
"last_error": "http_503",
"last_attempt_at": "2026-09-12T10:31:02.118Z",
"delivered_at": null
}
}status is pending (task not finished yet), in_progress, delivered, or
failed. last_error is one of http_<status>, timeout, connect_failed,
dns_failed, tls_error, blocked_private_address, request_failed,
render_timeout, render_failed (reAPI could not build the payload in time —
the task result itself is unaffected), stale_claim (an attempt was
abandoned because it could no longer finish inside its window), or
attempt_lost (the last attempt died before it could report; nothing more
will be sent — poll the task).
webhook is null when the task was submitted without a webhook_url.
Without ?include=webhook the task response is unchanged.