Everything a store that is not on Shopify needs — a custom cart, WooCommerce, Magento, a till or a mobile app — in the order you will build it. Everything here is plain HTTP, so it works from any language. Use a sticky_test_… key throughout: it writes to a sandbox that mirrors your live rules and tiers, and you can wipe it from the admin.
1. Use your own customer ids
Give every member your customer id as external_ref. From then on, anywhere the API takes a member id you can send ref:<your id> (or phone:<number>) — you never need to store ours.
curl -X POST https://stickytier.com/v1/members \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: signup-CUST-1001" \
-d '{"email":"asha@example.com","phone":"+919812345678","external_ref":"CUST-1001",
"accepted_terms_version":"1","age_confirmed":true,"marketing_consent":true}'
curl https://stickytier.com/v1/members/ref:CUST-1001 -H "Authorization: Bearer $KEY"Send accepted_terms_version only when the customer really agreed to your programme terms: enrolling then records that agreement. Members must be 18 or older: a call that enrols someone (a new member, or a prospect joining) must send age_confirmed: true, and only when the shopper ticked an "I am 18 or older" box beside your terms. Without it the call answers 422 and nothing changes; returning an existing member does not need it. marketing_consent applies to new and existing members: send only the choice the shopper made in this interaction (a ticked or unticked box) and omit it otherwise — never send it as a constant, or you will overwrite an earlier opt-out. A change is recorded as a marketing consent event; sending the current value does nothing. If your programme has a welcome bonus, a member enrolled this way gets it within a minute or so (not in the sandbox). If you only need to look someone up (or create a silent prospect who has not agreed to anything yet — this needs an email), use POST /v1/members/resolve. Update profile fields with PATCH /v1/members/{id} (a birthday is day and month only, MM-DD); honour an erasure request with DELETE /v1/members/{id}.
2. Send orders as events
Send an event when an order is placed, paid, fulfilled, cancelled or refunded. Processing is asynchronous: you get 202 and an event id. Identify the customer with customer_ref, email or phone. gross_cents is the amount points are earned on — send your eligible merchandise total, in hundredths of your programme's currency. If you send currency, it must be the programme's (GET /v1/programs → currency); an order in any other currency is rejected with 422 rather than earning the wrong amount.
curl -X POST https://stickytier.com/v1/events \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"id":"ORD-5001:v2","type":"order.paid","order_ref":"ORD-5001","customer_ref":"CUST-1001",
"gross_cents":249900,"tax_cents":38100,"currency":"INR","payment_method":"prepaid",
"aggregate_version":2,
"lines":[{"product_ref":"SKU-1","amount_cents":249900,"quantity":1,"tags":["apparel"]}]}'Retrying is always safe: an event is identified by its id (or Idempotency-Key), and without one by its type and order_ref — so a retry with the same body is the same event, never a second one. That default also means a changed order (a newer aggregate_version, or a correction with supersedes_event_id) sent with no id collides with the first and is refused with 409 conflict (in a batch: accepted: false with an error at path id). So give every version and every correction its own id, for example <order_ref>:v<n> as above, and send aggregate_version (any number that only goes up per order): a late event with a distinct id but an older version is accepted, recorded and ignored (its status says why), so it can never overwrite a newer one. Backfilling history? Send batches of up to 100 — a batch costs one request per 10 events against your rate limit.
Points may stay pending until your return window passes. To see what became of what you sent — and why an event was ignored — list them:
curl "https://stickytier.com/v1/events?status=failed" -H "Authorization: Bearer $KEY"
curl "https://stickytier.com/v1/events?order_ref=ORD-5001" -H "Authorization: Bearer $KEY"3. Show the balance
GET /v1/members/{id} returns balances.spendable_cents (what can be spent right now), pending_cents, the member's tier and their referral code. GET /v1/programs returns your earning rules and the whole tier ladder for a “how it works” page. Call these from your server — API keys must never reach a browser.
4. Redeem at checkout
Never debit points while the customer is still paying. Reserve them, take payment, then confirm — or release if the payment fails. A reservation expires on its own (15 minutes by default), so an abandoned cart costs the customer nothing.
# a) What may this customer spend on this cart?
curl -X POST https://stickytier.com/v1/redemption-quotes -H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"member_id":"ref:CUST-1001","eligible_total_cents":249900,"payment_method":"prepaid"}'
# → spendable_cents, minimum_cents, maximum_cents, tender_blocked
# b) Hold the points while they pay
curl -X POST https://stickytier.com/v1/redemption-reservations -H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" -H "Idempotency-Key: cart-88431" \
-d '{"member_id":"ref:CUST-1001","amount_cents":50000,"cart_ref":"cart-88431",
"eligible_total_cents":249900,"payment_method":"prepaid"}'
# c) Payment succeeded → burn them d) Payment failed → give them back
curl -X POST .../v1/redemption-reservations/$ID/confirm -d '{"order_ref":"ORD-5001"}'
curl -X POST .../v1/redemption-reservations/$ID/releaseOr skip (c): put "reservation_id" on the order event and we confirm it when the order lands. 422 rule-ineligible means your own programme rules said no (minimum, share of the order, payment method) — the quote tells you the allowed range up front.
5. Or issue a voucher code
For a till or a phone order: POST /v1/members/{id}/redemptions debits the points and returns a single-use code (valid 72 hours by default). Whoever takes the payment looks it up and confirms it — no need to have stored anything:
curl "https://stickytier.com/v1/redemptions?code=RWD-7K2M-Q9XA" -H "Authorization: Bearer $KEY"
curl -X POST https://stickytier.com/v1/redemptions/$ID/confirm -H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" -d '{"order_ref":"POS-7781"}'Unconfirmed vouchers return their points automatically when they expire; /void does it at once.
6. Refunds and cancellations
curl -X POST https://stickytier.com/v1/events -H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"type":"order.refunded","order_ref":"ORD-5001","refund_ref":"RF-220","refunded_cents":249900}'Points earned on the refunded amount are taken back; a partial refund takes back a proportional share. If the customer already spent them, we take back only what they still hold — nobody goes below zero — and the rest is listed under Recovery cases on the Finance page for you to decide. order.cancelled reverses the whole order.
To correct an order (a wrong amount, an edited basket), send the order event again with the corrected facts and supersedes_event_id set to the id of the event it replaces. A correction states the order's value before refunds: refunds you already sent still stand and are taken again from the corrected earn, so don't subtract them from gross_cents. A correction to a cancelled order earns nothing.
7. Referrals from your own signup form
Show each member their referral_code. When a new customer signs up with a code, enrol them and then tell us:
curl -X POST https://stickytier.com/v1/referrals -H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"code":"K7Q2M9XA","friend_member_id":"ref:CUST-1002"}'By default both rewards — the advocate's and the friend's — wait for the friend's first paid order. They land pending through your return window and are taken back if that order is returned. A referral our fraud checks distrust comes back held (with a hold_reason) and pays nothing until someone reviews it. A paused or suspended advocate's code attributes nobody — it is treated like an unknown code, so the friend gets no friend reward either. If the advocate's account is paused or suspended when the friend's order qualifies, the friend is still rewarded but the advocate is not: the referral comes back qualified with hold_reason: "advocate_blocked", and it is never paid later. Referrals over your programme's limits are held the same way, never dropped: by default 5 rewarded referrals per advocate per calendar month, and a monthly budget of 50 referrals' worth of rewards. If your programme approves referrals by hand, a qualifying purchase waits for you before anything is paid.
8. Stay in sync with webhooks
Subscribe to balance.updated and you hear about every change to a balance — earns, releases from pending, redemptions, expiries, tier and referral credits — within seconds, however it happened. Verify the signature (see Webhooks), make the handler idempotent on the delivery id, and use Send test event on the Developers page (or POST /v1/webhook_endpoints/{id}/test) to check your receiver before going live.
9. Go live
Swap the test key for a sticky_live_… key with only the scopes that server needs. A live key with act or ingest is created by an owner or admin, and may need a second approver first — see Authentication. Every response carries an X-Request-Id; rejected calls are listed for 14 days in the API activity panel on the Developers page. Watch X-RateLimit-Remaining, back off on 429, and retry 5xx with the same Idempotency-Key.