Documentation

Set up Axel. Send your first webhook.

Start on Axel Cloud to use the managed service. These guides cover sources, routing, delivery, and recovery. You can also self-host the Apache-2.0 application.

Self-hosting

Your infrastructure, the same application.

The small install runs Postgres, the dashboard, and delivery in Docker, with Cloudflare Workers, Queues, and R2 for ingest. You maintain the host, backups, and upgrades. Add ClickHouse for searchable event history and usage charts. There is no Axel license fee. Provider charges depend on your traffic and setup.

Get started

Quickstart: accept your first webhook

Create an Axel Cloud workspace and a workspace API key with write access. Set AXEL_API_KEY in your shell before running the example. For self-hosting, use your dashboard and ingest URLs in place of the Cloud URLs below.

1.

Create a source via the API

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

The response includes the source ID, ingest URL, and secret_token. Axel shows the token only once. Set AXEL_INGEST_URL to the returned ingest URL and AXEL_SOURCE_TOKEN to the token before the next command.

2.

Send an event to the ingest URL

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

Axel returns 202 after storing the payload and queueing it for routing. This confirms acceptance, not destination delivery.

3.

Check the dashboard

Open app.axelapp.ai. Check Overview for accepted events and Usage for traffic by source. To deliver events onward, create a destination and an active route from this source, then send another event and check Deliveries.

Axel Cloud keeps searchable event history for 30 days. Raw payloads expire after 30 days by default, or sooner if you configure shorter retention.

Workspace overview

Overview

Welcome back. Here's what's happening across Northwind.

Events ingested1.24M +32%
Deliveries2.41M +28%
Success rate99.997% +0.08 pts
Unresolved11 -42%
Active sources124 routes
Volume · last 14 days1,241,790 events14.2 GB received · 8.2 GB raw payloads in R2
Events
Deliveries · last 14 days2,409,664 deliveries2.39M succeeded · 18,243 retried · 11 unresolved DLQ
Success / FailureFailure
Top sources
By eventsBy bytes
stripe.webhooks
612,403
github.webhooks
318,772
shopify.webhooks
164,210
partner.webhook
92,481
internal.webhook
44,108
Activity
All deliveries →
evt_01HZQ8R7XKHTTP 503 from warehouse-webhook · retry 2/12
RETRY
evt_01HZQ7N4WTTLS handshake timeout · destination paused
DEAD
evt_01HZQ6M2VRReplay batch resolved · 11 deliveries
OK
evt_01HZQ5L1UQSchema guard rejected · depth > 24
DROPPED
The dashboard with sample traffic and delivery outcomes.

To use the CLI, follow the installation guide. Create a source in the dashboard, then run 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. For self-hosting, sign in with axel auth login --api-base https://axel.example.com, using your own dashboard URL.

Sources

Custom / generic webhook sources

Use the generic source for producers that can POST JSON, form data, or bytes.

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 with a five-minute timestamp tolerance. See webhook authentication for sender setup and URL authentication.

Concepts

Webhook sources

Each source has its own authentication, rate limit, body size limit, and nesting depth limit.

  • Per-source rate limits (events/min, operator-configured) protect downstream systems.
  • The default limits reject bodies over 1 MB and nesting deeper than 100 levels.
  • An accepted event is written to R2 before the 202 response is sent.
  • Axel stores source tokens as SHA-256 hashes. Use headers when your sender supports them; authenticated URLs can appear in sender or proxy logs.
Source detail

stripe.webhooks

Active
Rate limit2,500/min
Body cap256 KB
Depth cap24
Retries12×
Events / 14d612,403
Success / 14d99.998%
{
  "event_id": "evt_01HZQ8R7XK",
  "source": "stripe.webhooks",
  "event": "invoice.paid",
  "customer": "cus_PqJ8XKr",
  "amount": 14250,
  "currency": "usd",
  "routes": ["payments.live"]
}
A source's detail page: its limits, live traffic, and the payloads it receives.

Routes: declarative filters & transforms

Routes choose destinations and define which fields each destination receives.

  • Filter by event type or payload fields, then preview which events match.
  • Declarative transforms: select/rename fields by JSON path, drop fields, pass the whole payload through, or wrap it as a JSONB column.
  • The route engine is eval-free and does not execute customer JavaScript.
  • Send one source to multiple destinations. Each route and destination pair has its own delivery idempotency key.
Routes

Live fan-out

4 routes healthy
Source
Filter
Destination
p95
stripe.webhooks
payments.live
mongo://orders
41ms
github.webhooks
push.archive
s3://raw-ledger
68ms
shopify.webhooks
orders.live
postgres://billing
37ms
partner.webhook
fanout.signed
https://hooks.example
57ms
The Routes view: each source's filter, its destination, and live p95 delivery latency.

Destinations

Deliver 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.
  • BigQuery: stream each event into a table via insertAll. New routes recursively map JSON objects to nested RECORD fields, normalize scalar leaves to STRING for schema-drift tolerance, and map compatible object arrays to REPEATED RECORD fields. Axel creates the table when needed. Enable Allow new fields on the route binding to add fields to an existing table automatically. Otherwise, update the schema yourself. Axel does not widen existing column types. Legacy flat-column and single-STRING-column modes remain available.

Delivery guarantees & idempotency

Axel provides at-least-once delivery to external destinations. Delivery attempts use a stable key of the form workspace:event:route:destination, and retries reuse that key. A signed HTTP receiver should deduplicate on X-Axel-Event-Id because a process can fail after the destination accepts a request but before Axel records the acknowledgement. Replays use a new event ID and are intentionally delivered again.

Operate

Replays

Replay an accepted event while its original payload is retained.

Use the dashboard to replay through your routes. Use the CLI to send stored bytes directly to a local handler:

# Dashboard: click any event → Replay
# CLI (exact bytes to your laptop)
axel replay evt_01HZQ8R7XK --forward-to http://localhost:3000/webhook
  • Replay requires the original payload to remain in storage. Retained metadata excludes authentication and other sensitive headers.
  • The CLI strips supported provider signature headers by default. --keep-signature preserves only headers still present in the retrieved metadata; it cannot restore headers removed at ingestion or make an expired signature valid.
  • Dashboard replays create a new event ID. CLI forwarding goes directly to your handler and does not create an Axel delivery record.
Event trace
evt_01HZQ8R7XK · stripe.payments
01
Edge acceptedPOST /v1/webhooks/in · 202
8ms
02
R2 persistedshard=7 · 1.4KB · sha256:c2…3f
14ms
03
Route matchedfilter payments.live · matched 1 of 3
23ms
04
MongoDB deliveredmongo://orders · POST · 200 OK
41ms
05
ClickHouse indexedevents.live · queued · partition=2026-06-16
queued
An event's trace: every hop from accept to store to route to deliver, with timings.

Failed deliveries & the Inbox

Terminal failures appear in the Inbox after retries are exhausted or the destination returns a non-retryable error. Open the failure to inspect its history, retry after a fix, or mute it.

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.

Capacity and retention

  • Set per-source rate limits and payload limits to control incoming traffic.
  • Router and delivery workers use bounded concurrency and sharded queues (Cloudflare Queues + internal).
  • Axel Cloud raw payloads: 30 days by default, configurable from 0 to 30. ClickHouse traces expire after 30 days.
  • Dead letters: 90 days by default (1–365 configurable). Replay requests: 30 days by default (1–90 configurable).
  • Audit logs: 365 days by default (30–3,650 configurable).
  • Dashboard and API give you per-route and per-destination delivery metrics, including p95 delivery latency.
Reference

Event metadata

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": {}
}

Default retry policy

Up to 12 delivery attempts with exponential backoff. Retries are included in the inbound event price. The retry policy is fixed and not per-source configurable.

Event storage and search

ClickHouse stores receipt, routing, and delivery metadata for 30 days. The dashboard and CLI use it for event search and delivery history. Raw payloads stay in R2. The small self-host profile omits ClickHouse, so analytics-backed search and usage views are unavailable. Its raw payload retention is fixed at 30 days; see the self-hosting guide before choosing a profile.

Response codes and failure 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 help with setup?

Ask setup and usage questions in GitHub Discussions. Contact Axel Cloud support privately for account or billing questions.