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

# Call a webhook from a flow

> The HTTP Request step: send flow data to any public URL mid-conversation, and save fields from the JSON response for later steps.

The **HTTP Request** step, in the flow builder's **Actions** group, calls a URL of yours in
the middle of a conversation, to look something up in your own system, push a lead into a
tool DMLY has no connector for, or notify a backend that a step was reached. It sends one
request, optionally saves fields from the JSON reply, and the flow moves on.

It is the mirror image of [workspace webhooks](/api-reference/webhooks): those push *events*
to your endpoint whenever they happen anywhere in the workspace; this step fires only when a
contact walks through this exact point in this flow.

## The fields

| Field                       | What it does                                                                                                         |
| --------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| **Method**                  | **GET**, **POST** or **PUT**.                                                                                        |
| **Endpoint URL**            | Where the request goes. Must be a public `http(s)` address. See [What it refuses to call](#what-it-refuses-to-call). |
| **Headers**                 | Key/value pairs sent as HTTP headers: an API key, a content type your endpoint insists on.                           |
| **JSON body**               | The request body, for POST and PUT (it disappears on GET). Written as JSON, e.g. `{"email": "{{email}}"}`.           |
| **Save response to fields** | Pairs of *path in the JSON reply* → *field name*, e.g. `data.id` → `crm_id`.                                         |

**Variables only work in the body.** Any `{{token}}` in the JSON body is substituted per
contact before sending. See the [variables reference](/automation/variables). The URL and
the headers are sent exactly as typed: a `{{token}}` there is not replaced, so don't build
per-contact URLs or per-contact header values.

<Note>
  The **Send test request** button in the step's panel shows a canned demo response. The
  panel says so itself: *"Demo response — live requests run when the flow engine is
  enabled."* It does not call your endpoint. To test for real, publish the flow, walk
  through it, and watch your endpoint's logs and [Logs](/troubleshooting/logs).
</Note>

## Using the response

Each **Save response to fields** row plucks one value out of the JSON reply by its path
(dots reach into nested objects, so `data.id` reads `{"data": {"id": …}}`) and saves it as a
flow variable under the field name you chose. From the next step onward, use it like any
other token: name the field `crm_id` and write `{{crm_id}}` in a later message.

Two boundaries to know:

* The saved value lives in **this run of the flow** only. It is not written to the contact,
  and the next conversation starts without it. To keep something permanently, have your
  endpoint write it back through the [REST API](/api-reference/introduction).
* A path that doesn't exist in the reply (or a reply that isn't JSON) saves nothing, and
  the token resolves to empty text later. Nothing errors.

**Two tokens you always get.** Whatever you map, or don't, the step leaves
`{{_last_http_status}}` and `{{_last_http_error}}` behind for the rest of the run. They aren't
in the builder's `{}` picker, so type them by hand. `{{_last_http_status}}` is the HTTP status
your endpoint returned (`200`, `404`, `500`), or `0` when the request never got a response: a
timeout, an unreachable host, or a body DMLY couldn't render as JSON. `{{_last_http_error}}`
carries the reason in that `0` case, either the network error text or `invalid_json_body`. A
successful response doesn't rewrite `{{_last_http_error}}`, so if an earlier HTTP Request step
in the same run set it, the old text is still sitting there; only read it alongside a
`{{_last_http_status}}` of `0`. Neither token is set when the URL is refused outright, because
that stops the run.

<Note>
  A **Condition** step can't branch on these. Conditions compare a fixed list of contact and
  trigger fields, and none of those fields reads a variable a step saved. To see the value at
  all, print it somewhere a later step writes: a **Send Slack message** step, or a **Send to
  Google Sheets** row next to `{{flow_run_id}}`.
</Note>

## When the call fails, the flow does not

The step has a single output, and the run continues through it whatever happens: a timeout,
an unreachable server, a `500`, a `404`. The conversation is never held hostage by your
endpoint: the contact just moves to the next step, with any unsaved response fields left
empty.

This is about calls that were actually made. A URL the step refuses to call isn't a failed
call, it stops the run: see [What it refuses to call](#what-it-refuses-to-call).

If a call silently "did nothing", check [Logs](/troubleshooting/logs) and your endpoint's own
logs. Three lines are worth searching for:

* `HTTP Request rejected by the endpoint`, with the status. Your endpoint answered and said no.
* `HTTP Request failed`, with the error text. The request never reached a reply: a timeout, a
  DNS failure, a refused connection.
* `HTTP Request body is not valid JSON`. Nothing was sent at all; see below.

The step waits at most **5 seconds** for a response (3 to connect). An endpoint that needs
longer should acknowledge immediately and do the slow work after replying.

## What it refuses to call

The URL must be a public `http://` or `https://` address on the open internet. Requests to
`localhost`, private network addresses and other reserved ranges are blocked outright, and
unlike an ordinary network error, a blocked URL **fails the run**: the contact's run stops at
that step and shows as failed in the automation's analytics and in
[Logs](/troubleshooting/logs). If your endpoint lives on an office network or a dev machine,
publish it to the internet (a tunnel works) before pointing a flow at it.

Anything that isn't an `http(s)` address at all is refused the same way, and fails the run the
same way: an empty **Endpoint URL**, free text, or the bare `https://` a freshly added step
starts out with.

## An empty URL publishes anyway

Publishing never checks this step's URL. A missing or malformed endpoint only shows as an
amber **Needs attention** badge on the node (`Endpoint URL is missing or not http(s).`), and
a freshly added step wears that badge from the start, because its URL box begins as just
`https://`. The badge also flags `Body is not valid JSON.` Clear both before you publish;
nothing else will stop you. See [Publish a flow](/automation/publishing-bots).

Neither badge is cosmetic, though, and at runtime the two behave in opposite ways:

* **A URL still left as `https://`, empty, or not an `http(s)` address fails the whole run**
  when a contact reaches the step, exactly like a blocked address does. See
  [What it refuses to call](#what-it-refuses-to-call).
* **A body that can't be rendered as JSON sends nothing at all**, not an empty request. DMLY
  writes `HTTP Request body is not valid JSON` to [Logs](/troubleshooting/logs) and the run
  carries on through the step's single output, with the response fields empty and
  `{{_last_http_error}}` set to `invalid_json_body`.

<Note>
  The badge's JSON check is stricter than the engine. It runs a plain JSON parse on what you
  typed, so a body with a token outside the quotes, like `{"qty": {{count}}}`, wears the badge
  even though the step renders and sends it correctly. If your body only "fails" because of a
  numeric token, the badge is the thing that's wrong, not your flow.
</Note>

<Columns cols={2}>
  <Card title="Variables reference" icon="brackets-curly" href="/automation/variables">
    Every token you can put in the JSON body.
  </Card>

  <Card title="Workspace webhooks" icon="bolt" href="/api-reference/webhooks">
    Event push for everything that happens in the workspace, no flow required.
  </Card>

  <Card title="Connect n8n" icon="diagram-project" href="/integrations/n8n">
    Point this step (or workspace webhooks) at an n8n workflow.
  </Card>

  <Card title="Build a flow" icon="diagram-project" href="/automation/flow-builder">
    The canvas this step lives on.
  </Card>
</Columns>
