earl 0.5.2

AI-safe CLI for AI agents
---
title: HTTP
icon: Globe
description: Call REST and HTTP APIs from an Earl template.
---

The HTTP protocol lets you make GET, POST, PUT, PATCH, DELETE, and HEAD requests to any URL.

## A complete example

Here is a template that lists GitHub repositories for the authenticated user:

```hcl
version    = 1
provider   = "github"
categories = ["scm"]

command "list_repos" {
  title       = "List repositories"
  summary     = "List repos for the authenticated user"
  description = "Returns all repositories visible to the authenticated user, sorted by last push."

  annotations {
    mode    = "read"
    secrets = ["github.token"]
  }

  param "per_page" {
    type        = "integer"
    required    = false
    default     = 30
    description = "Results per page (max 100)"
  }

  operation {
    protocol = "http"
    method   = "GET"
    url      = "https://api.github.com/user/repos"

    auth {
      kind   = "bearer"
      secret = "github.token"
    }

    headers = {
      Accept               = "application/vnd.github+json"
      X-GitHub-Api-Version = "2022-11-28"
    }

    query = {
      per_page = "{{ args.per_page }}"
    }
  }

  result {
    decode = "json"
    output = "{{ result | length }} repos:\n{% for r in result %}  - {{ r.full_name }}\n{% endfor %}"
  }
}
```

Store your token and run it:

```bash
earl secrets set github.token
earl call github.list_repos
earl call github.list_repos --per_page 10
```

## Walk-through

### annotations

```hcl
annotations {
  mode    = "read"
  secrets = ["github.token"]
}
```

`mode = "read"` skips the write-mode confirmation prompt. The default is `"write"`, which prompts even when your request is read-only — so any GET command should set this explicitly.

Every secret referenced in the `auth` block must appear in `secrets`. Earl validates this at load time.

### param

```hcl
param "per_page" {
  type        = "integer"
  required    = false
  default     = 30
  description = "Results per page (max 100)"
}
```

The agent supplies param values at call time. Everything else — types, defaults, descriptions — is up to the template author. `type = "integer"` means Earl validates the value and rejects it before the request goes out if it is not an integer.

### operation

```hcl
operation {
  protocol = "http"
  method   = "GET"
  url      = "https://api.github.com/user/repos"
  ...
}
```

HTTP fields sit at the top level of the `operation` block. There is no nested `http { }` wrapper — that is different from the GraphQL, gRPC, Bash, and SQL protocols, which wrap protocol-specific fields in their own sub-block.

`url` supports Jinja expressions. You can interpolate params, environment variables, or any other template context into the URL string.

### auth

```hcl
auth {
  kind   = "bearer"
  secret = "github.token"
}
```

`kind = "bearer"` sends `Authorization: Bearer <value>`, where the value is pulled from the OS keychain at request time. Four auth kinds are available:

- `bearer` — Bearer token header
- `api_key` — API key in a header, query param, or cookie
- `basic` — HTTP Basic with username and password
- `o_auth2_profile` — OAuth2 managed token; see [Secrets & Authentication](/docs/secrets-and-auth) for setup

### headers and query

```hcl
headers = {
  Accept               = "application/vnd.github+json"
  X-GitHub-Api-Version = "2022-11-28"
}

query = {
  per_page = "{{ args.per_page }}"
}
```

Both are maps. Values support Jinja expressions. Query params are URL-encoded automatically — you don't need to percent-encode anything yourself.

### result

```hcl
result {
  decode = "json"
  output = "{{ result | length }} repos:\n{% for r in result %}  - {{ r.full_name }}\n{% endfor %}"
}
```

`decode = "json"` parses the response body. The parsed object is available as `result` in the output template. The output template is Jinja — you can iterate, filter, and format the data however you want.

## POST with a JSON body

Write operations follow the same structure. The difference is `method = "POST"` and a `body` block:

```hcl
command "create_issue" {
  title       = "Create issue"
  summary     = "Open a new issue in a GitHub repository"
  description = "Creates a GitHub issue with a title and optional body."

  annotations {
    mode    = "write"
    secrets = ["github.token"]
  }

  param "owner" { type = "string" required = true  description = "Repo owner" }
  param "repo"  { type = "string" required = true  description = "Repo name"  }
  param "title" { type = "string" required = true  description = "Issue title" }
  param "body"  { type = "string" required = false default = "" description = "Issue body (Markdown)" }

  operation {
    protocol = "http"
    method   = "POST"
    url      = "https://api.github.com/repos/{{ args.owner }}/{{ args.repo }}/issues"

    auth {
      kind   = "bearer"
      secret = "github.token"
    }

    headers = {
      Accept               = "application/vnd.github+json"
      X-GitHub-Api-Version = "2022-11-28"
    }

    body {
      kind  = "json"
      value = {
        title = "{{ args.title }}"
        body  = "{{ args.body }}"
      }
    }
  }

  result {
    decode = "json"
    output = "Created issue #{{ result.number }}: {{ result.html_url }}"
  }
}
```

The `body` block supports six kinds: `json`, `form_urlencoded`, `multipart`, `raw_text`, `raw_bytes_base64`, and `file_stream`.

For streaming responses — Server-Sent Events or newline-delimited JSON — see [Streaming](/docs/streaming#http-streaming).

To switch between production and staging URLs without duplicating commands, see [Environments](/docs/environments).

For naming conventions, secret declarations, and other patterns that apply across all protocols, see [Best Practices](/docs/best-practices).

For the full field reference, see [Template Schema — HTTP](/docs/template-schema#http).