Agent skill
Nitrosend Webhooks Skill
Receive and verify Nitrosend webhooks, which follow the Standard Webhooks specification. Use when setting up a Nitrosend webhook endpoint, getting the whsec_ signing secret, debugging webhook-id / webhook-timestamp / webhook-signature verification, or handling events like email.delivered, email.bounced, email.complained, email.received, campaign.completed and sending.limit_reached.
Install this skill
npx skills add hookdeck/webhook-skills --skill nitrosend-webhooks
Nitrosend is a transactional and marketing email platform with an agent inbox. It POSTs a signed JSON event to each endpoint you register (up to 10 per brand) when a transactional email is sent, delivered, bounced, complained about, opened, clicked or fails, when a reply reaches one of your inboxes, and when account state changes (campaigns, sending limits, imports, domains). Signatures follow Standard Webhooks, so each delivery carries webhook-id, webhook-timestamp and webhook-signature headers.
When to Use This Skill
- How do I receive Nitrosend webhooks?
- How do I verify a Nitrosend webhook signature (
webhook-signature)? - Where do I find my Nitrosend webhook secret (
whsec_...)? - How do I handle
email.delivered,email.bounced,email.complainedoremail.receivedevents? - How do I react to
sending.limit_reached,domain.verification_lostorwebhook.disabled? - Why is my Nitrosend webhook verification failing, or failing only on retries and replays?
Verification (core)
| Header | Value |
|---|---|
webhook-id | evt_...; retries reuse it. Your idempotency key |
webhook-timestamp | Unix seconds |
webhook-signature | One or more space-separated v1,<base64> entries |
Signed content is {webhook-id}.{webhook-timestamp}.{raw body}, HMAC-SHA256, base64 output. The key is the base64-decoded part of the whsec_... secret after the prefix. Compare against every v1, entry; ignore other versions. If webhook-id is absent, read svix-id, svix-timestamp and svix-signature instead, as Hookdeck does. The examples use the standardwebhooks library (npm and PyPI):
const { Webhook } = require('standardwebhooks');
// rawBody: exact request body (Buffer or string), NOT re-serialised JSON
function verifyNitrosend(rawBody, headers, secret = process.env.NITROSEND_WEBHOOK_SECRET) {
const p = headers['webhook-id'] ? 'webhook' : 'svix'; // fall back to the svix-* names
new Webhook(secret).verify(rawBody, {
'webhook-id': headers[`${p}-id`],
'webhook-timestamp': headers[`${p}-timestamp`],
'webhook-signature': headers[`${p}-signature`],
}, { jsonParse: false }); // throws on a bad signature or stale timestamp → respond 401
}
The library always enforces a fixed 5-minute timestamp window, which is also what Nitrosend tells you to apply. Hookdeck retries and replays resend the original webhook-timestamp, so the library rejects them once they are more than 5 minutes old. If you receive through Hookdeck and need late retries to pass, use the manual verifier in references/verification.md, which can drop or widen the window. In Express, read the body with express.raw({ type: '*/*' }) so any content type verifies, and parse JSON only after the check.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Event Type and Payload
Every body is { type, timestamp, data }. The event name is the top-level type (dotted, such as email.bounced), timestamp is when the event happened, and the event fields sit under data:
{
"type": "email.bounced",
"timestamp": "2026-09-30T10:00:00.000Z",
"data": {
"message_id": 48213,
"to": "ada@example.com",
"subject": "Reset your password",
"idempotency_key": "reset-8841",
"tags": { "user_id": "42" },
"bounce": { "type": "hard", "subtype": "NoEmail" }
}
}
data.bounce.type (hard) is the bounce kind, not the event type. Test events sent from the dashboard or POST /v1/my/webhooks/{id}/test carry "test": true in data with null ids.
Common Event Types
| Event | Sent when |
|---|---|
email.sent | The provider accepted the message |
email.delivered | The recipient's mail server accepted it |
email.bounced | It bounced, hard or soft (data.bounce) |
email.complained | The recipient marked it as spam |
email.opened | First open (scanners and bots don't count) |
email.clicked | First human click of each link |
email.failed | It failed for good |
email.received | A reply, automatic reply or new mail reached one of the brand's inboxes |
campaign.completed / campaign.failed / campaign.stopped | A campaign finished, ended before reaching anyone, or was paused for a high bounce or complaint rate |
flow.paused | A flow was paused for a high bounce or complaint rate |
sending.paused | All sending on the account was paused |
sending.limit_ / sending.limit_reached | Most of, or all of, the rolling 24-hour sending allowance is used |
import.completed / import.failed | A contact import finished or failed |
domain.verified / domain.verification_lost / domain.dns_degraded | A sending domain passed verification, lost it, or its DNS could not be confirmed |
webhook.disabled | Nitrosend turned off a webhook after 3 days of failures |
These 21 are the documented events; the list may not be exhaustive, so ignore types you don't recognise. See references/overview.md.
Delivery Semantics
Return any 2xx within 10 seconds. Another status, a timeout or a redirect (not followed) is a failed attempt, retried after 1 min, 5 min, 30 min, 2 h, 8 h and 24 h, then marked failed. After 3 days of continuous failure the endpoint is turned off, the account owner is emailed and a webhook.disabled event is sent; events that occur while it is off are never sent, even after you re-enable it. Events can repeat and a retry can arrive after later events: deduplicate on webhook-id and order by timestamp. Return 400 for missing headers and 401 for a bad signature.
Environment Variables
NITROSEND_WEBHOOK_SECRET=whsec_xxxxx # full value returned when the webhook is created; handlers fail closed (500) without it
Local Development
# Express / Next.js (use 8000 for FastAPI). No install or account required.
npx hookdeck-cli listen 3000 nitrosend --path /webhooks/nitrosend
Register the printed URL under Settings > Webhooks, then choose Send test event to deliver a signed event.
Reference Materials
- references/overview.md - Event catalog, envelope and payload fields
- references/setup.md - Create an endpoint, find the secret, test
- references/verification.md - Library and manual verification, gotchas, debugging