Receiving webhooks
Receive webhooks from outside services through a relay endpoint that verifies senders before anything reaches your app.
Your app can receive webhooks from outside services — a payment completed, a GitHub push.
Only the AxHub receive endpoint's address is exposed outside, and only verified requests reach your app.
How it works
outside service → receive endpoint (hooks.{company}… /r/{endpoint id}) → sender verification → your app's route- The endpoint address is separate from your app's address. Redeploy the app or change internal paths, and the address you handed out stays the same.
- Only verified requests reach the app, with the original method, body, and query preserved.
What you need
- A connected app — webhooks are received per app. If you don't have one yet, start with Build and deploy your app.
- CLI login — endpoints are managed with the
axhub relaycommands.
Create a receive endpoint
axhub relay create --app <app> --name <endpoint-name> --verify-mode keyThe output shows the receive address, the key, and the receive contract. That output is the authority.
The key is shown only this once. If you lose it, issue a new one with axhub relay rotate-key.
Pick the sender verification (--verify-mode) that matches the outside service.
| verify-mode | What the sender provides |
|---|---|
key (default) | The shared key in an X-AxHub-Key header |
hmac | HMAC-SHA256 of the raw body as X-Signature-256: sha256=<hex> |
none | No verification. Only for senders you trust |
Receive it in your app
The receiving side is not the SDK — it's a plain route handler in your framework. In Next.js it looks like this.
import { createHmac, timingSafeEqual } from 'crypto';
export async function POST(req: Request): Promise<Response> {
const secret = process.env.AXHUB_RELAY_SIGNING_SECRET ?? '';
const rawBody = await req.text();
const provided = req.headers.get('x-axhub-signature') ?? '';
const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex');
const ok = secret !== '' && provided.length === expected.length &&
timingSafeEqual(Buffer.from(provided), Buffer.from(expected));
if (!ok) {
return new Response('unauthorized', { status: 401 });
}
const deliveryId = req.headers.get('x-axhub-delivery') ?? '';
// Retries can deliver the same event again — handle it once, keyed by deliveryId.
console.log('inbound webhook', deliveryId, rawBody.length);
// Return 2xx once handled; anything else gets redelivered.
return new Response('ok', { status: 200 });
}- Every delivery carries an
X-AxHub-Signature: sha256=<hex>signature. Recompute the HMAC-SHA256 of the raw body with theAXHUB_RELAY_SIGNING_SECRETenvironment variable, injected automatically at deploy time, and only requests that came through the receive endpoint get through. Nothing to configure. - Every delivery also carries an
X-AxHub-Deliveryheader (a unique delivery id). Retries can bring the same delivery twice, so make handling idempotent on that value. - The key is used only between the sender and the receive endpoint. Once verification passes, the endpoint strips credential headers like
X-AxHub-Keybefore forwarding, so don't check the key again in your app.
Delivery and retries
| Delivery mode | Behavior |
|---|---|
durable (default) | Redelivers with growing backoff until your app returns 2xx (1m → 5m → 30m → 2h → 6h, then marked failed) |
sync | Passes your app's response straight back to the sender |
Request bodies default to 1MiB (413 beyond that). Intake defaults to 300 per minute, and going over returns 429.
Operating it
| What you want | Command |
|---|---|
| List endpoints | axhub relay list --app <app> |
| See deliveries (status, attempts, response code) | axhub relay deliveries <endpoint id> --app <app> |
| Resend a failed delivery | axhub relay replay <endpoint id> <delivery id> --app <app> --execute |
| Rotate the key | axhub relay rotate-key <endpoint id> --app <app> --execute |
| Close an endpoint (irreversible) | axhub relay delete <endpoint id> --app <app> --execute |
You've succeeded when
Put the receive address and key into the outside service's webhook settings and send a test event.
If axhub relay deliveries records the delivery with a 2xx response code, you're done.