Run the incentives ledger
The incentives ledger is the shared reward engine behind referrals, surveys, and loyalty. This guide is for the standalone side: operators who mint one-off promos and support credits directly, then track them through to redemption. It covers the console at/marketing/incentives (and its identical mirror under /outbound/incentives) plus the programmatic promotions API. For the per-endpoint contract, see the Incentives API reference.
1. What an incentive is — and what it is not
An incentive here is a single issued reward with a lifecycle: it sits asissued, ends redeemed or voided. It is not a referral payout, a loyalty redemption, or a survey thank-you — those are sources that flow rewards INTO the same engine. The API tags each record with source (one of referral, survey, loyalty, api) so one shared ledger serves every program, and a standalone POST /incentives/issue mints source: "api" alongside them. Choose the standalone flow when the trigger is yours (support-credit, win-back, a one-off promo) rather than a program rule.
Reward types are: credit (tenant account balance, routed to your billing ledger), discount_code (Orbit mints a redeemable code), gift_card (a Tremendous/Tango-style network, recorded until your provider credentials are provisioned), and custom (freeform reward the tenant fulfils itself).
2. Issue one-off promos and support credits
Issue from the console or overPOST /incentives/issue. The fields below match both surfaces:
- Reward type — pick the reward class above. For a
credit, set the billing ledger as the destination; for adiscount_codeorgift_card, give anamount(percentage points for a discount, minor units — cents — for a gift card) andcurrency(ISO 4217). - Recipient — attach a
contact_idor an out-of-bandemail; at least one is required so the ledger can trace the reward back to a person. - Source ref — any caller-side reference (ticket id, order id, campaign run label) up to 200 chars. It keeps ledger rows auditable later.
- Provider id — optional; defaults per reward type when omitted.
creditis handled internally; gift cards route manual until you wire provider credentials. - Idempotency key — send an
Idempotency-Keyheader or theidempotency_keybody field to make retries safe: a matching retry replays the original incentive instead of minting a duplicate. Retries after a redeem or void return the CURRENT lifecycle, not a stale snapshot.
Idempotency-Key so a queued retry after a crash does not double-award a credit.
3. Audit the issued-incentive ledger
The ledger page (console) andGET /incentives/issued (API) both read the SAME projection over your tenant’s CDP event history — there is no separate incentive table to sync.
Filter on server: source, reward_type, contact_id. Page with limit + offset (max 200 per page); the response carries has_more so you know when to keep paging. The status filter is applied after terminal redeem/void events fold into the row, so status=redeemed returns only currently redeemed rows rather than rows redeemed then re-issued. Each row reports status (lifecycle: issued, redeemed, voided), fulfillment_status (the engine), provider_id, reward_type, amount/currency, code (for discount codes), issued_at, and terminal timestamps.
Common reconciliation patterns: a support team lists source=api&reward_type=credit to audit credits granted; a loyalty team drills contact_id to see every reward for a member; the console’s ledger table is the same rows with the same filters for an operator.
4. Redeem or void a reward
Redeem (consumption) and void (cancel) are the only transitions the engine permits, and both apply ONLY fromissued. A terminal incentive rejects further transitions with a clean 409 describing the current state rather than a generic error.
POST /incentives/{id}/redeemwith an optionalreference(order id, ticket id, …) andmetadatastampsredeemed_atand marks the rowredeemed.POST /incentives/{id}/voidwith an optionalreason(audit-friendly note) stampsvoided_atand marks the rowvoided.
status from one call. The per-incentive advisory lock makes two concurrent redemptions safe: one wins, the loser gets the conflict. Console operators reach the same actions on the ledger row detail.
5. Wire incentive codes into outbound campaigns
A standalone reward only reaches the customer when a message delivers it. The cleanest path is adiscount_code issue: Orbit mints a unique code per issuance, and the issued record carries code, which you can splice into an outbound campaign template as a variable. Two styles work:
- Per-recipient campaign codes — the campaigns engine has a
{{coupon_code}}merge-tag that derives a deterministic code from the recipient identity, so a whole audience receives a unique value that nothing shares. - Standalone issue then campaign — for your API-minted rows, read
codefromGET /incentives/issued?…, put it into your template variables for that send, then mark the row redeemed when fulfillment actually happens so the lifecycle stays honest.
Worked example — end to end
The sequence below issues a support credit, audits it, redeems it, and voids one you scrapped. Replace{YOUR_API_KEY} and {API BASE} (the API root for your workspace) with your credentials, e.g. https://orbit.devotel.io.
code only for discount-code reward types; credit shows no code because the billing ledger owns the balance.