Email is asynchronous. A successful send means a provider accepted the message; delivery, bounce, failure, and reply events resolve later. Webhooks push those outcomes to your application without polling.
Choose events
| Event | Fires when |
|---|---|
message.sent | A provider accepted the email. |
message.delivered | The recipient's server confirmed delivery. |
message.bounced | A message was rejected or reported as spam. |
message.failed | The email could not be delivered. |
message.received | An inbound email was routed to you. |
Register an endpoint
import { createClient } from "samva";
const samva = createClient({ apiKey: process.env.SAMVA_API_KEY! });
const result = await samva.webhooks.create({
name: "Delivery events",
url: "https://your-app.com/webhooks/samva",
eventTypes: ["message.delivered", "message.bounced", "message.failed"],
channels: ["email"],
});
// Store this now. Later reads do not return it.
const signingSecret = result.secret;
Custom endpoint headers cannot override webhook-id, webhook-timestamp,
webhook-signature, or other reserved delivery headers. Secret-looking values
are redacted in API responses and logs.
Verify the exact request
webhook-id: evt_01…
webhook-timestamp: 1786796531
webhook-signature: v1,base64-signature
Verification covers those headers and the exact raw body. The timestamp tolerance is five minutes.
import { verifyRequest, WebhookVerificationError } from "samva/webhooks";
export async function POST(request: Request) {
try {
const event = await verifyRequest(
request,
process.env.SAMVA_WEBHOOK_SECRET!,
);
await persistAndHandleOnce(request.headers.get("webhook-id")!, event);
return new Response(null, { status: 204 });
} catch (error) {
if (error instanceof WebhookVerificationError) {
return new Response("Invalid webhook", { status: 401 });
}
throw error;
}
}
Use samva/webhooks/effect when Effect owns your control flow. Both exports are
runtime-neutral; there is no /node adapter.
Design for delivery
Delivery is at least once and unordered. A retry retains its stable
webhook-id, but gets a fresh timestamp and signature. Persist the id under a
unique constraint before applying side effects.
Samva gives each POST 30 seconds. Non-2xx responses and timeouts retry after
approximately 5s, 5m, 30m, 2h, 5h, 10h, and 10h, with jitter.
Retry-After can delay the next attempt. Return 2xx after durably accepting
the event.
Test deliveries are queued through the normal lifecycle. Manual redelivery is a one-shot attempt, not a new retry series. Logs are retained for 90 days. Secret rotation provides 24 hours of overlap, and an endpoint is disabled after five continuous days without a successful delivery.
For exact payloads, see the webhook event catalog.