Samva is in early access — self-serve signup is limited. Have a team invite? Sign up with that email. Contact us for access.

Samva

Receive webhooks

Register a webhook endpoint, subscribe to email events, and verify the X-Webhook-Signature header in your handler.

This guide shows you how to receive Samva email events in your own application: register a webhook endpoint, choose which events you want, and verify each request's signature before you trust it.

These are customer webhooks — Samva delivering events to an endpoint in your app (for example, when an email is delivered or fails). They are distinct from provider ingress, which is how Samva receives events from upstream email infrastructure. This guide is only about the former.

Email first. Samva is launching with email. SMS, WhatsApp, and voice are staged and will be documented as they ship.

Before you start

  • An API key with permission to manage webhooks. See Authentication.
  • A publicly reachable HTTPS endpoint in your app that can accept POST requests.
  • A shared webhook secret stored in your environment (for example WEBHOOK_SECRET) so your handler can verify signatures.

1. Register a webhook endpoint

Create a webhook by sending a POST to /v1/webhooks with the public url Samva should call and the list of events you want delivered to it.

curl -X POST https://api.samva.app/v1/webhooks \
  -H "X-API-Key: sk_sm_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Order events",
    "url": "https://your-app.com/webhooks/samva",
    "events": ["message.delivered", "message.failed"]
  }'

Or with the TypeScript SDK:

import { createClient } from "samva";

const samva = createClient({ apiKey: process.env.SAMVA_API_KEY! });

const result = await samva.webhooks.create({
  name: "Order events",
  url: "https://your-app.com/webhooks/samva",
  events: ["message.delivered", "message.failed"],
});

if (result.error) {
  console.error("Failed to register webhook:", result.error);
} else {
  console.log("Registered webhook:", result.data?.id);
}

2. Choose your events

Subscribe only to the events your app acts on. The email send lifecycle emits:

EventFires when
message.deliveredThe recipient's mail server accepted the message.
message.failedThe message could not be delivered.

Pass the events you want in the events array when you register or update the endpoint. You can register multiple endpoints to route different events to different services.

3. Verify the signature in your handler

Every webhook request includes an X-Webhook-Signature header — an HMAC-SHA256 of the request body keyed with your webhook secret, formatted as sha256=<hex digest>. Samva also sends X-Webhook-Event (the event name) and X-Webhook-Id (the endpoint id), which help you route requests. Recompute the HMAC over the exact payload you received, prefix it with sha256=, and compare it in constant time. Reject any request whose signature does not match before you process it.

import crypto from "crypto";

function verifyWebhookSignature(payload: string, signature: string, secret: string): boolean {
  const hmac = crypto.createHmac("sha256", secret);
  const expected = `sha256=${hmac.update(payload).digest("hex")}`;
  const signatureBuffer = Buffer.from(signature);
  const expectedBuffer = Buffer.from(expected);
  // timingSafeEqual throws on differing lengths — guard before comparing.
  if (signatureBuffer.length !== expectedBuffer.length) return false;
  return crypto.timingSafeEqual(signatureBuffer, expectedBuffer);
}

// Register the raw body parser on this route so `req.body` is the exact bytes
// Samva signed. A JSON parser would re-serialize the payload and break the HMAC.
app.post("/webhooks/samva", express.raw({ type: "application/json" }), (req, res) => {
  const signature = req.headers["x-webhook-signature"];
  const webhookSecret = process.env.WEBHOOK_SECRET;
  const rawBody = req.body.toString("utf8");

  if (typeof webhookSecret !== "string" || webhookSecret.length === 0) {
    return res.status(500).send("Webhook secret is not configured");
  }

  // Reject when the header is missing or duplicated (string[]) before verifying —
  // passing a non-string into the HMAC comparison would throw.
  if (
    typeof signature !== "string" ||
    !verifyWebhookSignature(rawBody, signature, webhookSecret)
  ) {
    return res.status(401).send("Invalid signature");
  }

  // Signature verified — parse and handle the event.
  const event = JSON.parse(rawBody);
  res.status(200).send("OK");
});

Compare against the raw request body bytes. If your framework re-serializes the body, the HMAC will not match — capture the payload exactly as received.

Return a 2xx status as soon as you have verified and accepted the event. Samva treats any 2xx as accepted; non-2xx responses are retried by the delivery queue. Returning 401 (or any non-2xx) when the signature does not match is your handler's own choice — it signals rejection and is not a Samva-defined response code.

Next steps

On this page