The gap
The SDK ships verifySignature and constructEvent, so a JavaScript consumer is fine. But the scheme those functions implement is published nowhere:
X-Sendly-Signature and X-Sendly-Timestamp do not appear in the OpenAPI spec (https://api.sendly.now/api/openapi.json, all 57 paths, checked 2026-09-03).
- The webhooks guide at
https://docs.sendly.now/guides/webhooks does not mention them.
- No digest algorithm, no encoding, and no signed-payload construction (what exactly is concatenated, in what order, with what separator) is documented in any of the three places.
The only description is on the /sdks page, which is prose, not a spec.
The practical consequence: a consumer in any language without an official SDK cannot verify a Sendly webhook at all. They have to read sendly-js's source and reverse-engineer the construction — and if the scheme is ever versioned or rotated, their handler breaks with no notice. That is a security surface, not a convenience gap: the alternative most people pick under time pressure is not verifying.
Suggested fix
Document the scheme where it belongs — the webhooks guide, plus a section in this repo's README since this is where people land when they go looking for the implementation:
- header names and their exact formats
- the algorithm (HMAC-SHA256, presumably) and encoding (hex/base64)
- the signed payload string, spelled out character by character
- the timestamp tolerance the SDK enforces and why
- whether multiple signatures can appear (key rotation), and how a verifier should handle that
- a worked example with a fixed secret, payload and expected digest, so an implementation in any language can self-check
A test vector is the highest-value single item there — it is what lets a Go or Ruby consumer confirm they got it right without a live send.
Related
The event-name vocabulary has the same shape of problem: POST /api/webhooks accepts email.sent, email.delivered, email.opened, email.clicked, email.bounced, email.complained, email.failed, contact.created, contact.unsubscribed, contacts.bulk_created, while the webhooks guide documents email.delivery, email.open, email.click, email.bounce, email.complaint, contact.subscribed. A reader who follows the guide subscribes to events that are never sent. Filing here because the SDK's event types are the closest thing to a canonical list.
Context
Found while migrating a production app onto Sendly. Verified 2026-09-03.
The gap
The SDK ships
verifySignatureandconstructEvent, so a JavaScript consumer is fine. But the scheme those functions implement is published nowhere:X-Sendly-SignatureandX-Sendly-Timestampdo not appear in the OpenAPI spec (https://api.sendly.now/api/openapi.json, all 57 paths, checked 2026-09-03).https://docs.sendly.now/guides/webhooksdoes not mention them.The only description is on the
/sdkspage, which is prose, not a spec.The practical consequence: a consumer in any language without an official SDK cannot verify a Sendly webhook at all. They have to read
sendly-js's source and reverse-engineer the construction — and if the scheme is ever versioned or rotated, their handler breaks with no notice. That is a security surface, not a convenience gap: the alternative most people pick under time pressure is not verifying.Suggested fix
Document the scheme where it belongs — the webhooks guide, plus a section in this repo's README since this is where people land when they go looking for the implementation:
A test vector is the highest-value single item there — it is what lets a Go or Ruby consumer confirm they got it right without a live send.
Related
The event-name vocabulary has the same shape of problem:
POST /api/webhooksacceptsemail.sent, email.delivered, email.opened, email.clicked, email.bounced, email.complained, email.failed, contact.created, contact.unsubscribed, contacts.bulk_created, while the webhooks guide documentsemail.delivery,email.open,email.click,email.bounce,email.complaint,contact.subscribed. A reader who follows the guide subscribes to events that are never sent. Filing here because the SDK's event types are the closest thing to a canonical list.Context
Found while migrating a production app onto Sendly. Verified 2026-09-03.