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 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.


Was this article helpful ?