# http

The `http` step type performs an HTTP request from a workflow or custom command — call an API, notify a service, trigger a CI job, hit a deployment webhook, or poll a health endpoint. It supports any HTTP verb, query-string parameters, headers, and a request body (raw or form/JSON), with per-attempt timeouts and HTTP-aware retries that compose with the step's [`retry`](/steps/retry) policy. No shelling out to `curl`.

:::note Alias
`webhook` is an accepted alias for `http` — `type: webhook` behaves identically and reads naturally for the fire-a-notification use case.
:::

```yaml
steps:
  - name: notify
    type: http
    url: "https://ci.example.com/hook/{{ .env.JOB_ID }}"
    method: POST
    query:
      ref: "{{ .env.GIT_SHA }}"
    headers:
      Authorization: "Bearer {{ .env.TOKEN }}"
      Content-Type: application/json
    body: '{"status":"deployed"}'
    expect:
      status: [200, 201, 202, 204]
      response:
        - /"status"\s*:\s*"deployed"/
    timeout: 30s
    retry:
      max_attempts: 5
      backoff_strategy: exponential
      initial_delay: 1s
      max_delay: 30s
```

Send form parameters instead of a raw body with `form` (mutually exclusive with `body`). By default `form` is sent as `application/x-www-form-urlencoded`; set a JSON `Content-Type` header to send it as a JSON object instead:

```yaml
steps:
  - name: notify-slack
    type: http
    url: "{{ .env.SLACK_WEBHOOK_URL }}"
    method: POST
    headers:
      Content-Type: application/json
    form:
      text: "Deployment of {{ .steps.plan.value }} complete"
```

## Fields

- **`url`** — Required. Request URL. Supports Go templates.
- **`method`** — HTTP method/verb: `GET` (default), `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, `OPTIONS`.
- **`query`** — Query-string parameters as key-value pairs (supports templates).
- **`headers`** — Request headers as key-value pairs (supports templates).
- **`body`** — Raw request body (supports templates). Mutually exclusive with `form`.
- **`form`** — Form/JSON body parameters as key-value pairs. Sent as `application/x-www-form-urlencoded` unless the `Content-Type` header is `application/json`. Mutually exclusive with `body`.
- **`expect.status`** — List of acceptable HTTP status codes. When set, the response status must be in this list. Defaults to any `2xx`.
- **`expect.response`** — List of regular expressions; the response body must match at least one. Patterns may be written as `/.../ ` literals (surrounding slashes are stripped) or bare regex strings.
- **`timeout`** — Per-attempt timeout (e.g., `30s`, `1m`). Defaults to `30s`. Each retry attempt gets its own deadline. The `http` step gives `timeout` this meaning of its own, so it is a limit on each request and not on the step as a whole.
- **`retry`** — Retry policy (see [retry](/steps/retry)). Transport errors, `5xx`, and `429` responses are retried by default; other `4xx` responses fail fast. For `GET`, `HEAD`, and `OPTIONS`, when `expect` is set, a retry policy also retries any response that fails the expectation (a status outside `expect.status`, or a body that matches none of `expect.response`) until `max_attempts` runs out, which is what makes polling work. `POST`, `PUT`, `PATCH`, and `DELETE` are not automatically repeated solely because a status or body expectation fails: the request may already have completed a write. Use `retry.conditions` (regexes matched against `"<status> <body>"`) to retry additional cases. These explicit conditions and the transport, `5xx`, and `429` retry rules apply to all methods; configure retries for write requests only when the endpoint safely handles repeated calls. The failure error names the last status received and the number of attempts.

## Polling

Because `http` retries are HTTP-aware, the step doubles as a poller — `GET` an endpoint and retry until both the status and the body match the expectation:

```yaml
steps:
  - name: health
    type: http
    url: "{{ .env.HEALTH_URL }}"
    expect:
      status: [200]
      response:
        - /"status"\s*:\s*"(ok|healthy)"/
    retry:
      max_attempts: 10
      backoff_strategy: constant
      initial_delay: 2s
```

For HTTP validation after deployment, see the
[advanced CloudFormation example](/examples/cloudformation-advanced). Its
`after.aws/cloudformation.apply` hook checks the deployed Lambda's status code
and response body, and fails the command if either is unexpected.

## Result

The response body becomes the step's value, and useful details land in metadata (see [outputs](/steps/outputs)):

- **`{{ .steps.<name>.value }}`** — The response body.
- **`{{ .steps.<name>.metadata.status_code }}`** — The numeric HTTP status code (e.g., `200`).
- **`{{ .steps.<name>.metadata.status }}`** — The HTTP status text (e.g., `200 OK`).
- **`{{ .steps.<name>.metadata.response_headers }}`** — Response headers as a map.
