By AndyPublished

curl JWT Bearer Token and Postman JWT Authentication, With Examples

To call an API with a JWT from curl, send it in the Authorization header with the Bearer scheme: curl -H "Authorization: Bearer $TOKEN" https://api.example.com/me. In Postman, open the request's Authorization tab, choose Bearer Token and put {{access_token}} in the Token field. Both produce exactly the same HTTP header.

This guide covers curl's built-in --oauth2-bearer option, fetching a token with the client credentials grant, Postman's separate JWT Bearer auth type that signs tokens for you, a script that saves tokens to a Postman environment automatically, and the mistakes behind most 401 and 403 responses.

curl JWT Header: Sending a Bearer Token

RFC 6750 §2.1 defines the header: the word Bearer, one space, then the token. For a JWT the token is the whole compact string, all three dot-separated parts, with no quotes and no line breaks.

TOKEN='eyJhbGciOiJSUzI1NiIsImtpZCI6ImsxIn0.eyJzdWIiOiJ1c2VyLTEyMyJ9.c2ln...'

# GET with the token
curl -H "Authorization: Bearer $TOKEN" https://api.example.com/me

# POST JSON with the token (--json needs curl 7.82.0 or later)
curl -H "Authorization: Bearer $TOKEN" \
  --json '{"name":"Ada"}' https://api.example.com/profile

# Show the status line and response headers, useful for 401s
curl -i -H "Authorization: Bearer $TOKEN" https://api.example.com/me

Use double quotes around the header. With single quotes, the shell does not expand $TOKEN and the server receives the literal text Bearer $TOKEN. -i prints the response headers, including any WWW-Authenticate header, which often names the exact problem (for example error="invalid_token", error_description="The access token expired"). -v shows the request headers too, but it also prints your token to the terminal and anything capturing it.

curl --oauth2-bearer

curl has had a dedicated option since version 7.33.0. It builds the same header for you:

curl --oauth2-bearer "$TOKEN" https://api.example.com/me
# sends: Authorization: Bearer eyJhbGciOi...

It cannot be combined with --basic, --digest, --negotiate or --ntlm. The benefit is mostly readability, and that you cannot forget the Bearer prefix.

curl JWT Authentication: Getting a Token First

Before you can send a token you need one. For machine-to-machine access with an OAuth 2.0 authorisation server (Auth0, Okta, Keycloak, Microsoft Entra ID, Amazon Cognito and others), that is the client credentials grant from RFC 6749 §4.4:

# Client authenticates with HTTP Basic (-u); scope and audience parameters vary by provider
TOKEN=$(curl -s -X POST https://auth.example.com/oauth/token \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -d grant_type=client_credentials \
  -d scope="orders:read" | jq -r .access_token)

curl -H "Authorization: Bearer $TOKEN" https://api.example.com/orders

Some providers expect client_id and client_secret as form fields instead of Basic authentication, and some require an audience or resource parameter to decide which API the token is for. Check the token endpoint documentation for your provider. A home-grown API with a login endpoint usually looks like this instead:

TOKEN=$(curl -s --json '{"email":"ada@example.com","password":"..."}' \
  https://api.example.com/auth/login | jq -r .token)

If jq prints null, the response did not contain that field; run the request without the pipe to see the error body. Once you have a token, inspect its aud, scope and exp claims in the jwtdecode.app decoder or with the command-line one-liners before blaming the API.

curl JWT Token Example: Reusable Script

Keeping the token in a file rather than on the command line keeps it out of shell history. Strip newlines and carriage returns when reading it back:

#!/usr/bin/env bash
set -euo pipefail

token_file="${HOME}/.cache/api-token"
TOKEN=$(tr -d '\r\n' < "$token_file")

status=$(curl -s -o /tmp/resp.json -w '%{http_code}' \
  -H "Authorization: Bearer $TOKEN" https://api.example.com/me)

echo "HTTP $status"
jq . /tmp/resp.json

The tr -d '\r\n' step is not decoration. A token file saved on Windows ends in \r\n; command substitution strips the newline but leaves the carriage return, and curl then sends it inside the header. In testing, a Node.js server rejected that request with 400 Bad Request before any JWT code ran.

Browser-facing APIs often read the token from an HttpOnly cookie instead of the Authorization header. curl sends cookies with -b, and can capture the ones a login endpoint sets with -c:

# Send a known token as a cookie (the cookie name depends on the API)
curl -b "access_token=$TOKEN" https://app.example.com/api/me

# Log in, store the cookies the server sets, then reuse them
curl -c cookies.txt --json '{"email":"ada@example.com","password":"..."}' https://app.example.com/login
curl -b cookies.txt https://app.example.com/api/me

Sending a bearer header to a cookie-based API, or a cookie to a header-based one, produces the same "token missing" 401 as sending nothing. The storage guide explains why an API might choose either.

Postman JWT Bearer Token: the Bearer Token Auth Type

For a token you already have, use Postman's Bearer Token auth type:

  • ·Open the request, or better the collection, and go to the Authorization tab.
  • ·Set Auth Type to Bearer Token.
  • ·In Token, enter {{access_token}} rather than pasting the raw JWT, so the value lives in a variable.
  • ·Set individual requests to Inherit auth from parent so every request in the collection or folder uses the same token.

Postman adds the Bearer prefix itself. Pasting Bearer eyJ... into the Token field produces Authorization: Bearer Bearer eyJ..., a common cause of baffling 401s. The Postman Console shows the headers that were actually sent.

Postman JWT Bearer Authorization: Signing Tokens in Postman

Postman also has a separate auth type called JWT Bearer. Instead of sending a token you paste in, it builds and signs a new JWT for each request from settings you provide:

  • ·Add JWT token to: the request header or a query parameter.
  • ·Algorithm: HS, RS, ES or PS families.
  • ·Secret for HS algorithms, with a Secret Base64 encoded checkbox if your secret is stored as Base64 (the distinction explained in the secret key guide).
  • ·Private key for RS, ES and PS algorithms, in PKCS#8 format (BEGIN PRIVATE KEY).
  • ·Payload: the claims as JSON. Advanced options set the Request header prefix and extra JWT headers such as kid.

Use it when the client is supposed to sign its own token: testing your own HS256 API locally, or APIs that authenticate clients with a self-signed JWT. For tokens issued by an identity provider, use Bearer Token or Postman's OAuth 2.0 auth type, which can run the token request for you. If you need a key pair to try it, generate one with OpenSSL.

Postman JWT Authentication: Save the Token Automatically

Rather than copying tokens by hand, let the login or token request store the result. Add this to that request's Scripts > Post-response tab (called Tests in older Postman versions):

pm.test('token issued', () => {
  pm.response.to.have.status(200);
});

const body = pm.response.json();
pm.environment.set('access_token', body.access_token); // or body.token for a custom login API

Every request using {{access_token}} now picks up the fresh token. Use pm.collectionVariables.set instead if you prefer to keep it on the collection. Postman variables have a local value, which stays on your machine, and a shared value that syncs to the Postman cloud for your team. Keep tokens and client secrets as local values or in Postman Vault, never as shared values in a workspace other people can see.

Postman JWT token example: tracking expiry

Most token endpoints return expires_in (seconds) next to the token. Storing an absolute expiry time lets a collection-level pre-request script warn you before requests start failing:

// Post-response script on the token request
const body = pm.response.json();
pm.environment.set('access_token', body.access_token);
pm.environment.set('access_token_expires_at', Date.now() + body.expires_in * 1000);

// Pre-request script on the collection
const expiresAt = Number(pm.environment.get('access_token_expires_at') || 0);
if (Date.now() > expiresAt - 30000) {
  console.warn('access_token has expired or expires within 30s: run the token request again');
}

The warning appears in the Postman Console. Checking 30 seconds early leaves a margin for clock skew between your machine and the API.

Common Mistakes: Missing Bearer, Quotes and "Jwt is missing"

SymptomLikely cause
401, server log says token missing, or "Jwt is missing"No Authorization header reached the API: typo in the header name, the token sent as a query parameter or cookie instead, or a proxy stripping the header. "Jwt is missing" is the wording of Envoy's JWT filter, used by Istio and other gateways.
401 with a token that decodes fineMissing "Bearer " prefix, "Bearer Bearer" doubled by a tool that adds its own prefix, or an ID token sent where an access token is expected.
Literal $TOKEN arrives at the serverThe header was wrapped in single quotes, so the shell did not expand the variable. Use double quotes.
400 Bad Request from Node.js or a proxyA carriage return or newline inside the header value, usually from a token file saved with Windows line endings.
401 only after a whileThe token expired. Check exp; see the expiry section of the errors guide.
403 ForbiddenThe token is valid but lacks the scope, role or audience the endpoint requires. Getting a new token of the same kind will not help.

For expired, wrong-audience and wrong-issuer failures, the common JWT errors guide maps library error messages to causes.

401 vs 403 With a JWT

RFC 6750 §3.1 separates the two. 401 Unauthorized with error="invalid_token" means the token itself was rejected: missing, malformed, expired, badly signed, or issued for another audience. 403 Forbidden with error="insufficient_scope" means the token was accepted but does not grant this operation. The fix for a 401 is a correct token; the fix for a 403 is a token with different permissions, which usually means requesting other scopes or changing the user's roles. Not every framework follows the RFC exactly, so read the WWW-Authenticate header and the server log together. The JWT debugging routine walks through the checks in order.

⚠
Do not send JWTs in URLs (?access_token=...) unless an API gives you no choice. RFC 6750 discourages it because URLs end up in server logs, proxy logs and browser history. The same goes for Postman's JWT Bearer option to add the token to query parameters.

Summary

  • ·curl: -H "Authorization: Bearer $TOKEN" in double quotes, or --oauth2-bearer "$TOKEN" (curl 7.33.0+).
  • ·Fetch machine tokens with the client credentials grant and jq -r .access_token; strip \r and \n from token files.
  • ·Postman: Bearer Token auth with {{access_token}}, inherited from the collection; JWT Bearer auth only when Postman should sign the token itself.
  • ·Save tokens with pm.environment.set in a post-response script, as local values.
  • ·401 means the token was rejected; 403 means it was accepted but lacks permission.
Ready to decode a token?
Use the free JWT decoder — paste any token for instant results, entirely in your browser.
Open JWT Decoder