Webhooks

Receive events at your own HTTPS endpoint

Webhooks are available on the Pro, Team, and Enterprise plans. Register an endpoint in the dashboard or with POST /v1/webhooks — the signing secret is shown once, at creation.

Event types

Only these events are delivered today:

| Event | Fires when | data fields | |---|---|---| | api.call.succeeded | An API request completes 2xx | endpoint, method, status_code, model, timestamp | | api.call.failed | An API request completes non-2xx | endpoint, method, status_code, model, timestamp | | key.created | A new API key is created | key_id, name, key_prefix, created_at | | team.member.invited | Someone is invited to your team | email, role, team_id, invited_at | | webhook.test | You trigger a test delivery | message, webhook_id, user_id |

Payload

Every delivery is a POST with this envelope:

JSON
{
  "id": "d3f1c8a2-5b6e-4c11-9a0f-7e2d4b8c1a90",
  "event": "api.call.succeeded",
  "data": { "endpoint": "/v1/chat/completions", "method": "POST", "status_code": 200, "model": "claude-opus-4-6", "timestamp": "2026-09-03T10:30:00.000Z" },
  "webhook_id": "b1a2c3d4-...",
  "timestamp": "2026-09-03T10:30:00.123Z"
}

| Field | Meaning | |---|---| | id | Unique per delivery. Stable across retries of the same event — dedupe on this. | | webhook_id | Your endpoint's config ID. The same for every delivery to this URL — not a dedupe key. | | timestamp | When the delivery was generated (ISO 8601). |

Headers

| Header | Value | |---|---| | X-Webhook-Signature | HMAC-SHA256 of the raw request body, hex, no prefix | | X-Webhook-Event | The event name (data.event) | | X-Webhook-Id | Same as id in the body — the dedupe key | | X-Webhook-Timestamp | Same as timestamp in the body | | User-Agent | TarqaAI-Webhook/1.0 |

Verifying the signature

Compute HMAC-SHA256 over the exact bytes of the request body with your signing secret and compare, in constant time, against X-Webhook-Signature.

Node (Express)
import crypto from 'crypto';

// Needs the raw body: app.use(express.raw({ type: 'application/json' }))
function verify(req, secret) {
  const expected = crypto.createHmac('sha256', secret).update(req.body).digest('hex');
  const got = req.get('X-Webhook-Signature') || '';
  return expected.length === got.length &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(got));
}

app.post('/tarqa-webhook', express.raw({ type: 'application/json' }), (req, res) => {
  if (!verify(req, process.env.TARQA_WEBHOOK_SECRET)) return res.sendStatus(401);

  const ts = Date.parse(req.get('X-Webhook-Timestamp'));
  if (Math.abs(Date.now() - ts) > 5 * 60 * 1000) return res.sendStatus(401); // replay window

  const event = JSON.parse(req.body.toString());
  if (!seen(event.id)) { enqueue(event); }   // dedupe on event.id, then process async
  res.sendStatus(200);                        // ack fast — see below
});
Python (Flask)
import hmac, hashlib, time

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

@app.post("/tarqa-webhook")
def hook():
    raw = request.get_data()
    if not verify(raw, request.headers.get("X-Webhook-Signature"), SECRET):
        return "", 401
    ts = float(request.headers["X-Webhook-Timestamp"] and
               dateutil.parser.isoparse(request.headers["X-Webhook-Timestamp"]).timestamp())
    if abs(time.time() - ts) > 300:
        return "", 401
    event = request.get_json()
    if not seen(event["id"]):
        enqueue(event)
    return "", 200
Rotating the secret breaks verification until you redeploy

POST /v1/webhooks/:id/rotate issues a new secret and returns it once. The old secret stops working immediately, so deploy the new one first.

Delivery semantics

  • At-least-once. A delivery that times out or returns non-2xx after your handler already processed it will arrive again. Dedupe on X-Webhook-Id.
  • Retries. Up to 4 attempts total — the initial send plus retries after 1s, 5s, 15s. Retries survive a restart of our side (they are not held in memory).
  • Timeout. Each attempt waits 10 seconds for your response. A slower handler is treated as a failure and retried — so return 2xx within 10s and do the real work asynchronously.
  • Success = any 2xx. The response body is stored for your delivery log but is not interpreted. There is no "received vs processed" signal — if you return 200 and then fail to process, we will not know.
  • No ordering guarantee. A retried older event can land after a newer one. Use timestamp if order matters to you.
  • Auto-disable. After 15 consecutive failed deliveries the endpoint is set inactive and stops receiving events. Re-enable it in the dashboard once your endpoint is healthy.
  • Replay protection is yours to do. Check X-Webhook-Timestamp against a tolerance window (5 minutes is reasonable). We do not reject replays server-side.

Inspecting deliveries

GET /v1/webhooks/:id/deliveries returns recent attempts with status code, response snippet, attempt count, and error message — use it to debug a failing endpoint before it auto-disables.