MailSlurp logo

guides

Testing Webhook Responses for MailSlurp Events

Learn how to test MailSlurp webhooks with deterministic success and failure scenarios, including retry handling and endpoint reliability checks.

Webhook integrations fail most often because teams only test the happy path. This guide focuses on reliability checks you should run before production.

MailSlurp webhook behavior summary

  • MailSlurp sends events to your endpoint via HTTP POST
  • 200/201 within timeout window is treated as success
  • non-success responses and timeouts are retried
  • failed deliveries are queued for retry until processed or exhausted

Test scenarios you should always run

Scenario Expected result Why it matters
Endpoint returns 200 quickly Event marked delivered Confirms baseline integration
Endpoint returns 500 Event is retried Validates retry path
Endpoint times out Event is retried Validates latency/error handling
Endpoint returns malformed response Event handled as failed Confirms parser/contract resilience

Manual testing option

For early local testing:

  • expose local service with ngrok or equivalent
  • send a controlled event
  • verify payload handling and response code

Useful during initial development, but not sufficient as a release gate.

Automated webhook testing option

Use the MailSlurp test webhook helpers to generate deterministic endpoints and simulate known response patterns.

Success-path example:

import { CreateWebhookOptionsEventNameEnum, MailSlurp } from "mailslurp-client";
import {
  Configuration as TestWebhookConfiguration,
  CreateRulesetOptionsStrategyEnum,
  EndpointControllerApi,
} from "@mailslurp/test-webhooks";

const apiKey = process.env.API_KEY!!;
const mailslurp = new MailSlurp({ apiKey });
const devhooksEndpointController = new EndpointControllerApi(
  new TestWebhookConfiguration({
    basePath: "http://api.infrahooks.com",
  }),
);

describe.skip("NEW_EMAIL webhooks", () => {
  test("can create NEW_EMAIL webhook and receive successfully", async () => {
    // create an inbox, webhook, and a test endpoint
    const testEndpoint = await devhooksEndpointController.createEndpoint({});
    const inbox = await mailslurp.createInbox();
    const webhook = await mailslurp.webhookController.createWebhook({
      createWebhookOptions: {
        eventName: CreateWebhookOptionsEventNameEnum.NEW_EMAIL,
        url: testEndpoint.url!!,
      },
      inboxId: inbox.id!,
    });
    // can see that endpoint has not received an event
    const endpointHistory = await devhooksEndpointController.getEndpointHistory({
      endpointId: testEndpoint.id!,
    });
    expect(endpointHistory.items?.length).toEqual(0);
    // send email to inbox
    await mailslurp.sendEmail(inbox.id!, {
      to: [inbox.emailAddress!],
      subject: "email1",
    });
    // can fetch the email directly
    const email = await mailslurp.waitForLatestEmail(inbox.id!, 60000, true);
    expect(email.subject).toEqual("email1");
    // endpoint receives the payload (note the expected length to wait for)
    const endpointHistory2 = await devhooksEndpointController.getEndpointHistory({
      endpointId: testEndpoint.id!,
      expectedLength: 1,
    });
    expect(endpointHistory2.items?.length).toEqual(1);

    // assert correct payload was sent to endpoint
    const payload = JSON.parse(endpointHistory2.items?.[0]?.request?.body!);
    expect(payload.webhookId).toEqual(webhook.id);
    expect(payload.eventName).toEqual("NEW_EMAIL");
    expect(payload.inboxId).toEqual(inbox.id);
    expect(payload.emailId).toEqual(email.id);
    expect(payload.to).toEqual([inbox.emailAddress]);
    expect(payload.from).toEqual(inbox.emailAddress);
    expect(payload.subject).toEqual("email1");

    // can see webhook results via mailslurp
    const results = await mailslurp.webhookController.getWebhookResults({
      webhookId: webhook.id!,
    });
    expect(results.numberOfElements).toEqual(1);
    const result = await mailslurp.webhookController.getWebhookResult({
      webhookResultId: results.content?.[0]?.id!,
    });
    expect(result.resultType).toEqual("SUCCESS");
    expect(result.responseStatus).toEqual(200);

    await mailslurp.webhookController.deleteWebhook({
      inboxId: inbox.id!,
      webhookId: webhook.id!,
    });
  });
});

Failure-path example:

test.skip("can create NEW_EMAIL webhook and see failed results when endpoint fails to accept payload", async () => {
  // create a test endpoint that always returns a 401 error
  const testEndpoint = await devhooksEndpointController.createEndpoint({});
  await devhooksEndpointController.createEndpointRuleset({
    endpointId: testEndpoint.id!,
    createRulesetOptions: {
      strategy: CreateRulesetOptionsStrategyEnum.SINGULAR,
      responses: [
        {
          statusCode: 401,
        },
      ],
    },
  });

  // create inbox and webhook
  const inbox = await mailslurp.createInbox();
  const webhook = await mailslurp.webhookController.createWebhook({
    inboxId: inbox.id!,
    createWebhookOptions: {
      eventName: CreateWebhookOptionsEventNameEnum.NEW_EMAIL,
      url: testEndpoint.url!!,
    },
  });

  // send email to inbox
  await mailslurp.sendEmail(inbox.id!, {
    to: [inbox.emailAddress!],
    subject: "email2",
  });

  // wait for endpoint to receive payload
  const endpointHistory = await devhooksEndpointController.getEndpointHistory({
    endpointId: testEndpoint.id!,
    expectedLength: 1,
  });
  expect(endpointHistory.items?.length).toEqual(1);

  // can see webhook results via mailslurp
  const results = await mailslurp.webhookController.getWebhookResults({
    webhookId: webhook.id!,
  });
  expect(results.numberOfElements).toEqual(1);
  const result = await mailslurp.webhookController.getWebhookResult({
    webhookResultId: results.content?.[0]?.id!,
  });
  expect(result.resultType).toEqual("BAD_RESPONSE");
  expect(result.responseStatus).toEqual(401);

  await mailslurp.webhookController.deleteWebhook({
    inboxId: inbox.id!,
    webhookId: webhook.id!,
  });
});

Production hardening checklist

  • enforce idempotency for repeated event deliveries
  • log request IDs and event IDs for traceability
  • keep response time low (do heavy work asynchronously)
  • add alerting for retry spikes and sustained failures
  • run webhook contract tests in CI
  1. Validate payload structure and signature (if enabled).
  2. Persist the event quickly.
  3. Return 200.
  4. Process downstream asynchronously.

This pattern avoids accidental retries caused by slow business logic.