Webhook Verification
Every webhook LaunchMyStore delivers is signed with HMAC-SHA256 using a signing secret, and the signature is sent in theX-LMS-Hmac-SHA256
header. Your endpoint MUST verify this signature before trusting the
payload — otherwise anyone who knows your callback URL can post forged
events to it.
Which secret signs the delivery depends on how the webhook was registered:
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:webhook_secretis the signing secret for this webhook — see the table above for which one applies (the per-subscriptionsecretfromPOST /api/v1/webhooks.json, your app’sclientSecretfor manifest-declared and GDPR webhooks, or the per-webhooksecretshown in the merchant admin for store-level webhooks). Treat it like a password — never embed it in client-side code. (TheWEBHOOK_SECRETvariable in the examples below holds whichever of these applies to your webhook.)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: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.
Total: up to 3 retries after the initial attempt = 4 send attempts
spread across roughly 16 minutes. Each attempt’s number is exposed in
the
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).
Re-delivering a past event
There is no synthetic “send a sample payload” test endpoint. What you can do is re-send a real delivery that already happened — useful when your endpoint was down or returned an error and you want to replay the exact same payload (and signature) again. This is a merchant/developer-portal action, authenticated with the merchant or partner session (not an OAuth app access token):{deliveryId} is the id of an existing delivery-log entry (the same
value sent in the X-LMS-Webhook-Id header). The retry re-POSTs the
original payload to your endpoint with the live wire format — same
signing key, same headers, same HMAC computation — so it’s a faithful
way to confirm your verifier handles a real delivery. The endpoint
returns the updated delivery-log record in the standard envelope:
See also
Webhooks Overview
Subscribe to topics, register endpoints, manage subscriptions.
Topics
The 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.