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

# Task Webhooks

> Receive signed ToAPIs completion events for asynchronous generation tasks

> [!NOTE]
>
> **Note for users in mainland China:** Please use `https://toapis.cn` as the API endpoint (Base URL). Replace `https://toapis.com` with `https://toapis.cn` in the examples in this document.

ToAPIs sends one final event when an asynchronous image or video task completes or permanently fails. Delivery is at least once; deduplicate by event `id`.

## Configure a Token

In the ToAPIs console, edit the Token, set an HTTPS default callback URL, generate a signing secret, enable Task Webhooks, save, and send a test event. The 32-byte secret is shown only when generated or rotated.

If a task request omits `callback_url`, ToAPIs uses the Token default. A request-level URL may only change the path and query while keeping the same scheme, hostname, and port. If Webhooks are disabled or the Token lacks a default URL or secret, sending `callback_url` returns `400 webhook_not_configured`.

## Events

* `generation.completed`
* `generation.failed`
* `endpoint.test`

```json theme={null}
{
  "id": "evt_01abcdef",
  "type": "generation.completed",
  "api_version": "v1",
  "created_at": 1783953302,
  "data": {
    "task_id": "tsk_01abcdef",
    "client_business_id": "order_123",
    "task_type": "image",
    "model": "model-name",
    "status": "SUCCESS",
    "progress": "100%",
    "result": {
      "type": "image",
      "data": [
        {
          "url": "https://cdn.example/result.png"
        }
      ]
    },
    "usage": {
      "charge_type": "per_request",
      "requests": 1
    },
    "billing": {
      "status": "settled",
      "credits": "100",
      "cost_usd": "0.5"
    },
    "error": null,
    "created_at": 1783953000,
    "finished_at": 1783953300,
    "completed_at": 1783953300,
    "expires_at": 1784039700
  }
}
```

Failed events are sent only after internal retries and refund or billing finalization have completed. Temporary failures do not emit an event.

See the [error reference](https://docs.toapis.com/docs/en/api-reference/errors.md) for meanings and handling guidance. Webhook `data.error` does not guarantee the `reason` field available on some task queries.

```json theme={null}
{
  "id": "evt_01failure",
  "type": "generation.failed",
  "api_version": "v1",
  "created_at": 1783953302,
  "data": {
    "task_id": "tsk_01failure",
    "client_business_id": "order_456",
    "task_type": "video",
    "model": "model-name",
    "status": "FAILURE",
    "progress": "100%",
    "result": null,
    "billing": {
      "status": "refunded",
      "credits": "0",
      "cost_usd": "0"
    },
    "error": {
      "code": "generation_failed",
      "message": "Generation failed after retries"
    },
    "created_at": 1783953000,
    "finished_at": 1783953300
  }
}
```

Test deliveries use the same signed contract:

```json theme={null}
{
  "id": "evt_01test",
  "type": "endpoint.test",
  "api_version": "v1",
  "created_at": 1783953302,
  "data": {
    "task_id": "endpoint_test",
    "task_type": "image",
    "status": "SUCCESS",
    "progress": "100%",
    "result": {"message": "ToAPIs webhook endpoint test"},
    "error": null,
    "created_at": 0
  }
}
```

`task_type` is `image` or `video`. Results match the public task-status representation and never contain channel IDs, upstream task IDs or keys, internal credentials, or private task data.

## Usage, billing, and query differences

Successful image and video events may contain `data.usage`, including billing quantities, available model-token details, and tool counts. Definitions match task queries; see [Task usage fields](https://docs.toapis.com/docs/en/api-reference/account/pricing.md). Video events may also contain `data.generation`; see [Video task queries](https://docs.toapis.com/docs/en/api-reference/tasks/video-status.md) for Seedance metadata and compatibility usage.

`data.billing` may contain `status`, `credits`, and `cost_usd`. Amounts are decimal strings, omitted for `pending`; `refunded` means the confirmed net charge is zero. An unconfirmed object is omitted and does not imply free usage. Webhooks and task queries use the same confirmation rules: when a charge is confirmed, including fixed-price tasks charged once at submission, both return `settled` with the actual amount. Whenever `data.usage` contains billing quantities such as `charge_type`, `data.billing` is also `settled`.

### `data.billing` field values

| Field | Type | Values |
| - | - | - |
| `status` | string | `pending`: settlement or refund is not confirmed yet; amounts are omitted. `settled`: the final charge is confirmed and the actual amount is returned; free tasks report `"0"`. `refunded`: refunded, or confirmed to have no net charge after failure; both amounts are `"0"` |
| `credits` | string | ToAPIs Credits actually charged, as a decimal string such as `"100"`. Returned only for `settled` and `refunded` |
| `cost_usd` | string | USD amount of the same charge, as a decimal string such as `"0.5"`. Returned under the same conditions as `credits`; includes discounts and multipliers and is not the provider cost |

Successful events are `settled`, or `refunded` when settlement returns the full reservation. Failed events are sent after the refund completes and are `refunded`. When the amount cannot be confirmed, the whole `data.billing` object is omitted.

### `data.usage` field values

| Field | Type | Values |
| - | - | - |
| `charge_type` | string | Billing mode: `per_request`, `per_second`, or `per_token` |
| `requests` | integer | Billed request count, returned for `per_request`. The image request `n` is not multiplied directly |
| `billed_tokens` | integer | Tokens used for pricing, returned for `per_token` |
| `output_seconds` | integer | Billed output video duration in whole seconds, returned for `per_second` |
| `input_seconds` | integer | Billed input video duration in whole seconds; omitted when zero |
| `input_images` | integer | Billed input images after the free allowance; omitted when zero |
| `input_tokens`, `output_tokens`, `total_tokens` | integer | Input, output, and total tokens reported by the model |
| `input_tokens_details` | object | Image input breakdown: `text_tokens`, `image_tokens`, `cached_tokens`, and `cached_tokens_details.text_tokens`, `cached_tokens_details.image_tokens`. Cached tokens are already included in the input total |
| `output_tokens_details` | object | Image output breakdown: `text_tokens`, `image_tokens`; `text_or_reasoning_tokens` is added when text and reasoning share a price and are not split, and is not counted in `text_tokens` |
| `prompt_tokens`, `completion_tokens` | integer | Tokens reported by video models |
| `tool_usage` | object | Tool counts: `web_search`, `image_search`, `grounding_queries`, returned when used |

`charge_type` through `input_images` are billing quantities and are returned only together with `data.billing.status=settled`. Token details and tool counts are returned independently, as provided by the upstream model. Fields that are not returned are omitted, not filled with zeros.

A Webhook is a snapshot taken when the event is created; a task query reads the latest confirmed state. Delivery does not wait for missing usage or amounts and retry rules do not change. Existing events are not backfilled; retries reuse the original event payload.

The event envelope and `api_version: "v1"` remain unchanged. Webhooks use `data.task_id`, `SUCCESS` / `FAILURE`, and string progress `"100%"`; queries use `id`, lowercase statuses, and integer progress. Time fields share names and semantics with the public `generation.task` query, all in Unix seconds: `data.created_at` is the task submission time, `data.finished_at` is the finish time, and successful events also include `data.completed_at` and `data.expires_at` (result expiry, finish time plus 24 hours). Fields without a value are omitted. The former `data.submitted_at` has been replaced by `data.created_at` and is no longer sent. Top-level event `created_at` is the event creation time, not the task submission time. Media URLs can differ between the two because access policies differ.

### Seedance video completion example

Examples illustrate the fields, not current model prices or a fixed billing mode. Use the fields and amounts returned for the actual task.

Seedance completion events may also include `fallback_url` and `fallback_last_frame_url` in `data.result.data[]`. Prefer `url` and `last_frame_url`; use the corresponding fallback only if the original URL cannot be accessed. Missing fallback fields do not mean the task failed. Events retain their URL snapshot at creation: later changes to copies do not rewrite an existing event or its retry payload. See [Video task status](https://docs.toapis.com/docs/en/api-reference/tasks/video-status.md#seedance-original-and-fallback-urls) for field definitions and usage.

```json theme={null}
{
  "id": "evt_seedance_example",
  "type": "generation.completed",
  "api_version": "v1",
  "created_at": 1792400061,
  "data": {
    "task_id": "task_seedance_example",
    "client_business_id": "order_123",
    "task_type": "video",
    "model": "seedance-2-global",
    "status": "SUCCESS",
    "progress": "100%",
    "result": {
      "type": "video",
      "data": [
        {
          "url": "https://cdn.example/result.mp4",
          "fallback_url": "https://files.toapis.com/videos/task_seedance_example/result.mp4",
          "format": "mp4",
          "last_frame_url": "https://cdn.example/last-frame.jpg",
          "fallback_last_frame_url": "https://files.toapis.com/videos/task_seedance_example/last-frame.jpg"
        }
      ]
    },
    "usage": {
      "charge_type": "per_token",
      "billed_tokens": 87300,
      "completion_tokens": 87300,
      "total_tokens": 87300
    },
    "billing": {
      "status": "settled",
      "credits": "87.3",
      "cost_usd": "0.4365"
    },
    "generation": {
      "duration": 5,
      "resolution": "720p",
      "ratio": "16:9",
      "generate_audio": true,
      "usage": {
        "completion_tokens": 87300,
        "total_tokens": 87300
      }
    },
    "error": null,
    "created_at": 1792400000,
    "finished_at": 1792400060,
    "completed_at": 1792400060,
    "expires_at": 1792486460
  }
}
```

## Headers and signature

```text theme={null}
User-Agent: ToAPIs-Webhook/1.0
X-ToAPIs-Webhook-Id: evt_01abcdef
X-ToAPIs-Webhook-Timestamp: 1783953302
X-ToAPIs-Webhook-Signature: v1=<hex-signature>[,v1=<previous-hex-signature>]
```

The signed bytes are:

```text theme={null}
event_id + "." + timestamp + "." + raw_request_body
```

Compute HMAC-SHA256 with the Token secret and compare signatures in constant time. Reject a first delivery whose timestamp differs from local time by more than five minutes. Retries use the actual resend timestamp and keep the same event ID. During the 24 hours after secret rotation, ToAPIs sends signatures from both the current and previous secret; accept any valid `v1` value.

### Python

```python theme={null}
import hashlib, hmac, time

def verify(headers, raw_body, secrets):
    event_id = headers["X-ToAPIs-Webhook-Id"]
    timestamp = int(headers["X-ToAPIs-Webhook-Timestamp"])
    if abs(time.time() - timestamp) > 300:
        return False
    signed = f"{event_id}.{timestamp}.".encode() + raw_body
    received = [v.removeprefix("v1=") for v in headers["X-ToAPIs-Webhook-Signature"].split(",")]
    return any(hmac.compare_digest(hmac.new(s.encode(), signed, hashlib.sha256).hexdigest(), r)
               for s in secrets for r in received)
```

### Node.js

```javascript theme={null}
import crypto from 'node:crypto';

export function verify(headers, rawBody, secrets) {
  const id = headers['x-toapis-webhook-id'];
  const ts = Number(headers['x-toapis-webhook-timestamp']);
  if (Math.abs(Date.now() / 1000 - ts) > 300) return false;
  const signed = Buffer.concat([Buffer.from(`${id}.${ts}.`), rawBody]);
  const received = headers['x-toapis-webhook-signature'].split(',').map(v => v.replace(/^v1=/, ''));
  return secrets.some(secret => received.some(sig => {
    const expected = crypto.createHmac('sha256', secret).update(signed).digest('hex');
    return expected.length === sig.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
  }));
}
```

### Go

```go theme={null}
func Verify(id, timestamp, signature string, body []byte, secrets []string) bool {
    ts, err := strconv.ParseInt(timestamp, 10, 64)
    now := time.Now().Unix()
    if err != nil || now-ts > 300 || ts-now > 300 { return false }
    signed := append([]byte(id+"."+timestamp+"."), body...)
    for _, secret := range secrets {
        mac := hmac.New(sha256.New, []byte(secret)); mac.Write(signed)
        expected := hex.EncodeToString(mac.Sum(nil))
        for _, value := range strings.Split(signature, ",") {
            if hmac.Equal([]byte(expected), []byte(strings.TrimPrefix(value, "v1="))) { return true }
        }
    }
    return false
}
```

Persist the event ID before processing side effects so duplicate deliveries return `2xx` without repeating work.

## Delivery and security

Only `2xx` is successful. ToAPIs does not follow redirects, times out after 10 seconds, and retries after approximately 10 seconds, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, and 24 hours. Use the task-status API if delivery is delayed or exhausted.

Callback endpoints must use HTTPS and cannot contain userinfo or fragments. Private, loopback, link-local, reserved, or disallowed DNS/IP destinations and illegal ports are rejected. DNS and SSRF checks run again for every delivery.

Result URLs may expire. Download and store required assets promptly. Real-avatar verification callbacks, payment Webhooks, and OAuth callbacks keep their existing contracts and are not task Webhooks.

See [Async task rate limits](https://docs.toapis.com/docs/en/api-reference/rate-limits/async-tasks.md).


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