Rewards
Rewards & redemption
Points buy things. Publish a catalogue of rewards — vouchers, digital assets, physical prizes, third-party coupons — and let participants spend their balance to redeem them. Flyhalf validates balance and stock atomically, so concurrent redemptions can't oversell or overdraw.
The catalogue
GET /v1/rewards returns the active catalogue with presentation fields
(subtitle, description, image_url), the point
cost_points, live stock_remaining, and is_claimable
(active, inside its window, and in stock).
curl https://api.flyhalf.run/v1/rewards \ -H "Authorization: Bearer fh_live_…"
For a single participant, GET /v1/participants/{external_id}/rewards returns
the same catalogue plus can_afford computed against their balance, and
meta.balance — their spendable points. Use it to render a storefront where
each card knows whether this person can claim it right now.
{
"data": [
{
"id": 12,
"name": "R50 airtime voucher",
"subtitle": "Any network",
"image_url": "https://…/rewards/1/abc.png",
"type": "voucher_code",
"cost_points": 500,
"stock_remaining": 88,
"is_claimable": true,
"can_afford": true
}
],
"meta": { "balance": 750 }
}
Reward types
voucher_code— hands out a code from a pool you pre-load (see Code pools below). Stock tracks the remaining codes.digital_asset— a virtual reward (gift card, licence key, download link). Load a code pool to hand out one-time codes, or deliver from config when there's no pool.physical— a physical prize; the redemption startspendingand you fulfil it out of band.points_multiplier— a perk that multiplies future point earning.external— the code is generated on the fly by a third-party endpoint you configure (see below).
Code pools (manual fulfilment)
The manual alternative to on-the-fly external generation: pre-load a batch
of codes and Flyhalf hands out one per redemption. In the dashboard, open the reward and
use Import codes to bulk-load from a supplier — paste them or upload a
CSV/TXT file (one code per line; first CSV column; duplicates skipped) — or
Generate codes to mint a batch with an optional prefix. Available codes
are the reward's stock; each redemption claims the next one and marks it redeemed. Works
for voucher_code and digital_asset (virtual) rewards.
Redeem
POST /v1/redemptions spends the participant's points and returns the
fulfilment. Identify the reward by reward_id or by reward (its
name). Pass an idempotency_key so a retried call never double-charges — the
replay returns the original redemption untouched.
curl -X POST https://api.flyhalf.run/v1/redemptions \ -H "Authorization: Bearer fh_live_…" \ -d '{ "participant": "wc_1042", "reward_id": 12, "idempotency_key": "redeem-abc" }'
Success is 201 with the redemption and its fulfilment — for a
code-backed reward the code is right there:
{
"id": 3391,
"reward_id": 12,
"status": "fulfilled",
"fulfilment": {
"type": "voucher_code",
"code": "AIR-7F2K-9QLM"
},
"is_sandbox": false,
"created_at": "2026-07-08T10:15:02+00:00"
}
A business-rule rejection is 422 with an error.type you can
switch on:
insufficient_points— the participant can't afford it.out_of_stock— no stock (or no codes) left.reward_unavailable— inactive, or outside its start/end window.external_fulfilment_failed— the third-party endpoint didn't deliver a code (see below).
{
"error": {
"type": "insufficient_points",
"message": "Participant has 300 points; this reward costs 500."
}
}
External fulfilment
An external reward fetches its code from an endpoint you own at redemption
time — handy when a partner mints coupon codes on demand. Configure it under
Rewards → your reward → External fulfilment. The settings ride on the
reward's config:
{
"endpoint_url": "https://coupons.partner.com/generate",
"method": "POST",
"headers": { "Authorization": "Bearer …" },
"body": {
"customer": "{{participant.external_id}}",
"reward": "{{reward.name}}",
"reference": "{{reference}}"
},
"code_path": "data.coupon_code"
}
String values in headers and body may use these tokens, which
are substituted at redemption time (plain allowlisted substitution — never evaluated):
{{participant.external_id}},{{participant.email}},{{participant.name}}{{reward.name}}{{reference}}— a stable reference (the idempotency key) so the partner can be idempotent too.
code_path is the dot-path to the code in the JSON response — for the shape
above, data.coupon_code. Use the Test fulfilment endpoint
action on the reward's edit page to fire a live request through the same guard before you
go live.
The debit is reversed if the partner fails
External codes are fetched after the points debit commits, outside the
transaction (no HTTP under a row lock). If the endpoint is unreachable, returns a non-2xx,
or has no code at code_path, Flyhalf reverses the points debit, restores the
stock unit, cancels the redemption, and returns 422 with
external_fulfilment_failed — the participant never loses points for a code
that never arrived.
URLs must be public
The fulfilment endpoint is called through the same SSRF egress guard as outbound webhooks: http/https only, no redirects, and any URL that resolves to a private, loopback, link-local or metadata address is rejected. Flyhalf will not make requests into private networks on your behalf.