> ## Documentation Index
> Fetch the complete documentation index at: https://help.pipelime.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook testing, delivery rules and record IDs

> Read the test result, understand why a 2xx is not proof, and fix the endpoints we refuse to call.

export const Screenshot = ({id, alt, caption, frame = 'browser', url, marks = [], lang = 'en', workspace = 'northwind-outbound'}) => {
  const src = `/images/screenshots/${lang}/${id}.png`;
  const chrome = url ? `app.pipelime.ai/${workspace}${url}` : 'app.pipelime.ai';
  return <figure className={`pl-shot pl-shot--${frame} not-prose`}>
      <div className="pl-shot__frame">
        {frame !== 'bare' && <div className="pl-shot__bar">
            <span className="pl-shot__dots">
              <span className="pl-shot__dot" />
              <span className="pl-shot__dot" />
              <span className="pl-shot__dot" />
            </span>
            <span className="pl-shot__url">{chrome}</span>
          </div>}
        <div className="pl-shot__media">
          <img src={src} alt={alt} loading="lazy" />
          {marks.map(mark => <span key={mark.n} className="pl-shot__mark" style={{
    left: `${mark.x}%`,
    top: `${mark.y}%`
  }} aria-hidden="true">
              {mark.n}
            </span>)}
        </div>
      </div>
      {caption && <figcaption className="pl-shot__caption">{caption}</figcaption>}
    </figure>;
};

A webhook connector can say a test worked and still deliver nothing, and it can deliver cleanly and still leave a lead unsynced. Both come down to reading the same test result correctly.

<div className="pl-availability">
  <div className="pl-availability__row">
    <div className="pl-availability__label">Where</div>
    <div className="pl-availability__value"><span className="pl-path">Integrations<span className="pl-path__sep">→</span>Integrations<span className="pl-path__sep">→</span>Apps & CRM</span></div>
  </div>

  <div className="pl-availability__row">
    <div className="pl-availability__label">Your role needs</div>
    <div className="pl-availability__value">Read access to Apps & CRM <code>read-integrations</code>. Admin, Member and Viewer have it by default.</div>
  </div>

  <div className="pl-availability__row">
    <div className="pl-availability__label">To create or change</div>
    <div className="pl-availability__value"><code>create-integrations</code> to add one, <code>update-integrations</code> to change one, on top of the permission above.</div>
  </div>

  <div className="pl-availability__note">If you cannot find this in your sidebar, your workspace may have a custom menu configuration. Contact support and we will check it for you.</div>
</div>

## Where this happens

Everything on this page happens in the webhook connect dialog, whether you are [setting up a new connector](/en/integrations/connect-a-webhook) or reopening one you already saved. Choose **Send test** and the result opens as its own window on top of the form. Closing it leaves a one-line recap behind on the form, with **View details** to open it again.

## "Test delivered" isn't proof anything was created

A green **Test delivered** heading means two things and no more: the endpoint answered with a 2xx, and nothing in the reply looked like a declared failure. Under the heading, the message “Your endpoint accepted the test payload.” sits next to an `HTTP 200` chip, and the panel then says the rest itself: “A 2xx only means they accepted the request. Check the reply below — and your own system — to confirm the record was actually created.”

The two monospace panels underneath are how you actually check. **What we sent** is the exact payload, built from a fake lead (Ada Lovelace, at a company called Example Inc.) run through the same field mapping a real lead would use, so a wrong or empty mapping shows up here before it costs you a real one. **Their reply** is what came back from the endpoint, with JSON indented so you can read it. A reply with no body at all shows as “(empty response body)”.

<Note>
  The payload's own `event` field reads `test`, not `lead`. That's how your endpoint, or your own logs, can tell a connection test apart from a real push, if you filter on it.
</Note>

<Screenshot id="integrations/webhook-testing-and-delivery-rules--delivered" url="/integrations" alt="The test result window headed “Test delivered”, with the message “Your endpoint accepted the test payload.” next to an HTTP 200 chip, the “What we sent” and “Their reply” panels below it, and a detected record ID line at the bottom" caption="The record ID line only appears when the reply carries something we can recognize as one. This reply used the simplest shape there is: a bare id at the top of the JSON." />

When the reply doesn't carry anything recognizable, the panel says that too: “No record ID found in the reply, so leads won't be marked as synced. Set the path above if your endpoint returns one.” A later section covers exactly what "recognizable" means.

## The endpoint answered 2xx and still failed

A **Test failed** heading over what still looks like a success code is the case worth the most attention: the transport worked, and the endpoint said no anyway. We read the body of every 2xx reply, as long as it parses as JSON: a plain-text or XML body is never inspected. We call it a failure only when the shape is unambiguous:

* `success` or `ok` set to `false`.
* `status`, `result` or `state` holding the word error, fail, failed, failure or ko.
* `error`, `errors`, `error_message` or `errorMessage` holding something real: `true`, a nonzero number, non-empty text other than the literal word "false", or a non-empty list.

Deliberately left off that list: a `0`, `null`, an empty list, or `false`, whether it's the boolean or the literal word, sitting in one of those last four keys. Plenty of APIs ship those keys on every response, success included, so treating their mere presence as failure would flag half the endpoints on the internet.

When one of those shapes matches, a hint under the heading names the status the endpoint returned, then says “but reported an error in its response, so nothing was created. Fix the fields it names below and test again.” The line at the top of the panel carries the message itself, pulled from whichever field in the reply reads like a sentence, `message` and `detail` included, or one of our own flat sentences (“The endpoint reported a failure.”, “The endpoint reported an error.”) when nothing in the reply does. The record ID line disappears entirely: a reported failure never gets one, whatever the reply contains.

<Screenshot id="integrations/webhook-testing-and-delivery-rules--reported-error" url="/integrations" alt="The test result window headed “Test failed”, with the message “Contact rejected: owner_id is required.” next to an HTTP 200 chip, a hint explaining that nothing was created, and the “What we sent” and “Their reply” panels below it" caption="The status line looks fine. The body did not: this endpoint created nothing, and reading the reply is the only way to know that from here." />

## "The endpoint must be a publicly reachable address"

This is what you get from an address that resolves to something private: a LAN IP, `localhost`, a Docker service name, or any other reserved range. Every IP the host resolves to is checked, not just the first one, and a host that doesn't resolve at all is refused the exact same way, on the theory that an address nobody can prove is public isn't one.

That rule applies whether you are testing or saving, so a bad address gets you the identical message either way. Saving goes further: unless you have already run a test that came back clean, the save fires the same test payload and refuses to store the connector when it comes back a failure, reported errors included.

<Screenshot id="integrations/webhook-testing-and-delivery-rules--private-host" url="/integrations" alt="The webhook connect dialog after trying to save a LAN address, with a red alert reading “The endpoint must be a publicly reachable address.” at the bottom of the form" caption="Point it at the real public address instead. A staging tunnel or a public test endpoint both pass this check; your own laptop never will." />

Two other things get refused for their own reasons, with their own message:

* Cloud metadata hostnames are refused outright, whatever else is true about them.
* A URL carrying its own credentials (`https://user:pass@host/…`) is refused with a message telling you to use the authentication fields instead.

And one case is refused after the fact rather than before it: a redirect. We never follow one, so a `3xx` counts as a failed delivery, because whatever the redirect pointed to never actually received the payload.

## How long we wait, and when we try again

Every request to a webhook connector, however it's triggered, waits up to 10 seconds for a reply, of which at most 5 may go on opening the connection. On failure we try again, but only when the failure looks temporary: a connection error, a `408`, a `429`, or a `5xx`. Everything else, including every `400`, every `401`, every `404` and any `3xx`, is sent once and left alone. When we do retry, it's up to 3 attempts in total, 500 milliseconds apart.

**Send test** is the one exception: it never retries, so a dead endpoint fails fast while you're watching it, and it keeps a lot more of the reply than a real push logs, which is trimmed to 500 characters. If a run log shows a reply cut off mid-sentence, testing the same endpoint by hand is how you see the rest of it.

## Getting a record ID to show up

Three things decide it: the switch, where we look, and whether the value survives.

**Store the ID returned by the endpoint** is on by default. Turn it off and no id is ever read from the reply, whatever it contains: the lead just never gets marked as synced from this connector.

With it on and **Where to find it (optional)** left blank, we walk a fixed list of the usual shapes and take the first one that yields a usable value: a bare `id`, then `data.id`, `data.0.id`, `result.id`, `results.0.id`, `record.id`, `contact.id`, and more after that. Type a dotted path into that field instead and that becomes the only place checked. Either way the reply has to be JSON, so an id buried in plain text or XML is not one we can read.

<Screenshot id="integrations/webhook-testing-and-delivery-rules--record-id" url="/integrations" alt="The connect dialog's Record ID section, with the Store the ID returned by the endpoint switch on and data.contact.id typed into the Where to find it (optional) field" caption="Leave this blank and we check the common shapes on our own. Set a path only when your endpoint puts the id somewhere unusual." />

If the id genuinely is in the reply and it's still not showing up, we're rejecting the value itself. Anything that isn't text or a whole number is out, and so is a value that's implausibly long or a bare acknowledgement word like `ok`, `success`, `true`, `false`, `null`, `none` or `0`, even sitting exactly where the path says to look.

## Inside a workflow, the Send Webhook block skips the body check

The **Send Webhook** block's **Saved connector** mode builds its payload from the connector's own field mapping, adds whatever extra fields you put on the block, and sends it directly instead of going through the connector's own push logic. That skips both checks this page is otherwise about: it never reads the body for a hidden failure, so a `2xx` carrying a reported error still takes the **Delivered** branch, and it never extracts a record id, so a lead delivered this way is never marked as synced, whatever **Store the ID returned by the endpoint** says.

If you need either of those inside a workflow, point a **Send to CRM** block at the same connector instead. It goes through the connector's real push, the same one auto-push and **Push to CRM** use, so the body is inspected and the record id is captured, and stored on the lead unless a record-creating CRM already owns it.

## Related

<CardGroup cols={2}>
  <Card title="Connect a webhook (any CRM, Zapier, Make, n8n)" icon="webhook" href="/en/integrations/connect-a-webhook">
    Set up the connector step by step and test it before you save.
  </Card>

  <Card title="Auto-push analyzed leads and stage filters" icon="funnel" href="/en/integrations/auto-push-and-stage-filters">
    What triggers an automatic push, which stages it fires on, and every reason a lead is silently skipped.
  </Card>

  <Card title="CRM and integration blocks" icon="layers" href="/en/workflows/crm-and-integration-blocks">
    Push a lead to your CRM, post to Slack or call any endpoint, as a step inside a flow.
  </Card>

  <Card title="Failed to push lead to CRM" icon="unplug" href="/en/troubleshooting/leads-not-reaching-my-crm">
    Auto-push off, a stage filter, a lead with no email, a lead another connector already owns, or an expired authorisation.
  </Card>
</CardGroup>
