> ## 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.

# Outbound webhooks (there is no event subscription API)

> How leads leave the platform (the webhook connector and the workflow block), plus the payload shape and the network rules.

export const UiPath = ({children}) => {
  const parts = String(children).split(/\s*(?:→|>)\s*/).filter(Boolean);
  return <span className="pl-path">
      {parts.map((part, index) => <span key={`${index}-${part}`}>
          {index > 0 && <span className="pl-path__sep">→</span>}
          {part}
        </span>)}
    </span>;
};

There's no API call that registers a callback URL and fires as things happen to your leads. Getting a lead out to your own systems is a workspace feature you configure in the interface, and it works by push: we hold the address and decide when to call it.

<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>

## How it behaves

### There's no endpoint to subscribe to

The whole public, versioned API is five endpoints under `/api/v1`: `GET /me`, `GET /tags`, `POST /leads`, `POST /leads/batch` and `GET /leads/{id}`. There is nothing alongside them to register a callback, list what you've subscribed to, or choose which events you want. That surface does not exist. Everything below is configuration you hold in the workspace, not something you register from your own code.

### Two surfaces, one HTTP layer

Two things hold the configuration, and both are set up through the interface:

The **Webhook** connector, set up once on <UiPath>Integrations → Integrations → Apps & CRM</UiPath>. It holds an **Endpoint URL**, a **Method**, a **Body format** (**JSON body** or **Form-encoded**), authentication (**No authentication**, **Bearer token**, **Custom header** or **Basic auth**), a **Field mapping** from lead attributes to the key names your endpoint expects, and constants (custom fields and headers) sent with every request. Leave the mapping empty and we send a fixed default shape instead; in the dialog's own words, "we’ll send our standard fields (id, name, email, phone, job title, company, stage, tags…)". Three separate things fire a connector, all through the same code: the **Auto-push analyzed leads** switch on its card (off when you first connect it), the **Push to CRM** button on a lead, and the **Send to CRM** workflow block, which lists webhook connectors alongside the real CRMs.

The **Send Webhook** block, a step on a workflow canvas. Its **Endpoint** control chooses the shape. **Saved connector** reuses a connector, taking its endpoint, method, body format, authentication and field mapping exactly as saved. Rotating a key is then one edit on the connector instead of one per workflow. **Custom URL** calls a one-off endpoint typed into the block, with its own **Method**, **Body format**, headers and fields, stored nowhere but that workflow. Values you type into the block are rendered as merge tags before the call, the custom URL included, so a lead's email, a trigger value or an earlier block's output can drop straight into the request. A saved connector's own URL and headers are used exactly as stored. Those are not templated.

Every one of those paths, plus the **Send test** button in the connect dialog, goes out through the same HTTP layer, so every rule below applies whatever sent the request.

### The payload

A connector-built payload (every path that fires the connector, and the block's **Saved connector** shape) is assembled in one order: your constant custom fields, then your mapped fields (or the standard default shape if you mapped nothing), then a reserved envelope added last. The envelope is `event`, `lead_id` and `sent_at`, and because it goes on last your field mapping cannot override those three key names. With the block, the fields you added on the block are merged on top of all of that, so those *can* override the envelope.

`event` takes one of three values, and it tells you which path sent the request rather than which connector. `lead` is the default and covers everything that goes through the connector's push: auto-push, the **Push to CRM** button and the **Send to CRM** block. `workflow` means a **Send Webhook** block sent it, in either shape. `test` means someone pressed **Send test** in the connect dialog, so nothing about that request should be treated as a real lead.

A **Custom URL** block builds its own envelope instead: `event` (always the string `workflow`), `lead_id`, `workflow_id` and `sent_at`, plus whatever you map into the block's fields. So a block with nothing configured beyond a URL still sends something usable. `workflow_id` appears only in this shape, and here the envelope wins the other way round: a block field whose name collides with an envelope key we are sending is dropped in favour of ours. `lead_id` is left out of the envelope entirely when the run has no lead, and a block field called `lead_id` then survives, because there is nothing to collide with.

Whichever authentication you picked on the connector is attached to every call as a header for you: a bearer token and basic auth as `Authorization`, a custom header under the name you gave it. You never re-enter it in the block.

### The network and delivery rules

Every call, whatever sent it, runs through the same guard rail:

* Only `http://` and `https://` addresses are accepted, and credentials embedded in the URL itself (`https://user:pass@host/…`) are rejected outright: put them in the authentication fields instead.
* Redirects are never followed. A `3xx` counts as delivery failing, because the real endpoint never received the payload.
* Every IP address the host resolves to is checked, and private, loopback, link-local and other reserved ranges are refused by default. A host that won't resolve at all is treated the same way: not demonstrably public, so not called.
* Two cloud metadata hosts are refused outright, whatever the private-network setting says.
* Header names we own (`Host`, `Content-Length`, `Connection`, `Transfer-Encoding`) are always stripped from anything you send, along with any header whose name isn't a valid header token. Line breaks are removed from header values.
* The number of fields you can map and the number of headers you can add are both capped; rows past the cap are dropped silently rather than sent.

Blocking private-network addresses is on by default, and the configuration itself says to turn it off only for local development against a LAN endpoint. That is the usual explanation for a webhook that fires cleanly from a local setup and goes nowhere against a real workspace: the endpoint has to be reachable from the public internet, so a LAN address, a Docker service name or `localhost` no longer qualifies.

Requests use a bounded timeout, so a slow endpoint can't hold a workflow run open indefinitely, and the reply is truncated before it is stored on the run. Only a connection failure, a `408`, a `429` or a `5xx` is retried; every other outcome (a `400`, a `401`, a `404`, a `3xx`) is sent once and left alone. **Send test** is the exception: it never retries and it keeps far more of the reply, because you are reading that one.

### What happens when a call fails

The **Send Webhook** block doesn't fail the whole workflow because the far end rejected the request. It branches (**Delivered** or **Failed**) and carries the outcome forward: the status code, and either the reply or a short error.

It branches on the HTTP status alone. An endpoint that answers `2xx` and reports a problem in its own body takes the **Delivered** branch; the connector's push and **Send test** do read the body and call that a failure. The same split applies to record IDs: only the connector's push reads the created record's id out of the reply and stores it on the lead, so a lead delivered by a **Send Webhook** block is never marked as synced. If you want the id stored from inside a workflow, use the **Send to CRM** block pointed at the connector instead, because that block goes through the connector's push.

The only failures treated as fatal are configuration mistakes, and they stop the run for that lead instead of quietly taking **Failed** on every lead:

* The block is set to **Saved connector** and the connector isn't there: never picked, deleted since, or not a webhook connector in this workspace.
* The block is set to **Custom URL** and the URL is empty, or rejected by the rules above. The URL is rendered first and then checked, so a merge tag that resolves to a blocked address is fatal too.

Set to **Saved connector** with no lead in the run yet, the block takes **Failed** without calling the endpoint at all. A **Custom URL** block still calls, just without `lead_id`.

## Limits

|                                        |                                                                                                              |
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| Event subscription API                 | None: no callback registration, no event list                                                                |
| HTTP methods available                 | GET, POST, PUT, PATCH, DELETE                                                                                |
| Methods that carry a body              | POST, PUT, PATCH. GET and DELETE send the payload as a query string instead                                  |
| Body formats                           | JSON, form-encoded                                                                                           |
| Schemes accepted                       | `http`, `https`                                                                                              |
| Credentials in the URL                 | Rejected: use the authentication fields instead                                                              |
| Redirects                              | Never followed; a `3xx` is a failed delivery                                                                 |
| Private, loopback and reserved IPs     | Blocked by default, checked against every IP the host resolves to                                            |
| A host that won't resolve              | Treated as not public, and not called                                                                        |
| Cloud metadata hosts                   | Always blocked, even with private-network blocking turned off                                                |
| Retried failures                       | A connection error, `408`, `429` or `5xx`                                                                    |
| Failures that are not retried          | Everything else, including `400`, `401`, `404` and any `3xx`                                                 |
| Reserved header names                  | `Host`, `Content-Length`, `Connection`, `Transfer-Encoding`: always stripped                                 |
| Mapped fields and custom headers       | Both capped; rows past the cap are dropped                                                                   |
| Envelope keys                          | `event`, `lead_id`, `sent_at` on a connector payload; `workflow_id` as well on a **Custom URL** block        |
| Values `event` can take                | `lead`, `workflow`, `test`                                                                                   |
| Record ID stored on the lead           | From the connector's push (auto-push, **Push to CRM**, **Send to CRM**), never from a **Send Webhook** block |
| An unconfigured **Send Webhook** block | Stops the run for that lead, rather than taking the **Failed** branch                                        |

## 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 (the endpoint, the authentication, the field mapping) and test it before you save.
  </Card>

  <Card title="Webhook testing, delivery rules and record IDs" icon="shield-check" href="/en/integrations/webhook-testing-and-delivery-rules">
    Read a test result, understand why a 2xx isn't proof, and fix the endpoints we refuse to call.
  </Card>

  <Card title="Auto-push and stage filters" icon="funnel" href="/en/integrations/auto-push-and-stage-filters">
    Turn on automatic sending per connector, and narrow it to the stages you care about.
  </Card>

  <Card title="CRM and integration blocks" icon="layers" href="/en/workflows/crm-and-integration-blocks">
    Every block that pushes a lead out from inside a workflow, including this one.
  </Card>
</CardGroup>
