webhook and webhook_secret to the JSON body when you submit an
asynchronous task. When the task reaches a final state, FreeModel sends one
signed POST to your URL, so you don’t have to keep polling. Webhooks work on
POST /v1/videos and on the GPT Image models (GPT Image 2 and GPT Image 2.5
Ext) on POST /v1/images/generations.
GET /v1/tasks/{id} stays the source of truth.
The webhook body is an event: the task sits under data.task, uses the same
fields as Get task status, and reports gateway statuses such as succeeded.
Video POST /v1/videos still returns object: "video" at submit time.Quick start
openssl rand -hex 32, and keep it
on your server.
string
The public
https URL that receives the event. Up to 2,048 characters, with
no user:password@ credentials and no #fragment.string
Required with
webhook. 16 to 256 characters, with no control characters.
FreeModel signs every delivery with it, stores it encrypted, and never returns
it. Each task keeps the secret it was submitted with.400 and error code
invalid_task_command, and nothing is charged.
Events
Each task sends one event, when it reaches a final state. There are no progress events.type is task. followed by the final status:
task.succeeded, task.failed, task.expired, or
task.reconciliation_required.
task.succeeded: save the output fromdata.task.result.- Any other type:
data.task.errorhas acodeand amessage. Fortask.reconciliation_required, the final outcome isn’t confirmed yet: checkGET /v1/tasks/{id}later, and don’t resubmit the job to another route.
Payload
Most tasks list their files in
result.urls. Some video models return
result.artifacts without a direct URL, so for videos, download through
GET /v1/videos/{id}/content,
which works for every video model.
Headers
Verify the signature
The signature is an HMAC-SHA256 of<timestamp>.<raw body>, keyed with your
webhook_secret and hex-encoded, where <timestamp> is the
X-Gateway-Webhook-Timestamp value. Compute it over the raw body bytes before
you parse the JSON, compare in constant time, and reject timestamps more than
five minutes old. Every retry is signed again with a new timestamp.
Retries and deduplication
- Return any
2xxwithin 10 seconds. Acknowledge first, then process asynchronously. - Timeouts, connection errors,
408,409,425,429, and5xxare retried, up to 12 attempts in total. The first retry follows within a few seconds, and the wait doubles after each failure, up to one hour, with random jitter. The last attempt comes roughly one to two and a half hours after the first. - A
Retry-Afterheader on your response, in seconds or as an HTTP date, sets the next wait, up to one hour. - Any other
4xx, any3xx(redirects are not followed), and TLS certificate errors stop delivery at once. - The same delivery can arrive more than once, for example when your
2xxis lost on the way back. Deduplicate on the bodyid. - Webhooks are an optimization, not the source of truth. If delivery stops or
every attempt fails,
GET /v1/tasks/{id}still has the result, so keep a reconciliation poll for tasks you haven’t heard back about.
Webhook URL requirements
Developing locally? Private addresses can’t receive webhooks. Expose your
handler with a tunnel, for example
cloudflared tunnel or ngrok http 3000, and use the tunnel’s https URL, or poll
GET /v1/tasks/{id} during development.Minimal receiver
Both receivers use theverifyWebhook / verify_webhook function from
Verify the signature. A 401 for a bad signature
stops retries for that delivery, which is what you want for forged requests.
FAQ
I submitted with a webhook but received nothing
I submitted with a webhook but received nothing
Check, in order: (1) has the task finished? Fetch
GET /v1/tasks/{id};
webhooks fire only on final states. (2) Is the URL a public https address
with a valid certificate? (3) Does your endpoint return 2xx within 10
seconds without redirecting? A 3xx, or a 4xx other than 408, 409,
425, and 429, stops delivery.The submission failed with invalid_task_command
The submission failed with invalid_task_command
The webhook fields didn’t pass validation. Send both
webhook and
webhook_secret, use a public https URL, and make the secret 16 to 256
characters long.Why is result.urls an array?
Why is result.urls an array?
A task can produce more than one file, for example an image request with
n greater than 1. Handle it as an array even when it holds one URL.How long do result links work?
How long do result links work?
Result links are temporary. Save the files to your own storage when the
event arrives, and don’t count on a link working more than 24 hours later.
Do webhooks replace polling?
Do webhooks replace polling?
Polling stays fully supported. Webhooks are an optimization: if a delivery
is missed,
GET /v1/tasks/{id} always has the latest state. For anything
critical, keep a reconciliation poll as the fallback.Can I receive webhooks on localhost?
Can I receive webhooks on localhost?
No. Private addresses are rejected. Use a tunnel such as
cloudflared or
ngrok during development, or poll instead.