# Jate — encaisser depuis votre application

Jate encaisse par mobile money, prélève la commission convenue et crédite le
solde du commerce. Votre application n'ouvre aucun compte chez un opérateur et
ne manipule jamais d'argent : elle demande un paiement, Jate s'occupe du reste.

**Adresse de l'API** — `https://<votre-déploiement>.convex.site`
Elle est affichée dans le tableau de bord, section **Développeurs**.

> **Vous codez en Node.js ?** Prenez le client officiel :
> [**`@jate/sdk`**](sdk.md) — types, erreurs à `code` stable, vérification de
> signature webhook en une ligne, zéro dépendance. Et pour développer en
> local : [**`@jate/cli`**](cli.md) — `npx @jate/cli init`, `listen`,
> `checkouts create`. La suite de ce document décrit l'API brute, en curl.

---

## 1. Avant de commencer

Trois choses, dans cet ordre :

1. Un administrateur Jate ouvre l'**accès API** sur votre commerce.
2. Vous créez une **application** dans *Développeurs* : son nom, et les
   **adresses de retour** autorisées (`https://monapp.com`, `monapp://`).
3. Vous créez une **clé**. Elle s'affiche **une seule fois**.

```bash
# Convex ; ailleurs, l'équivalent chez votre hébergeur (Vercel, Fly, Docker…)
npx convex env set JATE_SECRET_KEY jate_live_…
```

C'est cette clé que chaque requête portera dans son en-tête `Authorization`
(§ 3) — vous n'aurez rien d'autre à présenter à Jate.

> ⚠️ **La clé vit sur un serveur, jamais dans une application mobile.**
> Elle permet de créer des encaissements. Dans un bundle mobile, elle est
> lisible par n'importe qui — le montant deviendrait négociable côté client.
> Si vous n'avez pas de backend, vous n'êtes pas prêt à utiliser cette API.

Une clé perdue ne se retrouve pas : on la révoque et on en crée une autre.
Pour changer de clé sans coupure : créer la nouvelle → déployer → révoquer
l'ancienne.

---

## 2. Le parcours, en entier

```
Votre backend            Jate                        Le client
     │                     │                              │
     ├─ POST /v1/checkouts ►                              │
     ◄──── paymentUrl ─────┤                              │
     │                     │                              │
     ├──────────── ouvre paymentUrl ─────────────────────►│
     │                     ├─ page de paiement ──────────►│
     │                     ├─ PI-SPI ou Orange Money ────►│
     │                     ◄──── le client confirme ──────┤
     │                     │                              │
     ◄─ webhook (signal) ──┤                              │
     ├─ GET /v1/checkouts/<id> (la vérité) ►              │
     │                     │                              │
     └─ vous livrez        │              retour vers votre application
```

**Ce qu'il faut retenir :** le webhook prévient, il ne prouve pas. On livre
après avoir relu l'encaissement. C'est la règle que Jate s'applique à
elle-même face aux prestataires de paiement.

La page de paiement est **directe** : votre client y voit le libellé et le
montant, saisit son adresse de paiement PI-SPI (ou choisit Orange Money), et
paie. Pas de catalogue, pas d'étape intermédiaire — c'est votre application qui
a déjà fait tout ce travail.

---

## 3. Authentification

Toutes les requêtes portent la clé, dans un en-tête — et c'est la seule façon de
s'authentifier : pas de clé en paramètre d'URL (elle finirait dans les journaux
de tous les serveurs traversés), pas de session, pas de signature à calculer.

```
Authorization: Bearer jate_live_…
```

Les exemples qui suivent lisent la clé dans `$JATE_SECRET_KEY`. **C'est la
variable de votre terminal, pas celle du serveur posée au § 1** — les deux
portent le même nom sans être la même chose. Pour que les `curl` de ce document
répondent, définissez-la aussi ici :

```bash
export JATE_SECRET_KEY=jate_live_…
```

En-tête absent ou mal formé, clé inconnue, clé révoquée : la réponse est un
`401`, rendu avant tout traitement — rien n'est créé, rien n'est encaissé. Le
message sépare l'en-tête oublié de l'en-tête envoyé sans clé dedans, parce que
c'est ce second cas qu'on rencontre vraiment : `Bearer $JATE_SECRET_KEY` avec une
variable vide envoie bel et bien un en-tête, mais sans jeton.

```json
{
  "error": {
    "code": "unauthorized",
    "message": "En-tête Authorization présent mais sans clé lisible. Attendu : « Authorization: Bearer jate_live_… » ou « jate_test_… » — un jeton vide vient le plus souvent d'une variable d'environnement non définie dans le shell qui lance l'appel."
  }
}
```

Une clé de **test** s'authentifie exactement comme une clé réelle : `jate_live_…`
dans les messages est un exemple de forme, jamais une exigence — l'authentification
ne lit aucun préfixe. C'est `GET /v1/me` qui vous dit dans quel mode vous êtes
(§ 4), pas le début de la clé.

L'état est relu à **chaque appel** : une clé révoquée (`key_revoked`), une
application désactivée, un accès API fermé ou un commerce suspendu coupent
l'encaissement à la requête suivante.

---

## 4. Vérifier sa configuration

```bash
curl https://<déploiement>.convex.site/v1/me \
  -H "Authorization: Bearer $JATE_SECRET_KEY"
```

```json
{
  "mode": "live",
  "app": {
    "id": "n17…", "name": "PrayerLink",
    "returnUrls": ["prayerlink://", "https://prayerlink.app"],
    "maxAmount": null, "webhookConfigured": true
  },
  "business": { "name": "OKTRA", "currency": "FCFA", "commissionPct": 5 },
  "scopes": ["checkouts:write", "checkouts:read"]
}
```

`mode` vaut `test` quand le déploiement parle au bac à sable d'Orange, `live`
quand les paiements sont réels. **Vérifiez-le au premier appel** : croire
encaisser alors qu'on est en bac à sable est l'erreur la plus coûteuse d'une
intégration de paiement.

---

## 5. Créer une demande de paiement

```bash
curl -X POST https://<déploiement>.convex.site/v1/checkouts \
  -H "Authorization: Bearer $JATE_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "reference": "cmd_142",
    "amount": 15000,
    "description": "Abonnement mensuel",
    "returnUrl": "prayerlink://paiement/retour",
    "customer": { "phone": "76440000", "name": "Awa" },
    "metadata": { "userId": "u_88" }
  }'
```

| Champ | | |
|---|---|---|
| `reference` | **obligatoire** | Votre identifiant de commande. C'est aussi la clé d'idempotence — voir ci-dessous. |
| `amount` | **obligatoire** | Entier, en unité entière de la devise. `15000` = 15 000 FCFA. Pas de centimes. |
| `description` | **obligatoire** | 2 à 140 caractères. C'est ce que le client lit sur la page de paiement. |
| `returnUrl` | facultatif | Doit correspondre à une adresse déclarée pour l'application. |
| `customer.phone` | facultatif | Préremplit le formulaire. Le client peut le corriger. |
| `customer.ref` | facultatif | Votre identifiant de client, ≤ 64 caractères. Distinct du téléphone — voir §&nbsp;11. |
| `seller` | facultatif | `{ id, name }` — pour qui vous encaissez. Voir §&nbsp;11. |
| `item` | facultatif | `{ id, name }` — à quoi ça se rattache. Voir §&nbsp;11. |
| `metadata` | facultatif | Objet libre, ≤ 4 ko, rendu tel quel dans les lectures et les webhooks. |

Réponse `201` :

```json
{
  "id": "jn71…", "status": "pending",
  "reference": "cmd_142", "amount": 15000, "currency": "FCFA",
  "description": "Abonnement mensuel",
  "paymentUrl": "https://…/x/f2cc45…",
  "expiresAt": 1785632640813,
  "customer": { "phone": "76440000", "name": "Awa", "ref": null },
  "seller": null,
  "item": null,
  "metadata": { "userId": "u_88" },
  "paidAt": null, "paymentMethod": null,
  "createdAt": 1785629040813
}
```

Ouvrez `paymentUrl` chez le client — sur mobile, dans un navigateur intégré
(`WebBrowser.openAuthSessionAsync` en Expo). La demande vaut **une heure**.

Sur cette page, le client paie par **PI-SPI** (il saisit son adresse de
paiement, valide dans son application bancaire, et la page bascule toute seule)
ou par **Orange Money** (il continue sur la page d'Orange). Les moyens affichés
sont ceux ouverts pour le commerce au moment où la page se charge — votre code
n'a rien à décider ni à savoir.

### L'idempotence tient dans la référence

Il n'y a **pas d'en-tête à gérer**. Renvoyez simplement la même requête :

| Situation | Réponse |
|---|---|
| Référence inconnue | `201` — créée |
| Demande encore payable, même montant | `200` + `Jate-Resumed: true` — **la même**, jamais une seconde |
| Demande encore payable, montant différent | `409 reference_amount_mismatch` |
| Référence déjà **payée** | `409 reference_already_paid` |
| Précédente tentative échouée ou expirée | `201` — on repart |

C'est ce qui protège du double paiement quand le réseau coupe, quand votre
serveur réessaie, ou quand le client retape sur « Payer » : il reprend la même
page, donc le même paiement.

Si votre commande n'a pas d'identifiant naturel, générez-en un (`crypto.randomUUID()`)
et **conservez-le** — c'est lui qu'il faudra réutiliser pour réessayer.

---

## 5 bis. Le retour vers votre application

Quand le paiement est **réglé** ou **échoué**, la page affiche un bouton
« Revenir vers *votre application* » qui pointe vers votre `returnUrl`, avec
deux paramètres ajoutés :

```
prayerlink://paiement/retour?jate_checkout=jn71…&jate_reference=cmd_142
```

| Paramètre | |
|---|---|
| `jate_checkout` | L'identifiant Jate de l'encaissement — celui de `GET /v1/checkouts/<id>` |
| `jate_reference` | Votre `reference`, telle que vous l'avez fournie |

### ⚠️ Le retour est un chemin de confort, jamais une preuve

Trois faits commandent toute l'implémentation :

1. **Le retour peut ne jamais arriver.** Le client peut fermer l'onglet après
   avoir payé — le paiement aboutit quand même, et seul le webhook vous le dira.
2. **Le retour peut arriver sans paiement.** Un client peut revenir, ou rouvrir
   l'URL depuis son historique, sans avoir réglé.
3. **L'URL est falsifiable.** Elle passe par le navigateur du client ; c'est
   pour cela qu'elle ne porte **aucun statut**. Rien de ce qu'elle contient ne
   doit déclencher une livraison.

L'implémentation correcte tient en une phrase : **au retour, votre application
demande à son propre backend, qui relit `GET /v1/checkouts/<id>` et répond
`paid` ou non.** Jamais de logique de livraison dans le client.

```ts
// Mobile (Expo) — le retour ferme le navigateur intégré
const result = await WebBrowser.openAuthSessionAsync(paymentUrl, "prayerlink://");
// result.url contient jate_checkout et jate_reference si le client a cliqué
// « Revenir ». Quoi qu'il arrive (retour, fermeture, timeout) :
const { status } = await monBackend.verifierPaiement(reference);
// → le backend appelle GET /v1/checkouts/<id> et rend le statut. C'est LUI qui
//   livre (via le webhook, de préférence) ; l'écran ne fait qu'afficher.
if (status === "paid") afficherSucces();
else if (status === "pending") afficherEnAttente(); // le webhook tranchera
else proposerDeReessayer();
```

Le cas `pending` au retour est **normal** : la confirmation du prestataire peut
prendre quelques secondes de plus que le client. Affichez « vérification en
cours » et laissez le webhook conclure — ne l'interprétez ni comme un succès ni
comme un échec.

### Site web — rien à installer côté navigateur

Le parcours est le même, et il ne demande **aucun paquet Jate dans la page**.
Ce n'est pas un manque : le client n'a rien à décider, donc il n'a besoin de
rien.

```ts
// 1. ALLER PAYER. Votre backend a créé l'encaissement et rendu `paymentUrl`.
//
//    UNE REDIRECTION PLEINE PAGE, pas une fenêtre surgissante : les navigateurs
//    mobiles bloquent les popups qui ne suivent pas immédiatement un geste de
//    l'utilisateur, et le retour se fait de toute façon par URL.
window.location.href = paymentUrl;

// 2. AU RETOUR, sur la page que désigne votre `returnUrl`.
const params = new URLSearchParams(location.search);
const reference = params.get("jate_reference"); // ou jate_checkout

// 3. LA VÉRITÉ VIENT DE VOTRE BACKEND, jamais de ces paramètres — ils ont
//    traversé le navigateur du client, donc ils ne prouvent rien. Le backend
//    relit l'encaissement avec SA clé et répond.
const { status } = await fetch(`/api/paiement/${reference}`).then((r) => r.json());
if (status === "paid") afficherSucces();
else if (status === "pending") afficherEnAttente(); // le webhook tranchera
else proposerDeReessayer();
```

Pour le web, `returnUrl` est une adresse `https://` ordinaire — elle doit
figurer parmi les adresses autorisées de l'application, sinon Jate refuse la
demande de paiement **avant** sa création.

> **Pourquoi il n'existe pas de `@jate/web`.** Ce qui précède, c'est une
> redirection et une lecture de paramètres d'URL : trois lignes de plateforme,
> sans logique. Un paquet qui les envelopperait ajouterait un numéro de version
> à maintenir, un bundle à charger et une surface de plus — pour rien. Et il
> enverrait un mauvais signal : il laisserait croire qu'il se passe côté client
> quelque chose d'important, alors que toute la sûreté du parcours vient
> précisément de ce qu'il ne s'y passe rien.

Côté serveur, en revanche, c'est [`@jate/sdk`](sdk.md) qui travaille : il crée
l'encaissement et vérifie le webhook. C'est là que vit la clé, et nulle part
ailleurs.

---

## 5 ter. Le reçu en PDF

Jate compose un reçu pour tout encaissement **réglé**, servi à une adresse
publique, sans clé :

```bash
curl -o recu.pdf https://<déploiement>.convex.site/c/<jate_checkout>/recu.pdf
```

`<jate_checkout>` est l'identifiant rendu par `GET /v1/checkouts/<id>` — le même
que le paramètre `jate_checkout` du retour.

```
Content-Type: application/pdf
Content-Disposition: attachment; filename="recu-jate-<id>.pdf"
Cache-Control: no-store
```

Le document porte l'en-tête du business, les lignes de la commande, le montant
réglé, la date, le moyen de paiement, une référence courte lisible et un QR qui
ramène à la page du reçu. En bac à sable, il est marqué « ENVIRONNEMENT DE
TEST — aucun argent réel n'a été débité ».

### ⚠️ C'est à votre application de le proposer

La page de paiement **n'affiche plus** de bouton « Télécharger le reçu » quand
l'encaissement porte une `returnUrl`. Dans une WebView d'application — sans
onglets, sans barre d'adresse, sans gestionnaire de téléchargements — ce bouton
proposait une opération que l'hôte avale souvent en silence, juste au-dessus du
seul geste qui termine le parcours. Votre application a le `checkoutId` : c'est
elle qui sert le reçu, dans son propre écran, où elle sait le faire.

**Ne poussez pas cette adresse dans la WebView du paiement.** Vous y
reproduiriez exactement ce qui vient d'en être retiré : le PDF s'affiche
par-dessus, votre client n'a plus rien pour en sortir, et il perd le bouton de
retour — après avoir payé. Récupérez les octets et présentez le fichier avec les
outils natifs de votre plateforme.

### ⚠️ Un échec ne se lit PAS dans le code de statut

Cette adresse sert d'abord des pages à des humains. Les deux cas d'échec
répondent **200 avec `Content-Type: text/html`**, jamais un 4xx :

| Situation | Réponse |
|---|---|
| Encaissement `paid` | 200 · `application/pdf` |
| Encaissement inconnu, ou pas encore `paid` | 200 · `text/html` — « Reçu indisponible » |
| Débit de documents dépassé | 200 · `text/html` — « Trop de documents demandés » |

**Branchez sur `Content-Type`, jamais sur `response.ok`** — sinon vous
enregistrerez une page HTML sous un nom en `.pdf`, et le client ouvrira un
fichier illisible en croyant tenir sa preuve.

```ts
const res = await fetch(`${base}/c/${checkoutId}/recu.pdf`);
const type = res.headers.get("content-type") ?? "";
if (!type.startsWith("application/pdf")) {
  // Pas encore réglé, ou débit dépassé. Rien à enregistrer.
  return null;
}
const pdf = await res.arrayBuffer();
```

Le plus sûr reste de ne demander le reçu qu'après avoir lu `status: "paid"` sur
`GET /v1/checkouts/<id>` — la même lecture qui autorise la livraison.

### Débit

**60 documents par heure et par IP**, avec une réserve de 20 d'un coup. C'est un
plafond humain : il vise le client qui appuie deux fois, pas votre serveur. Si
vous archivez les reçus, faites-le **une fois**, à la réception du webhook, et
non à chaque affichage d'écran.

### L'adresse est publique

Aucune clé ne la protège : l'identifiant d'encaissement est le seul secret qui
la garde, et il vaut le reçu. Ne le laissez pas fuiter là où il n'a rien à faire
— journaux partagés, URL d'une page publique, capture d'écran de support.

---

## 6. Lire les encaissements

```bash
# Un encaissement précis — la source de vérité avant de livrer
curl https://<déploiement>.convex.site/v1/checkouts/<id> \
  -H "Authorization: Bearer $JATE_SECRET_KEY"

# La liste, paginée. Filtres : reference, sellerId, itemId, status
curl "https://<déploiement>.convex.site/v1/checkouts?status=paid&limit=50" \
  -H "Authorization: Bearer $JATE_SECRET_KEY"
```

```json
{ "data": [ … ], "nextCursor": "…" }
```

Repassez `nextCursor` tel quel dans `?cursor=` jusqu'à ce qu'il vaille `null`.
`status` ∈ `pending` · `paid` · `failed` · `cancelled` · `expired`.
`sellerId` et `itemId` filtrent par ventilation — voir [§ 11](#11-ventilation--place-de-marché-billetterie-agences).

Une application ne lit que **ses** encaissements ; ceux d'une autre application
n'existent pas pour elle.

---

## 7. Webhooks

Déclarez l'adresse dans *Développeurs*. Elle doit être en **HTTPS** et joignable
depuis Internet. Un secret `whsec_…` s'affiche alors, une seule fois.

```bash
npx convex env set JATE_WEBHOOK_SECRET whsec_…
```

Événements : `checkout.paid`, `checkout.failed`, `checkout.expired`.

```http
POST /jate-webhook
Jate-Signature: t=1785630604,v1=6f3a…
Jate-Event-Id: evt_21fbbb84…
Jate-Event-Type: checkout.paid
```

```json
{
  "id": "evt_21fbbb84…",
  "type": "checkout.paid",
  "created": 1785630604,
  "data": {
    "id": "jn71…", "reference": "cmd_142", "status": "paid",
    "amount": 15000, "currency": "FCFA",
    "paidAt": 1785630604857, "paymentMethod": "orange_money",
    "metadata": { "userId": "u_88" },
    "seller": { "id": "org_42" }, "item": { "id": "evt_9" }
  }
}
```

La notification ne contient **ni numéro de téléphone ni nom** : elle voyage vers
un serveur tiers et s'écrit dans ses journaux. Si vous avez besoin du client,
faites une lecture authentifiée.

Même règle pour la ventilation : les **identifiants** de `seller` et `item`
passent — ce sont vos propres clés, vous les avez déjà — mais pas leurs
libellés, ni `customer.ref`. Voir [§ 11](#11-ventilation--place-de-marché-billetterie-agences).

### Vérifier la signature

```ts
// convex/http.ts de VOTRE application
import { httpRouter } from "convex/server";
import { httpAction } from "./_generated/server";
import { internal } from "./_generated/api";

const http = httpRouter();

http.route({
  path: "/jate-webhook",
  method: "POST",
  handler: httpAction(async (ctx, request) => {
    // LE CORPS BRUT, jamais un JSON re-sérialisé : la signature porte sur les
    // octets reçus. C'est le bug numéro un des intégrations de webhook.
    const body = await request.text();
    const header = request.headers.get("Jate-Signature") ?? "";

    const t = Number(/t=(\d+)/.exec(header)?.[1]);
    const received = /v1=([a-f0-9]+)/.exec(header)?.[1] ?? "";

    // Fenêtre de rejeu : cinq minutes.
    if (!t || Math.abs(Date.now() / 1000 - t) > 300) {
      return new Response("stale", { status: 400 });
    }

    const key = await crypto.subtle.importKey(
      "raw",
      new TextEncoder().encode(process.env.JATE_WEBHOOK_SECRET!),
      { name: "HMAC", hash: "SHA-256" },
      false,
      ["sign"],
    );
    const mac = await crypto.subtle.sign(
      "HMAC",
      key,
      new TextEncoder().encode(`${t}.${body}`),
    );
    const expected = Array.from(new Uint8Array(mac), (b) =>
      b.toString(16).padStart(2, "0"),
    ).join("");

    // Comparaison en temps constant.
    if (
      expected.length !== received.length ||
      expected.split("").reduce((d, c, i) => d | (c.charCodeAt(0) ^ received.charCodeAt(i)), 0) !== 0
    ) {
      return new Response("bad signature", { status: 401 });
    }

    const event = JSON.parse(body);

    // ON RÉPOND VITE, ON TRAVAILLE APRÈS. Jate attend dix secondes au plus.
    await ctx.scheduler.runAfter(0, internal.paiements.traiter, {
      eventId: event.id,
      checkoutId: event.data.id,
    });

    return new Response("ok", { status: 200 });
  }),
});

export default http;
```

Puis, dans le traitement :

```ts
// 1. Déduplication sur event.id — la livraison est « au moins une fois ».
// 2. Relecture chez Jate AVANT de livrer :
//      GET /v1/checkouts/<id>  →  status === "paid" ?
// 3. Livraison idempotente sur checkoutId.
```

### Ce que Jate garantit, et ce qu'il ne garantit pas

- **Au moins une fois**, jamais exactement une fois. Une réponse perdue nous
  fait réessayer un envoi pourtant reçu : déduplication obligatoire sur `id`.
- **Six tentatives** sur environ huit heures : immédiatement, puis 30 s, 2 min,
  10 min, 1 h, 6 h. Seul un **2xx** compte comme reçu ; les redirections ne sont
  pas suivies.
- Au-delà, la livraison est marquée échouée et se **renvoie à la main** depuis le
  tableau de bord, avec le même identifiant d'événement.
- L'ordre d'arrivée n'est pas garanti. Fiez-vous à l'état lu, pas à la séquence.

---

## 8. Erreurs

```json
{ "error": { "code": "invalid_amount", "message": "…" } }
```

Branchez votre logique sur `code`, jamais sur `message` : le premier est stable,
le second peut être réécrit.

| HTTP | `code` | |
|---|---|---|
| 400 | `invalid_reference` · `invalid_amount` · `invalid_description` · `invalid_status` · `amount_too_large` · `metadata_too_large` · `return_url_not_allowed` · `invalid_seller` · `invalid_item` · `invalid_customer_ref` · `invalid_request` | La requête |
| 401 | `unauthorized` · `key_revoked` | La clé |
| 403 | `app_disabled` · `api_closed` · `business_inactive` · `insufficient_scope` | L'accès |
| 404 | `not_found` | |
| 409 | `reference_already_paid` · `reference_amount_mismatch` | L'idempotence |
| 429 | `rate_limited` | Voir `Retry-After` |

Limites : **120 requêtes/minute par clé**, 300/minute par adresse IP.

---

## 9. Ce qu'une clé volée ne peut pas faire

C'est la propriété qui décide de tout le reste du dessin.

Une clé compromise permet de créer des encaissements **vers le solde de votre
propre commerce** (une nuisance, pas un vol) et de lire les paiements de son
application.

Elle ne permet **jamais** de : déclencher un reversement, changer un numéro de
destination, lire un dossier d'identité, créer une application ou une autre clé,
modifier une commission. Les reversements restent une décision humaine, prise
depuis le tableau de bord et approuvée par la plateforme.

Un plafond par application (`maxAmount`) borne ce qu'une clé perdue peut créer.

## 10. Aller en production

- [ ] `GET /v1/me` répond `"mode": "live"`
- [ ] La clé est une variable d'environnement du serveur, absente du dépôt
- [ ] `reference` est votre identifiant de commande, conservé pour les réessais
- [ ] Vous relisez l'encaissement avant de livrer
- [ ] Votre webhook vérifie la signature et déduplique sur `event.id`
- [ ] Vos adresses de retour sont déclarées
- [ ] Au retour du payeur, vous vérifiez côté serveur — jamais d'après l'URL
- [ ] Vous avez posé un plafond par paiement

---

## 11. Ventilation : place de marché, billetterie, agences

**Jate ne verse rien à vos vendeurs.** Commençons par là, parce que c'est la
question qu'on se pose en lisant le mot `seller`, et parce que la réponse décide
de tout le reste : il n'existe ni partage de paiement, ni sous-compte, ni solde
par vendeur, ni portée d'API pour les reversements. L'argent d'un encaissement
arrive **en entier** sur le solde de votre commerce. C'est vous qui redistribuez
ensuite, avec vos propres moyens.

Ce que ces champs font, alors : ils **retracent** et ils **ventilent**. Si votre
plateforme encaisse pour des tiers, vous savez qui a rapporté quoi.

| Votre plateforme | `seller` | `item` |
|---|---|---|
| Billetterie | l'organisateur | l'événement |
| Immobilier | l'agence | la propriété |
| Place de marché | la boutique | le produit |
| Un seul vendeur | — | le produit |

```json
{
  "reference": "tk_8891",
  "amount": 5000,
  "description": "Pass 1 jour",
  "seller":   { "id": "org_42", "name": "Comité Balani Show" },
  "item":     { "id": "evt_9",  "name": "Concert du 12 août" },
  "customer": { "phone": "76440000", "ref": "u_88" }
}
```

| Règle | |
|---|---|
| `id` | 1 à 64 caractères. **Obligatoire** dès que le bloc est présent. C'est votre clé, stable dans le temps. |
| `name` | 1 à 80 caractères, facultatif. Absent, le libellé retombe sur l'`id`. |
| Un `name` sans `id` | Refusé — `400 invalid_seller` / `invalid_item`. Un nom change ; vos totaux se scinderaient en deux au premier renommage. |
| `seller` et `item` | Indépendants. Une application mono-vendeur ne remplit que `item`. |
| Les deux | Facultatifs de bout en bout. Ne rien envoyer ne change rien. |

### Pourquoi pas `metadata` ?

Parce que `metadata` est **opaque** : Jate ne l'interprète jamais, donc elle
n'est ni indexée, ni filtrable, ni comptée. `seller` et `item` sont de vraies
colonnes — c'est ce qui permet de filtrer une liste et d'afficher un classement.
Elles ne consomment d'ailleurs **pas** votre budget de 4 ko de `metadata`.

### Filtrer

```bash
# Tout ce qu'a rapporté un organisateur
GET /v1/checkouts?sellerId=org_42

# Un événement en particulier, réglés seulement
GET /v1/checkouts?itemId=evt_9&status=paid
```

Un seul index sert par requête. La précédence est `reference`, puis `itemId`,
puis `sellerId` : les autres critères s'appliquent ensuite comme filtres, donc
combiner `reference` et `itemId` reste juste, simplement moins direct.

### Dans les webhooks : les identifiants, pas les libellés

```json
"data": { "…": "…", "seller": { "id": "org_42" }, "item": { "id": "evt_9" } }
```

Les **noms** n'y sont pas, et `customer.ref` non plus. Même raison que pour le
téléphone et le nom du payeur : une notification voyage vers votre serveur et
s'écrit dans ses journaux, or un nom de vendeur est souvent un nom de personne.
Les identifiants, eux, sont vos propres clés — vous les avez déjà.

Ces deux clés sont **facultatives dans le type**, et pas seulement nulles : un
événement émis avant l'arrivée de la ventilation et renvoyé aujourd'hui ne les
porte pas du tout. Le corps signé est figé à l'émission et n'est jamais
recalculé.

### Et pour reverser à mes organisateurs ?

Vous lisez `netAmount` par vendeur dans votre tableau de bord Jate — c'est le
montant réellement entré, commission déduite — et vous payez vos organisateurs
comme vous le faisiez déjà. Reverser le **brut** vous ferait payer la commission
de votre poche.

---

## Guides par plateforme

Le parcours est le même partout ; les pièges, non.

- **[integration-convex.md](integration-convex.md)** — `action` contre
  `mutation` (pas de `fetch` en mutation), `httpAction` pour le webhook, et la
  discipline qui remplace le RLS que Convex n'a pas.
- **[integration-supabase.md](integration-supabase.md)** — `--no-verify-jwt` sur
  la fonction de webhook (sans quoi elle ne s'exécute jamais), rôle de service,
  et les politiques RLS qui empêchent un client de se déclarer payé.
- **[sdk.md](sdk.md)** — le client officiel `@jate/sdk` : encaissements typés,
  idempotence, webhooks vérifiés en une ligne. Il vit sur un **serveur** —
  Node, Next.js côté serveur, Convex, Deno — jamais dans un navigateur ni dans
  un bundle mobile.
- **`@jate/expo`** — le parcours côté **Expo / React Native** : ouvre la page de
  paiement, lit le retour, ne décide de rien. Pour un **site web**, il n'y a
  rien à installer : voir [§ 5 bis](#site-web--rien-à-installer-côté-navigateur).
- **[cli.md](cli.md)** — le CLI `@jate/cli` : tester sa clé, créer un
  encaissement de test, relayer les webhooks en local.
- **[prompt-agent.md](prompt-agent.md)** — vous intégrez avec Claude Code,
  Cursor ou Codex ? Le prompt à leur donner tel quel : il porte les six règles
  et la liste de vérification, pour qu'un agent n'écrive pas l'intégration de
  paiement qu'il a vue ailleurs.
