# Webhook setup, testing and debugging guide

WEBHOOK SETUP, TESTING AND DEBUGGING GUIDE

You’ve picked a topic, written a handler, deployed it to a server. Now what?
This guide walks through a complete webhook integration from local testing to
verifying HMAC signatures, inspecting delivery logs, and recovering from failed
deliveries.


STEP 1: BUILD YOUR HANDLER

The simplest webhook handler reads the body, verifies the signature, queues a
job, and returns 200. Here’s an Express example:

const express = require('express');
const crypto = require('crypto');
const app = express();

const PORT = 4000;
const APP_SECRET = process.env.LMS_APP_SECRET;

app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
  const topic = req.get('X-LMS-Topic');
  const deliveryId = req.get('X-LMS-Webhook-Id');
  const hmacHeader = req.get('X-LMS-Hmac-SHA256');

  const expected = crypto
    .createHmac('sha256', APP_SECRET)
    .update(req.body)
    .digest('base64');

  if (hmacHeader !== expected) {
    return res.status(401).send('invalid signature');
  }

  // Idempotency: skip if we’ve already processed this delivery
  // (look up deliveryId in your DB, return 200 if found)

  res.status(200).send('ok');
  // Queue background processing here
});

app.listen(PORT);

Important: the body MUST be the raw bytes — not the parsed JSON object — or the
HMAC won’t match.


STEP 2: TUNNEL YOUR LOCAL SERVER

LMS needs to call a publicly reachable URL. While testing, use a tunnel like
ngrok or cloudflared:

ngrok http 4000
# → https://abc123.ngrok-free.app

cloudflared tunnel --url http://127.0.0.1:4000
# → https://xxxx.trycloudflare.com

Use the HTTPS URL as your callbackUrl when registering the webhook.


STEP 3: REGISTER THE WEBHOOK

Via the merchant API:

curl -X POST https://api.launchmystore.io/webhooks \
  -H "Authorization: Bearer <merchant-jwt>" \
  -H "Content-Type: application/json" \
  -d '{
    "event": "orders/paid",
    "callbackUrl": "https://abc123.ngrok-free.app/webhooks",
    "isEnabled": true
  }'

Or from Settings → Notifications → Webhooks → Add webhook in your admin.


STEP 4: FIRE A REAL EVENT

The cleanest way to test is to fire a real event:

 * orders/paid — complete a test checkout with the test gateway.
 * products/update — change a product title.
 * customers/create — sign up a customer from the storefront.

Tail your local server log and watch the request arrive.


STEP 5: VERIFY THE HMAC SIGNATURE

For app webhooks, the request includes X-LMS-Hmac-SHA256 — a base64-encoded
HMAC-SHA256 of the raw body using your app secret:

const expected = crypto
  .createHmac('sha256', APP_SECRET)
  .update(rawBody)
  .digest('base64');

const valid = crypto.timingSafeEqual(
  Buffer.from(expected),
  Buffer.from(hmacHeader),
);

Use timingSafeEqual — a regular string compare leaks timing information.


STEP 6: INSPECT THE DELIVERY LOG

Every dispatch is recorded in the delivery log with:

 * Status — PENDING, SUCCESS, RETRYING, FAILED.
 * Attempts — 1, 2, or 3.
 * Response code — the HTTP code your endpoint returned.
 * Response body — the first 2000 chars of the response.
 * Error message — network errors or non-2xx reason.
 * Next retry at — when the next attempt is scheduled (if any).

The log shows exactly what LMS sent and exactly what your server returned — if
HMAC verification fails, this is where you confirm whether you stored the wrong
secret or parsed the body wrong.


COMMON PITFALLS

 * Parsing the body before verifying HMAC. JSON parsing can re-serialise floats
   and reorder keys — verify on the raw bytes, then parse.
 * Returning the wrong status code. Any 4xx (except 429) skips retries. If your
   handler is temporarily overloaded, return 503 or 429 to keep the retry going.
 * Slow handlers. The 10-second timeout is shared across the connection and the
   response. Return 200 immediately, then process async.
 * Sequence assumptions. Don’t assume orders/create arrives before orders/paid.
   Persist whatever state is in each payload and reconcile.


FAQ


HOW DO I VERIFY THE WEBHOOK CAME FROM LAUNCHMYSTORE?

App-scoped webhooks include the X-LMS-Hmac-SHA256 header. Compute HMAC-SHA256 of
the raw body with your app secret and compare with a timing-safe equality check.
If they don’t match, reject with 401.


WHY IS MY WEBHOOK BEING RETRIED — DID MY SERVER RETURN A NON-2XX?

Yes. Anything outside the 200–299 range schedules a retry, except for permanent
4xx errors (400, 401, 403, 404, 422, etc.). 429 is treated as retryable. Look at
the response code column in your delivery log to confirm.


CAN I DISABLE A WEBHOOK TEMPORARILY?

Yes — flip the isEnabled flag from the admin or via the API. Disabled webhooks
stop receiving new events but the existing delivery log is preserved.


WHAT IF I MISS EVENTS WHILE MY SERVER IS DOWN?

Each event is retried 3 times (1m / 5m / 15m). If all attempts fail, the
delivery is marked FAILED. You can re-trigger from the delivery log, or call the
relevant REST endpoint to backfill state.


IS THERE A WAY TO REPLAY A DELIVERY?

Yes — open the failed delivery in the webhook’s log and click Re-send. The same
payload is dispatched again as a new attempt.


CAN I TEST WEBHOOKS AGAINST LOCALHOST WITHOUT A TUNNEL?

Not directly — LMS needs a publicly reachable HTTPS URL. ngrok and cloudflared
are free for development; for production, host on any provider that exposes
HTTPS.