Skip to content

Changelog

Every change to the API, newest first.

Updated 2026-10-13

Breaking changes always ship under a new version prefix — /v1 responses only gain fields. See Versioning & deprecation for the exact promise.

Live read and webhooks keys need an owner or admin

  • Live keys with the read or webhooks scope can now only be created or rotated by an owner or admin on a recent sign-in, like act and ingest keys, and need a second owner or admin's approval when the programme asks for it. Test keys are unchanged. Existing keys keep working.

Enrolment needs the shopper's age confirmation; birthdays are day and month

  • Breaking: POST /v1/members must send age_confirmed: true whenever the call enrols someone — a new member, or a prospect joining. Send it only when the shopper ticked an "I am 18 or older" box beside your terms. Without it the call answers 422 validation (errors[0].path: age_confirmed) and nothing changes. Returning an existing member is unchanged. Members must be 18 or older.
  • PATCH /v1/members/{id}: birthday takes MM-DD. YYYY-MM-DD is still accepted, but the year is not kept; a date that makes the member younger than 18 answers 422 and is not saved. Responses carry the birthday on the placeholder year 2000, not the member's year of birth.

Reservation lookups, SDK retries and 402 in the spec

  • Fixed: GET /v1/redemption-reservations/{id} with an id that is not a UUID answered 500. It now answers 404 not-found, like any id that is not yours.
  • The Node.js and PHP SDKs (beta, available on request) derive an event's Idempotency-Key from the event itself when you give no id — the same key the API derives (type and order_ref, plus refund_ref for refunds) — so a retried send is always the same event.
  • The OpenAPI spec now documents 402 payment-required on every operation: a programme with no active plan or trial answers 402 to every call. Documentation only; behaviour is unchanged.

Ledger correctness: debits, idempotency, ledger order, export downloads

  • Fixed: POST /v1/members/{id}/adjust could take a member below zero. A debit larger than the member's spendable balance (available minus points held by reservations) is now refused with 422 balance-insufficient, and nothing is written.
  • Fixed: an adjust Idempotency-Key reused for a different member, amount, reason or note replayed the first adjustment with 201. It now answers 409 conflict, as the idempotency guide always said. The same key with the same body still replays the original 201, now with Idempotent-Replay: true.
  • Fixed: GET /v1/members/{id}/ledger returned entries in no meaningful order. It is now oldest first (created_at, then id), stable across pages. A limit that is not a whole number from 1 to 200, or a cursor that did not come from this member's ledger, is 422 validation; it used to be 404 (or silently clamped).
  • New: GET /v1/admin/export-jobs/{id} (scope read) and GET /v1/admin/export-jobs/{id}/download (scope act, like creating the job). Jobs in the list now carry download_url once ready. Downloads answer 409 reason: not_ready while the export is being built and 410 gone once the link has expired. Additive: no existing field changed.
  • Reversing a redemption now returns its points to the original expiry lots, so they expire on the original date, and grace credits now expire on their date. GET /v1/tenant/liability gains disputed_cents (earns under review, which are not part of outstanding_cents). Additive.

Adjustments follow the programme's approval limit

  • POST /v1/members/{id}/adjust: an adjustment over the programme's approval limit is no longer posted straight away. It is sent for approval and the call answers 202 with data.status: pending_approval and data.approval_request_id; a retry with the same Idempotency-Key returns the same 202 with the request's current status. Programmes without an approval limit see no change.
  • POST /v1/members/{id}/adjust: credits through one key are capped per UTC day (the programme's setting, else 20× its self-serve limit, else 1,000,000). Over the cap: 429 rate-limited with reason: daily_credit_cap and Retry-After until midnight UTC. Debits are not capped. The cap is shared by a key and its rotated replacements (rotating does not reset the day) and holds for parallel calls.
  • POST /v1/members/{id}/adjust: an Idempotency-Key is matched per key (and its rotated replacements), not across the whole programme — for queued and posted adjustments alike. The same Idempotency-Key sent through a different key is a different request: it is posted on its own, never replayed from or refused because of another integration's adjustment. Revoking a key cancels the adjustments it left waiting for approval.
  • A key that has already been rotated (it is in its 24-hour grace period) can't be rotated again — rotate its replacement instead.
  • Live keys with the act or ingest scope can only be created or rotated by an owner or admin on a recent sign-in, and need a second owner or admin's approval when the programme asks for it. Existing keys keep working.

Programmes in any currency

  • A programme runs in one currency (ISO 4217), usually the store's own. Every *_cents amount is hundredths of one unit of that currency — the same convention as Shopify's Liquid prices, so ¥1,250 is 125000.
  • GET /v1/programs, GET /v1/tenant/liability and GET /v1/public/member now include currency (ISO 4217) and locale (BCP 47). Additive: no existing field changed.
  • An order event whose currency differs from the programme's is rejected with 422 validation (errors[].path: currency) — convert the amounts first, or leave currency out. In a batch, only that event is rejected.

Browser-safe tokens, a widget, SDKs and a WooCommerce plugin

  • New error: 402 payment-required. A programme with no active plan or running trial is switched off, and every call answers 402 until a plan is active again (a suspended programme still answers 401).
  • POST /v1/members/{id}/session-tokens mints a 15-minute, read-only member token; GET /v1/public/member reads that one member from a browser or app (CORS open on this endpoint only, no contact details).
  • Embeddable balance widget: /widget/stickytier-widget.js.
  • POST /v1/members/{id}/portal-links returns a single-use link that signs the member in to your hosted rewards page.
  • Node.js and PHP SDKs and a StickyTier for WooCommerce plugin — beta, not published to npm, Packagist or WordPress.org; available on request from support.
  • An order event carrying both customer_ref and email now teaches the member its external_ref.
  • Public status page: /status.

Everything a non-Shopify store needs

  • Your own customer ids: members have external_ref, and any member id may be sent as ref:<your id> or phone:<number>. Orders may identify their customer with customer_ref, email or phone.
  • New: PATCH /v1/members/{id} (profile, consent), DELETE /v1/members/{id} (erase personal data), GET /v1/events (list, with the reason an event failed or was ignored), GET /v1/redemptions (incl. ?code= voucher lookup) and GET /v1/redemptions/{id}, POST /v1/referrals.
  • Webhooks: balance.updated and ledger.entry.created now fire for EVERY balance change (reservations, expiries, tier and referral credits, releases from pending), and balance.updated carries reserved_cents and spendable_cents. Endpoints can be edited (PATCH), their secret rotated, and a webhook.test delivery sent.
  • Every response has X-Request-Id and X-RateLimit-Limit/-Remaining. A batch of events costs one request per 10 events. A tenant far behind on processing gets 429 reason: backlog.
  • Order events accept tax_cents, shipping_cents, discount_cents and currency (informational; points are earned on gross_cents).
  • Fixed: a confirmed voucher was reported as mode: direct.
  • New guide: Integrate a custom store.

Safer retries, one error contract, a real error reference

  • BREAKING (no known integrations affected): POST /v1/members/{id}/redemptions now reports a short balance as 422 balance-insufficient, the same as reservations. It used to answer 402 insufficient-balance.
  • POST /v1/members/{id}/redemptions accepts Idempotency-Key. A retried redemption replays instead of debiting twice; a request still in flight answers 409 with reason: in_progress.
  • Fixed: redeeming for a paused, suspended or deleted member returned 500. It is now 403.
  • An ingest-only key can read GET /v1/events/{id} for the events it sent.
  • customer.created / customer.updated events honour the plan's member limit, like POST /v1/members.
  • Webhooks are also signed as X-StickyTier-Signature (same value as X-CVOS-Signature, which stays). New subscribable event: referral.reversed.
  • Test mode: wiping the sandbox re-syncs its rules, tiers and redemption settings from live, so test results match production.
  • Docs: every error type now resolves at /docs/api/errors; the reference lists the scope, 401/403/429/500 and Idempotency-Key for every operation; action.completed is in the event schema.

Tier depth, benefits and branded member email

  • GET /v1/programs tiers now include criteria, criteria_mode, window_days and benefits.
  • Member tier objects include the tier's benefit flags for storefront gating.
  • Rules support segment eligibility (new/returning/lapsed) and min_tier_level.
  • Member email now sends from the tenant's own DNS-verified address with a branded template (no API changes; From addresses on transactional mail may change).

v1 general availability

  • Event ingestion (customer.*, order.*) with batch support and idempotent replays.
  • Members, ledger, referral, programs and liability read endpoints.
  • Enrollment, adjustments, voucher + direct redemptions with confirm/void/TTL.
  • Outbound webhooks: 9 event types, HMAC signatures, retries, delivery log.
  • Test mode: sticky_test_ keys write to an isolated, wipeable sandbox.
Changelog — StickyTier API