> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Run the incentives ledger: promos and support credits

> Issue one-off rewards from the console or the standalone promotions API, audit the issued-incentive ledger, and redeem or void a reward — the complete lifecycle behind referrals, surveys, and loyalty.

# Run the incentives ledger

The incentives ledger is the shared reward engine behind [referrals](/guides/referral-programs), [surveys](/guides/surveys-voc), and [loyalty](/guides/loyalty-program). 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](/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`.

```bash theme={null}
# 1) Issue the support credit (gift card example; the same shape fits discount codes
#    and account credits). Idempotency key makes the retry safe.
curl -X POST "{API_BASE}/incentives/issue" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: ticket-4891-gc-001" \
  -d '{
    "reward_type": "gift_card",
    "source_ref": "support-ticket-4891",
    "recipient": { "email": "customer@example.com" },
    "amount": 2500,
    "currency": "USD"
  }'
```

```json theme={null}
{
  "data": {
    "id": "ipo_d7a2f1c9e8b34f01a2c3d4e5f6a7b8c9",
    "status": "issued",
    "reward_type": "gift_card",
    "amount": 2500,
    "currency": "USD",
    "issued_at": "2026-08-29T11:04:00.000Z",
    "replayed": false
  }
}
```

```bash theme={null}
# 2) Audit the ledger filtered to this issue's class
curl "{API_BASE}/incentives/issued?source=api&reward_type=gift_card" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```json theme={null}
{ "data": [ { "id": "ipo_d7a2f1c9e8b34f01a2c3d4e5f6a7b8c9", "status": "issued" } ] }
```

```bash theme={null}
# 3) Redeem it
curl -X POST "{API_BASE}/incentives/ipo_d7a2f1c9e8b34f01a2c3d4e5f6a7b8c9/redeem" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "reference": "order-99821", "metadata": { "channel": "web" } }'
```

```bash theme={null}
# 4) Void if the credit is scrapped; a terminal row rejects further transitions
curl -X POST "{API_BASE}/incentives/ipo_d7a2f1c9e8b34f01a2c3d4e5f6a7b8c9/void" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "customer declined the credit" }'
```

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.
