Build on your store's intelligence.
A small, honest REST API over the same customer intelligence you see in the app — customers, opportunities, metrics, and segments — plus signed outbound webhooks so your systems hear about a drifting VIP the moment Retrics does. Read-only, JSON, and shaped exactly like what's documented here.
Authentication
Every request is authenticated with an API key you mint in Settings → API keys. Keys are shown once at creation and carry the rk_live_prefix; Retrics stores only a SHA-256 hash, so a key can never be recovered from us — rotate it if it's lost.
Pass the key as a bearer token. Each key is scoped to a single workspace, so the responses only ever contain that workspace's data.
curl https://retrics.ai/api/v1/customers \ -H "Authorization: Bearer rk_live_xxxxxxxxxxxxxxxxxxxx"
A missing or unrecognized key returns 401 unauthorized with a WWW-Authenticate header. Revoke a key from the same settings screen and it stops working immediately.
Base URL & conventions
All responses are JSON with a Content-Type of application/json. Money fields are returned as strings in your store's currency to preserve precision — parse them with Number(...) at the edge. Timestamps are ISO 8601 in UTC.
List endpoints paginate with two query parameters: limit (default 50, max 200) and offset (default 0). Each list response echoes them back alongside a total count so you can page deterministically.
Endpoints
Four read endpoints cover the surfaces you'd want to sync into a warehouse or a downstream tool. Every one accepts limit and offset.
/customers— your customers, ranked by lifetime value.{
"data": [
{
"id": "cus_8Q2f...",
"email": "amara@example.com",
"name": "Amara Okafor",
"lifetime_value": "742.00",
"orders_count": 6,
"first_order_at": "2024-11-03T14:22:00Z",
"last_order_at": "2026-05-19T09:41:00Z",
"lifecycle_stage": "loyal",
"lapse_risk": 0.18,
"segments": ["vip", "repeat_buyer"]
},
{ "id": "cus_3Kd1...", "email": "...", "...": "..." }
],
"limit": 2,
"offset": 0,
"total": 4193
}/opportunities— open retention opportunities, most valuable first.{
"data": [
{
"id": "opp_le92...",
"type": "vip_overdue",
"customer_id": "cus_8Q2f...",
"customer_email": "amara@example.com",
"value_at_risk": "742.00",
"score": 0.86,
"reason": "VIP, 41 days past expected reorder window.",
"status": "open",
"detected_at": "2026-07-06T02:14:00Z"
}
],
"limit": 1,
"offset": 0,
"total": 128
}/metrics— headline retention metrics for the workspace.{
"data": {
"customers_total": 4193,
"active_customers": 2610,
"repeat_rate": 0.34,
"second_order_rate": 0.29,
"avg_order_value": "58.40",
"avg_lifetime_value": "176.10",
"revenue_at_risk": "38210.00",
"as_of": "2026-07-07T04:00:00Z"
}
}/metrics returns a single object, not a list, so it ignores limit and offset.
/segments— your customer segments and their sizes.{
"data": [
{
"id": "seg_vip",
"name": "VIPs",
"description": "Top 5% by lifetime value.",
"customers_count": 209,
"revenue_share": 0.41,
"updated_at": "2026-07-07T04:00:00Z"
},
{ "id": "seg_lapsing", "name": "Lapsing", "...": "..." }
],
"limit": 2,
"offset": 0,
"total": 11
}Rate limits & usage
The API is intended for periodic syncs and dashboards, not per-pageview traffic. Keep requests reasonable — batch with limitup to 200 rather than fetching a page at a time in a tight loop — and stagger large backfills. Each key's last use is stamped so you can spot stale or leaked keys in Settings → API keys. If we introduce hard per-minute ceilings we'll return 429 with a Retry-After header, so handle that path defensively today.
Outbound webhooks
Rather than poll, register an HTTPS endpoint in Settings → Webhooks and Retrics will POST a signed JSON body to it whenever a subscribed event fires. Each endpoint gets its own signing secret and its own list of events; delivery attempts (status, body, retries) are logged against the endpoint so you can debug from the app.
opportunity.detectedA retention opportunity — a VIP going quiet, a lapse risk, a due reorder — is scored and surfaced.opportunity.actionedA member marks an opportunity resolved, sent, or dismissed from the app.vip.overdueA high-value customer passes their expected reorder window without ordering.customer.reorder_dueA customer enters their predicted reorder window for a consumable they've bought before.sync.completedA Shopify sync finishes and the workspace's orders, customers, and products are current.insights.completedThe nightly intelligence run finishes recomputing lifecycles, segments, and opportunities.Every delivery shares the same envelope. data carries the event-specific record; the outer fields are constant across all events.
{
"event": "vip.overdue",
"workspace_id": "ws_7Ht2...",
"delivered_at": "2026-07-07T04:03:11Z",
"data": {
"customer_id": "cus_8Q2f...",
"customer_email": "amara@example.com",
"value_at_risk": "742.00",
"days_overdue": 41
}
}Delivery requests carry two headers you can key on: X-Retrics-Event (the event name) and X-Retrics-Signature (the signature, described next). We wait up to 10 seconds for a 2xx; anything else is recorded as a failed delivery with its status and error.
Verifying webhook signatures
Confirm a delivery really came from Retrics before you trust it. X-Retrics-Signature is the hex-encoded HMAC-SHA256 of the rawrequest body, keyed with that endpoint's signing secret. Compute the same HMAC over the bytes you received and compare in constant time — if they match, the payload is authentic and untampered.
import crypto from "node:crypto";
// endpointSecret: from Settings → Webhooks, for THIS endpoint.
// rawBody: the exact bytes of the request body (do not re-serialize).
function verify(rawBody, signatureHeader, endpointSecret) {
const expected = crypto
.createHmac("sha256", endpointSecret)
.update(rawBody)
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(signatureHeader ?? "", "hex").length
? Buffer.from(signatureHeader ?? "")
: Buffer.alloc(0);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// Express example — capture the raw body for verification:
// app.use(express.raw({ type: "application/json" }));
app.post("/retrics/webhooks", (req, res) => {
const ok = verify(
req.body, // Buffer of raw bytes
req.header("X-Retrics-Signature"),
process.env.RETRICS_WEBHOOK_SECRET,
);
if (!ok) return res.status(401).end();
const payload = JSON.parse(req.body.toString("utf8"));
// ... handle payload.event / payload.data, then:
res.status(200).end();
});Verify against the raw bytes, not a re-encoded object — any whitespace or key-order change will break the HMAC. Always compare with a constant-time function like timingSafeEqual rather than ===.