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
        }
    ]
}

available_balance can be negative. This happens when cashback the customer already spent is taken back, for example because the payment that earned it was refunded (see When spent cashback is taken back below). A negative balance is the amount of cashback the customer still owes, and it means none of their credits has anything left to spend.

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. The movements always add up to that balance, including when it is negative.

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. A redemption above the available balance is refused with HTTP 412 and insufficient reward balance: available X.XX, requested Y.XX. While the balance is zero or negative every redemption is refused, so only offer a "Use your rewards" option when available_balance is above zero.

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.

Only the part of a credit that is still unspent expires. Value that was spent, or that went to pay back cashback the customer owed, never expires and is never paid out.

When spent cashback is taken back

Sometimes cashback is taken back after the customer has already spent it:

  • The payment that earned it is refunded. The earned credit is clawed back (see Reverting a payment that used rewards below).
  • The payment that earned it is charged back. The earned credit is clawed back the same way as on a refund.
  • A payment is confirmed late. When a payment that redeemed cashback fails or is deleted, its redemption goes back to the customer's balance. If the payment method later confirms it anyway, the payment keeps its full redemption, even if the customer has spent that cashback on another payment in the meantime.

In all three cases the amount is taken first from the credit it belongs to, then from the customer's other credits on the same reward account, soonest expiry first. Only what the credits can't cover makes the balance negative. Each credit touched appears as its own movement on the ledger (REWARD_REVERSAL for a clawback).

Example. A customer earns €4.00 on order 1 (expiring in June) and €2.00 on order 2 (expiring in December). They spend €3.00 on order 3, which uses the June credit first, leaving €1.00 + €2.00 and a balance of €3.00. Order 1 is then refunded in full, so its €4.00 must be clawed back: €1.00 comes from the June credit, €2.00 from the December credit, and the remaining €1.00 can't be covered. The balance is now −€1.00, and both credits are spent.

Paying back a negative balance

The debt stays on the balance until money comes back to the customer. The next movement that adds money pays the debt first:

  • a new credit earned on a later payment (REWARD);
  • cashback restored by a refund or a chargeback of a payment that redeemed it (REWARD_RESTORATION);
  • a redemption given back because its payment failed or was deleted. No new movement is written: the REDEMPTION entry simply leaves the ledger.

The movement still shows its full amount, and the balance rises by that full amount. In the example, if the customer then earns €3.00, the ledger shows a REWARD of €3.00 and the balance goes from −€1.00 to €2.00. Only €2.00 of the new credit can be spent, and only those €2.00 can expire.

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. If the customer's balance is negative, restored cashback pays that debt first.

The pool starts from the full value redeemed on the capture, even when the payment was confirmed late after its cashback had been spent elsewhere. A first TOTAL revert of such a payment can therefore restore its whole redemption.

Chargebacks on rewarded payments

A chargeback unwinds rewards the same way a refund does, with no request on your side. On a capture that earned rewards, the earned credits are clawed back (REWARD_REVERSAL). On a capture that redeemed cashback, the redeemed cashback is restored to the customer (REWARD_RESTORATION). A full chargeback restores all of it, and a partial chargeback restores the same share of the redemption as the share of the payment charged back. A payment fully paid with cashback charges nothing to a card, so it can't be charged back.

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