---
api: '[PREVIEW] Revolut Wallet Loyalty Callbacks'
---

# Issue tokens

:::warning [Pre-release preview]
This is a pre-release preview of upcoming documentation.
The features, workflows, and UI described here are subject to change before the final release.
:::

OAuth 2.0 token endpoint (RFC 6749): the `token_endpoint` in your discovery document.
Revolut's backend calls it server-to-server for the following grants:

- `authorization_code`: exchange the code from the browser redirect, together with the
  Proof Key for Code Exchange (PKCE) `code_verifier` (RFC 7636). Check that the verifier
  matches the `code_challenge` stored with the Pushed Authorization Request (PAR), and
  include an `id_token` (OpenID Connect).
- `client_credentials`: server-to-server access tokens Revolut uses for
  `POST /loyalty/auto-enrol` (scope `loyalty.enrol`) and, if you accept OAuth 2.0 there,
  `POST /loyalty/apply` (scope `loyalty.apply`).

**ID token.** A JSON Web Token (JWT) signed with a key published at your `jwks_uri`, using
the algorithm agreed during onboarding (`RS256` or `ES256`). Include the following
claims: `iss` (your issuer URL), `sub` (your stable identifier for the customer), `aud`
(Revolut's `client_id`), `exp`, `iat`, `nonce` (matching the PAR), and `member_id` (the
membership the customer enrolled in or linked). Revolut validates the signature, `iss`,
`aud`, `exp`, and `nonce`, and ends the flow if any check fails.

Authenticate Revolut on every grant (client secret, private key JWT, or mutual TLS
(mTLS), as registered during onboarding) and reject any unauthenticated request with
`401` (`invalid_client`). Unless mTLS is the registered method, Revolut sends a client
secret or JWT assertion in the request body; that's why `security` lists mTLS as
optional.

Authorisation codes are single-use and valid for 5 minutes. Reject a replayed code, or a
`code_verifier` that doesn't match, with `400` (`invalid_grant`). Don't consume the code
when you return `429` or `503`: Revolut retries the exchange with the same code. Return
every token response with `Cache-Control: no-store` (RFC 6749, section 5.1).

## Endpoint

POST `/oauth2/token`

## Parameters

### header parameters

- `Revolut-Api-Version` (string, optional)
  Contract version Revolut is calling with. Revolut sends it to your authorisation server
  too, but you may ignore it there: never reject a request because of this header.

- `Request-Id` (string, optional)
  Revolut's correlation ID for this request. You may ignore it; echoing it is optional on authorisation-server endpoints.

## Request body

### Attributes

- `grant_type` (enum)
    The OAuth 2.0 grant being exercised:
    
    | Value | Description |
    | ----- | ----------- |
    | `authorization_code` | Exchange the single-use code from the browser redirect, with the PKCE `code_verifier`. |
    | `client_credentials` | Obtain a server-to-server token for `POST /loyalty/auto-enrol` (scope `loyalty.enrol`) or `POST /loyalty/apply` (scope `loyalty.apply`). |
    Possible enum values:

    - `authorization_code`
    - `client_credentials`
- `code` (string, optional)
    The authorisation code from the browser redirect. Single-use and valid for 5 minutes;
    required with the `authorization_code` grant.
- `redirect_uri` (string, optional)
    Must match the `redirect_uri` from the PAR. Required with the `authorization_code` grant.
- `code_verifier` (string, optional)
    PKCE code verifier (RFC 7636): the secret whose SHA-256 hash Revolut sent as
    `code_challenge` on the PAR. Required with the `authorization_code` grant; reject a
    mismatch with `invalid_grant`.
- `scope` (string, optional)
    The scopes Revolut requests with the `client_credentials` grant: `loyalty.enrol`,
    `loyalty.apply`, or both. Not sent with the `authorization_code` grant.
- `client_id` (string)
    Revolut's OAuth 2.0 client ID, as registered during onboarding. Revolut always
    sends `client_id` in the request body, including when the registered
    client-authentication method is mTLS.
- `client_secret` (string, optional)
    Revolut's client secret. Omit it when the registered client-authentication method
    is private key JWT or mTLS.
- `client_assertion_type` (string, optional)
    Client-assertion type, used when the registered client-authentication method is
    private key JWT (RFC 7521).
- `client_assertion` (string, optional)
    The signed JWT assertion authenticating Revolut's client, used with
    `client_assertion_type` for private key JWT authentication.

## Returns

### 200

Tokens issued. Which fields appear depends on the grant.

#### Response attributes

- `access_token` (string)
    The access token Revolut presents to your loyalty API.
- `token_type` (enum)
    Token type. Always `Bearer`.
    Possible enum values:

    - `Bearer`
- `expires_in` (integer)
    Seconds before the access token expires. Keep tokens from the `authorization_code`
    grant short-lived: Revolut uses them only once, for `GET /loyalty/profile`.
- `id_token` (string, optional)
    OpenID Connect ID token (JWT compact serialisation), issued with the
    `authorization_code` grant. Sign it with a key published at your `jwks_uri` and
    include the `iss`, `sub`, `aud`, `exp`, `iat`, `nonce`, and `member_id` claims (see
    `POST /oauth2/token`).
- `scope` (string, optional)
    The scopes the access token carries.

## Error responses

| HTTP status code | Description |
| --- | --- |
| 400 | Malformed or invalid token request (RFC 6749, section 5.2). Authorisation codes are single-use: a used, expired, mismatched, or replayed code, or a `code_verifier` that doesn't match the stored `code_challenge`, returns `invalid_grant`. |
| 401 | Client authentication failed (`invalid_client`): Revolut's client credentials are missing, unknown, or invalid for the registered client-authentication method. |
| 429 | Rate limit exceeded (`429`) or temporary capacity issue (`503`) on your authorisation server. Revolut retries, honouring `Retry-After`. The body format is not specified. |
| 503 | Rate limit exceeded (`429`) or temporary capacity issue (`503`) on your authorisation server. Revolut retries, honouring `Retry-After`. The body format is not specified. |
