Launch special — let's split the check with SPLITCHECK for 50% off
3 min read

How to Test Webhooks in CI/CD Pipelines

Capture real webhooks once, replay them from CI on every PR. End-to-end webhook tests that don't depend on provider sandboxes.

TestingCI/CDWebhooks
O

Ozer

Developer & Founder of HookSense

Webhook handlers are notorious for "passes on my machine" bugs. Real provider events have subtle headers, encoding quirks, and signature schemes that hand-rolled fixtures miss. The fix: capture canonical events once, replay them from CI on every PR.

This guide covers a practical CI/CD setup for webhook testing — capturing fixtures, running replays in pipelines, and asserting on real responses.

The problem with hand-rolled fixtures

Most teams write webhook tests that look like this:

const fakePayload = {
  type: "payment_intent.succeeded",
  data: { object: { amount: 1000 } },
};
const res = await request(app).post("/webhooks/stripe").send(fakePayload);
expect(res.status).toBe(200);

This test passes whether or not signature verification works. It tests your business logic in isolation, not your webhook handling end-to-end. The result: signature bugs slip into production.

The fixture-capture approach

Better: capture real provider events once, commit them to your repo, replay them in CI.

Step 1: capture canonical events

Trigger each event you want to test (payment success, payment failure, subscription created, dispute opened) in the provider's test mode. HookSense captures them with original signatures and headers intact. Export each as JSON.

# In the HookSense UI:
# 1. Trigger event in Stripe test dashboard
# 2. Click the captured request → Export → JSON
# 3. Save as tests/fixtures/stripe-payment-succeeded.json

# Now committed to your repo, this is a canonical example
# that your tests will use forever (until the schema changes).

Step 2: replay from CI

Two patterns. The simpler: POST the fixture body directly to your handler in tests.

import fixture from "./fixtures/stripe-payment-succeeded.json";

test("handles payment success", async () => {
  const res = await request(app)
    .post("/webhooks/stripe")
    .set(fixture.headers)
    .send(fixture.body);

  expect(res.status).toBe(200);
  expect(await getOrderStatus(fixture.body.data.object.metadata.order_id))
    .toBe("paid");
});

The more powerful: use HookSense's CLI replay against a test-environment URL.

# .github/workflows/integration-tests.yml
- name: Replay webhook fixture
  run: |
    npx hooksense replay \
      --request-id req_stripe_payment_success \
      --target ${{ secrets.TEST_ENV_URL }}/webhooks/stripe

# Asserts response status + body against expected.

The CLI approach exercises your full HTTP stack (TLS, load balancer, auth middleware, your handler, your database). The in-process approach is faster but skips infrastructure.

Asserting on responses

Don't stop at "the handler returned 200." Assert the side effects:

  • The database row for the order is updated to paid.
  • The receipt email was queued (mock the email service, assert on the call).
  • The analytics event was fired.
  • Idempotency: replay the same fixture again, confirm side effects don't double.

Handling provider-specific quirks

Stripe: the signature includes a timestamp. Tests against a static fixture from 6 months ago will fail signature verification on the timestamp tolerance check. Either disable the timestamp check in test environments (read your code's tolerance config) or use HookSense's replay to re-sign on the fly with a fresh timestamp.

Shopify: signatures are computed over the raw body bytes. JSON middleware that re-serializes (changing key order) will break verification. Make sure your test setup preserves byte-exact bodies.

Slack: signature includes the URL the webhook was sent to. Replaying against a different URL in tests requires regenerating the signature — or skipping signature verification in tests, which we don't recommend.

What to test vs what to skip

Test in CI:

  • Signature verification with real headers (not mocked).
  • Each event type your handler processes — success, failure, edge cases.
  • Idempotency (replay the same event twice).
  • Database state after the handler runs.

Skip in CI (move to E2E or manual):

  • Triggering events through the provider's API (slow, rate-limited).
  • Testing the provider's retry logic (not your code).

Further reading

Related posts

Try HookSense Free

Inspect, debug, and replay webhooks in real-time. No credit card required.

Get Started Free