Orishare
Concepts

Programs

Currencies and the ledger, tiers, rewards, challenges, segments and historical counters.

Currencies and the ledger

A currency is a unit customers earn and spend: points, credits, coins, XP. An environment can have as many as it needs, and each one keeps its own ledger. POST /currencies creates one:

FieldMeaning
codeLowercase identifier (points, credits) that rules, rewards, tier programs and the API use. Fixed once created.
nameDisplay name; can be changed.
precisionDecimal places for display only, default 0. Amounts are always whole minor units: precision 2 shows 125 as 1.25. Fixed once created.
expiration_policy{ "type": "never" } (default) or { "type": "days", "days": 365 }: each award expires that many days after it is earned.
negative_balanceforbid (default) or allow: whether a spend may take the balance below zero.

Every award is a ledger entry in integer minor units. Entries are lots with a remaining balance; spends and expiries consume lots oldest-expiry first. Pending awards (confirm_after) count in pending until the confirmation job moves them to available. Manual adjustments are POST /ledger_entries with a reason; negative adjustments spend FIFO and respect the currency's negative_balance policy.

One ledger per currency

Balances are kept per customer and per currency (available, pending, lifetime_earned). A spend, an expiry or a reversal only ever touches lots of its own currency, so two currencies never mix: a reward costs a set amount of one currency, a tier program counts one currency, and a points transfer moves one currency with its own limits.

That is the reason to add a second currency. If some awards should expire (promotional credits after 30 days) and others should not (purchase points), put them in two currencies, each with its own expiration_policy. In one currency, a spend takes the soonest-expiring lots first and lifetime_earned adds both kinds together. A second currency is also the way to give a different unit (store credit with precision 2, coins next to XP) its own balance and its own negative_balance policy.

Expiry and negative balances

  • An award's expiry is stamped when it is earned: the currency's expiration_policy, unless the rule action sets its own expires_after. Changing the policy later (PATCH /currencies/{id}) applies to new awards only; lots already earned keep their date.
  • Expired lots stop counting at their expiry everywhere: spends, balances, rule conditions, segments, tiers and reports read the balance less any lot already past its expires_at. The expire ledger entry (and the points.expired webhook) follows before the customer's next spend or from a job that runs every 10 minutes, so for a few minutes the sum of a customer's ledger entries can be higher than the balance the API shows.
  • Under forbid, a rule's points.deduct takes what the customer has (the effect reports the requested and the deducted amount), while a manual negative adjustment or a reward redemption that exceeds the balance fails with insufficient_balance. Under allow they go through and the balance goes below zero. A store credit wallet must forbid.
  • A currency can be deleted only while no ledger entry, rule, challenge, reward, issuance, tier program, campaign budget or native program refers to it (resource_in_use otherwise).

Currencies created with capabilities (mechanics currencies, such as the XP that the levels_and_badges playbook installs) also declare whether they are spendable and transferable; an XP currency is neither. Their expiry is fixed at creation and they are never deleted.

Tier programs

{ name, currency, basis, levels[], downgrade_check }. basis is lifetime_earned, earned_rolling_days or balance; levels are ascending thresholds starting at 0. Rules upgrade customers with tier.evaluate; the downgrade job re-evaluates at each local day or month boundary; operators can set a level directly (audited). Removing a level customers hold needs reassign_to; changing the basis needs reevaluate: true.

Rewards

{ name, cost, currency, availability, expiry_after }. Rules issue them; customers redeem an issuance (POST /reward_issuances/{id}/redeem), which spends the cost in the same transaction and fails with insufficient_balance otherwise. Cancelling refunds the cost by a reversal entry and releases availability; expiry does not refund. An issuance reads expired from its expires_at on (also in the status filter), and it can then be neither redeemed nor cancelled. Its version and ETag move up by one at that moment, so a write sent with an If-Match taken before the expiry fails with stale_write. Campaigns and challenges past their ends_at read ended the same way.

Challenges

{ qualifying: { event, conditions? }, target, period, contribution_key?, enrollment, repeatable, completion_actions[] }. Each qualifying event contributes once per contribution_key value; reaching target completes the period exactly once and applies the completion actions in the same transaction. Explicit enrollment gates progress; revoke and reevaluate are audited operator actions.

Completion actions (points.award, points.deduct, reward.issue, attribute.set, webhook.emit) are planned like a rule's actions: an amount may be an expression over the completing event and the customer, rounded once to whole minor units, and confirm_after, expires_after and webhook payload paths work as in rules. If the list cannot be evaluated (an expression reads a property the event lacks) the challenge still completes, nothing of the list is applied, and the execution reports challenge.completion_skipped with the reason. When the challenge belongs to a campaign with a budget, its awards reserve that budget; once the budget is exhausted the challenge still completes, the awards are left out (budget_exhausted) and the other actions apply. reevaluate has no triggering event: a list that reads event.* or customer.counters.* is skipped with no_triggering_event, while customer.attributes, customer.balances, customer.tier, customer.segments and now resolve as usual.

Segments

Static segments have explicit members; dynamic segments are a condition over customer.attributes.* evaluated live at rule time. Campaign audiences and rule conditions reference segments by name, so a referenced segment cannot be renamed or deleted.

Historical counters

{ name, event, window, filter? } counts events per customer and window and is read as customer.counters.<name>. A definition registered after events exist starts backfilling; rules that read it skip with counter_not_ready until POST /counter_definitions/{id}/backfill (inline for small histories, a job otherwise) marks it ready.

Native program contracts

Native programs have an immutable key, a kind, versioned policies and customer participations. The native kinds are streaks, badges, quests (up to 32 steps), tiers (which also carry levels), leaderboards and referrals; Mechanics describes each, and GET /v1/capabilities says which are installed in an environment. Operators author them through change sets (Author a mechanic); POST /programs with its version and activation operations is the direct route (rules:write, and the publish scope for a version). Legacy tiers, rewards, campaigns and challenges retain their existing command behavior; their contract projections do not create an additional effect writer.

A participation pins an immutable program version and an IANA timezone. Publishing or activating a newer version does not silently upgrade existing customers. Persisted periods keep their original start, end, grace, admission and finalization boundaries, including the timezone-data version used to calculate them. Periods are half-open and ISO weeks start on Monday. Opening a new period preserves the previous period's history.

Commands and receipts

The following commands have canonical routes and equivalent aliases. API paths below are relative to the /v1 base.

CommandCanonical POSTAlias POSTRequired scope
Enrollment/program_enrollments/programs/{id}/enrollmentscustomers:write
Correction/program_corrections/program_participations/{id}/correctionsledger:write
Succession/program_successions/program_participations/{id}/successionscustomers:write

Use the customer's internal customer_id and the returned program/participation identifiers; these commands do not infer an external identifier from its spelling. Each POST requires Idempotency-Key, If-Match and a durable operation_key. If-Match fences the participation's state_revision, with "0" for an initial enrollment. Enrollment and succession additionally carry the expected catalog program_revision in the body. A stale revision returns stale_write; retry using freshly read state only after deciding that the operation is still appropriate.

An enrollment body contains program_id, customer_id, program_revision and operation_key. A correction body contains customer_id, participation_id, contribution_id, operation_key and a correction object. That object names kind (refund, void or fraud) and a reason, with either a decimal-string fraction or an integer amount, or neither for a full correction. It does not accept both. The authenticated operator path supplies provenance; callers cannot assert their own trust or mark an arbitrary source as verified.

The same operation key and input through either alias resolves to the same permanent result. Reusing the operation key for different input returns a conflict. HTTP idempotency and permanent command identity are separate: losing a short-lived HTTP replay cache does not allow the command to execute again. Authorization runs before replay, so removed permissions cannot retrieve a previously authorized result through a write route.

Each canonical resource supports GET /<resource>/{id} for its original receipt. Enrollment and succession reads require customers:read; correction reads require ledger:read. Receipts are immutable results of the command, not a live view of participation state. In particular, a timezone request's receipt remains scheduled after its due work completes. Do not treat a receipt as a refreshed participation revision for a different command.

Corrections and owed completion

A correction references a permanent contribution identity, appends a correcting revision and applies the configured compensation. It does not rewrite a historical transition. Qualification and spendable value can be reversed; recognition history follows the program's correction policy. After period finalization, corrections remain explicit revisions of that historical period.

A native program can reach its target while its own reward cannot yet be evaluated, for example because a required counter is still backfilling. The original completion remains owed with its original cause and trust. Recovery applies the reward once readiness returns; it does not create a new contribution or reset an allowance. A period may close while retaining that obligation. This native behavior is distinct from the legacy challenge completion behavior described above.

Finalization waits for already admitted work relevant to the period. Unresolved poison events and unattributed retained receipt obligations block closure until their disposition is resolved. A later period can open while the previous one drains. A late job closes missed periods in order, once each, using their persisted boundaries.

Explicit succession

A succession body contains customer_id, participation_id, program_revision, operation_key and a command object. Use { "type": "version", "program_version_id": "<target version id>" } for an explicit upgrade, or { "type": "timezone", "timezone": "Europe/London" } to request a timezone successor. The command carries compatible state into a successor and marks the predecessor superseded. Program-wide contribution and reward identities survive: upgrading cannot re-earn a milestone or turn a once-only program into a repeatable one. An incompatible upgrade is refused.

Timezone succession takes effect at the next boundary of the currently pinned calendar. It cannot rewrite the current period or produce an overlapping allowance. Multiple requests before that boundary replace the pending destination while retaining that original boundary. Pending accepted activity and owed completion must drain before succession can take effect. A version upgrade with an owed completion returns retryable concurrent_update with reason participation_pending; timezone work waits without moving its effective boundary. New ordinary activity at or after that boundary cannot earn another allowance under the old timezone while succession is blocked.

On this page