> ## Documentation Index
> Fetch the complete documentation index at: https://docs.apimart.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Task Completion Callback (Webhook)

> Include a callback URL when submitting an async generation task, and we'll POST the result to you once the task finishes (success or failure) — no polling required.

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:

```bash theme={null}
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`**.

<Note>
  Other async task endpoints (video, audio, etc.) work the same way — just add the `webhook` field to the request body.
</Note>

## URL rules

The `webhook` you provide is the **base URL**, and we automatically append `/callback`:

| Your `webhook`                 | Where we actually POST                 |
| ------------------------------ | -------------------------------------- |
| `https://your-server.com`      | `https://your-server.com/callback`     |
| `https://your-server.com/api`  | `https://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](/en/api-reference/tasks/status)" endpoint returns** — you can process it with the same parsing logic.

<CodeGroup>
  ```json Success (status: completed) theme={null}
  {
    "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 }]
    }
  }
  ```

  ```json Failure (status: failed) theme={null}
  {
    "id": "task_xxx",
    "status": "failed",
    "progress": 100,
    "created": 1781589029,
    "completed": 1781589050,
    "error": { "message": "reason for failure...", "code": "" }
  }
  ```
</CodeGroup>

<Note>
  For video tasks the result is in `result.videos`, and for audio in `result.audios`.
</Note>

<Warning>
  We only push when a task reaches a **terminal state** (`completed` / `failed`); we don't push while processing.
</Warning>

## 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:**

<Steps>
  <Step title="Return 2xx as soon as possible">
    Accept and enqueue first, then process asynchronously — don't make us wait for your processing to finish.
  </Step>

  <Step title="Deduplicate by id">
    Use `id` (task\_id) as the idempotency key to avoid double-processing.
  </Step>

  <Step title="Configure and verify the signature">
    In production, verify the origin of callback requests and reject forged ones.
  </Step>
</Steps>

## Requirements for the callback URL

For security, the callback URL must meet the following:

| Requirement         | Description                                                                                        |
| ------------------- | -------------------------------------------------------------------------------------------------- |
| Publicly accessible | **Cannot** be an internal / local address (e.g. `127.0.0.1`, `10.x`, `192.168.x` will be rejected) |
| Protocol            | `http` or `https` (`https` recommended)                                                            |
| Port                | Use standard ports (`80` / `443`); non-standard ports may be blocked                               |
| Domain              | Cannot point to our own service domain                                                             |

URLs that don't meet these requirements are dropped (no push, no retry).

## FAQ

<AccordionGroup>
  <Accordion title="I submitted a task with a webhook but didn't receive a push?">
    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?
  </Accordion>

  <Accordion title="Why is url in the result an array?">
    Some models produce multiple images at once, so `images[].url` may be an array — just handle it as an array.
  </Accordion>

  <Accordion title="Do result links expire?">
    If `result` includes `expires_at` (a Unix timestamp), it indicates the link's expiration time — transfer/store it promptly.
  </Accordion>

  <Accordion title="Do you push the 'processing' status?">
    No. We only push once, when the task finally succeeds or fails.
  </Accordion>
</AccordionGroup>

## Minimal receiver example

```python Python theme={null}
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.
