---
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).