Skip to main content

HTTP requests

Supply an HTTP URL for an action to use as an input, or to call an upstream API.

interfaces:
http/basic:
output: http
method: GET
actions:
- name: BasicHTTP
http:
url: https://jsonplaceholder.typicode.com/todos/1

HTTP actions also support auth (bearer / basic / digest), TLS options, multipart uploads, automatic pagination, and reverse-proxy passthrough. Every field is documented below.

Success and failure: expect_status

A response outside expect_status fails the action. The default is 2xx (plus 304), so a 4xx or 5xx upstream fails the step that called it — which is what makes retry, run_when_failed and http_code_inherit_error work without writing an assertion to translate a status into an outcome.

actions:
- name: Push
retry: {attempts: 3, exponential_backoff: true}
http:
url: https://api.example.com/orders
method: POST

A failed action keeps its payload — status, headers and body — so an error branch can still read them:

  - name: Recover
run_when_failed: [Push]
input: a|Push| # .status, .headers, .body are all here

When a non-2xx status is genuinely data rather than a failure, say so:

http:
url: https://api.example.com/users/42
expect_status: [2xx, 404] # 404 means "no such user", not an error
ValueMeaning
unset2xx or 304 is success (the default)
2xx / 4xx / 5xxa whole class
404one exact code
[2xx, 404]any of a list
anynever fail on status — the status is only data

304 Not Modified counts as success by default because RFC 9110 §15.4.5 defines it as the successful outcome of a conditional request whose validator matched — a poller using If-None-Match gets one on its happy path.

The outcome is declared, not inferred

expect_status decides whether the action failed. An assert validates the data and never decides the outcome, so this does not do what it looks like:

# WRONG — the action fails on the 403 before the assert runs
http: {url: https://api.example.com/thing}
assert:
tests:
- value: status
is_equal_to: 403
# RIGHT — say what you expect
http: {url: https://api.example.com/thing, expect_status: 403}

One rule, in one place: read an action and you can see what counts as failure without tracing which assertions happen to mention status. The engine says so when you get it wrong — the failure message names the expect_status line to add.

One declaration for a whole interface

An interface whose actions share an expectation — a test runner hitting routes that answer 403 and 500 on purpose, a health-check fan-out that records whatever comes back — declares it once. An action that sets its own always wins.

interfaces:
tests/all:
defaults:
expect_status: any
actions:
- name: ChecksA
http: {url: http://localhost:44111/route-a} # inherits `any`
- name: ChecksB
http: {url: http://localhost:44111/route-b, expect_status: 2xx} # overrides

Polling for something that is not there yet

A GET that answers 404 until a record exists now fails on the first attempt, and 404 is not in the default retry set, so the action gives up immediately. Say which one you mean:

# 404 is data; retry until the assert passes
http: {url: https://api.example.com/orders/42, expect_status: [2xx, 404]}
retry: {attempts: 10, delay: 500, exponential_backoff: true}
assert:
tests:
- value: body.id
is_not_null: true

Responses with no body

204 No Content and 304 Not Modified carry no body by definition, and a 200 with content-length: 0 is legal. The body decodes to null rather than failing the action, so a DELETE that answers 204 succeeds and a|Delete::status| still reads 204.

Retrying safely: idempotency

Retrying a POST can mean charging a customer twice. RFC 9110 §9.2.2 permits automatically retrying a non-idempotent request only when you have "some means to know that the request semantics are actually idempotent... or some means to detect that the original request was never applied". An idempotency key is that means.

http:
url: https://api.stripe.com/v1/charges
method: POST
idempotency: {} # engine generates the key

Every attempt of one action execution sends the same key — that is the whole point, and it is the part a config cannot express itself: a|uuid| is re-evaluated per attempt, so each retry would carry a fresh key and be processed as a new request.

Set the key yourself when the caller may retry the whole workflow and you want those attempts deduplicated too:

idempotency:
key: a|body::orderId| # derived from your data
header: PayPal-Request-Id # default: Idempotency-Key

HttpRequest

FieldTypeDescription
urlstringRequired. The URL to call. Interpolation applies, so it can be built from earlier steps or variables. Example ↓
expect_statusStatusExpectation (nullable)Which response statuses count as a SUCCESSFUL action. Defaults to 2xx, so a 4xx/5xx fails the action — which is what makes retry, run_when_failed and…
idempotencyIdempotency (nullable)Send an idempotency key with this request, so a RETRY cannot be processed twice. The engine sends the same key on every attempt of one action execution — that is the whole…
digest_auth_userstring (nullable)Username for HTTP Digest authentication.
digest_auth_passstring (nullable)Password for HTTP Digest authentication. Keep it in a secret or variable rather than in the config: a|ap_secret::UPSTREAM_PASS|.
userstring (nullable)Username for HTTP Basic authentication.
passstring (nullable)Password for HTTP Basic authentication. As with digest, reference a secret.
proxystring (nullable)Send this request through an HTTP proxy.
headersMap<string, string> (nullable)Request headers. Values interpolate, which is how a token is passed without writing it into the config. Example ↓
bodyanyRequest body. A mapping is sent as JSON; a string is sent as-is, which is what you want for a pre-rendered payload.
base64_bytesstring (nullable)Send raw bytes decoded from base64, for binary uploads that are not multipart.
methodstring (nullable)HTTP method. Defaults to GET. Deliberately a string rather than a closed set: the engine passes it through as a token, so an extension method a private API expects still…
data_typeDataType (nullable)How to encode the body and read the response. Inferred when unset.
bearer_authstring (nullable)Token for Authorization: Bearer. Reference a secret or variable rather than writing the token into the config: a|ap_var::API_TOKEN|.
multipartArray<MultipartPart> (nullable)A flat list of parts for a multipart/form-data request. Each entry is one form part. Whether it is a text or binary part is determined implicitly by which source field is… Example ↓
multipart_formMap<string, string> (nullable)Deprecated: use multipart instead.
multipart_filesArray<MultipartData> (nullable)Deprecated: use multipart instead.
accept_invalid_certsboolean (nullable)
castring (nullable)
root_certificatesArray<string> (nullable)
timeoutstring (nullable)
connect_timeoutstring (nullable)
paginationPagination (nullable)
proxy_bodyboolean (nullable)
proxy_methodboolean (nullable)
proxy_headersProxyHeaders (nullable)
proxy_queryboolean (nullable)

Field examples

url

The URL to call. Interpolation applies, so it can be built from earlier steps or variables.

url: https://api.example.com/users/a|params::id|

headers

Request headers. Values interpolate, which is how a token is passed without writing it into the config.

headers:
content-type: application/json
authorization: Bearer a|ap_var::API_TOKEN|

multipart

A flat list of parts for a multipart/form-data request.

Each entry is one form part. Whether it is a text or binary part is determined implicitly by which source field is provided:

  • value → plain text part
  • b64 → binary part decoded from base64
  • file_path → binary part read from disk (not allowed in managed/hosted mode)

filename and mime are always optional. When omitted on a binary part, mime is guessed from the filename extension, falling back to application/octet-stream.

multipart:
- name: image
b64: a|previous_action::body.image.data|
filename: photo.png # optional — sent as Content-Disposition filename
mime: image/png # optional — guessed from filename if absent
- name: model
value: gpt-image-1
- name: prompt
value: Transform into Studio Ghibli style

Pagination

FieldTypeDescription
original_urlstring (nullable)
next_linkstring (nullable)
no_pagesnumberDefault: 0.
page_markernumberDefault: 0.
cursor_markerstring (nullable)
incrementnumberDefault: 1.
max_pagesnumber (nullable)
max_pages_keystring (nullable)
page_startnumberDefault: 1.
page_next_keystring (nullable)
page_limitnumber (nullable)
page_limit_keystring (nullable)
payload_keystring (nullable)
next_cursor_keystring (nullable)
max_cursor_keystring (nullable)
next_link_keystring (nullable)
next_link_hoststring (nullable)

MultipartPart

A single part in a multipart/form-data request.

Exactly one of value, b64, or file_path must be provided per part. All other fields are optional.

FieldTypeDescription
namestringRequired. Form field name — the name attribute sent in Content-Disposition.
valuestring (nullable)Plain text value. Use this for non-binary form fields. Mutually exclusive with b64 and file_path.
b64string (nullable)Base64-encoded binary data. Decoded before sending. Mutually exclusive with value and file_path.
file_pathstring (nullable)Path to a file to read and send as binary data. Not permitted in managed/hosted mode — use b64 instead. Mutually exclusive with value and b64.
filenamestring (nullable)Filename sent in the Content-Disposition header for this part (e.g. photo.png). Optional for binary parts; not meaningful for plain text parts.
mimestring (nullable)MIME type for this part (e.g. image/png, application/pdf). If omitted on a binary part, guessed from filename's extension. Falls back to application/octet-stream when…

MultipartData

Deprecated: use [MultipartPart] via the multipart field instead.

One of:

MultipartFile

Deprecated: use [MultipartPart] via the multipart field instead.

FieldTypeDescription
file_pathstring (nullable)
file_namestringRequired.
b64string (nullable)
mime_strstring (nullable)
part_namestring (nullable)

MultipartText

Deprecated: use [MultipartPart] via the multipart field instead.

FieldTypeDescription
keystringRequired.
valuestringRequired.
mime_strstring (nullable)

ProxyHeaders

Controls which headers are passed through from the incoming request.

One of:

  • boolean — When true, pass through all incoming headers
  • Array<string> — When a list, pass through only the specified headers; empty list means pass none