Browse documentation

Webhook examples

Express webhook example

Verify Yanola webhook signatures against the raw request body in an Express server.

DevelopersAdvancedAvailable

What this guide covers

  • Capture raw request bytes before JSON parsing.
  • Verify the Yanola signature and reject invalid requests.
  • Acknowledge durable event acceptance quickly and process work asynchronously.

Install and configure

This example uses Express's route-local raw parser and the SDK verifier. Keep the signing secret in server-only environment configuration.

Install dependencies
bash
npm install express @yanola/whatsapp
.env
dotenv
YANOLA_WEBHOOK_SECRET=your_copy_once_endpoint_secret

Complete Express route

src/server.ts
ts
import express from "express"
import {
  verifyYanolaWebhookSignature,
} from "@yanola/whatsapp"

const signingSecret = process.env.YANOLA_WEBHOOK_SECRET
if (!signingSecret) {
  throw new Error("YANOLA_WEBHOOK_SECRET is required")
}

const app = express()

app.post(
  "/webhooks/yanola",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    const valid = verifyYanolaWebhookSignature({
      secret: signingSecret,
      signature: req.get("yanola-signature"),
      rawBody: req.body,
    })

    if (!valid) {
      return res.status(400).send("Invalid Yanola signature")
    }

    const event = JSON.parse(req.body.toString("utf8"))

    // Replace this with an atomic database insert or durable queue write
    // keyed by event.id before returning success.
    await acceptEventOnce(event)

    return res.sendStatus(204)
  }
)

app.use(express.json())
app.listen(3000)

Deduplicate at durable acceptance

The transaction or queue API is application-specific. The invariant is that the evt_ key and future work are durable before the route returns 2xx.

Application persistence sketch
ts
async function acceptEventOnce(event: { id: string }) {
  await database.transaction(async (transaction) => {
    const inserted = await transaction.events.insertIfAbsent({
      id: event.id,
      payload: event,
    })

    if (inserted) {
      await transaction.jobs.enqueue({
        kind: "process-yanola-event",
        eventId: event.id,
      })
    }
  })
}

Deploy the endpoint

  1. Expose POST /webhooks/yanola through public HTTPS for Live Mode.
  2. Create the Yanola endpoint with the matching URL and payload mode.
  3. Copy its secret into YANOLA_WEBHOOK_SECRET.
  4. Do not enable a body-transforming proxy in front of the route.
  5. Return 400 for invalid signatures and 2xx only after durable acceptance.

Common Express mistakes

Express webhook mistakes
MistakeEffectCorrection
app.use(express.json()) before the webhook routereq.body is parsed instead of a Buffer.Register the raw route first or use an isolated router.
JSON.stringify(req.body) for verificationSerialization changes signed bytes.Verify req.body directly.
Returning 204 before the database or queue writeAccepted work can be lost without retry.Await durable acceptance.
Doing slow business work in the routeYanola times out after 10 seconds.Queue the work and respond promptly.

Test locally

Start an HTTPS tunnel (cloudflared or ngrok) to your local receiver, create a Test Mode endpoint with the tunnel's public HTTPS URL, then send a dashboard test event. Confirm one durable evt_ record and a 204 delivery result.