Skip to content

Receiving

Inbound is the part every integration gets wrong, and it gets wrong in the same four ways. The SDK handler does all four for you.

app/api/webhooks/switchboard/[secret]/route.ts
import { createWebhookHandler } from "@atrixdigital/switchboard"

export const POST = createWebhookHandler({
  secret: process.env.SWITCHBOARD_WEBHOOK_SECRET!,
  seen: (id) => db.deliveries.exists(id),
  onEvent: async ({ event, meta }) => {
    if (event.kind !== "inbound") return
    await db.deliveries.insert(meta.deliveryId)
    await queue.push(event)        // do the WORK off the request
  },
  onError: (error, ctx) => logger.error({ error, id: ctx.meta.deliveryId }),
})

It takes a Request and returns a Response, so it drops into a Next.js route handler, Elysia, Hono, Bun.serve or plain Node without knowing which.

If you are not using the SDK#

Implement these four yourself.

Verify the secret#

It is the last path segment, not a header — the engine’s webhook producer cannot attach headers. Compare in constant time.

Fail closed when your configured secret is empty

Otherwise a missing environment variable turns your endpoint into an open one that anybody can post fabricated customer messages to.

Dedupe on X-Switchboard-Delivery#

Delivery is at-least-once. That is the guarantee, not a caveat: the alternative is losing messages. The same event arrives again whenever a delivery succeeded and the acknowledgement was lost.

A unique index on it is the whole implementation

The header is stable across retries. Without it, a handler that books a reply or sends an auto-response does it twice.

Never answer 5xx for a bug in your own handler#

A non-2xx tells Switchboard to retry, so one bug becomes hours of redelivery and eventually a dead letter for a message that was fine. Log it, answer 200, fix the bug — the payload stays replayable from /v1/deliveries.

Do answer 5xx when your dependency is down and a redelivery would genuinely help — a database you cannot reach to dedupe against, for instance.

Answer fast; do the work afterwards#

Events arrive in bursts. A handler that runs a database write and an LLM call before returning 200 holds the delivery worker’s connection for the duration. Enqueue and return.

Event kinds#

inbound, delivery, instance, ignored. The parser never throws — an unfamiliar shape becomes ignored with a reason rather than an exception, precisely so a surprise cannot become a retry storm.