Skip to content
AI Inbx
Esc
navigateopen⌘Jpreview
On this page

Next.js

A webhook route handler, and sending from a server action.

Install

npm install aiinbx
AI_INBX_API_KEY=aiinbx_...
AI_INBX_WEBHOOK_SECRET=whsec_...

One client

The constructor reads AI_INBX_API_KEY from the environment, so a module-scoped client needs no arguments. Keep it in one file — it’s cheap to construct but pointless to rebuild per request.

import AIInbx from "aiinbx"

export const aiinbx = new AIInbx()

Receiving webhooks

verifyWebhookRequest takes the Request itself, reads the raw body, and checks the aiinbx-signature header. Hand it the request before anything else parses it.

import { verifyWebhookRequest, WebhookSignatureError } from "aiinbx/webhooks"

export async function POST(request: Request) {
  let event

  try {
    event = await verifyWebhookRequest(request, process.env.AI_INBX_WEBHOOK_SECRET!)
  } catch (error) {
    if (error instanceof WebhookSignatureError) {
      return new Response("Invalid signature", { status: 400 })
    }
    throw error
  }

  // Application-defined: atomically store a durable job by event.id.
  await enqueueOnce(event.id, event)

  return new Response(null, { status: 204 })
}

enqueueOnce must persist the event before resolving and treat duplicates as successful no-ops. Storage failures must produce a non-2xx response. See durable event processing.

Implementation notes:

  • route.ts, not pages/api. The App Router hands you a real Request with an unread body. The Pages Router parses the body before your handler runs, which changes the bytes and breaks the signature — you’d have to disable bodyParser and reassemble the stream.
  • A 400 on a bad signature, not a 500. A failed check means the request could not be authenticated. A mismatched secret or clock skew can also reject a legitimate delivery. Retrying it would be pointless, and the delivery log should say rejected, not your server crashed.
  • 204 quickly. The response is an acknowledgement. Deliveries retry on a non-2xx, so slow work belongs on a queue, not in the handler.

verifyWebhookRequest uses Web Crypto, so this route runs on the Edge runtime unchanged.

Sending

From a server action, resolve the authenticated user on the server. requireCurrentUser below is an application-defined authentication function; do not accept an arbitrary recipient and user ID from the browser.

"use server"

import { aiinbx } from "@/lib/aiinbx"

export async function sendWelcome() {
  // Application-defined: authenticate the caller and load their verified address.
  const { id: userId, email: address } = await requireCurrentUser()
  await aiinbx.emails.send(
    {
      from: { name: "Acme", address: "hello@acme.com" },
      to: address,
      subject: "Welcome to Acme",
      text: "Your account is ready.",
    },
    { idempotencyKey: `welcome-${userId}` }
  )
}

The idempotency key is doing real work here: server actions can be re-invoked by a retry or a double submit, and a key derived from the user ID means the second attempt returns the first email instead of sending another one. Derive it from something stable — not a fresh crypto.randomUUID() per call, which defeats the purpose.

Last updated on September 9, 2026

Was this page helpful?