How to Test Email Webhooks

Test your email-to-webhook pipeline thoroughly before going live. Send real emails, use synthetic payloads, inspect delivery logs, and validate your handler in every environment.

Table of Contents
  1. Overview
  2. Prerequisites
  3. Step-by-Step Instructions
  4. Code Example
  5. Common Pitfalls

Overview

Testing email webhook integrations requires more than a unit test for your handler function. You need to test the full pipeline: from email send to SMTP receipt, MIME parsing, webhook delivery, signature verification, and handler processing. Each step can fail independently.

A thorough testing strategy includes:

  • Unit tests: Test your signature verification and payload parsing functions in isolation
  • Local integration tests: Run your webhook handler locally with a tunnel and send real emails to a test JsonHook address
  • Synthetic payload tests: Use the JsonHook API to deliver a synthetic payload to your endpoint without sending a real email
  • Production smoke tests: After deployment, send a canary email through the production pipeline to confirm end-to-end health

Prerequisites

Testing infrastructure you need:

  • A separate JsonHook address for testing (do not use production addresses for test emails)
  • A tunnel tool for local testing: ngrok, Cloudflare Tunnel, or Tunnelmole
  • An email client or curl with SMTP access for sending test emails
  • Your test framework of choice (Jest, Pytest, Go testing, RSpec, etc.) for unit tests

Test Your Email Webhook Integration

Synthetic payloads, delivery logs, and replay — all included. Start free.

Get Free API Key

Step-by-Step Instructions

Build a comprehensive testing workflow:

  1. Unit test signature verification:
    test("verifySignature accepts valid signature", () => {
      const body = Buffer.from('{"email":{"from":"[email protected]"}}');
      const secret = "test_secret";
      const sig = crypto.createHmac("sha256", secret).update(body).digest("hex");
      expect(verifySignature(body, secret, sig)).toBe(true);
    });
    
    test("verifySignature rejects tampered body", () => {
      const body = Buffer.from('{"email":{"from":"[email protected]"}}');
      const sig = "invalid_signature";
      expect(verifySignature(body, "secret", sig)).toBe(false);
    });
  2. Set up local webhook testing:
    ngrok http 3000  # Exposes https://abc123.ngrok.io
    Create a test JsonHook address pointing at the ngrok URL.
  3. Use the synthetic payload API to test without sending a real email:
    POST https://api.jsonhook.com/v1/addresses/{id}/test-delivery
    Authorization: Bearer YOUR_API_KEY
    This delivers a realistic synthetic payload to your webhook.
  4. Send real test emails covering edge cases: HTML-only, with attachments, with UTF-8 subjects, from addresses with display names.
  5. Check delivery logs after each test to confirm delivery success and response time.

Code Example

Unit tests for a JsonHook webhook handler in Jest/TypeScript:

import crypto from "crypto";
import { verifySignature, parseEmailPayload } from "./webhook-handler";

const TEST_SECRET = "test_secret_abc123";

function makeSignedRequest(body: object) {
  const raw = Buffer.from(JSON.stringify(body));
  const sig = crypto
    .createHmac("sha256", TEST_SECRET)
    .update(raw)
    .digest("hex");
  return { raw, sig };
}

describe("Signature verification", () => {
  it("accepts valid signature", () => {
    const { raw, sig } = makeSignedRequest({ test: true });
    expect(verifySignature(raw, TEST_SECRET, sig)).toBe(true);
  });

  it("rejects invalid signature", () => {
    const { raw } = makeSignedRequest({ test: true });
    expect(verifySignature(raw, TEST_SECRET, "bad_sig")).toBe(false);
  });
});

describe("Payload parsing", () => {
  it("extracts from, subject, and textBody", () => {
    const payload = {
      email: { from: "[email protected]", subject: "Hello", textBody: "World", attachments: [] },
      deliveryId: "dlv_test",
    };
    const result = parseEmailPayload(payload);
    expect(result.from).toBe("[email protected]");
    expect(result.subject).toBe("Hello");
  });

  it("handles null textBody gracefully", () => {
    const payload = {
      email: { from: "[email protected]", subject: "Hi", textBody: null, htmlBody: "<p>Hi</p>", attachments: [] },
      deliveryId: "dlv_test2",
    };
    const result = parseEmailPayload(payload);
    expect(result.body).toBe("<p>Hi</p>");
  });
});

Common Pitfalls

Testing pitfalls to avoid:

  • Only testing the happy path. Test edge cases: null textBody, HTML-only emails, emails with no subject, very large bodies, emails from addresses with display names and special characters.
  • Testing with a fixed payload structure. Real email payloads vary. Test with actual emails sent from Gmail, Outlook, and automated sending tools — each produces slightly different MIME structures that affect the parsed JSON.
  • Not testing signature verification failures. Ensure your handler returns 401 for invalid signatures and does NOT process the payload. A handler that logs an error but returns 200 for invalid signatures is a security hole.
  • Sharing test and production addresses. Always use separate JsonHook addresses for testing to avoid test emails polluting your production logs and triggering production side effects.
  • Not testing retry behavior. Simulate a retry by having your handler return 500 on the first call and 200 on the second. Verify idempotency — the email should be processed exactly once.

Frequently Asked Questions

Can I send a test payload to my webhook without sending a real email?

Yes. Use the POST /v1/addresses/{id}/test-delivery API endpoint to deliver a synthetic email payload to your configured webhook URL. This is useful for testing handler logic, verifying connectivity, and CI/CD pipeline checks where sending real email is impractical.

How do I test locally if my webhook runs on localhost?

Use a tunnel tool: run ngrok http 3000 and use the generated HTTPS URL as your JsonHook webhook URL during local development. The tunnel forwards requests from JsonHook's servers to your local server in real time.

How do I test signature verification without a live JsonHook address?

Generate a test HMAC signature in your test code using a known secret and a test payload body, then pass it to your verification function. This lets you unit-test the verification logic independently of JsonHook's infrastructure. See the code example above for a pattern that works in any test framework.

Should I have separate JsonHook addresses for staging and production?

Yes. Create at least two addresses per email flow: one pointing at your staging webhook and one pointing at your production webhook. This lets you test changes in staging with real email traffic before deploying to production without any risk of staging test emails triggering production side effects.