Skip to main content

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 as issued, 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 over POST /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 a discount_code or gift_card, give an amount (percentage points for a discount, minor units — cents — for a gift card) and currency (ISO 4217).
  • Recipient — attach a contact_id or an out-of-band email; 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. credit is handled internally; gift cards route manual until you wire provider credentials.
  • Idempotency key — send an Idempotency-Key header or the idempotency_key body 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.
The dashboard dialog pre-validates the same fields, and writes are gated to the owner/admin/developer roles. Support-credit automation usually belongs in an 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) and GET /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 from issued. A terminal incentive rejects further transitions with a clean 409 describing the current state rather than a generic error.
  • POST /incentives/{id}/redeem with an optional reference (order id, ticket id, …) and metadata stamps redeemed_at and marks the row redeemed.
  • POST /incentives/{id}/void with an optional reason (audit-friendly note) stamps voided_at and marks the row voided.
Both replies fold to the CURRENT lifecycle in the same serialized envelope, so a client can read 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 a discount_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 code from GET /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.
Either way, the issued ledger remains the system of record; outbound sends are the delivery mechanism, not a parallel code shelf.

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.
Verify the full sequence in the console ledger simultaneously: the issued-incentive table lists the latest transitions in real time. If you see a 409, the row is already terminal — re-issue with a new reference rather than fighting the lock. For a gift card or discount code, the response carries its minted code only for discount-code reward types; credit shows no code because the billing ledger owns the balance.