Webhook setup, testing and debugging guide

Markdown

View as Markdown

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 WEBHOOK_SECRET = process.env.LMS_WEBHOOK_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', WEBHOOK_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 Tools → Webhooks → Add Webhooks in your admin. The merchant Webhooks tab lets you add, edit, and delete webhooks; the per-delivery log and retry tools described below live in the app developer area.

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 webhook signing secret (the secret returned when you created the subscription — not your OAuth client secret):

const expected = crypto
  .createHmac('sha256', WEBHOOK_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

For app webhooks, every dispatch is recorded in the Webhook Logs screen in the app developer area. Each delivery row shows:

  • Time — when the delivery was dispatched.
  • App — the app the delivery belongs to.
  • Topic — the event topic (e.g. orders/paid).
  • Status — Delivered, Failed, Pending, or Retrying.
  • Response code — the HTTP code your endpoint returned.
  • Duration — how long the request took.

Expand any row to see the full request payload and response body. 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 webhook signing 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 the Webhook Logs screen 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.

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 it from the Webhook Logs screen, or call the relevant REST endpoint to backfill state.

Is there a way to replay a delivery?

Yes — in the app developer Webhook Logs screen, expand the failed delivery and click Retry Delivery. 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.

Was this article helpful?