Skip to main content
When submitting async generation tasks such as video / image / audio, you can include a callback URL. Once the task finishes (succeeds or fails), we’ll actively POST the result to your URL, so you don’t have to keep polling.

Quick start

When submitting a task, add a webhook field to the request body:
curl -X POST https://your-access-domain/v1/images/generations \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-image-2",
    "prompt": "a red apple on a table",
    "size": "1024x1024",
    "webhook": "https://your-server.com"
  }'
After the task is done, we’ll send a POST request to your URL + /callback.
Other async task endpoints (video, audio, etc.) work the same way — just add the webhook field to the request body.

URL rules

The webhook you provide is the base URL, and we automatically append /callback:
Your webhookWhere we actually POST
https://your-server.comhttps://your-server.com/callback
https://your-server.com/apihttps://your-server.com/api/callback
https://your-server.com/api/https://your-server.com/api/callback
So your server needs an endpoint that accepts POST .../callback.

What you’ll receive

The pushed payload is exactly the same as what the “Get Task Status” endpoint returns — you can process it with the same parsing logic.
{
  "id": "task_01KV7FXR8BEYS1BWHJCT3JMCJ5",
  "status": "completed",
  "progress": 100,
  "created": 1781589029,
  "completed": 1781589058,
  "actual_time": 29,
  "cost": 0.006,
  "credits_cost": 0.06,
  "result": {
    "images": [{ "url": ["https://.../result.png"], "expires_at": 1781675458 }]
  }
}
For video tasks the result is in result.videos, and for audio in result.audios.
We only push when a task reaches a terminal state (completed / failed); we don’t push while processing.

Retries and deduplication (important)

  • Retries: If your server doesn’t return 2xx within about 10 seconds, or returns 5xx, we’ll retry automatically, up to 3 times, at intervals of roughly 10s, 30s, and 60s. If all 3 fail we give up (within about 2 minutes).
  • No retry: If your endpoint returns 4xx (treated as a bad URL / request), we give up immediately without retrying.
  • Deduplication: Normally a task is pushed only once. But in extreme cases (e.g. a restart on our side after sending but before confirmation) you may receive duplicate pushes. Be sure to deduplicate idempotently by id (task_id) to avoid double-processing.
Recommendations for your receiving endpoint:
1

Return 2xx as soon as possible

Accept and enqueue first, then process asynchronously — don’t make us wait for your processing to finish.
2

Deduplicate by id

Use id (task_id) as the idempotency key to avoid double-processing.
3

Configure and verify the signature

In production, verify the origin of callback requests and reject forged ones.

Requirements for the callback URL

For security, the callback URL must meet the following:
RequirementDescription
Publicly accessibleCannot be an internal / local address (e.g. 127.0.0.1, 10.x, 192.168.x will be rejected)
Protocolhttp or https (https recommended)
PortUse standard ports (80 / 443); non-standard ports may be blocked
DomainCannot point to our own service domain
URLs that don’t meet these requirements are dropped (no push, no retry).

FAQ

Check the following one by one:
  1. Did the task actually finish? Check the task details — is status completed / failed (no push while processing)?
  2. Is your URL publicly accessible? Can we reach your /callback?
  3. Is the port a standard port (80 / 443)? Non-standard ports may be blocked by security policies.
  4. Did your /callback return 2xx promptly? Returning 4xx is given up immediately.
  5. Are you using https? Is the certificate valid?
Some models produce multiple images at once, so images[].url may be an array — just handle it as an array.
No. We only push once, when the task finally succeeds or fails.

Minimal receiver example

Python
from http.server import BaseHTTPRequestHandler, HTTPServer
import json

class H(BaseHTTPRequestHandler):
    def do_POST(self):
        n = int(self.headers.get("Content-Length") or 0)
        body = self.rfile.read(n)
        data = json.loads(body)
        print("Received task callback:", data["id"], data["status"])
        # TODO: deduplicate by id, verify the signature, then process
        self.send_response(200); self.end_headers()
        self.wfile.write(b'{"ok":true}')

HTTPServer(("0.0.0.0", 443), H).serve_forever()
Return 200 as soon as possible, and run your processing logic asynchronously in the background.