Example: Authenticated JSON POST (GitHub API)
curl -X POST https://api.github.com/repos/octocat/hello-world/issues \
-H 'Authorization: Bearer ***' \
-H 'Accept: application/vnd.github.v3+json' \
-H 'Content-Type: application/json' \
-d '{"title":"Weekly sync","body":"agenda TBD"}'
Accepthere selects an API version, not a media type — GitHub's v3 header is required for stable JSON.- No
--compressed? GitHub sends one anyway; the converter injects it only when the source had it, so output stays honest. - In Python:
requests.post(url, headers={...}, json={"title": ...})— neverdata="{\"title\"...}"stringly JSON; that's the bug from this guide.
Reading the response
A created issue comes back with a Location header and a body whose url is the canonical address of the new resource:
curl -sS -D - -o /tmp/issue.json -w '%{http_code}\n' \
https://api.github.com/repos/octocat/hello-world/issues \
-H 'Authorization: Bearer ***' \
-H 'Accept: application/vnd.github.v3+json' \
-H 'Content-Type: application/json' \
-d '{"title":"Weekly sync","body":"agenda TBD"}'
201 CreatedplusLocationmeans the resource exists.200 OKon this endpoint means something else answered — a proxy, a cached GET, or a redirect you didn't follow. Check-w '%{url_effective}\n'with-Lto see where you actually landed.401means the token is missing, expired, or was revoked by GitHub because it leaked (GitHub revokes tokens pushed in public repos — including ones pasted into a public gist).403withinsufficient scopein the body means the token exists but lacksrepo; the body is the diagnosis, so print it:--fail-with-bodykeeps it and still exits non-zero.422withresource: Issuealmost always means the repo doesn't exist or you renamed it — GitHub answers 422 for missing repos on authenticated requests to hide private repos from unauthenticated probes.
Idempotency: this POST has none
Re-run the command and you get a second issue. curl gives you no dedupe hook here, so wrap it:
# check-then-create in one shell function; still racy, but stops the double-paste
issue_exists() {
curl -sS -o /dev/null -w '%{http_code}' \
"https://api.github.com/search/issues?q=repo:octocat/hello-world+in:title+%22Weekly+sync%22" \
-H "Authorization: Bearer $GITHUB_TOKEN"
}
For anything that must not double-fire, keep the create behind a queue with at-most-once delivery, not behind a retried curl. And note what a retried curl does to this request through a redirect — see the redirect trap in curl works, my code fails: a 301 in front of this POST sends an empty POST that can 200 while creating nothing.
Storing the token
$GITHUB_TOKEN in the environment, referenced as -H "Authorization: Bearer $GITHUB_TOKEN" with double quotes. A single-quoted header sends the literal string $GITHUB_TOKEN and GitHub answers 401 Bad credentials — the most common failure of this exact command. The quoting guide covers why the shell, not curl, is at fault.
FAQ
Why does GitHub want Accept: application/vnd.github.v3+json instead of application/json? The vendor media type pins the API representation version. Plain application/json gets you the default version, which can shift under you.
Should the bearer token ever be in the URL? No. Query-string tokens are logged at every intermediary; header tokens aren't. See GET with a body for the same logging argument.