Get started
Quickstart
Five calls against your sandbox, from an empty project to a reward earned and redeemed.
You need two things, and you make both yourself. In your Trovy dashboard open
Integrations → Developer API and press Create sandbox; then, on the sandbox's
own Developer API page, create a test secret key (trv_test_…). The
Sandbox guide says what you get. Everything below runs against
api.sandbox.trovy.ca. No text reaches a real phone, and a sandbox cannot touch a
real customer.
export TROVY_SECRET_KEY="trv_test_…"1. Confirm which business you are
curl -X GET https://api.sandbox.trovy.ca/v1/business \
-H "Authorization: Bearer $TROVY_SECRET_KEY"// The business behind the key, its program and its currency.
import { Trovy } from "@trovy/sdk";
const trovy = new Trovy({ apiKey: process.env.TROVY_SECRET_KEY! });
const { business, program } = await trovy.business.get();
console.log(business.name, business.currency);
console.log(program?.rewardRateBps);environment reads sandbox. The currency in that response is the unit every
…Cents field in this API is counted in. Read it once at setup: a business has one
currency and it cannot change once it has transacted. Trovy does no conversion. Check
currency and timezone now; if either is wrong for your business, email
developers@trovy.ca before you record anything.
program holds the rules every order earns under: the rate (rewardRateBps), the most a
reward can grow to (capCents), the order a reward needs (minRedemptionPurchaseCents)
and how many days a new reward waits (redemptionDelayDays). If it is null, no rewards
program is linked to the Developer API, and every earn answers 409 NO_ACTIVE_PROGRAM
(see Going live).
2. Find a customer to act on
Your sandbox starts with one customer already linked, so you can skip the sign-up form for now. Their id is on the sandbox's Developer API page, under A customer to test with.
In production, a customer id comes from one place: the sign-up widget hands
your page a linkToken, and your server exchanges it.
curl -X POST https://api.sandbox.trovy.ca/v1/customers/link \
-H "Authorization: Bearer $TROVY_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"linkToken": "lt_7f3a9c1e4b8d2a6f"
}'// Exchange a widget link token for your customer id.
import { Trovy } from "@trovy/sdk";
const trovy = new Trovy({ apiKey: process.env.TROVY_SECRET_KEY! });
declare const linkToken: string;
declare const accountId: string;
declare function saveTrovyCustomerId(accountId: string, trovyCustomerId: string): Promise<void>;
// The token comes from the widget, through your own page. It is single-use and
// lives five minutes: exchange it as soon as it arrives.
const { customer } = await trovy.customers.link({ linkToken });
// Store customer.id against your own account record. Every later call takes it.
await saveTrovyCustomerId(accountId, customer.id);That id is yours alone. It is not the customer's phone number and it means nothing at any other business.
3. Earn on an order
ORDER_ID=ORD-10233 # your own order id: the key and the body name the same order
curl -X POST https://api.sandbox.trovy.ca/v1/earn \
-H "Authorization: Bearer $TROVY_SECRET_KEY" \
-H "Idempotency-Key: $ORDER_ID" \
-H "Content-Type: application/json" \
-d '{
"customerId": "cus_01HX8Z9K2M4N6P8Q",
"orderId": "ORD-10233",
"amountCents": 5000
}'// Record a paid order and earn a reward on it.
import { Trovy } from "@trovy/sdk";
const trovy = new Trovy({ apiKey: process.env.TROVY_SECRET_KEY! });
declare const customerId: string;
declare const order: { id: string; totalCents: number };
// Call this after the payment succeeds. amountCents is what the customer paid
// you, in your business's currency.
const earn = await trovy.rewards.earn(
{ customerId, orderId: order.id, amountCents: order.totalCents },
// Your own order id, so a retry from any process replays instead of earning twice.
{ idempotencyKey: order.id }
);
console.log(earn.earnedCents);At a 5% rate, a $50 order earns $3: rewards are whole dollars, rounded to the
nearest, and 50¢ rounds up ($2.50 becomes $3, $2.49 becomes $2). An order that
earns less than 50¢ is still recorded, with earnedCents: 0. Your sandbox earns at its
own program.rewardRateBps from step 1, and a reward never grows past capCents.
Now send the same request again, with the same Idempotency-Key. You get the
first result's amounts back, not a second reward, and replayed: true; no second text
is sent, so sms.status is not_sent. That is the single
most important habit in this API: a key names one event, so a retry of that event
can never happen twice.
Send it a third time with a new key and the same orderId, and you get
409 DUPLICATE_ORDER: an order is recorded once, whatever the key.
4. See what they can spend
curl -X GET https://api.sandbox.trovy.ca/v1/customers/cus_01HX8Z9K2M4N6P8Q/rewards \
-H "Authorization: Bearer $TROVY_SECRET_KEY"// A customer's live rewards, usable ones first.
import { Trovy } from "@trovy/sdk";
const trovy = new Trovy({ apiKey: process.env.TROVY_SECRET_KEY! });
declare const customerId: string;
declare const order: { totalCents: number };
const { rewards } = await trovy.rewards.list(customerId);
// Usable rewards first, then the soonest to expire. Offer the first only if it
// is usable now and this order reaches its minimum: the rewards guide says what
// to record when it does not.
const next = rewards[0];
if (next?.usable && order.totalCents >= next.minimumOrderCents) {
console.log(`Offer ${next.valueCents} off, until ${next.expiresAt}`);
}Look at usable. A reward with usable: true is one to offer now; it lists first.
One with usable: false is being held back until usableFrom, a local midnight
redemptionDelayDays after it was earned, and cannot be applied yet. A reward also has a
minimumOrderCents, and the order has to reach it.
5. Redeem it
ORDER_ID=ORD-10456 # a new order, so a new key: one key names one event
curl -X POST https://api.sandbox.trovy.ca/v1/redeem \
-H "Authorization: Bearer $TROVY_SECRET_KEY" \
-H "Idempotency-Key: $ORDER_ID" \
-H "Content-Type: application/json" \
-d '{
"customerId": "cus_01HX8Z9K2M4N6P8Q",
"rewardId": "rwd_01HX8Z9K2M4N6P8Q",
"orderId": "ORD-10456",
"amountCents": 12000
}'// Apply a reward to an order, once the order is paid.
import { Trovy } from "@trovy/sdk";
const trovy = new Trovy({ apiKey: process.env.TROVY_SECRET_KEY! });
declare const customerId: string;
declare const reward: { id: string; valueCents: number };
declare const order: { id: string; totalCents: number };
declare function capturePayment(amountCents: number): Promise<void>;
// Take the reward off the order yourself and capture the remainder FIRST. A
// redeemed reward is never handed back, so redeeming ahead of a capture that
// then fails would cost the customer their reward. A reward is used whole: on an
// order worth less than the reward, the rest is forfeited.
await capturePayment(Math.max(0, order.totalCents - reward.valueCents));
const redeem = await trovy.rewards.redeem(
// amountCents is the order's full total, before the reward.
{ customerId, rewardId: reward.id, orderId: order.id, amountCents: order.totalCents },
{ idempotencyKey: order.id }
);
// On a recurring program, what the customer paid may have earned the next one.
console.log(redeem.redeemedCents, redeem.newReward?.valueCents);Take the reward off the order yourself, capture the remainder, and then tell Trovy, exactly as you earn only after payment succeeds. A redeemed reward is never handed back, so redeeming ahead of a capture that then fails would cost your customer their reward.
The order has to reach the reward's minimumOrderCents, so use an amount at least that
large. If the reward says usable: false, your program holds new rewards back: to try
this step today, set the sandbox program's redemption delay to 0 days in the dashboard.
You call earn or redeem for an order, never both. On a recurring program the redeem
itself earns on what the customer paid, and returns it as newReward. An earn for a
customer with a usable reward waiting answers 409 ACTIVE_REWARD_EXISTS, with the
reward in details. Rewards has the whole decision,
including an order below the reward's minimum.
Not using TypeScript?
Every sample has a curl tab, and each is one plain HTTP request. The whole API is one
OpenAPI 3.1 document, /openapi/v1.json: import it into Postman or
Insomnia, or generate a client from it. Check the generated models against the
reference: an earn's reward and a redeem's newReward can be null.
The link route is yours to write in any stack; Exchange the
token says what it must check.
What to read next
- Rewards: the checkout decision in full, orders that change, and what a refund does.
- Idempotency and retries: why step 3 mattered.
- Customers and the widget: how a real customer joins.
- Errors: every code, and which ones to branch on.