# Callbacks

> We POST the result to your server when the task finishes — no recordInfo polling.

## How to enable

Pass `callBackUrl` to [createTask](https://docs.aimixmedia.site/en/endpoints/create-task). When the task becomes `success` or `fail`, we POST the same JSON that [recordInfo](https://docs.aimixmedia.site/en/endpoints/record-info) returns.

```http
POST <callBackUrl>
Content-Type: application/json
X-Signature: 3f1c9a…e07b        # signature, see below
X-Timestamp: 1790621128
X-Task-Id: task_nano-banana-2_1790621023886ca4e4d
X-Event: task.success           # or task.fail
X-Delivery-Attempt: 1
```

## What to reply

Any 2xx response within 15 seconds. Otherwise we retry after 30 s, 2 min and 10 min (4 attempts in total). An event may arrive twice — dedupe by `taskId`.

## Signature verification

The secret (`whsec_…`) is in [Dashboard → API keys](https://ai.aimixmedia.site/cabinet/keys#webhooks). The signature is the hex HMAC-SHA256 of the **raw** request body:

```python
import hmac, hashlib

def valid(raw_body: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)

# Flask: valid(request.get_data(), request.headers["X-Signature"], "whsec_...")
```

```javascript
import crypto from "node:crypto";

function valid(rawBody, signature, secret) {
  const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  return signature.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}

// Express: app.post("/callback", express.raw({ type: "application/json" }), (req, res) => {
//   if (!valid(req.body, req.get("X-Signature"), process.env.WEBHOOK_SECRET)) return res.sendStatus(401);
//   const task = JSON.parse(req.body).data; res.send("ok");
// });
```

```php
<?php
$raw = file_get_contents('php://input');
$ok = hash_equals(hash_hmac('sha256', $raw, 'whsec_...'), $_SERVER['HTTP_X_SIGNATURE'] ?? '');
if (!$ok) { http_response_code(401); exit; }
$task = json_decode($raw, true)['data'];
```

Tip: reject requests whose `X-Timestamp` is older than 5 minutes.

## Limits

The URL must be public http(s). Local and private addresses (localhost, 10.x, 192.168.x, etc.) are rejected with code 422. Delivery status is in `recordInfo.data.callback` and in the dashboard.
