Skip to content
Last updated

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.

Supported payment methods

Rewards depend on a payment that confirms immediately, so both sides of the flow are only available on these methods:

Payment MethodCodeRewards
Credit/Debit CardCC
MB WAYMBW
Apple PayAP
Google PayGP
Samsung PaySW
In-person PaymentsIPP
MultibancoMB
Virtual IBANVI
Direct DebitDD

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.

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.

{
    "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).
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'
{
    "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:

{
    "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:

{
    "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 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.

{
    "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.

{
    "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

Reward data on an unsupported method is refused with HTTP 412. The field is named as the endpoint receives it:

  • POST /single: capture.reward_redemption: not supported for payment method MB
  • POST /capture/{id}: reward_redemption: not supported for payment method DD
  • POST /checkout: payment.capture.reward_redemption: not supported for payment method MB
  • Any of the three, when the reward is on a split: capture.splits[0].reward: not supported for payment method DD

Reward data on an unsupported type is refused the same way:

  • POST /subscription: capture.reward_redemption: not supported for subscriptions
  • POST /checkout: payment.capture.reward_redemption: only supported when type is single

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