Skip to content

Mock Responses

Control what your endpoint returns to the sender. Set status codes, headers, bodies with placeholders from the request, delays, and conditional response rules, and preview them against captured requests.

Updated Oct 2026

How it works

By default, endpoints return 200 OK with an empty body. Configure a response to change this.

When a webhook hits your endpoint, the receiver captures the request and then answers. The request is stored first, so it shows up in the dashboard right away; the sender sees your status code, headers and body after any delay you set.

Responses apply to HTTP requests. Emails sent to the endpoint's address are always accepted with a standard mail-server reply.

The Responses section

Open the endpoint's Settings tab and find Responses. It is one list, in the order the receiver checks it:

  1. Rule 1, Rule 2, ...: each rule has conditions and the answer to send when they match. Rules are checked from the top, and the first that matches answers.
  2. Otherwise: the answer for everything no rule matches (Always when there are no rules).

Every answer has the same fields:

  • Status: any HTTP status from 100 to 599.
  • Delay: milliseconds to wait before answering (0 to 30,000). The request is captured at once; only the answer is delayed.
  • Body: JSON, XML, plain text or anything else. It can hold placeholders filled in from the request.
  • Headers: name and value pairs. Values can hold placeholders too. Set-Cookie, Strict-Transport-Security, Content-Security-Policy and X-Frame-Options are never sent.

Under the list, Preview with runs your unsaved settings against a request the endpoint captured (the newest one, or any of the last 20) and shows which rule answers and exactly what it sends, placeholders filled in. The row that answers is marked in the list.

Conditional response rules

How rules work

  1. When a request arrives, the receiver checks your rules from top to bottom, skipping rules that are switched off.
  2. A rule matches when all of its conditions match, or any of them, as you choose.
  3. The first rule that matches answers. Use Check earlier and Check later to change the order.
  4. If no rule matches, Otherwise answers; if nothing is set there either, the endpoint returns 200 OK.

What a condition can check

FieldReadsOperators
MethodPOST, GET, ... (any case)is, is not
Paththe path after the slug, such as /stripe/eventstext operators, matches pattern, matches regex
Headerone header, by name (any case)is present, is missing, text operators, number operators
Query parameterone parameter, by nameis present, is missing, text operators, number operators
Body fielda field of a JSON body, or of a form body, by its pathis present, is missing, text operators, number operators
Body textthe whole body as textcontains, does not contain, matches regex
Content typethe Content-Type header (any case)is, is not, contains, does not contain, starts with, regex
Size in bytesthe body's sizeis, is not, number operators

Text operators are is, is not, contains, does not contain, starts with, ends with, matches pattern and matches regex. Number operators are is more than, is at least, is less than and is at most; they compare numbers, so 2000 is more than 999.

Body field paths use dots: data.object.status reads {"data": {"object": {"status": "active"}}}, and items.0.sku reads the first item of an array. Write data["odd.key"] for a key with a dot in it. For a form body (application/x-www-form-urlencoded), the path is the field's name, such as email.

Matches pattern is a glob over the whole value: * matches within one path segment, ** across segments, and ? one character. /webhooks/*/events matches /webhooks/stripe/events; /webhooks/** matches anything under it.

Matches regex takes an RE2 regular expression and searches anywhere in the value. Start it with (?i) to ignore case. RE2 has no backreferences or lookarounds, and it runs in time linear in the input, so a pattern can never slow the endpoint down.

Example: route by provider

Simulate different responses for different providers on a single endpoint:

Rule 1, Stripe invoices

  • When all match: Header stripe-signature is present and Body field type is invoice.payment_succeeded
  • Answer: 201 with {"received": true, "event": "{{ json.id }}"}

Rule 2, GitHub

  • When: Header x-github-event is present
  • Answer: 200 with {"ok": true}

Otherwise: 200 with {"received": true}

Example: fail large or slow orders

Rule 1

  • When: Body field data.amount is more than 10000
  • Answer: 422 with {"error": "amount_too_large", "amount": {{ json.data.amount }}}

Rule 2

  • When: Query parameter slow is present
  • Answer: 200 after a delay of 8000 ms

Limits

  • At most 50 rules per endpoint and 10 conditions per rule.
  • Patterns and regular expressions: at most 500 characters. Condition values: at most 4,096 characters.

Placeholders

A body or header value can include {{ ... }} placeholders. The receiver fills them in from the request it is answering:

PlaceholderBecomes
{{ json.data.id }}a field of the JSON (or form) body, by path
{{ body }}the whole body as text
{{ headers.user-agent }}a header, by name in any case
{{ query.page }}a query parameter
{{ method }}, {{ path }}the method and the path after the slug
{{ id }}the captured request's id, as shown in the dashboard
{{ receivedAt }}when the request arrived (ISO 8601, UTC)
{{ ip }}, {{ size }}the sender's IP address and the body's size in bytes
{{ endpoint.slug }}the endpoint's slug ({{ endpoint.name }} for its name)
{{ uuid }}a new random UUID, the same one wherever it appears in a response
{{ now }}the current time (ISO 8601, UTC)

Filters change a value: {{ json.note | default: "none" }} uses none when the field is missing or empty, | upper and | lower change case, | truncate: 80 keeps at most 80 characters, and | json writes the value as JSON (a string gets its quotes). Filters chain from left to right: {{ json.name | default: "anonymous" | upper }}.

How a value is written depends on the body:

  • In a JSON body (a Content-Type that mentions JSON, or no Content-Type and a body that starts with { or [), text is escaped for a JSON string, so "{{ json.note }}" stays valid JSON whatever the note holds, and numbers, booleans, objects and arrays are written as JSON: {"echo": {{ json }}, "amount": {{ json.data.amount }}}.
  • In an HTML or XML body, values are HTML-escaped.
  • Anywhere else, including header values, values are written as they are. A header whose value ends up holding a line break is not sent.

A placeholder that leads nowhere (a missing field) becomes empty. A placeholder the request has no field for at all, such as {{ name }}, is sent as written, so bodies meant for another template engine pass through unchanged; the preview lists these in case one is a typo. Use Insert field next to the body to pick a placeholder instead of typing it.

Filling in placeholders never runs code: there are no loops or conditions, and a filled-in body stops at 1 MiB.

SDK and MCP

Rules, answers and placeholders work the same through the API, the SDK and the MCP server:

const client = new WebhooksCC({ apiKey: "whcc_..." });
 
await client.endpoints.create({
  name: "multi-response",
  responseRules: [
    {
      name: "Stripe invoice",
      logic: "and",
      conditions: [
        { field: "header", op: "exists", name: "stripe-signature" },
        { field: "body_path", op: "eq", path: "type", value: "invoice.paid" },
      ],
      response: {
        status: 201,
        body: '{"received": "{{ json.id }}"}',
        headers: { "x-request-id": "{{ id }}" },
      },
    },
  ],
  mockResponse: { status: 200, body: '{"ok": true}', headers: {} },
});

Try settings before saving them: previewResponse runs them against a captured request and returns the rule that answers and the filled-in response, without sending anything.

const preview = await client.endpoints.previewResponse("my-endpoint", {
  responseRules: [
    {
      conditions: [{ field: "body_path", op: "gt", path: "data.amount", value: "10000" }],
      response: { status: 422, body: '{"amount": {{ json.data.amount }}}', headers: {} },
    },
  ],
});
// preview.rule?.index, preview.response?.status, preview.response?.body

Update or clear rules on an existing endpoint:

await client.endpoints.update("my-endpoint", { responseRules: [/* new rules */] });
await client.endpoints.update("my-endpoint", { responseRules: null });

Agents use the update_endpoint and preview_response tools of the MCP server for the same.

Use cases

  • Simulate success: return 200 with a JSON body to satisfy senders that expect a confirmation.
  • Test error handling: return 500 or 503 to check that your sender retries. Mock webhook responses to test retry logic walks through how Stripe, Shopify, and GitHub retry after each kind of failure.
  • Test timeouts: add a delay to check that your sender handles slow answers.
  • Echo what arrived: return {{ body }} or a field such as {{ json.id }} so the sender can match the answer to its request.
  • Mint ids: answer 201 with {"id": "{{ uuid }}"}, like a real API creating a resource.
  • Route by provider or event: different answers for Stripe, GitHub and Shopify, or for each event type, on one endpoint.

Response delay

Set a delay (in milliseconds) to simulate a slow API. The request appears in the dashboard and SSE streams at once; only the answer to the sender waits. Every request gets its own timer, so concurrent requests are handled in parallel. The maximum delay is 30 seconds (30,000 ms); larger values are capped.