# Rewards & Redemptions

Rewards let you turn part of a payment into credits that a customer can spend on a later one. The credits sit on a reward account tied to your marketplace and follow the customer who earned them only that customer can redeem them, and the credits expire after a set period (12 months by default).

The flow has two sides. You **generate** rewards by attaching a `reward` block to one or more splits inside a capture. You **redeem** them later by sending a `reward_redemption` block on the next capture. In between, you can read what each customer has on hand through the Customer endpoints. And if a payment is refunded, its rewards are unwound the *Reverting a payment that used rewards* section below covers that in detail.

## How rewards work

The reward account is **marketplace-scoped**. It's provisioned during setup there's no API to create one and you reference it by its UUID inside every reward generation and redemption.

Each entry on a reward account is tied to one customer. When a payment includes a split with a `reward` block, the customer named at the top of the request is credited for the amount in `reward.value`. That credit becomes part of the customer's available balance until it's redeemed or expires.

Redemption works the other way around: on a later capture for the same customer, send a `reward_redemption` block and the matching value is debited from the balance and applied against the payment.

## Generating rewards on a capture

Attach a `reward` block to any split inside the `capture.splits[]` array. The customer at the top of the request is the one credited.

```bash
curl -X POST 'https://api.test.easypay.pt/2.0/single' \
  -H 'AccountId: YOUR_ACCOUNT_ID' \
  -H 'ApiKey: YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "type": "sale",
    "method": "cc",
    "value": 50.00,
    "currency": "EUR",
    "customer": {
      "id": "649e88cf-0b78-4c36-8f99-33f5ebb812a1",
      "name": "John Doe",
      "email": "john.doe@example.com"
    },
    "capture": {
      "descriptive": "Order #1029",
      "splits": [
        {
          "split_descriptive": "Marketplace seller",
          "account": {
            "id": "7e697e0c-c2bf-422a-9535-ab0b750bb832"
          },
          "value": 50.00,
          "reward": {
            "account": {
              "id": "458b2fc4-3092-4de3-abd4-fe1600c09420"
            },
            "value": 2.50,
            "expiration_date": "2027-04-02"
          }
        }
      ]
    }
  }'
```

What each reward field does:

- `reward.account.id`: the marketplace's reward account that holds the credit. Use the same UUID across every reward you want pooled on the same balance.
- `reward.value`: how much of that split becomes a credit. It doesn't change what the customer pays at the till; it's an amount the marketplace contributes off the split's value.
- `reward.expiration_date`: optional. Defaults to 12 months from the transaction date, and cannot be set beyond the one-year maximum. Past this date the credit can no longer be redeemed.


## Reading a customer's balance

Two endpoints expose what a customer has.

`GET /customer/{id}` returns the customer plus a `reward_balances` array with one entry per reward account they hold credits on. This is the authoritative available balance it reflects redemptions, refunds and reverts. Use it when you want a quick total before showing a "Use your rewards" option at checkout.

```json
{
    "id": "649e88cf-0b78-4c36-8f99-33f5ebb812a1",
    "name": "John Doe",
    "email": "john.doe@example.com",
    "reward_balances": [
        {
            "account_id": "458b2fc4-3092-4de3-abd4-fe1600c09420",
            "available_balance": 12.5
        }
    ]
}
```

`GET /customer/{id}/rewards` lists every individual movement on the ledger, newest first. It supports filtering by account, type, expiry, and creation date. Each movement carries a `type`:

- `REWARD`: a credit was earned.
- `REDEMPTION`: a credit was spent on a payment.
- `REWARD_EXPIRATION`: a credit expired.
- `REWARD_REVERSAL`: an earned credit was clawed back (the earning payment was refunded).
- `REWARD_RESTORATION`: a spent credit was given back (a redemption was reverted).


```bash
curl 'https://api.test.easypay.pt/2.0/customer/649e88cf-0b78-4c36-8f99-33f5ebb812a1/rewards?type[]=REWARD&account_id[]=458b2fc4-3092-4de3-abd4-fe1600c09420' \
  -H 'AccountId: YOUR_ACCOUNT_ID' \
  -H 'ApiKey: YOUR_API_KEY'
```

```json
{
    "metadata": { "next_cursor": null, "count": 2 },
    "data": [
        {
            "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
            "account_id": "458b2fc4-3092-4de3-abd4-fe1600c09420",
            "type": "REWARD",
            "amount": 10.0,
            "expiration_date": "2027-01-15",
            "capture": { "id": "c6056234-a3f9-42de-b944-3ed793fcb6bb" },
            "created_at": "2026-01-15T10:30:00Z"
        },
        {
            "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
            "account_id": "458b2fc4-3092-4de3-abd4-fe1600c09420",
            "type": "REWARD",
            "amount": 2.5,
            "expiration_date": "2027-04-02",
            "capture": { "id": "b6f53027-0478-4728-9269-8bcc0f8088ea" },
            "created_at": "2026-04-02T09:12:00Z"
        }
    ]
}
```

Use the ledger when you need a per-movement audit trail for example, when a customer asks where a particular credit came from, or when you want to warn about credits about to expire. For the current available total, prefer `GET /customer/{id}` it is the live figure.

## Redeeming rewards on a capture

To spend credits, add a `reward_redemption` block to the capture you're paying.

**Where the block goes depends on the endpoint:**

- **`POST /single`** (paying in a single step) the block goes **inside the `capture` object**: `capture.reward_redemption`.
- **`POST /capture/{id}`** (capturing an earlier authorisation) the block goes at the **capture body root**: `reward_redemption`.


Redemption is **per reward account**: one `reward_redemption` block targets one account UUID. The redeemed value must not exceed the available balance on that account, nor the value of the payment itself.

**Partial redemption on `POST /single`** apply only some of the balance:

```json
{
    "type": "sale",
    "method": "CC",
    "value": 50.0,
    "customer": {
        "id": "649e88cf-0b78-4c36-8f99-33f5ebb812a1"
    },
    "capture": {
        "reward_redemption": {
            "account": {
                "id": "458b2fc4-3092-4de3-abd4-fe1600c09420"
            },
            "value": 5.0
        }
    }
}
```

**Full redemption on `POST /single`** apply the entire payment value:

```json
{
    "type": "sale",
    "method": "CC",
    "value": 50.0,
    "customer": {
        "id": "649e88cf-0b78-4c36-8f99-33f5ebb812a1"
    },
    "capture": {
        "reward_redemption": {
            "account": {
                "id": "458b2fc4-3092-4de3-abd4-fe1600c09420"
            },
            "value": 50.0
        }
    }
}
```

The `value` you send is the **gross** the amount before rewards. The customer is charged `value − reward_redemption.value` through the chosen payment method (the **rails** amount); the redeemed part is covered by the balance. The redeemed amount is debited from the balance immediately and shows up as a `REDEMPTION` entry on the ledger.

When the redemption equals the whole payment value (**full redemption**), the rails amount is `0`: **nothing is charged at the payment gateway**, and the payment completes on creation with `payment_status = paid`.

## Earning and spending in the same request

A single payment can do both. On `POST /single` the `reward_redemption` block sits inside `capture` and reduces the amount the customer pays; the `capture.splits[].reward` blocks credit the customer based on the splits. They don't interact you can redeem €10 from a previous balance and earn €5 of fresh credits in the same capture.

The `Sale with Splits and Reward Redemption` example on [POST /single](/openapi#tag/Single-Payment) shows the combined shape end-to-end.

## What happens when credits expire

Credits a customer never spends don't linger. A scheduled job finds credits whose expiry date has passed and that still have a positive balance, marks them expired (available balance → 0), and pays the leftover money out from the reward account to the marketplace's payout account. From that point the customer can no longer spend the credit, and the movement appears as `REWARD_EXPIRATION` on the customer ledger.

## Reverting a payment that used rewards

When you refund a capture that carried rewards, the rewards have to be unwound too the earned credits clawed back, and any redeemed cashback given back to the customer. You control this through a **`revert`** block on the refund.

A refund with a `revert` block has three moving parts:

- **`value`** the **gross** you are reverting (rails + any redeemed cashback). The rails money actually paid back and the cashback restored are derived from this and the reversal field below.
- **`revert.mode`** either `TOTAL` (revert the whole capture) or `PARTIAL` (revert specific splits).
- **`revert.reward_redemption_reversal`** *(optional)* how much redeemed **cashback** to restore to the customer's wallet in this refund. The rails money refunded to the payer is `value − reward_redemption_reversal`.


### Total revert

A `TOTAL` revert reverses every split on the capture. The `value` must equal the capture's gross. If you omit `reward_redemption_reversal`, it **defaults to the remaining redemption** (all cashback not yet restored by an earlier refund), and the rails refunded is the rest.

```json
{
    "value": 50.0,
    "revert": {
        "mode": "TOTAL"
    }
}
```

### Partial revert

A `PARTIAL` revert targets one or more splits by id. The `value` must equal the **sum of the targeted splits' gross**. The `reward_redemption_reversal` is a **free amount** you choose, drawn from the capture's redemption **pool** it is *not* tied to the targeted split's proportional share, so a single partial revert can restore the entire redemption if you want.

```json
{
    "value": 12.0,
    "revert": {
        "mode": "PARTIAL",
        "reward_redemption_reversal": 5.0,
        "splits": [{ "id": "3f2a9c10-7b4e-4d21-9a55-0c1e2f3a4b5c" }]
    }
}
```

Here €12 of gross is reverted on the targeted split, €5 of cashback is restored to the customer's wallet, and `12 − 5 = €7` of rails money is refunded to the payer.

### How the cashback pool works

`reward_redemption_reversal` restores cashback from a shared **pool** the total redeemed on the capture, minus whatever earlier refunds already restored. Across a sequence of partial refunds you can never restore more than was originally redeemed. When you restore cashback, it lands back on the customer's available balance immediately (visible via `GET /customer/{id}`) as a `REWARD_RESTORATION` movement, and the earned credits on any reverted split are clawed back as `REWARD_REVERSAL`.

### Balance check before a revert

If the merchant-balance check is enabled for your client (`SPLIT_REFUND_BALANCE_CHECK_ENABLED`), a revert is only accepted when the accounts being debited hold enough balance to cover it. If they don't, the revert is **refused** (HTTP 412) with `refund value can not exceed the balance of the debited accounts, try again later` try again once the balance is available.

### Validation errors

Reverts return **HTTP 412** with one of these messages when a rule is broken:

- `revert.reward_redemption_reversal cannot be negative`: the reversal is below 0.
- `revert.reward_redemption_reversal: the capture has no reward redemption to revert`: you sent a reversal but the capture never redeemed.
- `revert.reward_redemption_reversal cannot exceed the refund value`: the reversal is larger than `value`.
- `revert.reward_redemption_reversal exceeds the remaining redemption (X.XX)`: the reversal is larger than the cashback still available in the pool.
- `revert.reward_redemption_reversal must equal the remaining redemption (X.XX) on a TOTAL revert`: on a TOTAL revert the reversal isn't free; it must clear the whole remaining redemption.
- `revert.splits: the value of the splits to revert X.XX is different than the requested refund value V`: on a PARTIAL revert, `value` doesn't match the targeted splits' gross.
- `the rails refund (X.XX) exceeds the remaining refundable rails value (Y.XX)`: the rails money (`value − reversal`) exceeds what's still refundable.
- `revert_mode TOTAL is not supported for partial refund`: TOTAL sent with a `value` below the capture gross.
- `revert.mode PARTIAL requires at least one split id to be specified`: PARTIAL sent with no `revert.splits`.


To read back exactly what a revert restored, `GET /refund/{id}` exposes the `reward_redemption_reversal` object with `value` (restored in this refund) and `value_remaining` (cashback still restorable on the capture), alongside the embedded capture's `reward_redemption` and per-split `reward` objects. `GET /capture/{id}` likewise exposes the capture's `reward_redemption` and each split's `reward`.

## Next Steps

- [Customer endpoints](/openapi#tag/Customer): balance and ledger reference
- [Single Payment](/openapi#tag/Single-Payment): earn and redeem in one step
- [Captures](/openapi#tag/Captures): earn and redeem on a separate capture
- [Refunds](/openapi#tag/Refunds): revert a payment that used rewards
- [Authorizations & Captures](/docs/guides/authorizations-captures): when to split auth and capture