Developer webhooks receive JSON events at your HTTPS URL. Every payload is signed with HMAC-SHA256 using a shared secret — always verify the signature before processing.

Prerequisites

  • API key — create one in Workspace settings → API keys.
  • A public HTTPS URL (or a tunnel like ngrok for local development).

Steps

1

Implement a minimal handler

Your endpoint must read the raw body bytes for signature verification. Do not use a global JSON parser — it changes the byte sequence before you can verify the signature.
const express = require("express");
const crypto = require("crypto");
const app = express();

app.post(
  "/webhooks/arowana",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const sig = req.headers["x-arowana-signature"];
    const expected =
      "sha256=" +
      crypto
        .createHmac("sha256", process.env.WEBHOOK_SECRET)
        .update(req.body) // Buffer — do not parse before this
        .digest("hex");

    if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))) {
      return res.status(401).send("Invalid signature");
    }

    const payload = JSON.parse(req.body.toString());
    // enqueue async processing here — respond quickly
    res.status(200).send("OK");
  }
);
2

Register the endpoint

curl -sS -X POST "https://api.arowana.app/v1/webhooks" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-domain.com/webhooks/arowana",
    "events": ["message.delivered", "message.failed", "email.delivered", "email.bounced"],
    "secret": "whsec_your_random_secret"
  }'
The response includes the webhook id — save it to update or delete the endpoint later.
3

Send a test delivery

curl -sS -X POST "https://api.arowana.app/v1/webhooks/{id}/test" \
  -H "Authorization: Bearer $API_KEY"
This fires a synthetic payload to your URL without waiting for a real message event.
4

Understand the envelope

Every event, regardless of platform, uses the same outer shape:
{
  "id": "evt_a1b2c3d4",
  "event": "message.delivered",
  "timestamp": "2026-03-28T09:01:00Z",
  "data": { }
}
Use id for deduplication. Use event to route to the right handler. Respond with 2xx within 5 seconds and process asynchronously.

Learn more