> ## 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 once it finishes, with an optional language for failure messages.

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. For video and image generation tasks, you can also use `language` to choose the language of failure messages.

## Quick start

When submitting a task, add `webhook` at the top level of the request body. To translate failure messages, also add `language`:

```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",
    "language": "en"
  }'
```

After the task is done, we'll send a POST request to **`your URL + /callback`**.

<Note>
  Other async task endpoints (video, audio, etc.) use `webhook` in the same way. `language` currently applies to `POST /v1/videos/generations` and `POST /v1/images/generations`; both fields belong at the top level of the request body.
</Note>

## Choose the error message language

`language` is an optional string parameter that only affects `error.message` in failure callbacks. Task IDs, status, progress, cost, and result URLs do not change with the language. If omitted, the original error message from the upstream provider or platform is returned.

| Language | Value | Language | Value |
| - | - | - | - |
| English | `en` | Russian | `ru` |
| Simplified Chinese | `zh` | French | `fr` |
| Japanese | `ja` | German | `de` |
| Korean | `ko` | Indonesian | `id` |
| Portuguese | `pt` | Spanish | `es` |

* Values are case-insensitive, and surrounding whitespace is trimmed. For example, `"EN"` and `" en "` are both treated as `en`.
* Use the two-letter codes in the table. Regional tags such as `zh-CN`, `en-US`, and `pt-BR` are not recognized.
* Unsupported values do not cause task submission to fail; the callback keeps the original error message.
* If the original message is already in the target language, it is returned unchanged instead of being translated again.
* If translation fails, the original message is returned without delaying or dropping the callback.

<Note>
  When polling, use the [`language` query parameter on the task status endpoint](/en/api-reference/tasks/status#query-parameters) to choose the same error message language. A webhook has no query string, so `language` must be specified **when submitting the task**.
</Note>

<Warning>
  `POST /mj/submit/*` and `POST /v1/images/edits` do not support `webhook` / `language`. Official xAI image models do not support `language` and return `400 parameter "language" is not supported` when it is included. Do not send this parameter to those models.
</Warning>

## 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": "The input image cannot be accessed. Make sure the URL is publicly accessible.",
      "type": "task_failed",
      "param": "",
      "code": "task_failed"
    }
  }
  ```
</CodeGroup>

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

<Note>
  The failure example above uses `"language": "en"`. The language parameter only changes `error.message`; all other fields remain the same.
</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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.