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

# Enrol a customer automatically

:::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.
:::

Revolut calls this when a customer enrols in your loyalty program from Revolut Wallet, to
register them without any customer interaction. The request carries the details the
customer agreed to share: their name, email, and phone number, verified by Revolut's know
your customer (KYC) checks, plus any partner-configured fields they enter. Create the
membership and return its identifiers plus the complete loyalty pass envelope.

This endpoint is optional: tell the Revolut Wallet team during onboarding whether you
implement it. If you don't, Revolut starts every new enrolment with the OAuth 2.0 browser
flow (`POST /oauth2/par` with `prompt=create`).

If the customer must act in a browser (accept your terms, provide missing data), return
`403` with the matching UI-required code below and Revolut falls back to the OAuth 2.0
browser flow. Revolut does not retry the auto-enrol after a `403`.

Revolut does not call this endpoint when the customer starts "Link existing account" in
Revolut Wallet: that flow goes straight to the OAuth 2.0 browser flow with `prompt=login`
and no enrolment claims.

**Never silently link an existing membership:** if the shared data matches an existing
account, return `403` with `account_exists`. Revolut then runs the link flow, where the
customer signs in to you and consents to link their membership. Never create a duplicate
membership.

**Never succeed silently when the customer must accept terms or complete a step:** return
the matching `403` code instead.

**Idempotency (this operation has side effects on your side):** Revolut sends one
`Idempotency-Key` for each logical enrolment and reuses it for every retry. If the
enrolment has already completed, return the original `200` without creating another
membership. Reusing the same key with a different request body returns `409`. Do not
store `429` or `503` as the final result for a key; Revolut retries them with the
same key. Retain completed keys and their responses for at least 7 days.

**Status semantics (these drive Revolut's retry behaviour):**
- `200`: membership created; return the `member_id`, `program_id`, and the complete pass envelope.
- `403`: UI-required (see the codes below); Revolut never retries and falls back to the OAuth 2.0 browser flow. `insufficient_scope` is terminal.
- `400` / `401`: terminal; Revolut stops retrying.
- `409`: the `Idempotency-Key` was reused with a different request body; Revolut stops retrying.
- `429` / `503`: temporary capacity issue; Revolut retries, honouring `Retry-After`.

:::note
Validate Revolut's credentials (OAuth 2.0 client credentials, optionally hardened with mutual TLS (mTLS)) and reject any unauthenticated request with `401`.
:::

## Endpoint

POST `/loyalty/auto-enrol`

## Parameters

### header parameters

- `Revolut-Api-Version` (enum, required)
  Contract version Revolut is calling with. Currently `2026-09-17`.
  
  :::note
  If you receive a version you do not implement, respond with `400`.
  :::
  Possible enum values:

  - `2026-09-17`

- `Idempotency-Key` (string, required)
  UUID scoped to your Partner ID and identifying one logical operation. Revolut
  reuses it for every retry.
  
  **Key lifecycle.** Only a `200` completes and consumes the key: store the key with
  its request body and response, and replay the stored response when the key is seen
  again. Retryable responses (`202` on `POST /loyalty/apply` only, `429`, and `503`)
  must not complete or consume the key. Terminal failures (`400`, `401`, `403`, `404`,
  `409`, `410`) need not be retained, as Revolut does not retry them, but if you retain
  them anyway, replay the stored result for that key. Retain completed keys and their
  responses for at least 7 days.
  
  **Body comparison.** The same key with a different request body returns `409`.
  "Different" is semantic, not byte-for-byte: two bodies are the same if their parsed
  JSON is equal, regardless of key order or insignificant whitespace; any difference
  in field values is a conflict.

- `Request-Id` (string, optional)
  Revolut's correlation ID for this request. Echo it back in your response.

## Request body

### Attributes

- `program_id` (string, optional)
    Partner-issued identifier of the loyalty program the customer selected in Revolut
    Wallet. Revolut sends it only if you operate several loyalty programs; the valid
    values are agreed during onboarding.

## Returns

### 200

Membership created. Return the identifiers of the new membership and the complete
loyalty pass envelope for the customer's Revolut Wallet.

#### Response attributes

- `member_id` (string)
    Partner-issued identifier of the new membership. Revolut records it at enrolment
    and sends it back on `POST /loyalty/apply`.
- `program_id` (string)
    Partner-issued identifier of the loyalty program. Always returned, even when you
    operate a single program, so Revolut can record it for `POST /loyalty/apply`.
- `tier` (string, optional)
    Membership tier label.
- `points_balance` (object, optional)
    The customer's current points balance. Include it if you track balances.
  - `points_balance.value` (string)
      Current points balance as a non-negative decimal string with up to six fractional digits.
      Do not use a sign, exponent notation, or redundant leading zeros. Compare values as decimals,
      so `"1500"` and `"1500.00"` are equivalent.
  - `points_balance.unit` (string)
      Unit label for the balance (e.g. points, miles).
  - `points_balance.monetary_value` (object, optional)
      Optional monetary equivalent of the points balance.
    - `points_balance.monetary_value.amount` (string)
        Non-negative decimal amount as a string (e.g. "72.40").
    - `points_balance.monetary_value.currency` (string)
        ISO 4217 currency code (e.g. GBP, EUR, USD).
- `pass` (oneOf)
    The complete loyalty pass envelope for the new membership, in any format accepted
    by `POST /passes` on the [Wallet Partner API](https://developer.revolut.com/docs/api/revolut-wallet-partner). Its
    `partner_reference` is your stable key for the pass. To apply one of your
    [templates](https://developer.revolut.com/docs/guides/revolut-wallet/operations/template-management), set
    `template_id` in the envelope; without it, the pass uses only the styling it carries.

## Error responses

| HTTP status code | Description |
| --- | --- |
| 400 | Malformed request or invalid body. |
| 401 | Missing or invalid Revolut credentials: an mTLS certificate rejected at the application layer (a failed TLS handshake never produces an HTTP response), an invalid JWS signature, or a missing/invalid OAuth 2.0 access token. |
| 403 | The customer must complete steps in the browser (UI-required), or the token lacks the required scope. Revolut never retries a UI-required response; it falls back to the OAuth 2.0 browser flow with the `prompt` value shown below. `insufficient_scope` is terminal: fix the credentials during onboarding.  \| Code \| Description \| \| ---- \| ----------- \| \| `ui_required` \| Generic: the customer must complete steps in the browser before enrolment. Revolut starts the browser flow with `prompt=create`. \| \| `account_exists` \| The shared data matches an existing membership; never create a duplicate. Revolut starts the link flow: `prompt=login` and no enrolment claims. \| \| `terms_acceptance_required` \| The customer must accept your Terms and Conditions in the browser. Revolut starts the browser flow with `prompt=create`. \| \| `additional_data_required` \| Enrolment needs data the customer must provide in the browser. Revolut starts the browser flow with `prompt=create`. \| \| `insufficient_scope` \| Token lacks `loyalty.enrol` (terminal): fix credentials during onboarding. Revolut doesn't start the browser flow. \| |
| 409 | Unrecoverable conflict: the `Idempotency-Key` was reused with a different request body, or the transaction match is ambiguous. Revolut stops retrying. |
| 429 | Rate limit exceeded. Revolut retries, honouring `Retry-After`. |
| 503 | Temporary capacity issue. Revolut retries, honouring `Retry-After`. |
