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

# Apply a loyalty card to a transaction

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

## Endpoint

POST `/loyalty/apply`

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

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

## Request body

### Attributes

- `member_id` (string)
    Partner-issued identifier of the customer's loyalty membership, recorded when the
    customer enrolled or linked an existing membership through Revolut Wallet.
- `program_id` (string)
    Partner-issued identifier of the loyalty program associated with the membership,
    recorded during enrolment.
- `transaction_context` (object)
    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`.
  - `transaction_context.rrn` (string)
      ISO 8583 Retrieval Reference Number of the original payment.
  - `transaction_context.merchant_id` (string)
      Card acceptor identifier (DE 42 in ISO 8583 messaging); identifies the sub-merchant for aggregators.
  - `transaction_context.transaction_at` (string)
      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.
  - `transaction_context.total` (object)
      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.
    - `transaction_context.total.amount` (string)
        Non-negative decimal amount as a string (e.g. "72.40").
    - `transaction_context.total.currency` (string)
        ISO 4217 currency code (e.g. GBP, EUR, USD).

## Returns

### 200

Loyalty card applied. Returns the `outcome`, the points the transaction earned, and,
optionally, the customer's new balance.

#### Response attributes

- `points_earned` (object)
    The points earned for this transaction.
  - `points_earned.value` (string)
      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.
  - `points_earned.unit` (string)
      Unit label for the points (e.g. points, miles).
  - `points_earned.monetary_value` (object, optional)
      Optional monetary equivalent of the earned points.
    - `points_earned.monetary_value.amount` (string)
        Non-negative decimal amount as a string (e.g. "72.40").
    - `points_earned.monetary_value.currency` (string)
        ISO 4217 currency code (e.g. GBP, EUR, USD).
- `outcome` (enum)
    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"`. |
    Possible enum values:

    - `applied`
    - `no_points_earned`
- `points_balance` (object, optional)
    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`.
  - `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).

### 202

Transaction accepted but not ready yet (settlement lag). Revolut retries,
honouring `Retry-After`.

:::note
Return no body with `202`.
:::

## 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 | Authenticated, but the credential lacks the required scope/permission (e.g. an OAuth token without `loyalty.apply`). |
| 404 | The transaction or active loyalty membership is definitively not found. Revolut stops retrying. |
| 409 | Unrecoverable conflict: the `Idempotency-Key` was reused with a different request body, or the transaction match is ambiguous. Revolut stops retrying. |
| 410 | Transaction is void (refund/cancellation). Revolut will stop retrying. |
| 429 | Rate limit exceeded. Revolut retries, honouring `Retry-After`. |
| 503 | Temporary capacity issue. Revolut retries, honouring `Retry-After`. |
