GitHub CLI Cheatsheet
The api Command
Use this GitHub CLI reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
The REST Path Form
gh api <endpoint> sends an authenticated request and prints the JSON. The endpoint is a REST path with no host and no /api/v3 prefix, and a leading slash is optional.
gh api repos/cli/cli
gh api /repos/cli/cli/releases
gh api user
gh api users/monalisa
gh api repos/cli/cli/issues/123
gh api orgs/my-org/members
gh api repos/cli/cli/commits/main
gh api rate_limit{owner}, {repo}, and {branch} are filled in from the current directory's repository, or from GH_REPO.
gh api repos/{owner}/{repo}/releases
gh api repos/{owner}/{repo}/branches/{branch}/protection
gh api repos/{owner}/{repo}/issues --jq '.[].title'
GH_REPO=cli/cli gh api repos/{owner}/{repo}/labelsIn PowerShell, quote anything containing {...} so the shell does not eat the braces.
Methods and Fields
The method is GET by default, and switches to POST automatically the moment you add a field.
| Flag | Purpose |
|---|---|
-X, --method | Force GET, POST, PATCH, PUT, or DELETE |
-f, --raw-field | key=value, always sent as a literal string |
-F, --field | key=value with type conversion and @file reading |
--input <file> | Send a prebuilt body from a file, - for stdin |
-H, --header | Add a request header, key:value |
--hostname | Target another GitHub host |
-i, --include | Print the status line and response headers too |
--verbose | Print the full request and response |
--silent | Suppress the response body |
--cache <duration> | Cache the response, e.g. --cache 1h, --cache 600s |
-p, --preview | Opt into named API previews via the Accept header |
-f versus -F is the distinction that bites. -f always sends a string. -F converts true, false, null, and bare integers to real JSON types, expands {owner}/{repo}/{branch}, and treats a leading @ as a filename.
gh api -X GET search/issues -f q='repo:cli/cli is:open remote' gh api -X GET repos/{owner}/{repo}/commits -f per_page=100 -f sha=main gh api -F draft=true ... # JSON true gh api -f draft=true ... # the string "true" gh api -F body=@message.md ... # body read from a file gh api -F body=@- ... # body read from stdin gh api ... --input payload.json gh api -F 'files[notes.txt][content]=@notes.txt' # nested key gh api -F 'labels[]=bug' -F 'labels[]=p1' # array
Adding fields to a request you want to stay a GET needs an explicit -X GET, which moves them into the query string. Forget it and you accidentally POST.
Headers, Raw Content, Caching
gh api -H "Accept: application/vnd.github+json" repos/{owner}/{repo} gh api -H "X-GitHub-Api-Version: 2022-11-28" repos/{owner}/{repo} gh api -H "Accept: application/vnd.github.raw" \ repos/{owner}/{repo}/contents/README.md gh api -H "Accept: application/vnd.github.diff" \ repos/{owner}/{repo}/pulls/123 gh api -H "Accept: application/vnd.github.patch" \ repos/{owner}/{repo}/commits/9a3f21c gh api -i repos/{owner}/{repo} # see rate-limit headers gh api --cache 1h repos/{owner}/{repo}/labels
The Accept header is how you get something other than JSON. .raw returns file bytes instead of a base64 blob, .diff and .patch return the textual diff of a PR or commit.
Pagination
gh api --paginate repos/{owner}/{repo}/issues
gh api --paginate -X GET search/issues -f q='repo:cli/cli is:open'
gh api --paginate repos/{owner}/{repo}/stargazers --jq '.[].login' | wc -l
gh api --paginate --slurp repos/{owner}/{repo}/issues > all.json
gh api --paginate repos/{owner}/{repo}/issues \
--jq '.[] | {n: .number, t: .title}'Without --slurp, each page prints as its own JSON array, so the output is a stream of arrays and not one valid document. --slurp wraps every page into a single outer array, which is what jq and any JSON parser expect. Combining --paginate with a per_page of 100 halves the request count:
gh api --paginate -X GET repos/{owner}/{repo}/issues -f per_page=100GraphQL
The GraphQL API is the graphql endpoint, not a method. There is no -X GRAPHQL, that returns an HTML error page.
gh api graphql -f query='{ viewer { login } }' gh api graphql -F owner='{owner}' -F name='{repo}' -f query=' query($owner: String!, $name: String!) { repository(owner: $owner, name: $name) { releases(last: 3) { nodes { tagName } } } } '
Every field other than query and operationName becomes a GraphQL variable, so -F owner={owner} supplies $owner. Use -F when a variable must be a number or a boolean, since -f would send it as a string and the API would reject it.
Paginating GraphQL needs the query to cooperate: accept $endCursor: String and select pageInfo { hasNextPage endCursor }.
gh api graphql --paginate -f query=' query($endCursor: String) { viewer { repositories(first: 100, after: $endCursor) { nodes { nameWithOwner } pageInfo { hasNextPage endCursor } } } } ' --jq '.data.viewer.repositories.nodes[].nameWithOwner'
Reach for GraphQL when REST would need many round trips, or for data REST does not expose, such as discussions, projects v2, and reviewDecision.
Filtering the Response
--jq runs a jq expression in process, with no jq binary required. --template formats with a Go template.
gh api repos/{owner}/{repo} --jq .stargazers_count
gh api repos/{owner}/{repo}/issues --jq '.[].title'
gh api repos/{owner}/{repo}/issues \
--jq '.[] | select(.comments > 5) | "\(.number) \(.title)"'
gh api repos/{owner}/{repo}/labels --jq 'map(.name) | join(", ")'
gh api repos/{owner}/{repo}/releases \
--template '{{range .}}{{.tag_name}}{{"\t"}}{{.name}}{{"\n"}}{{end}}'
gh api user --template '{{.login}} joined {{timeago .created_at}}'Note that --jq and --json are different things. gh api has no --json flag because it already returns raw JSON, while gh pr/gh issue/gh repo need --json to choose fields first. A second consequence: REST returns snake_case keys (tag_name, created_at), while the --json commands return camelCase (tagName, createdAt). Templates written for one break on the other.
Rate Limits and Debugging
gh api rate_limit gh api rate_limit --jq '.rate' gh api rate_limit --jq '.resources.graphql' gh api rate_limit --jq '.rate | "\(.remaining)/\(.limit)"' gh api -i user 2>&1 | grep -i x-ratelimit
rate_limit itself does not count against your quota. Authenticated REST gets 5,000 requests per hour, GraphQL is metered in points, and search has its own much lower bucket, which is why --paginate over search/issues throttles first.
GH_DEBUG=api gh api repos/{owner}/{repo} # log the HTTP traffic
gh api --verbose repos/{owner}/{repo} # full request and response
gh api repos/{owner}/{repo}/nope # 404 body plus exit code 1A failed request exits non-zero and prints the API's error JSON, which usually names the missing scope or the exact invalid field.