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

# Async Task Model

> Polling and webhook callbacks for long-running image / video / music tasks.

LLM chat APIs are **synchronous** — one request returns the full result. But **image, video, and music** generation takes seconds to minutes, so they run as **async tasks**.

## Polling vs Webhook

<CardGroup cols={2}>
  <Card title="Client polling" icon="arrows-rotate">
    Create the task, then GET its status periodically until done. **Simple** — suits scripts, CLIs, notebooks, tests.
  </Card>

  <Card title="Webhook callback" icon="webhook">
    Pass `callback_url` at submit time; we POST the result when done. **Fewer requests** — best for production services.
  </Card>
</CardGroup>

You can **combine both** — passing `callback_url` does not disable manual polling.

## Creating a task

A POST returns `200` synchronously with a `task_id`:

```json theme={null}
{
  "success": true,
  "task_id": "27a1128f-14b5-440e-a82d-510c81e6fcd4"
}
```

The exact sync fields vary by endpoint:

* **Midjourney / Suno** style endpoints (which can wait for the result): return the complete result inline
* **Veo / Sora** style heavy video endpoints: return only `task_id`; the result is delivered later via polling or webhook

Refer to each API's [reference page](/en/api-reference) for specifics.

## Client polling

General pattern (using Midjourney `/midjourney/tasks` as an example):

```python theme={null}
import time, requests

API = "https://api.xhuoapi.ai/v1"
headers = {"Authorization": "Bearer YOUR_API_TOKEN"}

# 1. Create the task
r = requests.post(
    f"{API}/midjourney/imagine",
    headers=headers,
    json={"prompt": "A cat sitting on a table"},
).json()
task_id = r["task_id"]

# 2. Poll
while True:
    info = requests.post(
        f"{API}/midjourney/tasks",
        headers=headers,
        json={"action": "retrieve", "task_id": task_id},
    ).json()

    # Once finished_at is present, the task is done
    if info.get("finished_at"):
        print(info["response"])
        break

    time.sleep(5)
```

**Note**: `/midjourney/tasks` retrieve returns `{id, request, response, created_at, started_at, finished_at, ...}` — there is **no** `status` field. Check for the presence of `finished_at` or a non-empty `response` to detect completion. Other services' task-query endpoints use slightly different field names; consult each API's docs.

Use a reasonable polling cadence with backoff to avoid hitting `too_many_requests` (429). Prefer estimates based on each endpoint's typical processing time documented in its API reference.

## Webhook callbacks

Add `callback_url` to the request body:

```json theme={null}
{
  "prompt": "A cat sitting on a table",
  "callback_url": "https://your-server.com/midjourney/callback"
}
```

When the task finishes, we POST the result to that URL. The payload looks like:

**Success:**

```json theme={null}
{
  "success": true,
  "task_id": "...",
  "trace_id": "...",
  "image_url": "...",
  "...": "...(other business fields, same as sync success)"
}
```

**Failure:**

```json theme={null}
{
  "success": false,
  "task_id": "...",
  "trace_id": "...",
  "error": {
    "code": "...",
    "message": "..."
  }
}
```

### Webhook receiver requirements

* The endpoint must be **publicly reachable**, HTTPS recommended
* Return `2xx` quickly; defer your business processing to a background queue
* **Callbacks currently carry no signature header.** To guard against spoofing:
  * Include an unguessable token segment in your `callback_url`
  * Or verify the incoming `task_id` against tasks your service previously created

### Minimal Flask example

```python theme={null}
from flask import Flask, request

app = Flask(__name__)

# Track task IDs your service has submitted
my_tasks = set()

@app.post("/midjourney/callback")
def callback():
    payload = request.json
    task_id = payload.get("task_id")

    if task_id not in my_tasks:
        return "", 204  # not ours, ignore

    if payload.get("success"):
        save_image(payload["image_url"])
    else:
        log_error(payload.get("error"))

    return "", 200
```

## Choosing between them

| Aspect         | Polling                         | Webhook                            |
| -------------- | ------------------------------- | ---------------------------------- |
| Setup          | Trivial                         | Requires a public HTTPS endpoint   |
| Latency        | Bound by poll interval          | Lowest (pushed immediately)        |
| Request count  | Each poll is a request          | 0 (you only receive)               |
| Crash recovery | Restart and resume              | You must handle "missed callbacks" |
| Best for       | Scripts, local tools, debugging | Production services, batch jobs    |

## Failure handling

Either path can deliver a failed result. The `error.code` values match the synchronous error codes — see [Response format](/en/concepts/responses). **Failed tasks are generally not billed**; consult each service's detail page for the exact policy.

## Next

<CardGroup cols={2}>
  <Card title="Response format" icon="brackets-curly" href="/en/concepts/responses">
    All error codes and HTTP statuses
  </Card>

  <Card title="Gateway & auth" icon="key" href="/en/authentication">
    Tokens, endpoints, security
  </Card>
</CardGroup>
