# Get started with Tap to Pay on iPhone

Getting started with Tap to Pay on iPhone involves server-side and browser interactions that must be completed before any SDK code is written. This page covers app registration in the Developer Portal, the OAuth consent flow between your backend and the merchant's browser, and the token exchange that provides short-lived credentials to the iOS device.

:::info
Once you complete these steps, move on to the [Integration guide](/docs/guides/merchant/accept-payments/in-person-payments/tap-to-pay/integration-guide) to install the SDK and implement the payment flow.
:::

## Access management

Before diving into the implementation, understand the key concepts that underpin the Tap to Pay authentication model.

Tap to Pay on iPhone is a **partner integration**: your app acts on behalf of a merchant who has a Revolut Business account with payment acceptance enabled for their Merchant account. The merchant must explicitly authorise your application to access Tap to Pay on their behalf - this is done through the OAuth 2.0 authorisation code flow with PKCE (Proof Key for Code Exchange).

### What is a scope

A scope limits what an application can do, such as which API endpoints it can call. The `tap_to_pay_sdk` scope authorises your application to use the Tap to Pay SDK and its associated endpoints. When first authorising an application, the merchant must consent to the scope's access.

### What is PKCE

PKCE (Proof Key for Code Exchange, defined in [RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636)) is an extension to the OAuth 2.0 authorisation code flow that protects against authorisation code interception. In a standard OAuth flow, a client secret is used to authenticate the token exchange. Mobile and desktop apps can't safely store a client secret, so PKCE replaces it with a one-time cryptographic challenge:

1. Your backend generates a random `code_verifier` string.
1. Your backend sends a derived `code_challenge` (SHA-256 hash of the verifier) when redirecting the merchant to the consent page.
1. After the merchant approves, Revolut returns an authorisation code.
1. Your backend exchanges the authorisation code for tokens, sending the original `code_verifier` alongside.
1. Revolut hashes the verifier and compares it to the challenge sent earlier - if they match, the token exchange succeeds.

This ensures that even if an attacker intercepts the authorisation code, they can't exchange it for tokens without the verifier. For the full step-by-step implementation, see [Step 2: Get merchant consent](#2-get-merchant-consent).

The following diagram shows how the three parties interact during the PKCE flow:

```mermaid
sequenceDiagram
    participant App as Your backend
    participant Browser as Merchant's browser
    participant Revolut as Revolut

    App->>App: Generate `code_verifier` + `code_challenge`
    App->>Browser: Redirect to `/partner-confirm` with `code_challenge`
    Browser->>Revolut: Open consent page
    Revolut->>Browser: Show consent screen
    Browser->>Revolut: Merchant clicks **Authorise**
    Revolut->>Browser: Redirect to callback URL with authorisation code
    Browser->>App: Deliver authorisation code
    App->>Revolut: Exchange code for tokens (sends `code_verifier`)
    Revolut->>Revolut: Hash verifier, compare to challenge
    Revolut->>App: Return `access_token` + `refresh_token`
```

### Integration architecture

You can implement the OAuth flow for Tap to Pay in the following ways. Choose the one that best fits your use case.

- ![$Backend-led]

    Your backend handles the entire OAuth flow - consent, token exchange, and token storage. The iOS app requests short-lived device tokens from your backend. 
    
    **Recommended for merchants with multiple employees or multiple iPhones** - the merchant authorises once, and all devices receive tokens from your backend. Requires backend credential storage and token refresh logic.

- ![$App-led]

    The OAuth flow runs directly in the iOS app. The app obtains and stores the access token, and uses it directly with the SDK. 
    
    **Simpler to implement** - no backend credential management needed. Downside: the merchant must complete the OAuth flow on each device separately.

### Token architecture

The token architecture depends on your chosen [integration architecture](#integration-architecture):

- ![$Backend-led]

    Treat your iOS app as an untrusted client for long-lived credentials. Keep consent, refresh, and token exchange logic on your backend, and let the app request fresh device tokens when needed.

    | Component | Responsibility |
    |-----------|----------------|
    | **Your backend** | Stores the merchant's `access_token` and `refresh_token` securely. Exchanges the access token for short-lived device tokens. Serves device tokens to the iOS app via an authenticated endpoint. |
    | **Your iOS app** | Requests short-lived device tokens from your backend via the [`TokenProviderProtocol`](/docs/sdks/merchant-ios-sdk/tap-to-pay-sdk/methods-parameters#tokenproviderprotocol-protocol). Never stores long-lived OAuth credentials. Calls the SDK to start payment sessions. |
    | **Revolut Tap to Pay SDK** | Uses the device token for pairing and payment API calls. Requests a new token when the current one expires or receives an unauthorised error. Calls `onLogout` after two consecutive unauthorised errors - meaning the merchant is no longer authorised (consent revoked, not granted, or app permission removed). |

- ![$App-led]

    The app handles the OAuth flow directly and holds the access token. No backend credential storage or device token exchange is needed.

    | Component | Responsibility |
    |-----------|----------------|
    | **Your iOS app** | Handles the OAuth PKCE flow, stores the `access_token` and `refresh_token` securely (e.g., in the keychain), and refreshes the access token when it expires. Implements `TokenProviderProtocol` to return the stored access token to the SDK. Calls the SDK to start payment sessions. |
    | **Revolut Tap to Pay SDK** | Uses the access token for pairing and payment API calls. Requests a new token when the current one expires or receives an unauthorised error. Calls `onLogout` after two consecutive unauthorised errors - meaning the merchant is no longer authorised (consent revoked, not granted, or app permission removed). |

### Token lifecycle

The OAuth flow produces two types of tokens: 

- **long-lived** credentials (`access_token` and `refresh_token`) that are stored securely, and 
- **short-lived** device tokens that the iOS device receives for SDK use (backend-led architecture only).
 
Understanding the lifetime and refreshability of each token type helps you design a reliable token provisioning strategy.

| Token type | Lifetime | Refreshable | Stored on |
|------------|----------|-------------|-----------|
| `access_token` | ~30 minutes | Yes - use `refresh_token` to obtain a new one | Backend (backend-led) / iOS app keychain (app-led) |
| `refresh_token` | 7 days | N/A | Backend (backend-led) / iOS app keychain (app-led) |
| Device token | ~30 minutes | No - request a new one from your backend | iOS device (short-lived, backend-led only) |

:::danger
**In the backend-led architecture:** Never expose the `access_token` or `refresh_token` to the client device. Your iOS app should only ever receive short-lived device tokens obtained via the token exchange flow.

**In the app-led architecture:** The app holds the `access_token` and `refresh_token` directly. Store them securely in the iOS keychain — never in plain text or unprotected storage.
:::

### Grant types

The OAuth token endpoint accepts different `grant_type` values depending on the operation being performed. You'll encounter three grant types throughout this guide:

| Grant type | Description | Used in |
|------------|-------------|---------|
| `authorization_code` | Exchanges the authorisation code for an `access_token` and `refresh_token`. | [Step 2.4](#24-exchange-the-authorisation-code-for-tokens) |
| `refresh_token` | Refreshes the tokens and returns new ones when the `access_token` expires. | [Refresh the access token](#refresh-the-access-token) |
| `exchange_token` | Exchanges the merchant's `access_token` for a short-lived device token. Specific to the backend-led architecture. | [Step 3: Get a device token](#3-get-a-device-token) |

## How it works

The steps below cover the backend-led OAuth architecture (recommended). If you're using the app-led approach, Steps 2 and 3 are handled directly in the iOS app - see [Integration architectures](#integration-architecture).

The setup flow involves three main steps, all performed before touching the SDK:

1. **Register your application** in the Developer Portal, enable the Tap to Pay scope, set up sandbox authentication, and submit to production.
1. **Get merchant consent** by implementing the OAuth 2.0 authorisation code flow with PKCE. The merchant authorises your application, and your backend receives long-lived OAuth tokens.
1. **Get a device token** - your backend exchanges the merchant's OAuth token for a short-lived device token that the iOS app uses with the SDK.

### Implementation overview

1. [Register your application](#1-register-your-application)
1. [Get merchant consent](#2-get-merchant-consent)
1. [Get a device token](#3-get-a-device-token)

### Before you begin

Before you start, ensure you have the following:

- [ ] **Revolut Business account with Merchant features enabled.**
- [ ] **`API & integrations` permission** enabled on the merchant's Revolut Business account. Without this, the merchant will receive an error when accessing the OAuth consent page.
- [ ] **A backend** able to securely store OAuth tokens and perform server-to-server API calls.
- [ ] **An iPhone XS or newer** with iOS 16.4 or later (for later SDK testing).

:::note
**Sandbox is currently not supported for Tap to Pay.** 

Testing must be done in the production environment with real transactions.
:::

## Set up Tap to Pay authentication

The following steps guide you through registering your application, obtaining merchant consent, and provisioning device tokens - all server-side and browser interactions that must be completed before installing the SDK.

### 1. Register your application

Register your partner application and enable the Tap to Pay scope in the [Developer Portal](https://developer.revolut.com/portal).

#### 1.1 Create a new app

1. Log in to the [Developer Portal](https://developer.revolut.com/portal/signin).
1. Click **:Plus: Add new** to create a new application.
1. Give a name to the application. You may also choose to upload an application icon.

:::info
**Don't have a Developer account yet?** 

See [Sign up for a developer account](/docs/guides/build-banking-apps/get-started/register-your-application-in-the-developer-portal#sign-up-for-a-developer-account)
:::

#### 1.2 Enable the Tap to Pay scope

1. Go to **Settings > :Controls: Scopes**.
1. Enable **Accept Payments with Tap to Pay on iPhone through a 3rd party application**.
1. Click **Continue**.

#### 1.3 Set up Sandbox authentication

Sandbox authentication is required to submit your app for review and approval in the Developer Portal. However, Tap to Pay does not currently support the sandbox environment - these credentials are needed only for the app approval process, not for SDK testing.

1. Configure the **Redirect URLs** for the OAuth flow.
1. Configure one or more allowed **Bundle IDs** for the iOS apps embedding the SDK.
1. Generate a CSR and upload it (or upload an OBIE/eIDAS certificate):

    A Certificate Signing Request (CSR) is a file your backend generates to request a digital certificate from a certificate authority. The Developer Portal uses the CSR to issue sandbox certificates that authenticate your application's API calls. You can generate a CSR using `openssl`:

    ```bash
    openssl req -new -newkey rsa:2048 -nodes -out revolut.csr \
      -keyout private.key \
      -subj '/C=<COUNTRY_CODE>/ST=<STATE_OR_PROVINCE>/L=<LOCALITY>/O=<YOUR_APP_NAME>/OU=<ORG_UNIT>/CN=<COMMON_NAME>' \
      -sha256 -outform der
    ```

    | Parameter | Description | Example |
    |-----------|-------------|---------|
    | `C` | Two-letter country code | `GB` |
    | `ST` | State or province | `England` |
    | `L` | Locality (city) | `London` |
    | `O` | Your application or organisation name | `My Taxi App` |
    | `OU` | Organisation Unit ID - provided by Revolut in the Developer Portal | `001580000103UAvAAM` |
    | `CN` | Common Name - provided by Revolut in the Developer Portal | `2kiXQyo0tedjW2somjSgH7` |

    :::tip
    You can copy the full `openssl` command with the `OU` and `CN` values pre-filled from the **Sandbox authentication** tab in the Developer Portal.
    :::

    :::info
    For OBIE/eIDAS certificate alternatives, see [Certificate types](/docs/guides/build-banking-apps/introduction-to-the-open-banking-api/global-customer-access-controls#certificate-types).
    :::

1. Click **Continue**.

:::note
While sandbox authentication is required for app approval, Tap to Pay is not currently supported in sandbox - testing must be done in the production environment with real transactions.
:::

#### 1.4 Submit to production

1. Click the **Submit to production** widget. The scopes you enabled earlier are already loaded.
1. Configure the **Production endpoints > Redirect URLs**.
1. Configure the **iOS Applications > Bundle IDs** for production.
1. Upload a production certificate in the same format as the sandbox CSR (or OBIE/eIDAS certificate).
1. Click **Continue**.

After submitting, your application is assigned a **Production Client ID** (visible under the **Overview** tab) and the production environment becomes available for integration. Your app enters **:Time: In review** status - the review is triggered automatically, no additional action is required. You can begin implementing and testing with the production credentials during the review period.

:::warning [Bundle ID validation]
The bundle ID is used during the Tap to Pay pairing process. The Card Reader Token issued for [Apple's `ProximityReader` framework](https://developer.apple.com/documentation/ProximityReader/) includes the allowed app bundle ID. If your iOS app's bundle ID is not registered correctly, device pairing will fail.
:::

### 2. Get merchant consent

The merchant consent flow is triggered from your app - typically from a settings screen, payment setup page, or hardware pairing flow where the merchant connects their Revolut Business account. When the merchant clicks a "Connect your Revolut account" button (or similar), your app redirects them to Revolut's consent page to authorise your application.

This is a one-time setup for the merchant. Once consent is granted, your backend stores the OAuth tokens and can refresh them automatically - the merchant doesn't need to repeat this flow. Re-consent is only needed if the merchant revokes permission or the tokens can no longer be refreshed (see [`onLogout` handling](/docs/guides/merchant/accept-payments/in-person-payments/tap-to-pay/integration-guide#4-initialise-the-sdk) in the integration guide).

Your integration must use the OAuth 2.0 authorisation code flow with PKCE. The merchant grants consent to your application for the `tap_to_pay_sdk` scope, and your backend receives long-lived OAuth tokens in return.

#### 2.1 Generate the PKCE verifier

To start the PKCE flow, generate the following values on your backend (or locally and pass them to your backend) using a CLI:

| Value | Description | Used in |
|-------|-------------|---------|
| `code_verifier` | A random 32-byte string, `base64url`-encoded without padding. This is the secret your backend keeps and sends during the token exchange. | [Step 2.4](#24-exchange-the-authorisation-code-for-tokens) |
| `code_challenge` | The SHA-256 hash of the `code_verifier`, `base64url`-encoded without padding. This is sent to Revolut when redirecting the merchant to the consent page so Revolut can verify the exchange later. | [Step 2.2](#22-send-the-consent-request) |

Store them in your backend's session state or environment, so they're available for the subsequent steps:

```bash
# 1. Generate 32 random bytes and base64url-encode them - this is your code_verifier
CODE_VERIFIER=$(head -c 32 /dev/urandom | base64 | tr '+/' '-_' | tr -d '=')

# 2. SHA-256 hash the verifier and base64url-encode the digest - this is your code_challenge
CODE_CHALLENGE=$(printf '%s' "$CODE_VERIFIER" | shasum -a 256 | awk '{print $1}' | xxd -r -p | base64 | tr '+/' '-_' | tr -d '=')
```

:::warning
The code challenge must be exactly 32 bytes after SHA-256 hashing. Incorrect encoding will be rejected.
:::

#### 2.2 Send the consent request

Redirect the merchant's browser to Revolut's consent page by constructing a `GET` request to the consent endpoint with query parameters. The `code_challenge` generated in [Step 2.1](#21-generate-the-pkce-verifier) is included as a query parameter so Revolut can verify the token exchange later.

When the merchant's browser loads this URL, they see a consent page where they can review and approve your application's request to access Tap to Pay on their behalf.

```http [Consent request - browser redirect]
GET https://business.revolut.com/partner-confirm?response_type=code&scope=tap_to_pay_sdk&client_id=ae529bb9-eeb9-4065-b0f0-fa395c6e38f6&redirect_uri=https://yourapp.com/callback&code_challenge_method=S256&code_challenge=2MjyE9vGwLylv5_L_KKNF-LNMJpakVEVHQfdgWwatoQ&state=xyz123
```

The request consists of the consent endpoint and the following query parameters:

| Environment | Base URL |
|-------------|----------|
| Production | `https://business.revolut.com/partner-confirm` |

| Parameter | Value | Description |
|-----------|-------|-------------|
| `response_type` | `code` | Fixed value |
| `scope` | `tap_to_pay_sdk` | Fixed value - must be `tap_to_pay_sdk` for Tap to Pay |
| `client_id` | `{your_client_id}` | From [Step 1](#1-register-your-application) |
| `redirect_uri` | `{your_redirect_uri}` | The URI Revolut redirects to after consent. Depends on your implementation - it could be a callback to your website, the iOS app, or any other URL you choose to handle the OAuth redirect. Must match a URI registered in the Developer Portal. |
| `code_challenge_method` | `S256` | Fixed value |
| `code_challenge` | `{code_challenge}` | Computed in [Step 2.1](#21-generate-the-pkce-verifier) |
| `state` | `{random_string}` | Opaque value to prevent CSRF - generate a random string, send it here, and verify it matches when the redirect comes back in [Step 2.3](#23-handle-the-redirect) |

<!-- TODO: Add screenshot of the consent screen -->

:::warning
The merchant must have the **API & integrations** permission enabled on their Revolut Business account. Without this permission, they will receive an error when trying to access the consent page.
:::

The consent page has no deny button - the merchant can either click **Authorise** or close the page. If they close it without authorising, no redirect is sent back to your application. The merchant can rerun the consent flow from wherever it was initiated in your app (for example, from an admin settings screen).

After the merchant clicks **Authorise**, Revolut redirects their browser back to your application.

#### 2.3 Handle the redirect

Your backend must capture the query parameters from the redirect that Revolut sends to the `redirect_uri` you registered in [Step 1](#1-register-your-application) and specified in the [consent request](#22-send-the-consent-request):

```http [Redirect URL]
GET https://yourapp.com/callback?code={authorisation_code}&state={state}
```

| Parameter | Description |
|-----------|-------------|
| `code` | The authorisation code (one-time use, short-lived). Use this to exchange for tokens in [Step 2.4](#24-exchange-the-authorisation-code-for-tokens). |
| `state` | Must match the `state` you sent in [Step 2.2](#22-send-the-consent-request). If it doesn't match, reject the response - this indicates a possible CSRF attack. |

:::warning
The authorisation code is short-lived. Exchange it for tokens as soon as possible.
:::

#### 2.4 Exchange the authorisation code for tokens

Your **backend** sends a `POST` request to the OAuth token endpoint with the authorisation code and the `code_verifier`. Revolut checks if the verifier matches the challenge sent in [Step 2.2](#22-send-the-consent-request) and returns an `access_token` and a `refresh_token` in the response.

| Environment | Token endpoint |
|-------------|----------------|
| Production | `https://merchant-secure.revolut.com/api/oauth/token` |

- ![Request]

  ```http [Example request]
  POST https://merchant-secure.revolut.com/api/oauth/token
  Content-Type: application/x-www-form-urlencoded

  client_id={your_client_id}
  &grant_type=authorization_code
  &code=oa_prod_abcdefgh123456-a1b2c3d-...
  &code_verifier={code_verifier}
  ```

  **Form parameters:**

  | Parameter | Value | Description |
  |-----------|-------|-------------|
  | `client_id` | `{your_client_id}` | Same client ID from [Step 1](#1-register-your-application) |
  | `grant_type` | `authorization_code` | Fixed value |
  | `code` | `{authorisation_code}` | The code from the redirect in [Step 2.3](#23-handle-the-redirect) |
  | `code_verifier` | `{code_verifier}` | The same `code_verifier` string generated in [Step 2.1](#21-generate-the-pkce-verifier) |

  :::warning
  The `code_verifier` here is **not** the SHA-256 hash - it's the original random bytes, `base64url`-encoded. Revolut hashes it server-side and compares it against the `code_challenge` you sent in [Step 2.2](#22-send-the-consent-request).
  :::

- ![Response]

  ```json [Example response] {2, 4}
  {
    "access_token": "oa_prod_veKzTCONV8TKU0qOrahK4peTQt15PzkEPGTmeHBfQSY",
    "token_type": "Bearer",
    "refresh_token": "oa_prod_t_ytL6L6TCaJVmOwfDFBhqGPC08SrTNM_2x2IaTiQ8g",
    "expires_in": 1799
  }
  ```

  | Field | Description |
  |-------|-------------|
  | `access_token` | Short-lived token (~30 min). Use as `Bearer` token to obtain device tokens in [Step 3](#3-get-a-device-token). |
  | `refresh_token` | Long-lived. Use to obtain new access tokens without re-prompting the merchant. |
  | `expires_in` | Token lifetime in seconds. |

  Store the `access_token` and `refresh_token` securely on your backend. **Never expose them to the client device.**

### 3. Get a device token

The iOS device should not use the merchant's long-lived OAuth token directly. Instead, your backend exchanges the OAuth `access_token` obtained in [Step 2.4](#24-exchange-the-authorisation-code-for-tokens) for a short-lived device token that the iOS app can use with the SDK. This follows [RFC 8693 (OAuth 2.0 Token Exchange)](https://datatracker.ietf.org/doc/html/rfc8693) semantics.

To do this, your backend sends a `POST` request to the same OAuth token endpoint used in [Step 2.4](#24-exchange-the-authorisation-code-for-tokens). The `grant_type` parameter determines the type of exchange - setting it to `exchange_token` tells Revolut to issue a short-lived device token derived from the merchant's `access_token` rather than performing an authorisation code exchange.

| Environment | Token endpoint |
|-------------|----------------|
| Production | `https://merchant-secure.revolut.com/api/oauth/token` |

- ![Request]

  ```http [Example request]
  POST https://merchant-secure.revolut.com/api/oauth/token
  Content-Type: application/x-www-form-urlencoded

  client_id=ae529bb9-eeb9-4065-b0f0-fa395c6e38f6
  &grant_type=exchange_token
  &subject_token=oa_prod_veKzTCONV8TKU0qOrahK4peTQt15PzkEPGTmeHBfQSY
  ```

  **Form parameters:**

  | Parameter | Value | Description |
  |-----------|-------|-------------|
  | `client_id` | `{your_client_id}` | Same client ID from [Step 1](#1-register-your-application) |
  | `grant_type` | `exchange_token` | Fixed value - triggers the device token flow |
  | `subject_token` | `{backend_oauth_access_token}` | The OAuth access token obtained in [Step 2.4](#24-exchange-the-authorisation-code-for-tokens) |

  :::warning
  The `subject_token` is the **access token** your backend holds - not the refresh token, and not the authorisation code.
  :::

- ![Response]

  ```json [Response] {2-4}
  {
    "access_token": "oa_prod_child_9xKmT4vR8bWqJnL2cHsYpZfAeDgU7wXoI0tN5iB3rM",
    "token_type": "Bearer",
    "expires_in": 1800
  }
  ```

  | Field | Description |
  |-------|-------------|
  | `access_token` | Short-lived device token (~30 min). Send this to the device for SDK use. |
  | `token_type` | Always `Bearer`. |
  | `expires_in` | Token lifetime in seconds. The SDK requests a new token before this expires. |

  :::info
  No `refresh_token` is returned. Device tokens cannot be refreshed - when they expire, the device asks your backend for a new one.
  :::

  Your backend should expose an authenticated endpoint that returns the device token's `access_token` to the iOS app. The app calls this endpoint via the SDK's `TokenProviderProtocol` whenever it needs a fresh token.

  :::info
  For more information, see [Integration guide: 3. Implement the token provider](/docs/guides/merchant/accept-payments/in-person-payments/tap-to-pay/integration-guide#3-implement-the-token-provider).
  :::

## Refresh the access token

The `access_token` obtained in [Step 2.4](#24-exchange-the-authorisation-code-for-tokens) expires after approximately 30 minutes. When it expires, your backend needs to use the `refresh_token` to obtain a new `access_token` without re-prompting the merchant for consent.

Your backend sends a `POST` request to the same OAuth token endpoint, with `grant_type=refresh_token` and the stored `refresh_token`.

| Environment | Token endpoint |
|-------------|----------------|
| Production | `https://merchant-secure.revolut.com/api/oauth/token` |

- ![Request]

  ```http [Example request]
  POST https://merchant-secure.revolut.com/api/oauth/token
  Content-Type: application/x-www-form-urlencoded

  client_id={your_client_id}
  &grant_type=refresh_token
  &refresh_token={refresh_token}
  ```

  **Form parameters:**

  | Parameter | Value | Description |
  |-----------|-------|-------------|
  | `client_id` | `{your_client_id}` | Same client ID from [Step 1](#1-register-your-application) |
  | `grant_type` | `refresh_token` | Fixed value - triggers the refresh flow |
  | `refresh_token` | `{refresh_token}` | The `refresh_token` obtained in [Step 2.4](#24-exchange-the-authorisation-code-for-tokens) |

- ![Response]

  ```json [Example response] {2, 4}
  {
    "access_token": "oa_prod_newVeKzTCONV8TKU0qOrahK4peTQt15PzkEPGTmeHBf",
    "token_type": "Bearer",
    "refresh_token": "oa_prod_new_t_ytL6L6TCaJVmOwfDFBhqGPC08SrTNM_2x2IaTi",
    "expires_in": 1799
  }
  ```

  | Field | Description |
  |-------|-------------|
  | `access_token` | New short-lived access token (~30 min). Replace the old one on your backend. |
  | `token_type` | Always `Bearer`. |
  | `refresh_token` | New refresh token. **Store this immediately** - it replaces the previous one. |
  | `expires_in` | Token lifetime in seconds. |

  :::warning
  The response returns a new `refresh_token` - replace the old one immediately, as the previous `refresh_token` becomes invalid.

  The `refresh_token` is valid for 7 days. If you do not use it to refresh and obtain a new `access_token` and `refresh_token` within that period, the merchant will need to go through the consent flow again.
  :::

Once your backend has the new `access_token`, use it to request a new device token - see [Step 3: Get a device token](#3-get-a-device-token). The iOS app's `TokenProviderProtocol` will automatically request a fresh device token from your backend when the current one expires.


## Implementation checklist

Use this checklist to verify that each part of the Tap to Pay authentication setup is correctly implemented before moving on to the [Integration guide](/docs/guides/merchant/accept-payments/in-person-payments/tap-to-pay/integration-guide).

:::note
Since sandbox is not currently supported for Tap to Pay, verify your setup in the production environment using your Production Client ID.
:::

### App registration

- [ ] Application created in the [Developer Portal](https://developer.revolut.com/portal).
- [ ] Tap to Pay scope enabled.
- [ ] Sandbox authentication configured (Redirect URLs, Bundle IDs, CSR/certificate uploaded).
- [ ] App submitted to production and Production Client ID obtained.
- [ ] Production redirect URLs and bundle IDs configured.

### Merchant consent (OAuth PKCE)

- [ ] `code_verifier` and `code_challenge` generation implemented on backend.
- [ ] Consent URL constructed with all required query parameters.
- [ ] Redirect handler captures `code` and `state` from the callback.
- [ ] `state` parameter verified against the value sent in the consent request.
- [ ] Authorisation code exchanged for `access_token` and `refresh_token`.
- [ ] `code_verifier` sent in the exchange (not the hash).
- [ ] `access_token` and `refresh_token` stored securely on backend (never exposed to device).

### Device token

- [ ] Token exchange endpoint implemented (`grant_type=exchange_token`).
- [ ] `subject_token` uses the `access_token` (not refresh token, not auth code).
- [ ] Backend endpoint exposed for the iOS app to request device tokens.
- [ ] Token refresh strategy implemented (request new device token when expired).

### Token refresh

- [ ] Refresh endpoint implemented (`grant_type=refresh_token`).
- [ ] New `refresh_token` from response stored immediately (replaces the old one).
- [ ] Expired `access_token` replaced on backend when refresh succeeds.

:::tip
**You've completed the Tap to Pay authentication setup!** 

Your backend can now issue short-lived device tokens to your registered iOS application, with the OAuth PKCE consent flow implemented. With this, you're ready to install the SDK and start accepting payments.
:::

---

## What's next

- [:DocumentChecked: Integration guide](/docs/guides/merchant/accept-payments/in-person-payments/tap-to-pay/integration-guide 'Step-by-step iOS SDK integration')
- [:16/Code: SDK reference](/docs/sdks/merchant-ios-sdk/tap-to-pay-sdk/methods-parameters 'Methods and parameters for the Tap to Pay iOS SDK')
- [:HelpChat: FAQs](/docs/guides/merchant/accept-payments/in-person-payments/tap-to-pay/faq 'Common questions about Tap to Pay on iPhone')