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

# Calling an HTTP API

> Call any HTTPS endpoint the platform has no purpose-built step for — authenticated by selecting a credential, with the response parsed into flow context under a declared cap

`getdialed__http__request` sends **one HTTPS request** and puts the response into your flow. You choose the method, the address, the headers, the query parameters and the body. You authenticate by **selecting a stored credential**, which the platform applies to the request for you.

```json theme={null}
{
  "task_id": "task_fetch_contact",
  "task_name": "Fetch the contact record",
  "platform_id": "getdialed",
  "service_id": "getdialed__http",
  "action_id": "getdialed__http__request",
  "connection_id": "cred_e5f6g7h8",
  "parameters": {
    "method": "GET",
    "url": "https://api.example.com/v2/contacts",
    "query": { "email": "{{ input.email }}" }
  }
}
```

The step reports `status`, `headers` and `body` — see [the response](#the-response).

## When to reach for it

This is the **long-tail fallback**. Where a provider has purpose-built steps, use those: they know the provider's quirks, they are paced against that provider's published rate budget, and they report per-row outcomes. Browse them in the [catalog](/concepts/catalog).

Reach for this step when the endpoint you need has no purpose-built action — an internal-facing SaaS API, a customer's own service, a provider nobody has integrated yet. It is one request per step, not one per record: if you need a request per row, put this step inside a flow that the platform already runs per record.

<Note>
  Because the platform cannot know the rate limits of an address you typed, requests from this step are **not** rate-paced by GetDialed. Respecting the provider's own limits is yours to arrange — space the work out with [schedules](/concepts/schedules), or reduce the set first with a [record transform](/concepts/record-transforms).
</Note>

## Authentication is selection, never a pasted secret

The step never names a header and never holds a token. It carries a `connection_id` — the id of a [credential](/concepts/credentials) — and the platform applies that credential to the request when it runs.

The consequence is worth stating plainly, because the first instinct is to reach for a header:

<Warning>
  **`Authorization`, `Proxy-Authorization` and `Cookie` cannot be set in `headers` at all** — not as literal text, not as a template, not from an [account variable](/concepts/account-variables). Neither can the header or query parameter that your selected credential injects into. All of them are refused when you **save** the flow, not when it runs.

  There is no way around this and it is not a gap. Authentication reaches the request by credential selection and by no other route, which is what keeps a secret out of the flow definition, out of the API responses that return it, and out of every run record.
</Warning>

Every other header name is yours, and a header **value** may use variables and expressions freely:

```json theme={null}
{
  "action_id": "getdialed__http__request",
  "parameters": {
    "method": "GET",
    "url": "https://api.example.com/v2/contacts",
    "headers": {
      "Accept": "application/json",
      "X-Correlation-Id": "{{ input.trace_id }}"
    }
  }
}
```

Where the key goes is declared **once, on the credential** — a header name with an optional scheme, or a query-parameter name — so every step using that credential authenticates identically. See [declaring where an API key goes](/concepts/credentials#declaring-where-an-api-key-goes).

### The credential is optional

A public API that needs no authentication needs no credential. Leave `connection_id` off and the request goes out unauthenticated. This is the one action where a missing credential is a legitimate configuration rather than a `422` — but a credential you **do** supply is still checked for compatibility when you save.

## The address

Two rules, and the second reads stricter than it is.

**Only `https` is accepted.** A plain `http://` address is refused when you save, and refused again if one is ever reached at run time. A credential sent over an unencrypted connection is exposed by definition, so there is no carve-out — not for an unauthenticated call, not for a host on your own network. Any port may be named (`https://api.example.com:8443/v2/orders` is fine).

**The scheme, host and port must be written out literally.** They cannot come from a variable, an earlier step or a record field. That is what makes a saved flow auditable: reading it tells you every address it can reach.

**The path and the query string may template freely** — which is what makes pagination and per-record resource ids work:

```json theme={null}
{
  "action_id": "getdialed__http__request",
  "parameters": {
    "method": "GET",
    "url": "https://api.example.com/v2/contacts/{{ input.contact_id }}/notes",
    "query": {
      "since": "{{ step_previous.output.body.cursor }}",
      "per_page": "100"
    }
  }
}
```

A user name or password written into the address itself (`https://user:pass@host/`) is refused — that is a pasted secret wearing a different hat.

<Note>
  Query and header **names** must be literal too, for the same auditability reason. A templated name (`{ "{{ input.header_name }}": "..." }`) and a wholesale-templated `headers` object are both refused when you save, because a name that cannot be read off the definition cannot be checked against the names a credential claims. Values template freely in both.
</Note>

## What can be reached, and what cannot

**Any address on the public internet may be called.** There is no allow-list to maintain and no request to file before pointing a flow at a new provider.

**An address that resolves into private space is refused before anything is sent.** That covers private ranges, loopback, link-local, unique-local, carrier-grade NAT, multicast and reserved space, over both IPv4 and IPv6. The check runs on the address the hostname actually resolves to, and the connection is then pinned to **that** address — so a name that resolves to something public during the check and something internal a moment later still cannot be reached.

Every redirect is re-checked the same way, on every hop.

The reason is short: a step that can call any address is a door into the platform's own network unless that door is closed, so it is closed. An endpoint inside your network, or inside ours, is not reachable from a flow.

<Note>
  **An address refusal does not tell you what the address resolved to**, and every address refusal reports the same reason. That is deliberate rather than unhelpful: a message that distinguished "that host does not exist" from "that host resolves to something internal" would be a network scanner with an audit trail. If a call you expect to work is refused, check the address from outside — the answer is in DNS, not in the message.
</Note>

## Two things that are deliberately absent

<Warning>
  **There is no option to skip certificate verification, and no option to route through a proxy.** Neither is missing by accident and neither will be added on request.

  A toggle that turned off certificate checking would make the encryption the `https`-only rule exists to guarantee decorative, and a proxy setting is an address the private-space check cannot see past. If your endpoint presents a certificate that does not validate, the fix is on the endpoint. If it is only reachable privately, a flow is the wrong caller for it — the mechanism you need is a different one, not a setting on this step.
</Warning>

## The response

The step reports three things:

| Output    | What it holds                                                                                                                                                                            |
| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `status`  | The HTTP status code, always — whether the step succeeded or was configured not to fail on an error status. This is the thing to branch on.                                              |
| `headers` | The response headers as name/value pairs, with names in **lower case**. Where a pagination cursor, a rate-limit budget or a `location` comes back. Repeated fields are joined with `, `. |
| `body`    | The parsed body. Empty when the response had none — a `204` and most `DELETE` responses.                                                                                                 |

Read them like any other step output:

```text theme={null}
{{ step_call.output.status }}
{{ step_call.output.headers.link }}
{{ step_call.output.body.id }}
```

### How the body is parsed

The body is parsed from the response's own content type. Names are matched on the media type alone, so `application/json; charset=utf-8` and `APPLICATION/JSON` take the same path.

| Content type                                                                                        | You get                                                                                                                                                                                                                                                               |
| --------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `application/json`, and any `+json` suffix (`application/vnd.api+json`, `application/problem+json`) | A value you can read field by field.                                                                                                                                                                                                                                  |
| `application/x-www-form-urlencoded`                                                                 | A flat set of keys. A key sent once is a single value; a key sent more than once is a list, in the order it arrived.                                                                                                                                                  |
| `application/xml`, `text/xml`, and any `+xml` suffix                                                | A flat set of keys, with nested elements named using dots (`contact.email`) — the same convention the [XML parse step](/concepts/record-transforms) uses.                                                                                                             |
| `text/csv`                                                                                          | **Text, deliberately.** Rows belong in a record set, not in flow context: a record set is paged and referenced by id, while a parsed CSV in flow context is every row inside one step result. Pass the text to `getdialed__records__parse_csv`, which produces a set. |
| `text/*`, and anything else including an absent content type                                        | Text.                                                                                                                                                                                                                                                                 |

**A parse failure falls back to the text.** If a provider claims JSON and returns an HTML error page, you get the page as text and the step carries on — you can look at what actually arrived and pipe it, rather than losing the step to somebody else's header.

### Responses that are refused

<Warning>
  **A response that is not text is refused, not mangled.** A file, an image, a PDF, a `multipart/form-data` body — the step fails with a reason naming the content type and the byte count, and nothing from the body is repeated.

  A response that declares a **non-UTF-8 character set** — `iso-8859-1` is the common one — is refused the same way rather than decoded according to that declaration. Today the step reads UTF-8 and only UTF-8.

  The alternative would be to decode with replacement characters, which produces a string of question marks that looks exactly like a successful parse everywhere downstream. A step that needs the **bytes** of a response is waiting on file and object storage; until then, call an endpoint that returns JSON, XML, a form encoding or text.
</Warning>

## The limits

| Limit              | Value                                                     |
| ------------------ | --------------------------------------------------------- |
| Response body      | **131,072 bytes (128 KiB)**, measured after decompression |
| Response headers   | **8,192 bytes (8 KiB)** for the whole set                 |
| Request body       | **1,048,576 bytes (1 MB)**                                |
| Timeout            | **30 seconds** by default, **60 seconds** at most         |
| Redirects followed | **5** hops                                                |

<Warning>
  **Every one of these is a refusal, never a truncation.** A response over the body cap fails the step — it is not clipped to fit, and the read is abandoned partway rather than completed and then measured. A `timeout_seconds` above 60 is refused rather than quietly reduced to 60.

  Both choices are for the same reason. A silently clipped response produces flow context that looks complete to every step after it, and nothing downstream can tell a half-read response from a whole one. A step that appeared to accept a five-minute timeout and did not honour it is worse than one that told you it could not.
</Warning>

The 60-second ceiling exists so that a slow provider produces a clear failure **from this step**, naming the timeout, rather than an unexplained platform timeout further out with nothing in it to act on.

To work with a response larger than the cap, ask the provider for less of it: page it, filter it server-side, or request fewer fields.

## When the status is not 2xx

**Any status outside 200–299 fails the step by default.** The failure names the status code.

<Note>
  **The response body is never included in a failure message**, and this is not a bug to report. A failure is read in logs and stored in run records, both of which outlive whatever retention rule governed the payload — and a provider's error body routinely quotes the request that caused it, including the parts a credential was applied to. If you need to see an error body, use `fail_on_error_status` below and read `body` as data.
</Note>

Set `fail_on_error_status` to `false` when the status is information you want. The step then succeeds, and `status` and `body` are both available to branch on — which is what a "404 means no such record" flow needs:

```json theme={null}
{
  "step_id": "step_lookup",
  "tasks": [
    {
      "task_id": "task_lookup",
      "task_name": "Look the contact up",
      "platform_id": "getdialed",
      "service_id": "getdialed__http",
      "action_id": "getdialed__http__request",
      "connection_id": "cred_e5f6g7h8",
      "parameters": {
        "method": "GET",
        "url": "https://api.example.com/v2/contacts/{{ input.contact_id }}",
        "fail_on_error_status": false
      }
    }
  ],
  "exit_conditions": [
    {
      "condition": "{{ step_lookup.output.status }} == 404",
      "action": "skip_next_step",
      "reason": "No such contact — nothing to update"
    }
  ]
}
```

## Sending a body

`body` is text, and `body_format` says how to label and interpret it:

| `body_format`        | Sent as                             | Build it with                                                                                                       |
| -------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `json` (the default) | `application/json`                  | An expression and the `jsonEncode` [pipe](/concepts/expressions#pipes), so quoting and escaping are handled for you |
| `form`               | `application/x-www-form-urlencoded` | The `urlEncode` pipe                                                                                                |
| `raw`                | `text/plain`, exactly as given      | Whatever the endpoint wants                                                                                         |

Set `content_type` to override the label without changing how the body is built — for a vendor JSON variant, or a versioned media type.

## Redirects and retries

**Redirects are followed up to 5 hops.** Each hop's address is resolved and checked again, so a redirect toward private space is refused exactly like a first request would be. A chain longer than 5 hops fails rather than being followed further.

<Warning>
  **Your credential is dropped the moment a redirect leaves the address it was issued for.** Scheme, host and port together decide that — a hop to the same hostname on a different port is a different service, and the credential does not follow it. Once dropped it stays dropped for the rest of the chain.

  If a provider redirects the authenticated part of its API to another host, call the final address directly. A step that silently handed your provider's secret to whatever host a redirect named would be the more convenient behaviour and the wrong one.
</Warning>

**Retry safety is decided by the method.** `GET`, `HEAD`, `PUT` and `DELETE` are safe to repeat by definition, so a transient network failure on one is retried. `POST` and `PATCH` are attempted **once**: a repeated `POST` can charge a card, send a message or enrol somebody twice.

Set `idempotent` to `true` to allow a `POST` or `PATCH` to be retried — but only if the endpoint takes an idempotency key **and you are sending one**. That key is what makes the repeat safe; the setting on its own only removes the protection.

## What is refused when you save

These are caught when the flow is saved, so you never discover them at 2 a.m. from a run. Each is a `422` naming the field and the step, and none of them echoes the value:

| Refused                                                                                       | Because                                                          |
| --------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| A `url` that is not `https`                                                                   | Cleartext exposes a credential by definition                     |
| A templated scheme, host or port in `url`                                                     | A saved flow must state every address it can reach               |
| A user name or password inside `url`                                                          | That is a pasted secret                                          |
| `Authorization`, `Proxy-Authorization` or `Cookie` in `headers`                               | Authentication comes from selecting a credential                 |
| A header or query name your selected credential injects into                                  | Same reason — and the set depends on which credential you picked |
| A templated header or query **name**, or a `headers`/`query` value that is not a set of pairs | A name that cannot be read cannot be checked                     |
| A credential that is not compatible with the step                                             | Caught at save rather than at run                                |

## Next steps

<CardGroup cols={2}>
  <Card title="Credentials" icon="key" href="/concepts/credentials">
    Create the credential this step selects, and declare where its key goes.
  </Card>

  <Card title="Expressions" icon="code" href="/concepts/expressions">
    The templates and pipes that build a path, a query and a body.
  </Card>

  <Card title="Integration catalog" icon="grid" href="/concepts/catalog">
    The purpose-built steps to prefer where they exist, and this step's full parameter reference.
  </Card>

  <Card title="Record transforms" icon="table" href="/concepts/record-transforms">
    Turn a text response into a record set, and reduce a set before calling per record.
  </Card>
</CardGroup>
