Webhooks

Register HTTPS endpoints to receive HMAC-signed events. Currently emits on batch terminal states; more event types coming as the platform grows.

Endpoints

Method Path What it does
POST /v1/webhooks Register a webhook. Secret returned only once in the create response.
GET /v1/webhooks List your webhooks. last_delivery_at + last_error shown per row.
DELETE /v1/webhooks/{webhook_id} Delete a webhook. In-flight retries for queued deliveries will give up.

Supported events

Event Fires when
batch.completed Batch transitions to completed
batch.failed Batch transitions to failed

Register a webhook

r = httpx.post("https://api.epithre.com/v1/webhooks",
    headers={"Authorization": f"Bearer {EPITHRE_KEY}"},
    json={
        "url": "https://your-app.example.com/epithre-webhook",
        "events": ["batch.completed", "batch.failed"],
        "description": "Notify batch completion to my queue worker"
    },
).json()

print(r["id"])      # wh-abc123...
print(r["secret"])  # whsec_...  SHOWN ONCE. Store it. Used to verify signatures.

URL requirements

Delivery payload

Epithre POSTs JSON to your URL with these headers:

POST /your-webhook-path HTTP/1.1
Content-Type: application/json
Epithre-Event: batch.completed
Epithre-Signature: t=1778765156,v1=<hex_hmac_sha256>
User-Agent: Epithre-Webhooks/1.0

{"event":"batch.completed","created":1778765156,
 "data":{"id":"batch-abc...","status":"completed","endpoint":"/v1/embeddings",
         "request_counts":{"total":1000,"completed":1000,"failed":0},
         "total_cost_idr":0.024}}

Verify signature

The signature is computed as HMAC-SHA256(secret, f"{t}.{raw_body}") in hex.

import hmac, hashlib, time

def verify_webhook(secret, signature_header, raw_body, max_age=300):
    parts = dict(p.split("=", 1) for p in signature_header.split(","))
    t = int(parts["t"])
    if abs(time.time() - t) > max_age:
        return False  # replay protection
    signed = f"{t}.{raw_body.decode()}".encode()
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])

Reject deliveries that don't verify. Always use hmac.compare_digest (not ==) to avoid timing attacks.

Retry policy

Non-2xx response or timeout (10s) triggers retry with exponential backoff:

Attempt Wait before
1 (immediate)
2 5s
3 30s
4 5m
5 30m
(give up) after 2h

After 5 failed attempts the delivery is marked giveup and skipped. The most recent error appears in the webhook's last_error field via GET /v1/webhooks.

Best practices

See also