Skip to main content
Instead of polling GET /tasks/{task_id} in a loop, you can register a webhook endpoint and let Overten push results to you. Webhooks are the right default for production — especially for async jobs that run 10–25 minutes, where polling would make hundreds of wasted HTTP calls.

Two delivery patterns

Per-request

Include webhook_url on a single generation call. Fires once for that task. Good for ad-hoc scripts and testing.

Subscription

Call POST /webhooks once per org. Fires for every matching event thereafter. The right default for production.

Event types

Overten fires two events:
EventWhen it fires
run.completedA task transitioned to status=completed
run.failedA task transitioned to status=failed
Subscribe only to the events your code actually handles.

Creating a subscription

POST /api/v1/webhooks
Authorization: Bearer $OVERTEN_API_KEY
Content-Type: application/json

{
  "url": "https://myapp.example.com/webhooks/overten",
  "events": ["run.completed", "run.failed"],
  "description": "Production pipeline"
}
{
  "webhook_id": "whk_...",
  "url": "https://myapp.example.com/webhooks/overten",
  "events": ["run.completed", "run.failed"],
  "description": "Production pipeline",
  "active": true,
  "secret": "whsec_...",
  "created_at": "..."
}
The secret is shown only once at creation time. Save it immediately — you need it to verify every incoming signature. If you lose it, delete the subscription and create a new one to get a fresh secret.

Per-request webhooks

To fire a one-shot callback for a single task, include webhook_url in the generation call:
POST /excel/generate
{
  "prompt": "...",
  "async": true,
  "webhook_url": "https://myapp.example.com/webhooks/overten"
}
Per-request webhooks are signed with your org’s primary webhook secret (available via GET /verify), not a per-subscription secret. The signature format is the same.

Payload shape

run.completed

POST https://myapp.example.com/webhooks/overten
Content-Type: application/json
X-Overten-Signature: sha256=<hex_hmac>
X-Overten-Timestamp: 1713312345
X-Overten-Event: run.completed
User-Agent: Overten-Webhook/1.0

{
  "event": "run.completed",
  "run_id": "run_01HXYZ",
  "task_id": "task_01HXYZ",
  "agent_name": "excel_agent_api",
  "format": "excel",
  "status": "completed",
  "download_url": "https://...",
  "edit_url": "https://...",
  "summary": "Created a 12-row sales report...",
  "occurred_at_ms": 1713312345678
}

run.failed

{
  "event": "run.failed",
  "run_id": "run_01HXYZ",
  "task_id": "task_01HXYZ",
  "agent_name": "excel_agent_api",
  "format": "excel",
  "error": {
    "code": "internal_error",
    "message": "..."
  },
  "occurred_at_ms": 1713312345678
}

Verifying signatures

Every request Overten sends includes two headers:
  • X-Overten-Signaturesha256=<hex_hmac> of the signed payload
  • X-Overten-Timestamp — Unix timestamp (seconds)
The signature is computed as:
signed_payload = "{timestamp}.{raw_body}"
signature      = hmac_sha256(secret, signed_payload).hexdigest()
raw_body is the exact bytes of the POST body — not re-serialized JSON. Always verify against the raw bytes, before any JSON parsing.
import hmac, hashlib, time
from fastapi import Request, HTTPException

SECRET = "whsec_..."

async def verify(request: Request):
    body = await request.body()
    sig_header = request.headers.get("X-Overten-Signature", "")
    ts = request.headers.get("X-Overten-Timestamp", "")

    if not sig_header.startswith("sha256="):
        raise HTTPException(400, "missing signature")
    expected_sig = sig_header.removeprefix("sha256=")

    # Replay protection: reject requests older than 5 minutes
    if abs(time.time() - int(ts)) > 300:
        raise HTTPException(400, "timestamp too old")

    signed_payload = f"{ts}.{body.decode()}".encode()
    computed = hmac.new(SECRET.encode(), signed_payload, hashlib.sha256).hexdigest()

    if not hmac.compare_digest(computed, expected_sig):
        raise HTTPException(400, "bad signature")

Retry policy

If your endpoint returns a non-2xx response (or times out after 15 seconds), Overten retries with exponential backoff:
Attempt 1 (immediate) → 2 (1s) → 3 (4s) → 4 (16s) → 5 (64s) → 6 (256s) → 7 (1024s)
Seven attempts total, across approximately a 1.5-hour window. After the final attempt, delivery is abandoned — the task itself remains completed or failed on Overten’s side; only the notification was lost.
4xx responses are not retried. If your endpoint returns 400, 401, or 404, Overten treats that as an explicit “do not try again” and stops immediately.

Best practices

Return 200 OK immediately and enqueue the actual work internally. Overten times out each attempt at 15 seconds. If your handler runs synchronously longer than that, the delivery will be treated as failed and retried.
Retries and edge cases can cause duplicate deliveries. Key your processing logic on (run_id, event) and skip if you’ve already handled that combination.
Your webhook URL is publicly reachable, which means anyone can POST to it. Verifying the HMAC signature proves the payload came from Overten and hasn’t been tampered with. Never trust the payload without verifying first.
If your webhook secret leaks, rotate it by deleting the subscription and creating a new one — the new subscription will issue a fresh secret. Signatures from the old secret will stop validating immediately.

Managing subscriptions

# List all subscriptions
GET /api/v1/webhooks

# Get a specific subscription
GET /api/v1/webhooks/{webhook_id}

# Delete a subscription
DELETE /api/v1/webhooks/{webhook_id}

Debugging deliveries

When debugging failed deliveries, the most effective approach is to log the raw POST body and headers your endpoint receives on every incoming webhook. The run_id, task_id, and event fields in the payload are your primary identifiers — include them when opening a support ticket so the Overten team can trace the delivery history on their side.