Operational description
- Posted and reversed transaction counts
- Available point credits, debits, and net movement
- Current member balances and redemption states
- Environment-wide webhook delivery status
Follow tested request shapes for earning, reading balances, returns, engagement events, offers, and signed outcomes.
A member identity is not permission to join every loyalty program. Follow these four steps from a trusted backend so consent, value, and exit remain explainable.
POST the stable customer ID and identity fields to /members. This creates no program profile or points account. A legacy nonempty programIds body receives 422 explicit_program_enrollment_required.
PUT the exact program path with consentStatus: opted_in, the notice version shown, a nonempty evidence object, and one stable idempotency key. 201 means a profile or account was created; 200 means it already existed or the result was replayed. Existing balances are never reset.
Earn, reward, redemption, credit-adjustment, and partner-exchange entry paths check active program enrollment at the database write boundary.
DELETE the same program path with a reason, evidence, and idempotency key. Available, pending, and reserved balances must all be zero and no reservation may remain. The command expires scoped choice awards and cancels scoped quotes, but retains ledger, transaction, consent, audit, and reward history. It remains available while Production is locked.
Use the customer ID your own identity system already trusts.
{
"externalCustomerId": "<external-customer-id>",
"displayName": "<display-name>",
"email": "<customer-email>",
"consentStatus": "opted_in",
"noticeVersion": "<identity-notice-version>"
}PUT /api/v1/members/<external-customer-id>/programs/<program-id>
Authorization: Bearer <server-api-key>
X-Environment: <environment-id>
Idempotency-Key: <stable-enrollment-key>
Content-Type: application/json
{
"consentStatus": "opted_in",
"noticeVersion": "<rewards-notice-version>",
"evidence": { "source": "<consent-capture-channel>" }
}DELETE /api/v1/members/<external-customer-id>/programs/<program-id>
Authorization: Bearer <server-api-key>
X-Environment: <environment-id>
Idempotency-Key: <stable-unenrollment-key>
Content-Type: application/json
{
"reason": "<approved-withdrawal-reason>",
"noticeVersion": "<rewards-notice-version>",
"evidence": { "source": "<withdrawal-channel>" }
}Create the program, active member, and events:write key in the same environment before sending the event.
curl -X POST "https://app.loyumi.com/api/v1/events" \
-H "Authorization: Bearer <sandbox-key>" \
-H "X-Environment: <sandbox-environment-id>" \
-H "Idempotency-Key: order-10042" \
-H "Content-Type: application/json" \
-d '{
"type": "purchase.completed",
"programId": "<program-id>",
"externalCustomerId": "customer-123",
"amountMinor": 2599,
"channel": "web",
"currency": "USD",
"sourceReference": "order-10042",
"items": [{
"lineReference": "order-10042-line-1",
"sku": "sku-…",
"quantity": 1,
"unitPriceMinor": 2599
}]
}'By default, Loyumi evaluates a purchase when it is received, using the current published configuration and current member tier. That is the simplest choice for live commerce. For a late or replayed event that must use historical policy, explicitly pin the event to the configuration and tier evidence that were valid at occurredAt.
{
"type": "purchase.completed",
"occurredAt": "2026-08-01T18:42:00.000Z",
"context": {
"attributes": {
"loyumiEvaluation": {
"mode": "event_time_pinned_configuration",
"configurationVersion": 7,
"memberTier": "gold"
}
}
}
}receipt_time_current_configurationThe default. Uses the configuration and tier known when Loyumi receives the event.
event_time_pinned_configurationRequires occurredAt, a positive immutable configurationVersion that was published and effective then, and historical memberTier evidence.
Response evidenceKeep evaluationMode, evaluationAt, configurationVersion, loyaltyUnitPrecision, and ruleDecisions with your order evidence.
An earn rule can use integer numerator and denominator values, explicit currency and loyalty precision, and a named rounding policy. This avoids floating-point drift and makes every decision reproducible.
{
"match": "all",
"conditions": [
{ "field": "channel", "operator": "equals", "value": "app" }
],
"earn": {
"currencyExponent": 2,
"loyaltyUnitPrecision": 0,
"rateNumerator": 3,
"rateDenominator": 2,
"rounding": "floor",
"tierRounding": "nearest_half_up"
}
}URL-encode the external customer ID. The response includes identity status, consent state, tier, qualification points, and account balances by program.
members:read.X-Environment as the award.Cache-Control: no-store.GET /api/v1/members/customer-123
Authorization: Bearer <sandbox-key>
X-Environment: <sandbox-environment-id>Reference the original order, then select the original lineReference and returned quantity. Loyumi derives the exact attributable amount and points from the stored purchase lines.
curl -X POST "https://app.loyumi.com/api/v1/returns" \
-H "Authorization: Bearer <sandbox-key>" \
-H "X-Environment: <sandbox-environment-id>" \
-H "Idempotency-Key: return-10042-1" \
-H "Content-Type: application/json" \
-d '{
"sourceReference": "order-10042",
"returnReference": "return-10042-1",
"items": [
{ "lineReference": "order-10042-line-1", "quantity": 1 }
]
}'The event definition must already be active in the selected environment and the member must exist. Use a source reference that identifies the real behavior once.
events:write.awards.{
"type": "profile.completed",
"externalCustomerId": "customer-123",
"sourceReference": "profile-customer-123-v1",
"occurredAt": "2026-08-02T08:00:00.000Z",
"attributes": { "source": "account_page" }
}Use an offers:evaluate credential and send positive integer minor-unit prices plus the member, channel, currency, market, store, tag, and line context relevant to the decision. The result explains subtotal, discount, total, eligible effects, matched lines, and coupon validity.
{
"programId": "prg_…",
"externalCustomerId": "customer-123",
"couponCode": "WELCOME10",
"channel": "web",
"currency": "USD",
"market": "US",
"items": [
{
"lineReference": "cart-line-1", "sku": "SKU-42",
"category": "coffee", "quantity": 2, "unitPriceMinor": 650
}
]
}List APIs return at most 200 records at a time. Choose filters on the first request. If page.nextCursor is present, send that opaque value unchanged on the next request. Stop when it is null.
Choose a program, customer, source reference, state, or time window that matches the question.
Keep requestId with your investigation evidence.
Pass page.nextCursor exactly as returned. Do not edit it or build one yourself.
hasMore: false and nextCursor: null mean the search is complete.
GET /api/v1/transactions?sourceReference=<source-reference>&limit=50
Authorization: Bearer <server-api-key>
X-Environment: <environment-id>
GET /api/v1/transactions?cursor=<nextCursor>&limit=50
Authorization: Bearer <server-api-key>
X-Environment: <environment-id>Use the commerce sourceReference, customer ID, event type, state, or bounded time window.
Copy the returned transaction ID into the detail endpoint.
Confirm program, event, source reference, state, reversal link, timestamps, and balanced ledger entries before deciding on a correction.
GET /api/v1/transactions?sourceReference=<source-reference>
Authorization: Bearer <server-api-key>
X-Environment: <environment-id>
GET /api/v1/transactions/<transaction-id>
Authorization: Bearer <server-api-key>
X-Environment: <environment-id>/operations answers “what is this long-running job doing?” /audit-events answers “which credential or operator acted on which target?” Keep the scopes separate.
operations:readList jobs by type or status, then open one operation for progress and related import evidence.
audit:readFilter environment-attributed audit evidence by action, target type, target ID, or actor ID.
GET /api/v1/operations?status=<operation-status>&limit=50
GET /api/v1/operations/<operation-id>
GET /api/v1/audit-events?targetId=<target-id>&limit=50
Authorization: Bearer <server-api-key>
X-Environment: <environment-id>The ledger summary accepts a positive window of at most 90 days and an optional program. It returns actual ledger and operational aggregates as a best-effort request view.
GET /api/v1/analytics/ledger-summary?programId=<program-id>&from=<ISO-timestamp>&to=<ISO-timestamp>
Authorization: Bearer <server-api-key>
X-Environment: <environment-id>The close report reads posted ledger entries, uses decimal strings so large balances stay exact, checks both the member roll-forward and double-entry totals, and hashes the evidence payload with SHA-256.
Copy its program ID. A close never mixes programs or currencies.
Use a half-open period: from is included and to is excluded. Omit from to start at the first day of the UTC month.
Call the endpoint with an analytics:read server credential. The window may be up to 366 days.
Confirm ledger controls pass, a finance policy exists, that policy is approved, and its journal mapping is complete.
Retain the report ID, request ID, SHA-256 hash, boundaries, and response. Your finance owner or independent auditor still signs off outside Loyumi.
GET /api/v1/reports/ledger-close?programId=<program-id>&from=<ISO-timestamp>&to=<ISO-timestamp>
Authorization: Bearer <server-api-key>
X-Environment: <environment-id>
X-Request-Id: <close-run-id>points_only: no valuation policyprovisional: policy or journal approval remainsready_for_external_review: internal controls and policy evidence are readycontrol_exception: stop and reconcileSelect one to twenty public lifecycle events, then create the endpoint disabled. Existing points.earned-only subscriptions keep working unchanged.
The successful create response returns signingSecret once. Put it in your secret manager. An idempotent replay does not reveal it again.
Patch { "status": "active" } after the receiver can verify signatures. Changing the URL forces the endpoint back to disabled.
POST an exact empty object to /test. The internal test event contains no customer PII and cannot be selected as a business subscription.
Filter deliveries by endpoint, event type, or status. Public delivery views omit stored payloads.
Retry only a failed or dead-letter delivery with a stable key. Retry re-queues the same record; it does not contact your server synchronously or create a duplicate record.
POST /api/v1/webhook-endpoints
Idempotency-Key: <stable-create-key>
{ "url": "<public-https-endpoint>",
"eventTypes": ["points.earned", "redemption.committed", "adjustment.posted"] }
PATCH /api/v1/webhook-endpoints/<endpoint-id>
Idempotency-Key: <stable-activate-key>
{ "status": "active" }
POST /api/v1/webhook-endpoints/<endpoint-id>/test
Idempotency-Key: <stable-test-key>
{}
GET /api/v1/webhook-deliveries?endpointId=<endpoint-id>&status=failed
POST /api/v1/webhook-deliveries/<delivery-id>/retry
Idempotency-Key: <stable-retry-key>
{}Membership + programmember.enrolled, member.unenrolled, and program.published.
RedemptionReserved, committed, released, expired, reversed, fulfillment update, and dispute opened/resolved/rejected.
Governed operationsAdjustment requested/rejected/posted/reversed and import completed.
Partner clearingExchange settled and reversed, alongside the original points.earned event.
externalCustomerId and returns only merchant-defined, customer-safe data.GET /loyalty/v1/memberReturn the signed-in customer's member, available points, and tier summary.
PUT /loyalty/v1/programs/{programId}/enrollmentEnroll the authenticated customer with affirmative consent evidence; identity is derived by the backend, never supplied by the app.
DELETE /loyalty/v1/programs/{programId}/enrollmentRequest zero-balance unenrollment while retaining governed history.
GET /loyalty/v1/activityReturn a bounded customer-safe activity page and opaque next cursor.
GET /loyalty/v1/catalogReturn only rewards this customer surface may display.
POST /loyalty/v1/redemptions/quotePrice and validate the selected reward without spending points.
POST …/reserve → commitHold value first, then commit only after merchant checkout authorization succeeds.
POST …/release or reverseRelease an unused hold or request a governed reversal under merchant policy.