Receive webhooks
Create an endpoint, verify each signed request, and process retried deliveries once.
Webhooks push email events, such as deliveries, bounces, and inbound replies, to an HTTPS endpoint in your application. This guide creates an endpoint, verifies its requests, and handles retries. The webhook event catalog lists every event, its payload, and the delivery limits.
1. Create an endpoint
Send a public HTTPS URL and the event types it should
receive. Add channels: ["email"] to receive only email events, or leave it out to receive every
channel.
import { createClient } from "samva";
const samva = createClient({ apiKey: process.env.SAMVA_API_KEY! });
const created = await samva.webhooks.create({
name: "Order events",
url: "https://your-app.com/webhooks/samva",
eventTypes: [
"message.delivered",
"message.bounced",
"message.failed",
"message.opened",
"message.clicked",
],
channels: ["email"],
});
// Store this on your server.
const signingSecret = created.secret;curl -X POST https://api.samva.dev/v1/webhooks \
-H "X-API-Key: samva_sk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Order events",
"url": "https://your-app.com/webhooks/samva",
"eventTypes": ["message.delivered", "message.bounced", "message.failed", "message.opened", "message.clicked"],
"channels": ["email"]
}'Keep the signing secret on your server, for example as SAMVA_WEBHOOK_SECRET. To read it again,
open the endpoint in the dashboard and reveal or copy it under Signing secret, or call
GET /v1/webhooks/{id}/secret. Reading it needs the same permission as rotating the secret.
To replace the secret, call webhooks.regenerateSecret; the previous secret keeps working for a
limited overlap so you can deploy the new one.
The URL must be HTTPS, must not contain credentials, and must resolve only to public addresses.
Samva does not follow redirects, so register the URL that answers the request. You can add custom
headers, such as a token your gateway checks; they cannot replace the signature headers. Samva stores
custom header values encrypted and never returns them: endpoint responses report only
hasCustomHeaders.
message.opened and message.clicked are emitted for tracked sends when tracking is enabled for
the effective send settings and recipient preference. Apple Mail Privacy Protection can produce automatic opens, and
security scanners can produce automatic opens and clicks, often the first observation. Likely automatic observations carry machine: true and a
machineReason; they still produce webhooks. See the
event catalog for payloads and interpretation.
contact.unsubscribed and contact.resubscribed report each changed topic subscription. They include
the source and may include a validated message and campaign; see the
event catalog. contact.consent_changed reports
a change to a contact's recorded consent; see the
event catalog.
2. Verify each request
Verify the signature against the exact raw body before you parse it. verifyRequest from
@samva/webhooks reads the headers and body of a Fetch API Request, checks the signature and
timestamp, and returns the event.
import { verifyRequest, WebhookVerificationError } from "@samva/webhooks";
import { handleOnce } from "./handle-once";
export async function POST(request: Request) {
try {
const verified = await verifyRequest(request, process.env.SAMVA_WEBHOOK_SECRET!);
await handleOnce(verified.id, verified.event);
return new Response(null, { status: 204 });
} catch (error) {
if (error instanceof WebhookVerificationError) {
return new Response("Invalid webhook", { status: 400 });
}
throw error;
}
}For Express and other frameworks without a Request, and for Effect code, see
Verify webhooks.
3. Process each event once
Delivery is at least once and unordered, and a failed attempt is retried on a
fixed schedule. Every attempt of an event carries the
same verified.id, so record it under a unique constraint before you apply side effects, and skip an
ID you have already recorded.
import type { SamvaWebhookEvent } from "@samva/webhooks";
// Your storage: `recordOnce` inserts the ID under a unique constraint and
// resolves to false when the ID was already there.
import { markUndeliverable, recordOnce } from "./store";
export async function handleOnce(id: string, event: SamvaWebhookEvent) {
if (!(await recordOnce(id))) return;
if (event.type === "message.bounced" || event.type === "message.failed") {
await markUndeliverable(event.data);
}
}Return a 2xx status once the event is stored, and do slow work afterward: an answer that arrives
after the response timeout counts as a failure and is
retried. Branch on event.type and ignore types you do not handle.
4. Test the endpoint
Send a webhook.test event with webhooks.test, or from Developers → Webhooks in the
dashboard. Delivery logs show each attempt's status code and duration. A skipped replay
records that a completed delivery was received again without sending another request;
it does not consume an automatic attempt. You can redeliver an
event with webhooks.retryDelivery. An endpoint that fails for too long is disabled; the
delivery rules say when and how to turn it back on.
Next steps
Webhook event catalog
Event types, payloads, headers, and delivery limits.
Verify webhooks
Verifier functions, options, and errors for Promise and Effect code.
Webhooks API
Manage endpoints, test deliveries, logs, and redelivery.
Cloudflare challenges
If delivery logs show cloudflare_challenge, your Cloudflare zone is challenging Samva before the
request reaches your endpoint. Samva cannot solve browser challenges. For a WAF challenge, adjust
the matching rule or add a WAF skip rule
for your webhook path, and keep verifying webhook signatures. Bot Fight Mode on the Free plan
cannot be skipped per path; turn it off in your zone settings.
Then retry the delivery from the dashboard.
Related resources
- Inbound email API
See how replies arrive as parsed, threaded conversations and signed webhook events.