Documentation

Everything you need to run webhooks in production.

Axel docs are split into four lanes: get started, primitives, operate, and reference. Each page is short by design — if a page can't be skimmed in 60 seconds, it's split into smaller ones.

Get started

Quickstart — accept your first webhook

CLI or curl. Under two minutes to a durable, searchable event.

1.

Create a source via the API

curl -X POST https://app.axelapp.ai/api/v1/sources \
  -H "Authorization: Bearer $AXEL_API_KEY" \
  -d '{ "name": "my-webhook" }'

The response includes a per-source ingest token (secret_token), shown only once. Store it as a secret in your producer's environment.

2.

Point the producer at the ingest URL

POST https://ingest.axelapp.ai/in/{source_id}
  x-axel-token: <token>
  content-type: application/json

  { "type": "order.created", "id": "ord_123", ... }

The endpoint returns 202 on acceptance. Your event is durable in R2 before the response.

3.

Watch it land in the dashboard

# Open https://app.axelapp.ai
# → Overview shows the event count tick up
# → Usage tab shows the source-level breakdown
# → Deliveries shows any failed attempts

Every event you accept is queryable in ClickHouse for 30 days, including headers and query params. Raw payloads are kept in R2 for 30 days.

Prefer the CLI? Create a source in the dashboard, then axel auth login and axel listen --source <source_id> --forward-to <url>, or point your webhook producer at the ingest URL shown in the dashboard.

Sources

Custom / generic webhook sources

Any producer that can POST JSON (or form / bytes) is supported.

Create a source with curl or the dashboard, then POST to https://ingest.axelapp.ai/in/{source_id} with the x-axel-token header you received. No signature required.

curl -X POST https://ingest.axelapp.ai/in/src_01H... \
  -H "x-axel-token: $AXEL_SOURCE_TOKEN" \
  -H "content-type: application/json" \
  -d '{ "type": "order.created", "id": "ord_123", ... }'

You can also bring a custom HMAC secret and Axel will verify an X-Axel-Signature: t=<unix>,v1=<hex> header (timestamped, 5-minute tolerance) using constant-time comparison.

Primitives

Webhook sources

Sources are the entry points. Each source has its own token (or signing secret), rate limit, body size cap, and nesting depth guard.

  • Per-source rate limits (events/min, operator-configured) protect downstream systems.
  • Body cap (1 MB default) and depth cap (100 levels) reject pathological payloads early.
  • Every accepted event is written to R2 before the 202 response is sent.
  • Token values are stored only as SHA-256 hashes; never in logs.

Routes: declarative filters & transforms

Routes decide where events go and what they look like. Rules are data, not code.

  • Filter by event type (e.g. payments.live matches invoice.paid etc.).
  • Declarative transforms: select/rename fields by JSON path, drop fields, pass the whole payload through, or wrap it as a JSONB column.
  • Same route engine runs at the edge ingest worker and in the Node router — no eval, no sandboxed user code to escape.
  • Fan-out to many destinations from one source with per-(route, destination) idempotency keys.

Destinations

Deliver exactly once (with retries) to the systems you already run.

  • Signed webhook: POST with HMAC + deterministic idempotency header so receivers can dedupe safely.
  • Postgres: insert into a JSONB column or auto-flattened columns (column projection). MongoDB: insert into native collections.
  • S3: write JSON objects with templated keys ({date}, {event_id}). Cloudflare R2: write JSON objects under a configurable key prefix.
  • Databricks: drop JSON files into Unity Catalog volumes for Auto Loader ingest into Delta.

Idempotency

Every delivery attempt uses a stable key of the form workspace:event:route:destination. Retries reuse the same key, so re-attempting a failed delivery never produces a duplicate even across queue shards. (Replays are issued under a distinct key so they are delivered, not deduped against the original.)

Operate

Replays

Reproduce any event you ever received, exactly.

From the dashboard or CLI:

# Dashboard: click any event → Replay
# CLI (exact bytes to your laptop)
axel replay evt_01HZQ8R7XK --forward-to http://localhost:3000/webhook
  • Replays pull the original payload + headers from R2 (up to 30-day retention).
  • Provider signature headers (Stripe, GitHub, Shopify, Axel) are stripped by default since their signed timestamp is stale; pass --keep-signature to forward them anyway.
  • Each replay gets a distinct, replay-tagged event id so the re-delivery is recorded separately from the original.

Failed deliveries & the Inbox

Every delivery attempt is recorded. Permanent failures (after all retries) land in the Inbox with one-click Retry and Mute. No SQL, no digging through DLQ tables.

The event detail page shows the receipt, the stored payload, and a delivery history of every HTTP attempt with its status, latency, and the failure reason (TLS error, 503, timeout, etc.).

Transform & route errors

If a declarative filter excludes an event it is dropped; if a transform or filter errors at runtime the event is dead-lettered and shows up in the Inbox. Oversized or too-deep payloads are rejected at ingest with a 413 before they are ever stored or routed.

Scaling knobs

  • Per-source rate limits + body/depth caps are your first line of defense.
  • Router and delivery workers use bounded concurrency and sharded queues (Cloudflare Queues + internal).
  • Raw payloads: 30 days in R2. ClickHouse traces: 30 days TTL. Dead letters kept by a per-workspace setting (default 90 days, up to 1 year).
  • Dashboard and API give you per-route and per-destination delivery metrics, including p95 delivery latency.
Reference

Event envelope (what you receive back)

Every stored event carries Axel metadata and a pointer to the raw body in R2 (the body is referenced by r2_key rather than inlined). Example shape:

{
  "event_id": "evt_01HZQ8R7XK",
  "workspace_id": "ws_...",
  "source_id": "src_...",
  "received_at": "2026-06-16T14:22:09.123Z",
  "r2_key": "...",
  "size_bytes": 1024,
  "headers": { "x-my-signature": "..." },
  "query": {}
}

Retry policy (defaults)

Up to 12 delivery attempts with exponential backoff. Retries are never billable. The retry policy is fixed and not per-source configurable.

ClickHouse observability

All receipt, routing, and delivery rows are queryable in ClickHouse for 30 days (headers, query params, and failure reasons; raw payloads live in R2). The dashboard search and the axel CLI use these tables. Export or run your own queries from the usage / deliveries views.

Important status & error reasons

  • 202: accepted (written to R2, queued for routing)
  • 429: rate limited at source
  • 413 / depth errors: body or nesting exceeded caps
  • Signature / auth failures: rejected before durable store
  • Destination 5xx, timeouts, and connection errors: retried per policy, then DLQ
  • Permanent 4xx (e.g. 400/401/403/404/410): dead-lettered immediately

Need a question answered?

Docs are intentionally concise. If something is missing or unclear, email the founders — we read every message and improve the site from real questions.