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.
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.
Rewards depend on a payment that confirms immediately, so both sides of the flow are only available on these methods:
| Payment Method | Code | Rewards |
|---|---|---|
| Credit/Debit Card | CC | ✅ |
| MB WAY | MBW | ✅ |
| Apple Pay | AP | ✅ |
| Google Pay | GP | ✅ |
| Samsung Pay | SW | ✅ |
| In-person Payments | IPP | ✅ |
| Multibanco | MB | ❌ |
| Virtual IBAN | VI | ❌ |
| Direct Debit | DD | ❌ |
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.
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.
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 thecaptureobject: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.
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.
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.
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:
valuethe 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.modeeitherTOTAL(revert the whole capture) orPARTIAL(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 isvalue − reward_redemption_reversal.
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"
}
}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.
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.
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.
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 MBPOST /capture/{id}:reward_redemption: not supported for payment method DDPOST /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 subscriptionsPOST /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 thanvalue.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,valuedoesn'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 avaluebelow the capture gross.revert.mode PARTIAL requires at least one split id to be specified: PARTIAL sent with norevert.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.
- Customer endpoints: balance and ledger reference
- Single Payment: earn and redeem in one step
- Captures: earn and redeem on a separate capture
- Refunds: revert a payment that used rewards
- Authorizations & Captures: when to split auth and capture