---
updatedAt: 2026-08-19T09:51:08.000Z
agentTools:
  projectIndex: https://developer.penneo.com/llms.txt
---

# Obtain or refresh an access_token

Obtain a new `access_token` by exchanging an authorization `code`, a `refresh_token` or `api_keys`.


# OpenAPI definition

```json
{
  "openapi": "3.0.0",
  "info": {
    "title": "Penneo Authentication",
    "version": "v1",
    "description": "This page allows you to see the requests, parameters, and responses available from our API.<br><br>You will need an account to use these endpoints, you can get a trial account by visiting [penneo.com](https://penneo.com).<br><br>Please also read [the documentation](https://penneo.readme.io/docs/) before you begin.",
    "contact": {
      "name": "Penneo Support",
      "url": "https://www.support.penneo.com"
    }
  },
  "externalDocs": {
    "url": "https://penneo.readme.io/docs/",
    "description": "Penneo API documentation"
  },
  "servers": [
    {
      "url": "https://login-sandbox.penneo.com/",
      "description": "Sandbox server (uses your test data)"
    },
    {
      "url": "https://login.penneo.com/",
      "description": "Production server (uses your live data)"
    }
  ],
  "components": {
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          },
          "code": {
            "type": "integer"
          },
          "details": {
            "type": "array",
            "items": {
              "type": "object"
            }
          }
        }
      },
      "TokenWithoutRefreshToken": {
        "type": "object",
        "properties": {
          "access_token": {
            "type": "string"
          },
          "access_token_expires_at": {
            "type": "number",
            "format": "unix timestamp"
          },
          "expires_in": {
            "type": "string"
          },
          "token_type": {
            "type": "string"
          }
        }
      },
      "Token": {
        "type": "object",
        "allOf": [
          {
            "$ref": "#/components/schemas/TokenWithoutRefreshToken"
          },
          {
            "type": "object",
            "properties": {
              "refresh_token": {
                "type": "string"
              },
              "refresh_token_expires_at": {
                "type": "number",
                "format": "unix timestamp"
              }
            }
          }
        ]
      }
    },
    "responses": {
      "InternalServerError": {
        "description": "Internal Server Error",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "Unexpected server error occurred. If problem persists please contact the administrator.",
              "code": 500
            }
          }
        }
      }
    }
  },
  "paths": {
    "/oauth/token": {
      "post": {
        "summary": "Obtain or refresh an access_token",
        "description": "Obtain a new `access_token` by exchanging an authorization `code`, a `refresh_token` or `api_keys`.\n",
        "tags": [
          "OAuth"
        ],
        "operationId": "exchangeToken",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "grant_type": {
                    "type": "string",
                    "enum": [
                      "authorization_code",
                      "refresh_token",
                      "api_keys"
                    ]
                  },
                  "redirect_uri": {
                    "type": "string",
                    "format": "uri",
                    "example": "https://your.domain/callback",
                    "description": "Required only if using the Authorization Code grant.\nThe registered callback URI.<br>\nIt's the same URI used in the `POST /oauth/authorize` request.\n"
                  },
                  "refresh_token": {
                    "type": "string",
                    "description": "Required only if using the Refresh Token Grant.<br>\nUse the value of the previously stored `refresh_token`.\n"
                  },
                  "client_id": {
                    "type": "string",
                    "description": "The alphanumeric `client_id` (may be sent in the body or via `Authorization: Basic`).\n"
                  },
                  "client_secret": {
                    "type": "string",
                    "description": "The alphanumeric `client_secret` (may be sent in the body or via `Authorization: Basic`).\n"
                  },
                  "scope": {
                    "type": "string",
                    "description": "A list of space-separated scopes that your application will have access to on behalf of the user.\n"
                  },
                  "code": {
                    "type": "string",
                    "description": "Required only if using the Authorization Code Grant.<br>\nUse the value of the `code` parameter attached to callback request.\n"
                  },
                  "code_verifier": {
                    "type": "string",
                    "description": "Required only for Authorization Code Grant when PKCE is used.<br>\nIf `code_challenge` was provided in the `/oauth/authorize` request, you must include\nthe original `code_verifier` value here. If PKCE was not used (no `code_challenge` in\nthe authorization request), this parameter should be omitted.\n"
                  },
                  "key": {
                    "type": "string",
                    "description": "Required only for the `api_keys` grant.<br>\nAPI Key generated in Penneo user settings.\n"
                  },
                  "digest": {
                    "type": "string",
                    "description": "Required only for the `api_keys` grant.<br>\nConcatenation of the raw nonce, timestamp, and API secret, then *SHA-1 hashed*, and finally\n*Base64-encoded*. The nonce, while sent as a Base64-encoded string, is used in its raw form to\ngenerate this digest.\n"
                  },
                  "nonce": {
                    "type": "string",
                    "description": "Required only for the `api_keys` grant. Used to generate the `digest` parameter.<br>\nA randomly generated string, no longer than 64 characters. It should be sent *Base64-encoded*.\n"
                  },
                  "created_at": {
                    "type": "string",
                    "description": "Required only for the `api_keys` grant. Used to generate the `digest` parameter.<br>\nCurrent date and time in UTC or including timezone. Most standard formats are supported.\nA maximum clock skew of up to 60 seconds is allowed (in plus or minus).\n"
                  }
                },
                "required": [
                  "grant_type"
                ],
                "description": "Include `client_id` and `client_secret` in the body, **or** authenticate the client with\n`Authorization: Basic` (Base64 of `client_id:client_secret`) per RFC 6749 §2.3.1.\n"
              },
              "examples": {
                "code": {
                  "summary": "Authorization Code Grant (with PKCE)",
                  "description": "Example using PKCE. If you did not use `code_challenge` in the authorization request,\nomit the `code_verifier` parameter.\n",
                  "value": {
                    "grant_type": "authorization_code",
                    "code": "string",
                    "redirect_uri": "https://your.domain/callback",
                    "client_id": "string",
                    "client_secret": "string",
                    "code_verifier": "string"
                  }
                },
                "codeWithoutPKCE": {
                  "summary": "Authorization Code Grant (without PKCE)",
                  "description": "Example without PKCE. Use this if you did not provide `code_challenge` in the authorization request.\n",
                  "value": {
                    "grant_type": "authorization_code",
                    "code": "string",
                    "redirect_uri": "https://your.domain/callback",
                    "client_id": "string",
                    "client_secret": "string"
                  }
                },
                "refresh_token": {
                  "summary": "Refresh Token Grant",
                  "value": {
                    "grant_type": "refresh_token",
                    "refresh_token": "string",
                    "redirect_uri": "https://your.domain/callback",
                    "client_id": "string",
                    "client_secret": "string"
                  }
                },
                "api_keys": {
                  "summary": "Api Keys Grant",
                  "description": "This is a custom grant type, meant to exchange API keys for an access token for programmatic access.\nBeware that this grant type does not issue refresh tokens. If your token has expired you will have to\nuse the `api_keys` again to obtain a new one.\n",
                  "value": {
                    "grant_type": "api_keys",
                    "scope": "read",
                    "key": "paadeeesmmmdda",
                    "client_id": "d7898z66d91a56e6ee39d",
                    "client_secret": "taadtaadpstcsm",
                    "digest": "quR/EWLAV4xLf9Zqyw4pDmfV9OY=",
                    "nonce": "d36e316282959a9ed4c89851497a717f",
                    "timestamp": "2003-12-15T14:43:07Z"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/Token"
                    },
                    {
                      "$ref": "#/components/schemas/TokenWithoutRefreshToken"
                    }
                  ]
                },
                "examples": {
                  "withRefreshToken": {
                    "summary": "Access token and refresh token, which can be used to request a new access token.",
                    "value": {
                      "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
                      "access_token_expires_at": 1697703152,
                      "expires_at": "1234567890",
                      "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
                      "refresh_token_expires_at": 1698703152,
                      "token_type": "bearer"
                    }
                  },
                  "withoutRefreshToken": {
                    "summary": "api_keys grant",
                    "description": "The response for the `api_keys` grant does not include a refresh token. If your token has expired\n you will have to use the `api_keys` again to obtain a new one.\n",
                    "value": {
                      "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
                      "access_token_expires_at": 1697703152,
                      "expires_at": "1234567890",
                      "token_type": "bearer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "Invalid authorization code": {
                    "value": {
                      "error": "invalid_grant",
                      "error_description": "Invalid Authorization Code"
                    }
                  },
                  "Invalid redirect URI": {
                    "value": {
                      "error": "invalid_grant",
                      "error_description": "Redirect URI does not match"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "Invalid Client ID or Secret": {
                    "value": {
                      "error": "invalid_client",
                      "error_description": "Invalid client ID or secret"
                    }
                  },
                  "Invalid Client ID": {
                    "value": {
                      "error": "invalid_client",
                      "error_description": "Client ID does not match"
                    }
                  },
                  "Invalid refresh token": {
                    "value": {
                      "error": "invalid_token",
                      "error_description": "Invalid refresh token"
                    }
                  },
                  "Expired refresh token": {
                    "value": {
                      "error": "invalid_token",
                      "error_description": "Refresh token expired"
                    }
                  },
                  "Missing code verifier (when PKCE was used)": {
                    "value": {
                      "error": "invalid_grant",
                      "error_description": "Missing PKCE code verifier"
                    }
                  },
                  "Invalid code verifier": {
                    "value": {
                      "error": "invalid_grant",
                      "error_description": "Invalid PKCE code verifier"
                    }
                  },
                  "Code verifier provided but no challenge was used": {
                    "value": {
                      "error": "invalid_grant",
                      "error_description": "PKCE code verifier provided but no code challenge was used"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    }
  }
}
```