Rewards API
Issue and redeem loyalty points from your app. One integration powers points across all your merchants and channels.
Overview
The Rewards service is a central loyalty backend. Your apps call it server-to-server; your merchants and members never talk to it directly — they use your app, which calls this API on their behalf.
Each app you connect is a “channel” (e.g. dealerden, ych-hub) under your tenant. Members are identified globally by their horizonId, so the same person is one member everywhere. A merchant is identified by a stable merchant key, so a merchant’s shops in different apps resolve to one merchant and a buyer’s points pool across your apps.
Core concepts
- Member — a person, keyed by
horizonId. Auto-created on first use. Pass an optionaldisplayName. - Merchant — keyed by a stable
keyyou choose (e.g. the merchant’s horizonId). Pass the same key from every app and their shops pool into one merchant. The per-app shop id goes inexternalId(recorded as a link). - Points have two scopes — merchant points (earned at one merchant) and tenant points (usable across all your merchants). An
earncredits both. - Earning rules — configured in the dashboard. When you send a spend amount, the service computes points from your rules.
- Rewards — a catalog you define in the dashboard; members redeem points for a voucher. You can also do raw point
debits for app-defined perks.
Authentication
Every request sends an API key in the x-api-key header. Create one key per app in your tenant dashboard under Integration → New API key (the secret is shown once). Keys are channel-tagged and individually revocable.
x-api-key: rwd_dealerden_xxxxxxxxxxxxxxxxxxxxxxxxBase URL: https://reward.horizonden.dev/api/v1
Keys can be scoped at creation (least privilege): earn, redeem, debit, subscribe, read. A call outside the key's scopes returns 403. Provisioning endpoints (/members/resolve, /merchants/upsert) require the earn scope.
Conventions
- JSON in, JSON out. Success:
{ "ok": true, "data": … }. Error:{ "ok": false, "error": "…" }. - Mutating calls take an
idempotencyKey(unique per tenant). Retries with the same key never double-apply. - Money is sent as integer minor units (
amountCents) plus a 3-letter ISOcurrency. - Auth failures return
401; business errors (insufficient balance, out of stock) return409. - Each key has a per-minute rate limit (default 300). Over the limit returns
429— back off and retry. - While a tenant is suspended, mutating endpoints return
403; reads keep working.
Earn points
Credit points to a member at a merchant — both merchant and tenant balances. Auto-provisions the member and merchant. Two modes:
Explicit points
curl -X POST https://reward.horizonden.dev/api/v1/earn \
-H "x-api-key: $KEY" -H "content-type: application/json" \
-d '{
"horizonId": "hzn_buyer_1",
"displayName": "Ada L.",
"merchant": { "key": "hzn_merch_coffee", "externalId": "shop-42", "name": "Corner Coffee" },
"mode": "points",
"points": 30,
"idempotencyKey": "order-1001"
}'From a spend amount (uses your earning rules)
curl -X POST https://reward.horizonden.dev/api/v1/earn \
-H "x-api-key: $KEY" -H "content-type: application/json" \
-d '{
"horizonId": "hzn_buyer_1",
"merchant": { "key": "hzn_merch_coffee", "externalId": "shop-42" },
"mode": "spend",
"amountCents": 500,
"currency": "USD",
"idempotencyKey": "order-1002"
}'Response
{
"ok": true,
"data": {
"memberId": "…",
"merchantId": "…",
"credited": { "merchant": 30, "tenant": 30 },
"balances": {
"tenant": 30,
"merchants": [{ "merchantId": "…", "name": "Corner Coffee", "balance": 30 }]
}
}
}Get balances
A member’s tenant-wide balance plus every per-merchant balance. Returns zeros if the member has no activity yet.
curl https://reward.horizonden.dev/api/v1/members/hzn_buyer_1/balances -H "x-api-key: $KEY"Resolve a member
Ensure a member exists (idempotent) and read their balances. Useful before showing a loyalty UI.
curl -X POST https://reward.horizonden.dev/api/v1/members/resolve \
-H "x-api-key: $KEY" -H "content-type: application/json" \
-d '{ "horizonId": "hzn_buyer_1", "displayName": "Ada L." }'Upsert a merchant / link a shop
Create a merchant (by stable key) and link this app’s shop id. Optional — earn does this automatically — but useful to pre-register a merchant or attach a second shop.
curl -X POST https://reward.horizonden.dev/api/v1/merchants/upsert \
-H "x-api-key: $KEY" -H "content-type: application/json" \
-d '{ "key": "hzn_merch_coffee", "externalId": "shop-42", "name": "Corner Coffee", "url": "https://…" }'Redeem a reward
Redeem a catalog reward for a member. Debits the matching scope’s points and returns a voucher code. Fails with 409 if the balance is too low or stock is exhausted. Send an idempotencyKey — a retried request returns the original redemption (with deduped: true) instead of double-spending.
curl -X POST https://reward.horizonden.dev/api/v1/redeem \
-H "x-api-key: $KEY" -H "content-type: application/json" \
-d '{ "horizonId": "hzn_buyer_1", "rewardId": "<reward-id from /rewards>", "idempotencyKey": "redeem-9001" }'{ "ok": true, "data": { "redemptionId": "…", "voucherCode": "LEG2MITL", "balances": { … } } }Raw debit
Spend points without the catalog — for app-defined perks. Choose tenant or merchant scope (merchant scope needs merchantKey).
curl -X POST https://reward.horizonden.dev/api/v1/debit \
-H "x-api-key: $KEY" -H "content-type: application/json" \
-d '{
"horizonId": "hzn_buyer_1",
"scope": "merchant",
"merchantKey": "hzn_merch_coffee",
"points": 100,
"reference": "free-shipping-coupon",
"idempotencyKey": "perk-7781"
}'List rewards
Active rewards for your tenant (tenant-wide and per-merchant). Each includes scope, merchantId, costPoints, and remaining stock (or null = unlimited).
curl https://reward.horizonden.dev/api/v1/rewards -H "x-api-key: $KEY"Subscribe a member
Start a member’s paid subscription and get a Stripe Checkout URL to redirect them to. Two kinds:
- Member → tenant (a tenant-wide tier, e.g. “Premium” — unlocks features across all the tenant’s apps): omit
merchantKey; pass a tenant tierplanId. - Member → merchant: pass
merchantKey+ a merchantplanId.
The charge is a direct charge on the seller’s connected account (must be onboarded) + the platform fee.
curl -X POST https://reward.horizonden.dev/api/v1/subscribe \
-H "x-api-key: $KEY" -H "content-type: application/json" \
-d '{
"horizonId": "hzn_user_1",
"planId": "<tenant tier id>",
"returnUrl": "https://yourapp.com/premium/done"
}' # member→tenant (omit merchantKey){ "ok": true, "data": { "checkoutUrl": "https://checkout.stripe.com/…", "kind": "member_to_tenant" } }Check a member's entitlements
Gate features on a member’s tenant-wide subscription. One call tells your app whether the member is subscribed and what their tier unlocks — check features and read numeric limits.
curl https://reward.horizonden.dev/api/v1/members/hzn_user_1/entitlements -H "x-api-key: $KEY"{
"ok": true,
"data": {
"active": true,
"tier": { "code": "premium", "name": "Premium" },
"features": ["receipt-ocr", "unlimited-groups"],
"limits": { "maxGroups": 50 },
"status": "active",
"currentPeriodEnd": 1788...
}
}Example gate: if (!data.active || !data.features.includes("receipt-ocr")) return paywall(). When the member has no active subscription, active is false and features is empty.
Errors
401— missing or invalid/revoked API key.400— validation failed (theerrorstring says which field).404— reward not found.409—insufficient_balanceorout_of_stock.
Questions or need a key? Your tenant owner manages keys and earning rules in the app’s Integration and Earning rules sections.