Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.2.0
info:
title: '[PREVIEW] Revolut Wallet Callbacks Loyalty API'
version: '2026-09-17'
description: ':::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.
:::
Loyalty endpoints you host and Revolut calls. Implement them to this contract so loyalty cards
in customers'' Revolut Wallets are applied automatically when they pay with a Revolut card.
Revolut is always the client; you are the server.
- **Apply a loyalty card to a transaction**: `POST /loyalty/apply`. Called after a customer
enrolled in your loyalty program pays with a Revolut card at your business, we ask you to
apply their loyalty card and tell us how many points they earned.
Customers enrol in your loyalty program via Revolut Wallet. Revolut sends the partner-issued
membership and program identifiers recorded during enrolment, together with the transaction
context you use to match the purchase. Passes themselves are managed through the companion
APIs: the Revolut-hosted Wallet Partner API and the pass
endpoints you host in the Wallet Partner Callbacks API.
Revolut uses the merchant ID and acquiring institution ID you provide during onboarding to
route eligible transactions. The acquiring institution ID is routing data and is not included
in the callback request.
The base URL in `servers` is illustrative; you register your real base URL during onboarding.
:::info
Start with the Revolut Wallet guide and Get started to onboard your business.
:::'
contact:
name: Revolut Wallet partner onboarding
email: revwallet-partner-onboarding@revolut.com
url: https://developer.revolut.com/docs/guides/revolut-wallet/get-started
servers:
- url: https://passes.partner.example/revolut-wallet
description: Your registered base URL (example)
tags:
- name: Loyalty
description: 'Auto-apply loyalty cards to Revolut-card transactions.
:::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.
:::'
paths:
/loyalty/apply:
post:
tags:
- Loyalty
operationId: applyLoyaltyCard
summary: Apply a loyalty card to a transaction
description: ':::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 after detecting an eligible Revolut-card transaction by a customer enrolled
in your loyalty program. Locate the active membership using `member_id` and `program_id`, match
the purchase from `transaction_context`, apply the loyalty card, and award the points earned.
**Transaction matching:** `rrn` + `merchant_id` is the primary key; once you have a
candidate, validate `transaction_at` (±24 hours) and require `total.currency` and the decimal
numeric value of `total.amount` to match exactly. The `LoyaltyTransactionContext` schema
documents the authoritative algorithm and collision handling.
If `member_id` and `program_id` do not identify an active membership, return `404` with
`membership_not_found`. If the membership is active but the purchase earns no points under
your program rules, return `200` with `outcome` set to `no_points_earned` and `0` points.
**Idempotency (this operation has side effects on your side):** Revolut sends one
`Idempotency-Key` for each logical apply operation and reuses it for every retry. If the
operation has already completed, return the original successful result without awarding
points again. Reusing the same key with a different request body returns `409`. Do not store
`202`, `429`, or `503` as the final result for a key; Revolut retries them with the same key.
**Status semantics (these drive our retry behaviour):**
- `200`: loyalty card applied; return the `outcome`, the points earned, and optionally the new balance (`0` points with `no_points_earned` when the purchase earns no points).
- `202`: transaction not ready yet (settlement lag); we retry, honouring `Retry-After`.
- `404`: transaction or active membership definitively not found; we stop retrying.
- `409`: the `Idempotency-Key` was reused with a different request body, or the transaction match is ambiguous (`ambiguous_transaction`); we stop retrying.
- `410`: transaction existed but is void (refund/cancellation); we stop.
- `429` / `503`: temporary capacity issue; we retry, honouring `Retry-After`.
:::note
Validate Revolut''s credentials (mutual TLS (mTLS), JSON Web Signature (JWS), or OAuth 2.0) and reject any unauthenticated request with `401`.
:::'
security:
- mtls: []
- mtls: []
jws: []
- jws: []
- oauth2:
- loyalty.apply
- oauth2:
- loyalty.apply
mtls: []
parameters:
- $ref: '#/components/parameters/ApiVersion'
- $ref: '#/components/parameters/IdempotencyKey'
- $ref: '#/components/parameters/RequestId'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ApplyLoyaltyRequest'
examples:
apply:
value:
member_id: member_987654
program_id: program_gb_rewards
transaction_context:
rrn: '123456789012'
merchant_id: mer_example_01
transaction_at: '2026-09-17T12:00:00.000Z'
total:
amount: '72.40'
currency: GBP
responses:
'200':
description: 'Loyalty card applied. Returns the `outcome`, the points the transaction earned, and,
optionally, the customer''s new balance.
'
headers:
Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/PointsEarned'
examples:
points_earned:
summary: Points earned, with the new balance
value:
outcome: applied
points_earned:
value: '72'
unit: points
points_balance:
value: '4892'
unit: points
zero_points:
summary: Applied, but the purchase earned no points
value:
outcome: no_points_earned
points_earned:
value: '0'
unit: points
'202':
description: 'Transaction accepted but not ready yet (settlement lag). Revolut retries,
honouring `Retry-After`.
:::note
Return no body with `202`.
:::
'
headers:
Request-Id:
$ref: '#/components/headers/RequestId'
Retry-After:
$ref: '#/components/headers/RetryAfter'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
description: 'The transaction or active loyalty membership is definitively not found. Revolut stops
retrying.
'
headers:
Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
examples:
transaction_not_found:
value:
code: transaction_not_found
message: No transaction found for this rrn and merchant_id.
membership_not_found:
value:
code: membership_not_found
message: No active membership found for this member_id and program_id.
'409':
$ref: '#/components/responses/Conflict'
'410':
description: Transaction is void (refund/cancellation). Revolut will stop retrying.
headers:
Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
examples:
transaction_void:
value:
code: transaction_void
message: The transaction is void due to a refund or cancellation.
'429':
$ref: '#/components/responses/TooManyRequests'
'503':
$ref: '#/components/responses/ServiceUnavailable'
components:
headers:
RetryAfter:
description: 'Integer seconds Revolut should wait before retrying (not the HTTP-date form). `0` means
retry immediately. If a retryable response omits this header, Revolut uses its default
backoff schedule.
'
schema:
type: integer
minimum: 0
example: 30
RequestId:
description: Correlation id for the response. Echo the incoming `Request-Id` when present.
schema:
type: string
responses:
Unauthorized:
description: '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.
'
headers:
Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
examples:
invalid_credentials:
value:
code: invalid_credentials
message: Missing or invalid Revolut credentials.
ServiceUnavailable:
description: Temporary capacity issue. Revolut retries, honouring `Retry-After`.
headers:
Request-Id:
$ref: '#/components/headers/RequestId'
Retry-After:
$ref: '#/components/headers/RetryAfter'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
examples:
temporarily_unavailable:
value:
code: temporarily_unavailable
message: Service temporarily unavailable. Please retry.
TooManyRequests:
description: Rate limit exceeded. Revolut retries, honouring `Retry-After`.
headers:
Request-Id:
$ref: '#/components/headers/RequestId'
Retry-After:
$ref: '#/components/headers/RetryAfter'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
examples:
rate_limited:
value:
code: rate_limited
message: Rate limit exceeded. Retry after the specified delay.
Forbidden:
description: 'Authenticated, but the credential lacks the required scope/permission (e.g. an OAuth
token without `loyalty.apply`).
'
headers:
Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
examples:
insufficient_scope:
value:
code: insufficient_scope
message: The credential does not have the loyalty.apply scope.
BadRequest:
description: Malformed request or invalid body.
headers:
Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
examples:
validation_error:
value:
code: validation_error
message: Request validation failed.
Conflict:
description: 'Unrecoverable conflict: the `Idempotency-Key` was reused with a different request
body, or the transaction match is ambiguous. Revolut stops retrying.
'
headers:
Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
examples:
idempotency_conflict:
value:
code: idempotency_conflict
message: The Idempotency-Key was already used with a different request body.
ambiguous_transaction:
value:
code: ambiguous_transaction
message: Multiple transactions match this rrn, merchant_id, transaction_at, and total.
parameters:
ApiVersion:
name: Revolut-Api-Version
in: header
required: true
description: 'Contract version Revolut is calling with. Currently `2026-09-17`.
:::note
If you receive a version you do not implement, respond with `400`.
:::
'
schema:
type: string
enum:
- '2026-09-17'
pattern: ^\d{4}-\d{2}-\d{2}$
example: '2026-09-17'
IdempotencyKey:
name: Idempotency-Key
in: header
required: true
description: 'UUID scoped to your Partner ID and identifying one logical apply 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 `202`, `429`, and `503` must not complete or consume the key.
Terminal failures (`400`, `401`, `403`, `404`, `410`) need not be retained — 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.
'
schema:
type: string
format: uuid
example: 550e8400-e29b-41d4-a716-446655440000
RequestId:
name: Request-Id
in: header
required: false
description: Revolut's correlation id for this request. Echo it back in your response.
schema:
type: string
example: 01970c4c-5790-7fb5-8331-84dfa075bbe9
schemas:
Error:
type: object
description: Standard error body for 4xx/5xx responses.
required:
- code
- message
properties:
code:
type: string
description: Machine-readable snake_case error code.
message:
type: string
description: Human-readable explanation.
error_id:
type: string
description: Your trace id
if you have one.: null
ApplyLoyaltyRequest:
type: object
description: 'Request body for `POST /loyalty/apply`. Contains the partner-issued membership and program
identifiers recorded during enrolment, plus the transaction context to match.
'
required:
- member_id
- program_id
- transaction_context
properties:
member_id:
type: string
minLength: 1
description: 'Partner-issued identifier of the customer''s loyalty membership, recorded when the
customer enrolled or linked an existing membership through Revolut Wallet.
'
examples:
- member_987654
program_id:
type: string
minLength: 1
description: 'Partner-issued identifier of the loyalty program associated with the membership,
recorded during enrolment.
'
examples:
- program_gb_rewards
transaction_context:
$ref: '#/components/schemas/LoyaltyTransactionContext'
PointsEarned:
type: object
description: 'Result of applying a loyalty card to a transaction: the `outcome`, the points the
purchase earned, and optionally the customer''s new balance. Revolut ignores unknown
fields in this response, so you may add fields without breaking compatibility.
'
required:
- points_earned
- outcome
properties:
points_earned:
$ref: '#/components/schemas/PointsAmount'
outcome:
type: string
enum:
- applied
- no_points_earned
description: 'Why the customer earned what they did:
| Value | Description |
| ----- | ----------- |
| `applied` | The membership matched and the loyalty card was applied. `points_earned` contains the points awarded under your program rules. |
| `no_points_earned` | The membership matched and the loyalty card was applied, but the purchase earned no points. `points_earned.value` must be `"0"`. |
'
examples:
- applied
points_balance:
description: 'The customer''s new points balance after this transaction. Include it if you track
balances — when you do, `value` and `unit` are required, and `unit` must match
`points_earned.unit`.
'
$ref: '#/components/schemas/PointsBalance'
Money:
type: object
description: ISO 4217 monetary amount, represented as strings to preserve decimal precision. Amounts are non-negative.
required:
- amount
- currency
properties:
amount:
type: string
pattern: ^\d+(\.\d+)?$
description: Non-negative decimal amount as a string (e.g. "72.40").
examples:
- '72.40'
currency:
type: string
pattern: ^[A-Z]{3}$
description: ISO 4217 currency code (e.g. GBP, EUR, USD).
examples:
- GBP
PointsBalance:
type: object
description: Points balance for a loyalty pass.
required:
- value
- unit
properties:
value:
type: string
pattern: ^(0|[1-9]\d*)(\.\d{1,6})?$
description: '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.
'
examples:
- '1500'
unit:
type: string
minLength: 1
description: Unit label for the balance (e.g. points, miles).
examples:
- points
monetary_value:
description: Optional monetary equivalent of the points balance.
$ref: '#/components/schemas/Money'
LoyaltyTransactionContext:
type: object
description: 'Identifiers Revolut supplies so you can match the originating transaction.
**Authoritative matching algorithm.** Use `rrn` + `merchant_id` to find candidate
transactions. Confirm a candidate only when `transaction_at` is within ±24 hours of the
timestamp you hold, `total.currency` is the same, and the decimal numeric value of
`total.amount` is exactly equal. Compare amounts as decimal numbers rather than raw strings, so
`"72.4"` and `"72.40"` are equal. If no transaction matches, return `404` with
`transaction_not_found`. If more than one transaction matches all fields, return `409` with
`ambiguous_transaction`; Revolut stops retrying and flags the case for investigation.
Revolut uses the acquiring institution ID collected during onboarding to route eligible
transactions. This routing identifier is not included in `transaction_context`.
'
required:
- rrn
- merchant_id
- transaction_at
- total
properties:
rrn:
type: string
description: ISO 8583 Retrieval Reference Number of the original payment.
examples:
- '123456789012'
merchant_id:
type: string
description: Card acceptor identifier (DE 42 in ISO 8583 messaging); identifies the sub-merchant for aggregators.
examples:
- mer_example_01
transaction_at:
type: string
format: date-time
description: 'Timestamp of the purchase in UTC and ISO 8601 format. Accept it as a match only when it is
within ±24 hours of the timestamp you hold for the transaction.
'
examples:
- '2026-09-17T12:00:00.000Z'
total:
description: 'Transaction total used to confirm the match. Require the same currency and exact decimal
numeric amount; do not apply a tolerance for authorised-versus-settled differences.
'
$ref: '#/components/schemas/Money'
PointsAmount:
type: object
description: The points earned for this transaction.
required:
- value
- unit
properties:
value:
type: string
pattern: ^(0|[1-9]\d*)(\.\d{1,6})?$
description: 'Points earned 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 `"72.5"` and `"72.500000"` are equivalent. Use `"0"` when the purchase
earns no points.
'
examples:
- '72'
unit:
type: string
minLength: 1
description: Unit label for the points (e.g. points, miles).
examples:
- points
monetary_value:
description: Optional monetary equivalent of the earned points.
$ref: '#/components/schemas/Money'
securitySchemes:
mtls:
type: mutualTLS
description: '**Mutual TLS (mTLS).** Revolut presents a client certificate signed by the Revolut
Partner CA. Validate it against the Revolut CA chain from onboarding and reject any
request without a valid Revolut certificate. Your Partner ID is bound to it. Can be
used on its own, or layered on top of JSON Web Signature (JWS) signing for defence
in depth.
A failed mTLS handshake terminates at the TLS layer and produces no HTTP response.
Return `401` only when the connection was established but the client certificate is
missing, untrusted, or not bound to your Partner ID — for example, when TLS
terminates at a proxy in front of your application.
'
jws:
type: apiKey
in: header
name: Jws-Signature
description: "**Message signing (JSON Web Signature, JWS).** Revolut signs every request as a\ndetached JWS (RFC 7515, Appendix F) and sends the compact serialisation in the\n`Jws-Signature` header.\nVerify it exactly as follows:\n\n- **Signed bytes.** The signature covers the exact raw HTTP request body bytes as\n transmitted — nothing is re-serialised or re-encoded. The JWS uses\n `b64: false` (RFC 7797), so to verify, take the body bytes you received, verbatim,\n as the JWS payload.\n- **Protected JOSE header.** `alg` (`RS256` or `ES256`), `kid` (the id of the\n Revolut Partner signing key whose certificate you registered during onboarding),\n `iat` (integer Unix epoch seconds, UTC — when the signature was created),\n `b64: false`, and `crit: [\"b64\", \"iat\"]`.\n- **Replay protection.** Reject any request where `iat` differs from your current\n time by more than 5 minutes, allowing for clock skew.\n- **Key selection and rotation.** Verify only against the registered key whose\n `kid` matches the header. Revolut may register a new key (new `kid`) and keep the\n previous key valid for an overlap window before revoking it — accept any\n registered, unrevoked `kid`.\n- **Integrity scope.** The signature covers the request body only. The\n `Idempotency-Key`, `Request-Id`, HTTP method, path, and `Revolut-Api-Version`\n headers are not integrity-protected — TLS (optionally mTLS) and the\n `Idempotency-Key` semantics cover them.\n\nMay be used on its own or together with mTLS.\n"
oauth2:
type: oauth2
description: 'OAuth 2.0 **client credentials**. Revolut requests a short-lived access token from your
token endpoint (authenticating with its onboarding credential: `client_secret`,
`private_key_jwt`, or its mTLS certificate) and sends it as `Authorization: Bearer <token>`.
The token carries the `loyalty.apply` scope. May be used on its own or together with
mTLS.
'
flows:
clientCredentials:
tokenUrl: https://your-token-endpoint.example/oauth/token
scopes:
loyalty.apply: Apply loyalty cards to transactions.