Agent skill
Sendoso Webhooks Skill
Receive and verify Sendoso webhooks for corporate gifting and direct mail send status updates. Use when enabling Sendoso webhooks, subscribing an endpoint in the Sendoso webhooks portal (Svix), debugging Svix signature verification (svix-id, svix-timestamp, svix-signature, whsec_ secret), allowlisting Sendoso webhook IPs, or handling send status events like send.delivered, send.shipped, send.redeemed, send.opened, send.bounced, send.undeliverable and send.failed.
Install this skill
npx skills add hookdeck/webhook-skills --skill sendoso-webhooks
Sendoso is a corporate gifting and direct mail platform. Its webhooks push real-time send status updates, such as shipped, delivered, eGift opened or redeemed, to your endpoint. Delivery runs on Svix, so every webhook is signed with the svix-* headers.
When to Use This Skill
- How do I receive Sendoso webhooks?
- How do I turn on Sendoso webhooks? (They're opt-in. Ask Sendoso support.)
- How do I verify a Sendoso webhook signature?
- Why is my Sendoso
svix-signatureverification failing? - How do I handle
send.delivered,send.redeemedorsend.undeliverable? - Which IP addresses do Sendoso webhooks come from?
Enabling Webhooks
Webhooks are off by default. Email developers@sendoso.com to enable them for your organization. Then open Sendoso's integrations page to get your webhooks portal URL. In the portal you add endpoints, choose event types (an endpoint with none selected receives all events), copy each endpoint's whsec_ signing secret, and send test events. See references/setup.md.
Verification (core)
| Header | Value |
|---|---|
svix-id | Unique message id. Same across retries, so use it as your dedupe key |
svix-timestamp | Unix seconds of the delivery attempt |
svix-signature | Space-separated v1,<base64 HMAC-SHA256> entries |
Signed content is {svix-id}.{svix-timestamp}.{raw body}, HMAC-SHA256 keyed with the base64-decoded part of the whsec_ secret after the underscore. The result is base64-encoded. Sendoso says to reject timestamps more than 5 minutes from your clock. Each endpoint has its own secret.
import { Webhook } from 'svix'; // official Svix SDK (v2 is ESM-only)
// rawBody: the exact string received — never re-serialised JSON
function verifySendosoWebhook(rawBody, headers) {
const wh = new Webhook(process.env.SENDOSO_WEBHOOK_SECRET); // "whsec_..."
// Checks the ±5 min timestamp window, tries every v1 signature, constant-time compare.
// Throws WebhookVerificationError → respond 400.
wh.verify(rawBody, {
'svix-id': headers['svix-id'],
'svix-timestamp': headers['svix-timestamp'],
'svix-signature': headers['svix-signature'],
});
return JSON.parse(rawBody); // svix 2.x verify() returns nothing
}
In Python, from svix.webhooks import Webhook, then call Webhook(secret).verify(raw_ and json.loads(raw_body). Python's verify also returns None in svix 2.x. For manual HMAC code, see references/verification.md.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Payload
Sendoso documents the same schema for every event type:
{ "send_gid": "<string>", "status_changed_at": "<ISO 8601 string>" }
The documented body doesn't include the event type, and Svix doesn't send it in a header. To react to specific statuses, subscribe each endpoint only to the events it should handle, or look up the send's current status with the Retrieve All Sends API (match on send_gid). Use status_changed_at to drop stale, out-of-order updates. Before you build on any extra fields, inspect a real message in the portal.
Event Types
All 28 documented events are send.* status changes, mirroring the send tracker:
| Group | Events |
|---|---|
| Shipping / delivery | send.shipped, send.delivered, send.undeliverable, send.fulfilling, send.fulfillment_issue, send.order_received, send.out_of_stock |
| eGift engagement | send.sent, send.clicked, send.opened, send.redeemed, send.expired |
| Lifecycle | send.initiated, send.pending_approval, send.pending_, send.canceled, send.failed, send.refunded, send.bounced, send.insufficient_funds |
| Address confirmation | send.confirming_address, send.confirmation_ |
| Amazon | send.amazon_fulfilling, send.amazon_shipped |
| Recipient email checks | send.blocked, send.email_blacklist, send.invalid_, send.suspicious_email |
Each event's trigger is listed in references/overview.md.
Delivery Semantics
- Return a
2xxwithin 15 seconds. Anything else, including a timeout, counts as a failure. Queue the work and acknowledge fast. - Retries follow this schedule after the first attempt: 5s, 5m, 30m, 2h, 5h, 10h, 10h. If every attempt to an endpoint fails for 5 days, Sendoso disables it. You can resend or recover messages from the portal.
- Deliveries are at least once. Dedupe on
svix-id. - Source IPs (US):
44.228.126.217,50.112.21.217,52.24.126.164,54.148.139.208,2600:1f24:64:8000::/52. Allowlisting them is optional, and you still need to verify signatures.
Environment Variables
SENDOSO_WEBHOOK_SECRET=whsec_xxxxx # per-endpoint secret from the Sendoso webhooks portal
Local Development
# Express / Next.js (use 8000 for FastAPI). No install or account required.
npx hookdeck-cli listen 3000 sendoso --path /webhooks/sendoso
Add the printed URL as an endpoint in the webhooks portal, then use its Testing tab to send an example event.
Reference Materials
- references/overview.md - Every event type, payload, delivery and retry behaviour
- references/setup.md - Enabling webhooks, portal endpoints, secrets, testing, IPs
- references/verification.md - SDK and manual verification, gotchas, debugging