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-signature verification failing?
  • How do I handle send.delivered, send.redeemed or send.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)

HeaderValue
svix-idUnique message id. Same across retries, so use it as your dedupe key
svix-timestampUnix seconds of the delivery attempt
svix-signatureSpace-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_body, headers) 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:

GroupEvents
Shipping / deliverysend.shipped, send.delivered, send.undeliverable, send.fulfilling, send.fulfillment_issue, send.order_received, send.out_of_stock
eGift engagementsend.sent, send.clicked, send.opened, send.redeemed, send.expired
Lifecyclesend.initiated, send.pending_approval, send.pending_sendoso_approval, send.canceled, send.failed, send.refunded, send.bounced, send.insufficient_funds
Address confirmationsend.confirming_address, send.confirmation_canceled
Amazonsend.amazon_fulfilling, send.amazon_shipped
Recipient email checkssend.blocked, send.email_blacklist, send.invalid_email_format, send.suspicious_email

Each event's trigger is listed in references/overview.md.

Delivery Semantics

  • Return a 2xx within 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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Oct 28, 2026

View on GitHub →