Ghost docs
OAuth apps

Exchanging the code

Trade the one-time code for an access token, and the errors that can come back

In the examples, api.<domain> is your Ghost API host and <web> is the Ghost web app.

Your server sends the code and the client secret to:

POST https://api.<domain>/login/oauth/access_token
ParameterRequiredDescription
client_idYesYour app's client ID.
client_secretYesYour app's client secret.
codeYesThe code from the callback.
redirect_uriIf you sent oneThe same redirect_uri the authorize request used.
code_verifierWith PKCEThe verifier behind the code_challenge.

GitHub's clients send no grant_type here, and Ghost needs none. Send the body form-encoded or as JSON.

curl -X POST https://api.<domain>/login/oauth/access_token \
  -H "Accept: application/json" \
  -d client_id=DEyiWXBFvHvUTGkCIQzDrbNEuetuxVOr \
  -d client_secret="$GHOST_CLIENT_SECRET" \
  -d code=k8DfjBZl4QVfliGLImbhbIKc7qUaNvZE \
  -d redirect_uri=https://bot.example/oauth/callback

With Accept: application/json, Ghost answers JSON:

{
  "access_token": "ghost_pat_KOYduxMKlnMGQKszNbnWWRxpLOivafxX…",
  "token_type": "bearer",
  "scope": "repo,read:org"
}

Without it, Ghost answers form-encoded, as GitHub does:

access_token=ghost_pat_UxkahUQBptnqPZulFOGmJEniHcUnmCxK…&token_type=bearer&scope=repo%2Cread%3Aorg

scope lists, comma-separated, the scopes the person approved that Ghost knows. Check it: it can be narrower than what your app asked for.

Errors

Failures also answer HTTP 200, with an error field instead of a token, and an error_description that says what went wrong in words:

{
  "error": "bad_verification_code",
  "error_description": "The code passed is incorrect or expired."
}

Branch on error; show or log error_description. Its wording can change.

errorCause
incorrect_client_credentialsUnknown client_id, or a wrong or replaced client_secret.
bad_verification_codeThe code is wrong, expired, already used, belongs to another app, was issued for another redirect_uri, or the code_verifier does not match.
redirect_uri_mismatchThe redirect_uri matches none of the app's callback URLs.

A failed exchange uses the code up, so start again from the authorize step.

On this page