Skip to main content
Outbound webhooks push events out of Senderz. You register an HTTPS endpoint, choose the events you care about, and Senderz sends a signed POST to that endpoint whenever one of them fires. This is the outward direction. For events arriving into Senderz from your store or your email provider, see Inbound webhooks.

What works today

Endpoint management, signed delivery, retries and pause/resume are all live. Two things are worth knowing before you build against this:
  • The signing secret is only returned to the API caller. Neither in-app screen shows it. If you need to verify signatures, create the endpoint through the Public API.
  • There is no delivery log. Senderz records the outcome of every attempt internally, but nothing in the app or the API lets you read it back. Log deliveries on your side.
Some event names in the vocabulary have no producer yet. The tables below say which.

Create an endpoint

There are three ways in. They write to the same place, but they are not equivalent.
This is the best in-app option.
1

Open Integrations

Integrations is a top-level item in the sidebar. It is not inside Settings.
2

Click API · Webhook

The button sits in the page header, on the right. It opens the New webhook panel.
3

Fill in the panel

Give it a Display name, paste the Endpoint URL, and tick the events you want. All event types are listed here.
4

Create

Once you have at least one endpoint, a Custom webhooks section appears further down the page with Pause, Resume, Copy URL and Delete on each card.
The signing secret is not displayed. See The signing secret.
Creating, editing, pausing and deleting an endpoint requires the owner or admin role. Any workspace member can view the list.

The signing secret

The secret is 32 random bytes rendered as a 64-character hex string. It is generated when the endpoint is created and returned in that one response. It is never returned again, and there is no rotate or reveal action. Neither in-app screen renders it. If you create an endpoint from the Integrations page or from Settings, the secret exists on the server and signs every delivery, but you never see it, so you cannot verify the signature. That is the practical reason to create endpoints through the API.
If you have lost a secret, the only recovery is to delete the endpoint and create a new one through the API. Point your receiver at the new endpoint before deleting the old one so you do not drop events in between.

Endpoint URL rules

The URL is checked when you save it, and again immediately before every single delivery. An endpoint must be:
  • HTTPS. Plain http is rejected.
  • On the default port. Any explicit port, including :443, is rejected.
  • A hostname with a dot in it. Bare names like localhost are rejected.
  • Publicly resolvable. The hostname is resolved and every address it returns must be public.
A URL is rejected if any resolved address is a loopback, private, link-local, carrier-grade NAT, multicast or unique-local address, and also if the hostname does not resolve at all. If your DNS returns both a public and a private address for the same name, the endpoint is rejected. Rejections come back as HTTP 400 with the code webhook_url_blocked and the message “A webhook endpoint must be a public HTTPS address on the default port.” The message is deliberately the same for every reason, so it will not tell you which rule you tripped.
Testing locally? Put a public HTTPS tunnel in front of your machine and register the tunnel hostname. A private address will never be accepted, and a tunnel that later resolves to a private address stops delivering.

What a delivery looks like

Every delivery is a POST with Content-Type: application/json and these headers: The body always has the same three top-level keys:
data differs per event family. deliveryId matches the X-Sender-Delivery header.
amount is in the store’s own currency units, not minor units, and it is whatever the store platform reported. Any of these four can be null depending on the platform and the event. externalId is the customer or order identifier from the store.
Contact events raised by a connected store carry the store payload shape instead, matching the order block above.
A failed SMS raised at send time carries messageId, contactId, channel, reason and occurredAt instead.
One delivery per contact, so adding fifty contacts in one API call produces fifty deliveries.
Loyalty events carry vendor, points, tier and rewardCode instead.

Verify the signature

Compute the HMAC over the raw request body bytes, exactly as received. Do not parse the JSON and re-serialise it first: key order and whitespace will differ and the signature will not match.
1

Capture the raw body

Most frameworks parse JSON before your handler runs. Configure a raw or buffered body for this route.
2

Compute and compare

Build sha256= plus the hex HMAC-SHA256 of the raw body using your signing secret, and compare it against X-Sender-Signature in constant time.
3

Respond 2xx fast

Acknowledge first, process afterwards. Anything slower than 10 seconds is treated as a failure and retried.
4

Deduplicate

Delivery is at-least-once. Record deliveryId and ignore one you have already handled.

Event vocabulary

There are 29 subscribable event names. Subscribe to "*" instead of a list to receive all of them.
A wildcard endpoint can also receive event names that are not in this list, such as order.paid from a store connector. Build your receiver to ignore names it does not recognise rather than erroring on them.

Commerce

All ten require a connected store. Which of them you actually receive depends on the platform: not every store sends every lifecycle event. See Integrations overview.

Contacts and lists

Two real gaps here.Bulk import does not fire contact events. A CSV or Google Sheets import writes contacts in bulk and raises no contact.created or contact.updated deliveries. Importing 20,000 rows produces zero webhooks. Only single-contact writes fire.List events only come from the Public API. Adding someone to a list from the Contacts screen, from an automation’s list step, or from a popup does not fire list.subscribed. Only POST and DELETE on the Public API list-member routes do.
contact.unsubscribed covers the email channel only. An SMS-only opt-out arrives as contact.updated.

Messaging

campaign.sent never fires. You can select it in the UI and save it through the API, and no delivery will ever arrive. Do not build a campaign-completion trigger on it. Use the per-message events above, or poll the campaigns list, which carries each campaign’s status.

Reviews

Requires a connected reviews app.

Loyalty

Requires a connected loyalty app.
order.completed, order.refunded, order.fulfilled, order.partially_fulfilled, order.cancelled, shipment.confirmed, shipment.out_for_delivery, shipment.delivered, checkout.started, cart.abandoned, contact.created, contact.updated, contact.unsubscribed, list.subscribed, list.unsubscribed, campaign.sent, email.opened, email.clicked, sms.delivered, sms.failed, review.request_ready, review.submitted, review.question, review.answer, loyalty.points_earned, loyalty.points_expiring, loyalty.tier_changed, loyalty.redemption, loyalty.referral

Retries, timeouts and failure

Each delivery gets up to 5 attempts. Waits between attempts double, starting at 5 seconds: roughly 5s, 10s, 20s, then 40s. After the fifth failed attempt the delivery is dead-lettered and dropped. There is no replay action and no automatic catch-up. Retried:
  • Any response outside the 2xx range, including 4xx.
  • Connection errors.
  • Anything that takes longer than 10 seconds. The request is aborted at that point.
Not retried, failed on the first attempt:
  • A redirect. A 3xx response is never followed. The delivery fails immediately, and it is not resent to the redirect target. Register the final URL, not one that redirects to it.
  • A URL that no longer passes the URL rules. The hostname is re-resolved before every delivery, so an endpoint whose DNS later points at a private address silently stops delivering. Nothing in the app tells you this happened.
  • A paused or deleted endpoint. A delivery already queued when you pause an endpoint is dropped, not held. Pausing loses events for the duration; it does not buffer them.
Deliveries are at-least-once, and a retry after a timeout is the common case: your endpoint may have processed the event and simply answered too slowly. Always deduplicate on deliveryId.

Delivery visibility

Senderz records the outcome of every delivery internally, but there is no delivery log you can read. No screen lists deliveries, no API returns them, and no email tells you an endpoint has started failing. Practically, that means:
  • Log received deliveries on your side, including deliveryId and X-Sender-Event.
  • Alert yourself on a gap in traffic rather than waiting for Senderz to tell you.
  • When you change an endpoint URL, confirm the first delivery arrives before removing the old receiver.

Edit, pause and delete

In the app

Both screens give you the same two actions per endpoint:
  • Pause and Resume. Pausing stops deliveries immediately, including ones already queued for it. Nothing that happened while it was paused is resent when you resume.
  • Delete. Permanent, with no undo.
The Integrations page card menu adds Copy URL.
Neither screen lets you rename an endpoint, change its URL, or change which events it is subscribed to. In the app, those changes mean deleting the endpoint and creating a new one, which also issues a new signing secret.

Over the API

The Public API covers list, create and delete. It has no edit route, so an API-key client changes an endpoint by deleting it and creating a replacement. Create the replacement first, so you do not drop events in the gap.
A signed-in session can edit an endpoint in place with PATCH /api/v1/integrations/webhooks/:id, sending only name, url, events or active. That route needs a logged-in session rather than an API key, so it is not available to integrations authenticating with a workspace key. Changing url re-runs the URL rules, and a rejected URL leaves the old one in place. It will not accept "*" in events, even though create does: a wildcard endpoint can have its name, URL and paused state changed, but not its event list.

Troubleshooting

Work through these in order.
  1. Is the endpoint paused? Check the badge on the card.
  2. Did you subscribe to an event that has no producer? campaign.sent never fires, and profile.created from the Settings dialog is not a real event name.
  3. Does the event need a connected integration? Commerce, review and loyalty events all do.
  4. Are you expecting list.subscribed or contact.created from a path that does not raise them? See the warnings in Contacts and lists.
  5. Does your URL redirect? A 3xx is never followed. Register the final URL.
  6. Has your DNS changed? The hostname is re-checked before every delivery, and an address that is no longer public blocks delivery silently.
Almost always one of three things.
  • You are hashing a re-serialised body. Hash the raw bytes.
  • You are comparing only the digest. The header value includes the sha256= prefix.
  • You created the endpoint in the app, so you never received the secret. Recreate it through the API.
Expected. Delivery is at-least-once, and a slow response that is retried is the usual cause. Deduplicate on deliveryId and make your handler idempotent.
Your URL failed one of the URL rules. The error message is intentionally identical for every reason, so check them all: HTTPS, no explicit port, a dotted public hostname, and a hostname that resolves only to public addresses.
Webhook management sits behind the store integrations feature, which can be switched off for a workspace. If the screen is unavailable, contact support. Viewing the list is open to every member, but creating, editing and deleting need the owner or admin role.
If you would rather not run a receiver at all, Make can consume these events for you and hand them to hundreds of downstream apps with no code.