HTTP
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:
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:
earl secrets set github.token
earl call github.list_repos
earl call github.list_repos --per_page 10Walk-through
annotations
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
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
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
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 headerapi_key— API key in a header, query param, or cookiebasic— HTTP Basic with username and passwordo_auth2_profile— OAuth2 managed token; see Secrets & Authentication for setup
headers and query
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
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:
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.
To switch between production and staging URLs without duplicating commands, see Environments.
For naming conventions, secret declarations, and other patterns that apply across all protocols, see Best Practices.
For the full field reference, see Template Schema — HTTP.