Rewards: cashback, points, conversion
Cashback, points and conversion as rules the platform evaluates on every capture, with caps enforced under concurrency.
Rewards are rules, not calls. You do not grant cashback; you write a rule and the platform evaluates it on every capture. That keeps the arithmetic, the caps, and the refund clawbacks out of your code.
Three kinds:
| Kind | Effect |
|---|---|
cashback | Credits cash back to the payer |
accrual | Credits points, a separate commodity |
conversion | Defines the rate at which points may become cash |
Cashback
curl -sS -X POST "$WALLETD_API/v1/reward_rules" \
-H "Authorization: Bearer $WALLETD_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"kind": "cashback",
"params": {"rate_bps": 1000},
"scope": {"categories": ["restaurants"]},
"caps": {"per_user_day": 500}
}'10% (1000 basis points) back on restaurant payments, capped at $5.00 per user per day.
Always set per_user_day. The cap is what makes a rule safe to run.
Without one, a rule is an unbounded promise: a user who spends $10,000 in a day
gets $1,000 of your money.
Caps are enforced under concurrency, not best-effort. Eight simultaneous captures against a $5.00 daily cap grant exactly $5.00 in total.
Points
curl -sS -X POST "$WALLETD_API/v1/reward_rules" \
-H "Authorization: Bearer $WALLETD_API_KEY" \
-H "Content-Type: application/json" \
-d '{"kind": "accrual", "params": {"rate_bps": 100}, "scope": {"categories": ["restaurants"]}}'One point per 100 minor units spent. Points are a genuinely separate commodity in a separate account, not cash with a label, and they show up as their own balance:
[
{ "purpose": "cash", "commodity": "USD", "balance": 2460, "available": 2460, "held": 0 },
{ "purpose": "points", "commodity": "POINTS", "balance": 80, "available": 80, "held": 0 }
]Conversion
Points only become cash through an explicit conversion, governed by a rule:
curl -sS -X POST "$WALLETD_API/v1/reward_rules" \
-H "Authorization: Bearer $WALLETD_API_KEY" \
-H "Content-Type: application/json" \
-d '{"kind": "conversion", "params": {"minor_per_point": 1, "min_points": 500}}'Then a user converts:
curl -sS -X POST "$WALLETD_API/v1/rewards/convert" \
-H "Authorization: Bearer $WALLETD_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: convert-user-42-1" \
-d '{"user_id": "'$USER_ID'", "points": 650}'650 points become 650 minor units of cash; the points are debited and the cash credited in one transaction. Below min_points it is refused with 422 insufficient_points; with no conversion rule at all, 422 no_conversion_rule.
That minimum is not bureaucracy. Without a floor, a points programme becomes a currency with a fixed peg, and every accounting question you were avoiding comes back.
func (c *Client) ConvertPoints(ctx context.Context, userID string, points int64, key string) (*Transaction, error) {
var txn Transaction
body := map[string]any{"user_id": userID, "points": points}
err := c.Do(ctx, http.MethodPost, "/v1/rewards/convert", body, &txn, key)
return &txn, err
}Scopes and caps
{
"kind": "cashback",
"params": { "rate_bps": 500 },
"scope": { "categories": ["restaurants", "transport"], "merchant_ids": ["019f..."] },
"caps": { "per_user_day": 500, "per_user_month": 5000 },
"starts_at": "2026-09-01T00:00:00Z",
"ends_at": "2026-09-30T23:59:59Z"
}scope narrows which payments qualify: by merchant category, by specific merchants, or both. starts_at and ends_at make a rule a campaign that switches itself on and off, so you do not need a cron job to end a promotion.
Reading and stopping
curl -sS "$WALLETD_API/v1/reward_rules" -H "Authorization: Bearer $WALLETD_API_KEY"
curl -sS "$WALLETD_API/v1/users/$USER_ID/rewards" -H "Authorization: Bearer $WALLETD_API_KEY"
curl -sS -X POST "$WALLETD_API/v1/reward_rules/$RULE_ID/disable" \
-H "Authorization: Bearer $WALLETD_API_KEY"Disabling stops future grants and touches nothing already granted. Money that reached a user is theirs.
The response carries disabled_at, and that timestamp is not decoration: it is what decides whether a sale that was already made still earns. See below.
Refunds claw back
Refund a payment that earned cashback and the cashback is reversed proportionally, in the same transaction as the refund. You do not track it, and you cannot end up with a user who kept the reward for a purchase they returned.
Rewards are evaluated after capture, against the rules in force at capture
Grants happen on a background job triggered by the capture, so payment.captured can arrive slightly before cashback.granted. If your UI shows a reward, read the balance again on cashback.granted rather than assuming it is already there when the payment settles.
The important half is what that job reads. A sale earns what your rulebook said at the instant it was captured, not what it says when the job runs. Everything follows from that one sentence:
| You do this | A sale captured before it | A sale captured after it |
|---|---|---|
| Publish a rule | earns nothing from it | earns |
| Disable a rule | still earns | earns nothing |
A rule reaches its ends_at | still earns | earns nothing |
So publishing a promotion never pays out on yesterday's sales, and pulling one never claws back cashback from a sale made while it was live, even if that sale's grant has not landed yet. Both directions hold under load, when the job may run seconds or minutes behind the capture.
This is worth knowing precisely because the alternative is so easy to build by accident. If the job read your rules at the moment it ran, what a sale earned would depend on how busy the platform was, and you would have no way to explain a customer's balance to them. The one asymmetry: a rule disabled before disabled_at existed carries no timestamp, and those are excluded rather than given an invented one.
Client-funded rewards
In the ecosystem model, a client can fund cashback from its own marketing budget rather than the platform's. A rule pinned to a funding merchant draws down that merchant's marketing account, and when the budget runs dry the grant is skipped rather than falling back to platform money. A client's promise never spends someone else's balance. See Ecosystem.
Next
- Payments and refunds, which is what triggers evaluation.
- Ecosystem for client-funded budgets.
Subscriptions and dunning
Recurring charges against a wallet, with dunning retries and a billing period that can be charged at most once, structurally.
Limits and credit lines
Tier caps that refuse money before anything irreversible happens, and credit lines that let a business account spend below zero.