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

# Batch Webhooks: Receive Signed Task Completion Events

> Configure signed webhooks for batch tasks and safely receive every accepted item's successful or failed terminal event.

Batch Webhooks send one HTTP `POST` to your root-level `webhook_url` whenever an accepted batch item reaches a successful or failed terminal state.

<Info>
  Batch Webhooks are separate from the single-task `callback_url` workflow. They use an account-level signing secret and `X-Apixo-Signature` verification.
</Info>

## Lifecycle

```text theme={null}
Get the account-level secret with your API key
        ↓
Store the secret in your server-side secret manager
        ↓
Submit a batch with webhook_url
        ↓
An accepted item succeeds or fails
        ↓
APIXO signs and sends the webhook
        ↓
Verify, deduplicate, and process the event
```

No separate webhook account is required. The secret belongs to the account that owns the authenticated API key, and API keys under the same account share that account-level signing secret.

## Step 1: Get the signing secret

```http theme={null}
GET https://api.apixo.ai/api/v1/webhooks/secret
```

```bash theme={null}
curl "https://api.apixo.ai/api/v1/webhooks/secret" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```json theme={null}
{
  "code": 200,
  "message": "success",
  "data": {
    "version": 1,
    "secret": "YOUR_WEBHOOK_SIGNING_SECRET"
  }
}
```

The `secret` is a shared **signing secret**:

* APIXO uses it to calculate an HMAC-SHA256 signature for each webhook request.
* Your service uses it to verify that the request came from APIXO and that its body was not modified.
* Do not send the secret back in a batch request or webhook response.
* The webhook body is not an encrypted payload. HTTPS encrypts the transport; the secret authenticates the sender and protects message integrity.
* Store the secret only in a server-side KMS, Secret Manager, or protected environment variable. Never expose it in a client application, source repository, or logs.

## Step 2: Submit a batch with webhook\_url

Set `webhook_url` at the root of the batch request:

```bash theme={null}
curl -X POST "https://api.apixo.ai/api/v1/generateTask/seedream-4-5/batches" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: batch-20260915-002" \
  -H "Content-Type: application/json" \
  -d '{
    "webhook_url": "https://merchant.example.com/webhooks/apixo",
    "items": [
      {
        "client_item_id": "order-1003",
        "idempotency_key": "order-1003-v1",
        "input": {
          "mode": "text-to-image",
          "prompt": "A minimalist mountain coffee shop"
        }
      }
    ]
  }'
```

`webhook_url` must be a publicly reachable HTTPS address. Each batch retains its signing-secret version, so a secret rotation does not break verification for already accepted batches.

## Step 3: Receive a webhook

APIXO sends these headers:

```http theme={null}
POST https://merchant.example.com/webhooks/apixo
Content-Type: application/json
X-Apixo-Event-Id: evt_123
X-Apixo-Timestamp: 1789459200
X-Apixo-Signature: v1,2b1c...
```

| Header              | Description                                                       |
| ------------------- | ----------------------------------------------------------------- |
| `X-Apixo-Event-Id`  | Unique delivery event ID. Use it for receiver-side deduplication. |
| `X-Apixo-Timestamp` | Unix timestamp in seconds. Use it to limit your replay window.    |
| `X-Apixo-Signature` | `v1,` followed by a hexadecimal HMAC-SHA256 signature.            |

### Successful task payload

```json theme={null}
{
  "code": 200,
  "message": "success",
  "data": {
    "taskId": "task_123",
    "state": "success",
    "resultJson": "{\"resultUrls\":[\"https://cdn.example.com/output.jpg\"]}"
  },
  "batch": {
    "batchId": "batch_123",
    "clientItemId": "order-1003"
  }
}
```

### Failed task payload

```json theme={null}
{
  "code": 200,
  "message": "success",
  "data": {
    "taskId": "task_987",
    "state": "failed",
    "failCode": "CONTENT_VIOLATION",
    "failMsg": "Content violates usage policy"
  },
  "batch": {
    "batchId": "batch_123",
    "clientItemId": "order-1004"
  }
}
```

### Webhook payload fields

| Field                | Type    | Description                                                                                   |
| -------------------- | ------- | --------------------------------------------------------------------------------------------- |
| `code`               | integer | Webhook envelope code. `200` does not mean generation succeeded; inspect `data.state`.        |
| `message`            | string  | Webhook envelope message. `success` is used for normal terminal payloads.                     |
| `data.taskId`        | string  | Completed platform task ID. You can use it with Single Tasks > Status Task.                   |
| `data.state`         | string  | Terminal business state. `success` means generation succeeded; `failed` means it failed.      |
| `data.resultJson`    | string  | Usually present on success. JSON string containing public model output, such as `resultUrls`. |
| `data.failCode`      | string  | Usually present on failure. Programmatic failure code.                                        |
| `data.failMsg`       | string  | Usually present on failure. Developer-facing failure explanation.                             |
| `batch.batchId`      | string  | Batch that contains this task.                                                                |
| `batch.clientItemId` | string  | Caller-provided item locator. Use it to associate the event with your order or job.           |

## Step 4: Verify, deduplicate, and respond

The signed bytes are exactly:

```text theme={null}
{eventId}.{timestamp}.{rawRequestBody}
```

Calculate the signature as:

```text theme={null}
hex(HMAC-SHA256(secret, signingPayload))
```

`rawRequestBody` is the original HTTP body bytes. Do not parse, format, or serialize JSON again before signature verification.

### Node.js / Express verification example

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

const app = express();
const webhookSecret = process.env.APIXO_WEBHOOK_SECRET;

app.post('/webhooks/apixo', express.raw({ type: 'application/json' }), async (req, res) => {
  const eventId = req.get('X-Apixo-Event-Id');
  const timestamp = req.get('X-Apixo-Timestamp');
  const signatureHeader = req.get('X-Apixo-Signature') || '';
  const signature = signatureHeader.startsWith('v1,') ? signatureHeader.slice(3) : '';
  const nowSeconds = Math.floor(Date.now() / 1000);

  if (!eventId || !timestamp || !signature || Math.abs(nowSeconds - Number(timestamp)) > 300) {
    return res.status(400).send('Invalid webhook headers');
  }

  const signingPayload = Buffer.concat([
    Buffer.from(`${eventId}.${timestamp}.`, 'utf8'),
    req.body,
  ]);
  const expected = crypto
    .createHmac('sha256', webhookSecret)
    .update(signingPayload)
    .digest('hex');

  const receivedBytes = Buffer.from(signature, 'hex');
  const expectedBytes = Buffer.from(expected, 'hex');
  if (receivedBytes.length !== expectedBytes.length
      || !crypto.timingSafeEqual(receivedBytes, expectedBytes)) {
    return res.status(401).send('Invalid signature');
  }

  // Persist and deduplicate eventId with a database uniqueness constraint or a TTL-backed store.
  const payload = JSON.parse(req.body.toString('utf8'));
  if (payload.data.state === 'success') {
    // Find your order by payload.batch.clientItemId, then process the result asynchronously.
  } else {
    // Record payload.data.failCode / failMsg and update your failure state.
  }

  return res.status(204).end();
});
```

Process every event in this order:

1. Preserve the raw body and read the three `X-Apixo-*` headers.
2. Check that the timestamp is within your allowed window; this example uses five minutes.
3. Calculate the HMAC with the account secret and compare signatures in constant time.
4. Persist and deduplicate `X-Apixo-Event-Id`.
5. Only then parse JSON, update orders, or enqueue downstream work.
6. Return any HTTP `2xx` once the event is safely recorded.

<Warning>
  Do not verify a parsed or reserialized JSON body; even a byte-level formatting change invalidates the signature. Deduplicate by `X-Apixo-Event-Id`, not only by `taskId`.
</Warning>

## Retry and recovery

* Any HTTP `2xx` is a successful delivery.
* A non-`2xx` response, network error, or timeout schedules a retry.
* Retry intervals are **1 minute → 5 minutes → 30 minutes → 2 hours → 6 hours**. Including the first delivery, there are at most six attempts.
* Keep [Batch Status](/docs/api-reference/batch-status) and Single Tasks > Status Task as recovery and reconciliation paths.

## Rotate or revoke a secret

### Rotate

```http theme={null}
POST https://api.apixo.ai/api/v1/webhooks/secret/rotate
```

Newly submitted batches use the new secret. Already accepted batches continue using the previous secret during a seven-day grace period, so receivers should support both secrets during that time.

### Emergency revoke

```http theme={null}
POST https://api.apixo.ai/api/v1/webhooks/secret/revoke
```

Revocation suppresses all outstanding deliveries for the account and revokes existing secrets. To resume batch webhooks, get a new secret and submit new batches with `webhook_url`.

## Production checklist

* Your endpoint is public HTTPS and returns a 2xx quickly.
* The signing secret is stored securely and never exposed to client code or logs.
* The receiver verifies the raw body before parsing JSON.
* The receiver checks timestamp freshness and persistently deduplicates `X-Apixo-Event-Id`.
* Slow work runs in a queue or worker rather than blocking the HTTP response.
* You have Batch Status or Status Task reconciliation for missed events.
