Core concepts
Errors
One body shape, a stable code to branch on, and a request id to quote.
Every failure answers the same shape:
{
"error": "No order ORD-10233 was recorded for your business.",
"code": "ORDER_NOT_FOUND",
"requestId": "5f1e0e37-2795-4a07-9564-1e19ad52a023"
}| Field | Use it for |
|---|---|
code | Branching. Stable, documented, safe to compare. |
error | Showing a human. Written for your staff or your customer; wording may change. |
requestId | Quote it when you email developers@trovy.ca. It identifies the exact request in our logs. Log it. |
details | Machine-readable detail, when there is some: the failing fields on a 400, and on several 409s the figure you need to act on (minimumOrderCents, remainingRefundableCents, the waiting reward). |
Branch on code, never on the message. The messages are written to be read and
will be improved; the codes are a contract.
Statuses
| Status | Meaning | Retry? |
|---|---|---|
400 | The request is malformed. details says which field. | No — fix it. |
401 | The key is missing, malformed, unknown or revoked. | No. |
403 | The key is the wrong type, disabled, or the Developer API is not enabled. | No. |
404 | The thing does not exist for your business. | No. |
409 | A conflict: a duplicate order, a reused key with a different body, an already-redeemed reward. | No — read the code. |
429 | Over a rate limit. Retry-After says how long. | Yes, after waiting. |
5xx | Our fault. | Yes, same idempotency key. |
A 404 never distinguishes "does not exist" from "belongs to someone else". That is
deliberate: the difference would let a key holder probe for other businesses'
customers and orders.
Every code
These are the codes of the /v1 API. The sign-up form has codes of its own, listed
below.
| Code | What it means |
|---|---|
API_KEY_MISSING | No Authorization: Bearer <key> header was sent. |
API_KEY_INVALID | The key is malformed, unknown, or minted for the other environment. |
API_KEY_REVOKED | The key was revoked. Create a new one in the dashboard. |
API_KEY_DISABLED | Trovy disabled this key. Contact developers@trovy.ca — rotating will not help. |
API_KEY_WRONG_TYPE | A Shopify or publishable key was sent where a secret key is required. |
API_NOT_ENABLED | The Developer API is not enabled for this business. |
VALIDATION_ERROR | The request failed validation; details lists the issues. |
NOT_FOUND | No such route, or no such resource for this business. |
LINK_TOKEN_INVALID | The link token is unknown, expired, already used, or was issued for another business. The customer verifies again in the widget. |
CUSTOMER_NOT_FOUND | No member of your business matches — the phone number is not a member's, or the customer id is not one of yours. |
IDEMPOTENCY_MISMATCH | This Idempotency-Key was already used for a request with a different body. Use a new key for a new order event. |
DUPLICATE_ORDER | An order with this orderId is already recorded for your business. Order ids are unique per business. |
ACTIVE_REWARD_EXISTS | The customer has a usable reward waiting (details.reward). An order applies it with POST /v1/redeem or records nothing; it cannot earn on top. A reward that is not usable yet never blocks an earn: the order adds to it. |
NO_ACTIVE_PROGRAM | Your business has no active rewards program linked to the Developer API. |
FIRST_ORDER_ONLY | The program rewards a customer's first order only, and this customer has already earned. |
STORE_NOT_FOUND | storeId is not one of your active locations (GET /v1/stores). |
REWARD_NOT_FOUND | No such reward for this customer at your business. |
REWARD_ALREADY_REDEEMED | The reward was already redeemed. |
REWARD_EXPIRED | The reward has expired. |
REWARD_NOT_YET_USABLE | The program holds a newly earned reward back until details.usableFrom (a local midnight at your business), so it can't be applied to the order that earned it or to another order that day. |
REWARD_VOIDED | The reward was cancelled by refunds of the orders that earned it. |
REWARD_CHANGED | The reward changed between your read and this call (a refund landed). Read it again. |
NOT_OLDEST_REWARD | Rewards are redeemed oldest first; details.oldestRewardId is the one to apply. |
MINIMUM_ORDER_NOT_MET | The order does not reach the reward's minimum; details.minimumOrderCents. |
ORDER_NOT_FOUND | No order with this orderId was recorded through the API for your business. |
ORDER_AMBIGUOUS | More than one purchase is recorded under this orderId. Email developers@trovy.ca. |
NOT_REFUNDABLE | This order cannot be refunded. |
REFUND_EXCEEDS_REMAINING | amountCents is more than what is left to refund; details.remainingRefundableCents. |
ALREADY_FULLY_REFUNDED | The order has already been fully refunded. |
REWARDS_NOT_ENABLED | Instant rewards is switched off for your business, so orders earn nothing. Rewards customers already hold stay redeemable. |
RATE_LIMITED | Too many requests; wait Retry-After seconds. |
INTERNAL_ERROR | Something failed on our side. Quote requestId when you email developers@trovy.ca. |
The ones worth handling by name
IDEMPOTENCY_MISMATCH (409) — you reused a key with a different body. See
Idempotency.
DUPLICATE_ORDER (409) — you have already recorded a purchase under that order
id. If this is a retry, use the same idempotency key and it will replay instead.
ACTIVE_REWARD_EXISTS (409) — the customer has a usable reward waiting, so this
order cannot earn on top. details.reward is that reward: if the order reaches its
minimumOrderCents, offer it and redeem instead; if not, record nothing for this
order. See Rewards.
MINIMUM_ORDER_NOT_MET (409) — the order does not reach the reward's minimum. The
message carries the figure formatted in the business's currency, and details carries
minimumOrderCents, so you can show either.
CUSTOMER_NOT_FOUND (404) — that id is not one of your customers. Ids come only
from POST /v1/customers/link, and they are not phone numbers.
API_KEY_DISABLED (403) — Trovy switched this key off. Rotating will not help;
email developers@trovy.ca.
In the SDK
Two classes, split by whether the write might have landed:
import { isTrovyError, isTrovyConnectionError } from "@trovy/sdk";
try {
await trovy.rewards.redeem(body, { idempotencyKey: order.id });
} catch (err) {
if (isTrovyError(err)) {
if (err.status >= 500) {
// Our fault, and the SDK already retried. The write may have landed:
// try again later with the same key.
return queue.retry({ orderId: order.id });
}
// The API answered and decided. Final for this request.
if (err.code === "MINIMUM_ORDER_NOT_MET") return showThreshold(err.message);
if (err.status === 429) return backOff(err.retryAfterSeconds);
log.error({ code: err.code, requestId: err.requestId });
throw err;
}
if (isTrovyConnectionError(err)) {
// No answer came back. The write may or may not have applied — retry with
// the same key.
return queue.retry({ orderId: order.id });
}
throw err;
}Use the guards rather than instanceof: two copies of the package in one dependency
tree give two distinct classes, and instanceof then fails on an error that is
obviously ours.
The sign-up form's codes
The form reports through its onError callback: Trovy.mount, <TrovySignup> and
mount from @trovy/sdk/widget alike. The customer never reads these codes: the form
shows them a message written for them, or, for anything about your key or your setup,
"Sign-up isn't available right now".
| Code | What happened |
|---|---|
INVALID_CONFIG | The options are wrong: a key that is not a publishable key (a secret key is refused before anything is sent), or neither or both of linkUrl and onSuccess. The SDK only. |
WIDGET_UNAVAILABLE | The form did not load within ten seconds. On every load: this page's origin is not in the key's allowed origins, or the key lists none. Otherwise a revoked key or an outage. |
NETWORK_ERROR | The customer's browser got no answer from Trovy. They can try again. |
WIDGET_ERROR | Trovy failed without saying why (a 5xx). The customer can try again. |
VALIDATION_ERROR | Not a valid phone number, or not six digits. |
PHONE_NOT_SUPPORTED | A number Trovy does not text: anything outside the US, Canada and Sint Maarten (+1 721). See who can join. In a sandbox, also any number outside 555-0100 to 555-0199. |
CODE_INVALID | The code is wrong, expired or used up. The customer asks for a new one. |
TOO_MANY_REQUESTS | The form's send limits, counted exactly: too many codes asked for one number, or through your key or your business. Retry-After says how long, and the form counts the customer down. |
RATE_LIMITED | Too many requests from one visitor's address, before any of the above is counted. |
SMS_UNAVAILABLE | The text could not be sent. Usually brief, so the customer can try again, but it lasts for a number that replied STOP (until it replies START), and for everyone once the platform's daily sign-up ceiling is reached. |
API_KEY_REVOKED, API_KEY_DISABLED, API_KEY_INVALID, API_KEY_WRONG_TYPE, API_NOT_ENABLED | About your publishable key, as in the table above. |
LINK_… | With linkUrl only: handing the token to your route failed. Each carries a recovery saying what can be done; the package README lists them. |
Calling the widget endpoints yourself you may also meet WIDGET_NOT_CONFIGURED (the
key lists no origins) and ORIGIN_NOT_ALLOWED (the caller is not the form's own frame).
Through the form neither arrives: the frame cannot be drawn at all, so both show up as
WIDGET_UNAVAILABLE.