Skip to main content

sparkpost-webhooks

Receive and authenticate SparkPost event webhooks. Use when setting up SparkPost webhook handlers, debugging SparkPost webhook authentication (Basic Auth, OAuth 2.0 Bearer tokens, or the deprecated X-MessageSystems-Webhook-Token header), parsing the batched `msys` payload, or handling email events like delivery, bounce, click, open, spam_complaint, delay, and list_unsubscribe. SparkPost event webhooks are NOT signed — there is no HMAC and no signature header.

ソース情報

リポジトリ
hookdeck/webhook-skills
ソースの最終更新活動
2026年9月28日 14:28
検出された SKILL.md の言語
英語
スター
94
フォーク
16

インストール方法

デフォルトでは、最初にソースを確認する Prompt が選択されています。直接コマンドに切り替えるか、ローカルコピーをダウンロードすることもできます。

ソースファイルを確認

インストールを決める前に、SKILL.md と SkillsMP に表示されている付属ファイルをお読みください。

ファイルエクスプローラー
22 ファイル

SKILL.md を表示中

SKILL.md
ソースの指示 · 読み取り専用プレビュー
name
sparkpost-webhooks
description
Receive and authenticate SparkPost event webhooks. Use when setting up SparkPost webhook handlers, debugging SparkPost webhook authentication (Basic Auth, OAuth 2.0 Bearer tokens, or the deprecated X-MessageSystems-Webhook-Token header), parsing the batched `msys` payload, or handling email events like delivery, bounce, click, open, spam_complaint, delay, and list_unsubscribe. SparkPost event webhooks are NOT signed — there is no HMAC and no signature header.
license
MIT
metadata
{"author":"hookdeck","version":"0.1.0","repository":"https://github.com/hookdeck/webhook-skills"}
# SparkPost Webhooks ## When to Use This Skill - How do I receive SparkPost webhooks? - How do I authenticate SparkPost webhooks? (there is no signature to verify) - Why is my SparkPost webhook returning 401 / why won't SparkPost create my webhook? - How do I parse the SparkPost `msys` batch payload? - How do I handle `delivery`, `bounce`, `click`, `open`, `spam_complaint` events? - What is `X-MessageSystems-Batch-ID` and how do I deduplicate batches? - How do I handle SparkPost relay webhooks (inbound email)? ## Critical: SparkPost Event Webhooks Are Not Signed There is **no HMAC, no signature header, and no signing secret**. Do not look for one — any code that computes `createHmac` / `hmac.new` for a SparkPost event webhook is wrong. Authentication is credential-based and **optional**, set by the webhook's `auth_type` field, whose values are exactly `none` | `basic` | `oauth2`. From SparkPost's "Event Webhook Authentication and Security" doc: *"The authentication method is set to 'None' by default when creating a new webhook."* | Mode | `auth_type` | What SparkPost sends | |------|-------------|----------------------| | **Basic Auth** (recommended) | `basic` | `Authorization: Basic base64(username:password)` | | OAuth 2.0 client credentials | `oauth2` | `Authorization: Bearer {token}` | | Legacy token (deprecated) | — (`auth_token` field) | `X-MessageSystems-Webhook-Token: <token>` | | None (default) | `none` | nothing | Your handler must **fail closed**: if no credentials are configured, reject. Never accept unauthenticated batches silently. > Not to be confused with **Bird's new platform webhooks** (bird.com). SparkPost is now > owned by Bird, and the old `www.sparkpost.com/docs/tech-resources/webhook-authentication/` > URL 301-redirects to Bird's docs — but that is a different product with its own Standard > Webhooks signing (`webhook-id` / `webhook-timestamp` / `webhook-signature`, `whsec_` > secrets, `email.delivered`-style event names). None of that applies here. ## Authentication (core) Basic Auth is the primary path. Parse the header, split on the **first** colon only, and compare both halves in constant time: ```javascript const crypto = require('crypto'); // Fixed-length digest compare: safe even when the two strings differ in length. const eq = (a, b) => crypto.timingSafeEqual( crypto.createHash('sha256').update(a, 'utf8').digest(), crypto.createHash('sha256').update(b, 'utf8').digest() ); function verifyBasicAuth(authorizationHeader, username, password) { if (!authorizationHeader || username === undefined) return false; // fail closed const [scheme, encoded] = authorizationHeader.split(' '); if (!encoded || scheme.toLowerCase() !== 'basic') return false; const decoded = Buffer.from(encoded, 'base64').toString('utf8'); // Malformed base64 decodes to something without a colon — reject. const colon = decoded.indexOf(':'); if (colon === -1) return false; // Split on the FIRST colon only: passwords may contain colons. const user = decoded.slice(0, colon); const pass = decoded.slice(colon + 1); // `password` is NOT required by SparkPost's API — an empty password is legitimate. return eq(user, username) && eq(pass, password ?? ''); } ``` > **For complete handlers with tests**, see [examples/express/](examples/express/), [examples/nextjs/](examples/nextjs/), [examples/fastapi/](examples/fastapi/). The examples accept **either** a valid Basic header **or** `Authorization: Bearer <token>` (OAuth 2.0), plus the optional legacy `X-MessageSystems-Webhook-Token`. They also include a minimal demo `POST /oauth/token` endpoint. See [references/verification.md](references/verification.md). There is **no SDK verify helper** — the `sparkpost` npm and `sparkpost` PyPI clients manage webhook configuration but have no receive/authenticate function. Use built-in crypto (`crypto.timingSafeEqual`, `hmac.compare_digest`) as above. ## Payload: a JSON Array of `msys`-Wrapped Events SparkPost POSTs a **batch** — a JSON array. Each element has a single `msys` key wrapping **one event-class object**, which carries the `type` field: ```json [ { "msys": { "message_event": { "type": "delivery", "event_id": "92356927693813856", "message_id": "000443ee14578172be22", "timestamp": "1460989507" } } }, { "msys": { "track_event": { "type": "click", "target_link_url": "http://example.com" } } } ] ``` Read the **single key under `msys`** — do not hardcode `message_event` — then switch on `type`. | Wrapper key | Event types | |-------------|-------------| | `message_event` | `bounce`, `delivery`, `injection`, `spam_complaint`, `out_of_band`, `policy_rejection`, `delay`, `sms_status` | | `track_event` | `click`, `open`, `initial_open`, `amp_click`, `amp_open`, `amp_initial_open` | | `gen_event` | `generation_failure`, `generation_rejection` | | `unsubscribe_event` | `list_unsubscribe`, `link_unsubscribe` | | `relay_event` | `relay_injection`, `relay_rejection`, `relay_delivery`, `relay_tempfail`, `relay_permfail` | | `ab_test_event` | `ab_test_completed`, `ab_test_cancelled` | | `ingest_event` | `success`, `error` | **Most scalar fields are strings even when numeric**: `"timestamp": "1460989507"` (Unix **seconds** as a string), `"num_retries": "2"`, `"bounce_class": "1"`, `"subaccount_id": "101"`. `event_id` format is not consistent across event types (a large integer for some, a UUID for others) — treat it as an opaque string. ## The Test / Validation Batch When a webhook is created (and when its target URL changes) SparkPost sends a test POST. *"If this request does not receive an HTTP 200 response, your request to the Webhook API will fail with HTTP 400 and the webhook will not be created."* `POST /api/v1/webhooks/{id}/validate` sends the documented sample batch: ```json [ { "msys": {} } ] ``` An array whose element has an **empty `msys` object** — no event class at all. Your handler must accept it and return 200 rather than throwing on the missing event key. There is no "ping" event type. ## Environment Variables ```bash # Basic auth (auth_type: "basic") — PRIMARY. # These are credentials YOUR endpoint defines, NOT your SparkPost login. SPARKPOST_WEBHOOK_USERNAME="basicauthuser" SPARKPOST_WEBHOOK_PASSWORD="a-long-random-string" # may be empty: password is not required # OAuth 2.0 (auth_type: "oauth2") — optional; used by the demo token endpoint. SPARKPOST_OAUTH_CLIENT_ID="CLIENT123" SPARKPOST_OAUTH_CLIENT_SECRET="9sdfj791d2bsbf" # Legacy X-MessageSystems-Webhook-Token (deprecated; also used by relay webhooks) — optional. SPARKPOST_WEBHOOK_TOKEN="existing-webhook-token" ``` ## Delivery, Retries, and Idempotency - **Respond 200.** The create/validate test explicitly requires 200, and any non-2xx is retried. - **Timeout: 10 seconds** per batch posting attempt. - **Retries** increase logarithmically and stop after **8 hours** — *"12 total attempts will be made to POST the webhook batch (the initial attempt + 11 retries)"*. - **Batch size** *"may vary from 1 to 350 or more events"*, and a batch can mix event types. - Webhooks only POST to **ports 80 and 443**. Events begin flowing **~1 minute** after creation. - Store the raw batch, respond 200, then process asynchronously: *"if you do not return a 200 for the batch we will continue to resend even if you processed and stored part of the batch"*. - **Deduplicate on `X-MessageSystems-Batch-ID`** (*"useful for detecting and prevention of processing duplicate batches"*) and on each event's unique `event_id`. Look the header up **case-insensitively** — SparkPost's support docs spell it `X-Messagesystems-Batch-Id`. A duplicate batch should still return 200. ## Network Security HTTPS is recommended. **mTLS** *"is in route to deprecation on all regions at May 18th, 2026"* — treat it as going away and don't build on it. For IP allowlisting SparkPost maintains the hostname `wh.egress.sparkpost.com`, *"which lists the egress IPs under the host's A record"*; non-Enterprise customers should allowlist that hostname rather than individual IPs. **Never hardcode an IP list.** `custom_headers` (e.g. `{"x-api-key": "abcd"}`) is sent on every batch POST and SparkPost suggests it as an additional measure — optional, not a substitute for auth. ## Relay Webhooks (Inbound Email) Are a Separate API `/api/v1/relay-webhooks` delivers **inbound email content** as `msys.relay_message`, and its `auth_type` enum is only `none` | `oauth2` (**no Basic Auth**), plus `auth_token` → `X-MessageSystems-Webhook-Token`. Do not confuse `relay_message` (inbound email, relay webhooks) with `relay_event` (`relay_injection` / `relay_delivery` / … status events delivered via **event** webhooks). See [references/overview.md](references/overview.md). ## Local Development For local webhook testing, run the Hookdeck CLI via `npx` — no install required: ```bash npx hookdeck-cli listen 3000 sparkpost --path /webhooks/sparkpost ``` No account required — the CLI creates a guest account on first run and provides a local tunnel plus a web UI for inspecting requests. Note that SparkPost only accepts targets on ports 80/443, so a tunnel (or Hookdeck) is required for local development. > **Using Hookdeck in front of SparkPost:** Hookdeck's `SPARKPOST` source supports **Basic > Auth** only (no OAuth 2.0 token-URL flow). Configure Basic Auth on the SparkPost webhook and > the same credentials on the Hookdeck source. ## Resources - [overview.md](references/overview.md) - Event classes, all event types, payload fields, relay webhooks - [setup.md](references/setup.md) - Create and configure the webhook (app + API), pick an auth mode - [verification.md](references/verification.md) - Basic Auth, OAuth 2.0, legacy token, gotchas, debugging - [examples/express/](examples/express/) - Express handler with tests - [examples/nextjs/](examples/nextjs/) - Next.js App Router handler with tests - [examples/fastapi/](examples/fastapi/) - FastAPI handler with tests ## Recommended: webhook-handler-patterns For production-ready webhook handling, also install the webhook-handler-patterns skill: - [Handler sequence](https://github.com/hookdeck/webhook-skills/blob/main/skills/webhook-handler-patterns/references/handler-sequence.md) - Webhook processing flow - [Idempotency](https://github.com/hookdeck/webhook-skills/blob/main/skills/webhook-handler-patterns/references/idempotency.md) - Prevent duplicate processing (batch IDs and `event_id`) - [Error handling](https://github.com/hookdeck/webhook-skills/blob/main/skills/webhook-handler-patterns/references/error-handling.md) - Graceful error recovery - [Retry logic](https://github.com/hookdeck/webhook-skills/blob/main/skills/webhook-handler-patterns/references/retry-logic.md) - Handle transient failures ## Related Skills - [sendgrid-webhooks](https://github.com/hookdeck/webhook-skills/tree/main/skills/sendgrid-webhooks) - SendGrid email event webhooks with ECDSA verification - [mailgun-webhooks](https://github.com/hookdeck/webhook-skills/tree/main/skills/mailgun-webhooks) - Mailgun email event webhooks with HMAC-SHA256 - [postmark-webhooks](https://github.com/hookdeck/webhook-skills/tree/main/skills/postmark-webhooks) - Postmark email webhooks (Basic Auth / token, no signature) - [resend-webhooks](https://github.com/hookdeck/webhook-skills/tree/main/skills/resend-webhooks) - Resend email webhooks with Svix signatures - [mailchimp-webhooks](https://github.com/hookdeck/webhook-skills/tree/main/skills/mailchimp-webhooks) - Mailchimp list and campaign webhooks - [twilio-webhooks](https://github.com/hookdeck/webhook-skills/tree/main/skills/twilio-webhooks) - Twilio messaging webhooks with signature verification - [webhook-handler-patterns](https://github.com/hookdeck/webhook-skills/tree/main/skills/webhook-handler-patterns) - Idempotency, error handling, retry logic - [hookdeck-event-gateway](https://github.com/hookdeck/webhook-skills/tree/main/skills/hookdeck-event-gateway) - Production webhook infrastructure
GitHubで見る