# Sendoso Webhooks

[Sendoso](https://sendoso.com) 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](https://github.com/hookdeck/webhook-skills/blob/main/skills/sendoso-webhooks/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.

```javascript
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](https://github.com/hookdeck/webhook-skills/blob/main/skills/sendoso-webhooks/references/verification.md).

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/sendoso-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/sendoso-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/sendoso-webhooks/examples/fastapi/).

## Payload

Sendoso documents the same schema for every event type:

```json
{ "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](https://developer.sendoso.com/rest-api/reference/sends/get-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_sendoso_approval`, `send.canceled`, `send.failed`, `send.refunded`, `send.bounced`, `send.insufficient_funds` |
| Address confirmation | `send.confirming_address`, `send.confirmation_canceled` |
| Amazon | `send.amazon_fulfilling`, `send.amazon_shipped` |
| Recipient email checks | `send.blocked`, `send.email_blacklist`, `send.invalid_email_format`, `send.suspicious_email` |

Each event's trigger is listed in [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/sendoso-webhooks/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

```bash
SENDOSO_WEBHOOK_SECRET=whsec_xxxxx   # per-endpoint secret from the Sendoso webhooks portal

```

## Local Development

```bash
# 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](https://github.com/hookdeck/webhook-skills/blob/main/skills/sendoso-webhooks/references/overview.md) - Every event type, payload, delivery and retry behaviour
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/sendoso-webhooks/references/setup.md) - Enabling webhooks, portal endpoints, secrets, testing, IPs
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/sendoso-webhooks/references/verification.md) - SDK and manual verification, gotchas, debugging