# Repitoo in a partner's POS · guide for coding agents

You are wiring Repitoo (stamp cards in Apple and Google Wallet) into a partner's POS. Read this
before writing code against Repitoo. It works with **any language and any stack**: Repitoo is
an HTTP API with JSON, described in full at `https://api.repitoo.com/openapi.json`.
- JavaScript or TypeScript (Node 20+, Bun, Deno): `@repitoo/sdk` already follows these rules
  (`README.md`).
- Anything else (PHP, Python, Java, C#/.NET, Go, Ruby, Delphi, a desktop till…): call the HTTP
  API as described under **Over HTTP** below. Don't port the SDK; the rules are what matter.

First look at how the POS is built (its language, where the server is, how the till talks to
it, how it stores secrets and queues work) and fit Repitoo into those same patterns.

## Rules

1. **Keys never reach a browser or the till's front end.** The partner key (`rpt_partner_…`,
   `rpt_test_…`) and the business keys (`rpt_live_…`) are used only on a server, or in a
   desktop till's own process if it has no server. Never in front-end bundles, public env vars
   or logs. Where they are stored is the partner's call (env, a secrets manager, its database,
   encrypted).
2. **Turn loyalty on only when a business buys it**, with your own id in `externalId`
   (repeating it returns the same business, with no new key). Store the `apiKey` it returns
   the first time: it is shown only once. Lost it? Ask for a new business key.
3. **Every write has an idempotency key**: the invoice number for a sale, a stable id for a
   redemption or a reversal. Reuse it on retries; never generate a new one per attempt.
4. **Queue sales the till couldn't send.** Keep `occurredAt` (when it happened) and the same
   idempotency key; retry only what is retryable (no answer, 408, 429, 5xx) and set aside what
   Repitoo refused for good (other 4xx) for a person to review. Up to 7 days back.
5. **Phones in E.164** (`+58…`). A bare 8-digit number is read as Costa Rican.
6. **Verify every webhook** on the raw body before parsing, and dedupe by `event.id`.
7. **Develop with a test key** (`rpt_test_…`): its businesses are free and silent.

## Over HTTP

Base URL `https://api.repitoo.com`. Every call sends `Authorization: Bearer <key>` and, for
writes, `Content-Type: application/json`. Errors are JSON: `{ "error": "<code>", "message" }`.

| Need                                  | Key       | Call                                                                                                                                                 |
| ------------------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| Who am I (name, stamp modes, origins) | partner   | `GET /v1/partner`                                                                                                                                    |
| Turn loyalty on for a business        | partner   | `POST /v1/partner/businesses` `{ externalId, name, ownerEmail, ownerName?, program? }` → 201 with `apiKey`                                           |
| List the businesses                   | partner   | `GET /v1/partner/businesses`                                                                                                                         |
| Turn it off / back on                 | partner   | `POST /v1/partner/businesses/{id}/deactivate` · `…/activate`                                                                                         |
| Replace a leaked business key         | partner   | `POST /v1/partner/businesses/{id}/keys`                                                                                                              |
| Open the Repitoo window (30 min)      | partner   | `POST /v1/partner/businesses/{id}/embed-sessions` → `{ url, expiresAt }`                                                                             |
| Stamp a sale                          | rpt_live_ | `POST /v1/purchases` + `Idempotency-Key` `{ customer: { phone?, email?, cardCode?, name? }, amount?, stamps?, reference?, programId?, occurredAt? }` |
| Find a card                           | rpt_live_ | `GET /v1/cards/lookup?phone=…` (or `email`, `cardCode`; `programId`)                                                                                 |
| Hand out a reward                     | rpt_live_ | `POST /v1/cards/{cardId}/redemptions` + `Idempotency-Key`                                                                                            |
| Take back the stamps of a voided sale | rpt_live_ | `POST /v1/cards/{cardId}/stamps/reversals` + `Idempotency-Key` `{ stamps }`                                                                          |
| Which program                         | rpt_live_ | `GET /v1/programs` (send `programId` only with more than one)                                                                                        |

A replayed write answers with the first result and `Idempotent-Replayed: true`. `amount` is the
sale's total in the business's currency (by-amount programs); `stamps` is for by-visit ones.
`occurredAt` is ISO 8601 with its offset.

### Webhooks

Header `Repitoo-Signature: t=<unix seconds>,v1=<hex>`. `v1` is HMAC-SHA256 with the webhook's
secret (`whsec_…`, the whole string is the key) over `"<t>.<raw body>"`. Compare in constant
time, reject a `t` more than 5 minutes off, then parse. Answer 2xx within 10 s; Repitoo retries
after 1 min, 5 min, 30 min, 2 h, 12 h and 24 h with the same `id`.

Examples in `examples/`: `http` (curl), `node` (with and without the SDK), `react`,
`react-router`, `nextjs`, `angular`, `html` (no framework), `python`, `php`, `csharp`, `java`
and `go`.

### The Repitoo window

The window is a web page: the server asks for its `url` and the till shows it.
- **A web POS (any framework or none)**: `<repitoo-modal>` (`@repitoo/sdk/modal`, a plain
  custom element) or your own `<iframe src="{url}" allow="camera">`. The page's origin must be
  among the partner's allowed origins, or the window refuses to show inside it. It tells the
  page what happened with `postMessage` (`repitoo:stamped`, `repitoo:redeemed`,
  `repitoo:close`): check `event.origin` is Repitoo's and `event.source` is your iframe. See
  `examples/html/till.html`.
- **A desktop or mobile till (WPF, WinForms, Electron, Android, iOS…)**: open the `url` in a
  WebView or the system browser as a top-level page. It works the same; your app learns what
  changed from webhooks or by looking the card up.

## Errors worth handling

`401 unauthorized` (wrong key), `402 payment_required` (partner or business paused: tell the
cashier, don't retry), `404` (not this partner's business, or the feature is off), `409`
(an invoice already used, a card expired), `429 rate_limited` (60 calls a minute per key; wait
`Retry-After`).

## With @repitoo/sdk (JavaScript)

| Need                                   | Use                                                                               |
| -------------------------------------- | --------------------------------------------------------------------------------- |
| Turn loyalty on / off / back on        | `RepitooPartner.activateBusiness` / `deactivateBusiness` / `reactivateBusiness`   |
| List the businesses                    | `RepitooPartner.listBusinesses`                                                   |
| Replace a leaked business key          | `RepitooPartner.newBusinessKey`                                                   |
| Open Repitoo inside the POS            | `RepitooPartner.openWindow` (server) + `<repitoo-modal>` (`@repitoo/sdk/modal`)   |
| Stamp a sale                           | `RepitooTill.recordSale` (through `OfflineQueue`)                                 |
| Find a card / hand out a reward / void | `RepitooTill.findCard` / `redeemReward` / `reverseStamps`                         |
| Know which program                     | `RepitooTill.programs`                                                            |
| React to changes                       | Webhooks + `verifyWebhook`                                                        |
| Read keys from AWS                     | `secretsManagerKeys` / `parameterStoreKeys`, passing the partner's own AWS client |
