Webhook Verification
Every webhook LaunchMyStore delivers is signed with HMAC-SHA256 using your app’s client secret as the key. Your endpoint MUST verify this signature before trusting the payload — otherwise anyone who knows your callback URL can post forged events to it. This page covers:- The exact signing algorithm and which body is signed
- All headers sent with the request
- Verification examples in Node.js, Python, Ruby, PHP, and Go using timing-safe comparison
- Retry semantics — when LaunchMyStore re-attempts a failed delivery
Signature spec
The signature is computed exactly as:client_secretis the app’s secret returned by/apps/credentialson install. Treat it like a password — never embed it in client-side code.raw_request_bodyis the exact bytes of the HTTP request body as it appears on the wire. Do not parse-and-reserialize the JSON before computing — a single whitespace difference fails the check.base64is standard (not URL-safe) base64 with+//and padding.
X-LMS-Hmac-SHA256 header (see below).
Headers
Every webhook request includes these headers:| Header | Description |
|---|---|
X-LMS-Hmac-SHA256 | Base64-encoded HMAC-SHA256 signature. |
X-LMS-Topic | The webhook topic (e.g. orders/create). |
X-LMS-Shop-Domain | The store’s domain slug. |
X-LMS-API-Version | API version used to shape the payload. |
X-LMS-Webhook-Id | Globally-unique delivery id — use for idempotency. |
X-LMS-Delivery-Attempt | 1 on first try, 2 and 3 on retries. |
X-LMS-Triggered-At | ISO 8601 timestamp when the source event fired (not the delivery time). |
Content-Type | Always application/json. |
Verification examples
All examples below:- Read the raw body before any JSON parsing happens.
- Compute the same HMAC.
- Compare to the header using timing-safe comparison (not
==).
Common verification pitfalls
Parsing JSON before signing
Parsing JSON before signing
The HMAC is computed over the raw bytes of the body. If your
framework parses JSON and your handler re-serializes it,
whitespace, key order, and floating-point formatting differences
will fail the check. Always grab the raw body first.
Using == instead of timing-safe compare
Using == instead of timing-safe compare
Plain string comparison short-circuits on the first mismatched
byte, leaking the prefix of a valid signature to an attacker who
can measure response time. Use
crypto.timingSafeEqual (Node),
hmac.compare_digest (Python), hash_equals (PHP),
secure_compare (Ruby), or hmac.Equal (Go).Comparing buffers of different length
Comparing buffers of different length
Node’s
crypto.timingSafeEqual throws if the inputs differ in
length. Guard with an early length check and return false —
don’t let the exception propagate as a 500.Decoding the base64 before comparing
Decoding the base64 before comparing
The signature transmitted in the header is base64. Compare the
base64 strings directly — don’t decode them to bytes first
(works, but is two more lines of error-prone code).
Trusting the body when the header is missing
Trusting the body when the header is missing
If
X-LMS-Hmac-SHA256 is not present, reject with 401. Never
assume an unsigned request is legitimate.Idempotency
LaunchMyStore retries on non-2xx responses (see below). Implement idempotency keyed onX-LMS-Webhook-Id to avoid double-processing
when a retry races your slow first response:
Retry semantics
If your endpoint returns a non-2xx response (or fails to respond within the 10-second timeout), LaunchMyStore retries automatically.| Attempt | Status | Delay before next attempt | Notes |
|---|---|---|---|
| 1 | First send | — | Immediate on event. |
| 2 | First retry | 60 seconds ± 10% | Jitter prevents retry storms. |
| 3 | Second retry | 300 seconds (5 min) | ± 10% jitter. |
| 4 | Third (final) retry | 900 seconds (15 min) | ± 10% jitter. If this fails, delivery is marked FAILED. |
X-LMS-Delivery-Attempt header (1, 2, 3, 4).
Status codes and retry behaviour
- 2xx: Delivery marked
SUCCESS. No further attempts. - 429 / 5xx: Delivery retried up to
MAX_RETRIES(3) using the schedule above. After exhaustion:FAILED. - 4xx (except 429): Treated as a permanent client error. No
retry. The webhook is marked
FAILEDimmediately. If your endpoint returns400for a transient parse error, you will silently miss that event — return5xxfor transient errors so the retries run. - Network error / timeout: Treated the same as a transient failure and retried.
Timeout
Each delivery attempt has a 10-second HTTP timeout on LaunchMyStore’s side. If your handler doesn’t respond in 10 seconds the attempt is recorded as a network error and a retry is scheduled. Keep your handler fast — ack quickly, do work asynchronously:IP allowlist
LaunchMyStore does not publish a stable allowlist of source IPs for webhook delivery — the delivery worker runs in a cluster whose egress can rotate. Rely on the HMAC signature, not IP filtering, to authenticate webhooks. If your network forces source-IP allowlists, contact LaunchMyStore support to discuss a fixed-egress arrangement (typically only granted onenterprise tier).
Testing webhooks
Trigger a test delivery to your registered endpoint:See also
Webhooks Overview
Subscribe to topics, register endpoints, manage subscriptions.
Topics
The 31 supported topics and their payload shapes.
Authentication
OAuth flow and where the client secret comes from.
API Rate Limits
Limits on outbound API calls — webhooks are zero-cost on your side.