sdk-js (JavaScript/TypeScript)
À quoi sert cette brique
Section intitulée « À quoi sert cette brique »sdk-js est le package npm @titreo-epay/sdk. C’est un wrapper HTTP typé autour de l’API publique /v1/* de Titreo E-Pay (l’orchestrateur de paiement multi-instruments). Son seul rôle est le transport : signer les requêtes avec la clé d’API, gérer l’Idempotency-Key, retenter les erreurs serveur, désérialiser les réponses en types TypeScript, et exposer un helper de vérification de signature pour les webhooks entrants.
Il ne contient aucune logique métier. L’orchestration (séquencement des legs, rollback, charge en bloc) vit entièrement côté serveur API. Le SDK ne fait que parler HTTP avec lui. Il ne stocke rien (pas de cache, pas de DB) et n’embarque aucun composant UI — le rendu checkout est la responsabilité de checkout-sdk.
Qui l’utilise :
- e-commerçants headless qui intègrent directement l’API ;
- plugins CMS en JavaScript (ex. app Shopify Remix) ;
- console marchand / dashboard admin (Next.js).
Pour le rôle de chaque brique dans le système, voir la vue d’ensemble de l’écosystème. Pour ce que l’API expose, voir Backend et les contrats inter-briques.
flowchart LR A["Plugin CMS / Console / App headless"] -->|"import @titreo-epay/sdk"| B["TitreoEpayClient"] B --> C["HttpClient (fetch + bearer + idempotency + retry)"] C -->|"HTTPS /v1/*"| D["API Titreo E-Pay"] D -.->|"webhook signé HMAC"| E["verifyMerchantWebhook()"] E --> A
Installation et configuration
Section intitulée « Installation et configuration »pnpm add @titreo-epay/sdk# ounpm install @titreo-epay/sdkLe package est en ESM + CJS dual (build tsup), cible Node 20+ (fetch natif requis) et les navigateurs evergreen. Il est compatible Edge runtime (Vercel, Cloudflare Workers). Aucune dépendance lourde : il s’appuie sur le fetch global ; axios est proscrit.
Le constructeur prend une clé d’API et une URL de base. Les autres options ont des valeurs par défaut.
import { TitreoEpayClient } from '@titreo-epay/sdk'
const client = new TitreoEpayClient({ apiKey: 'tep_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', baseUrl: 'https://api.titreo-epay.io', // options ci-dessous facultatives : fetch: globalThis.fetch, // override (utile en test ou Edge) timeoutMs: 30_000, // default 30 s maxRetries: 3, // retry sur 5xx + erreurs réseau autoIdempotency: true, // génère un Idempotency-Key si non fourni (POST/PUT/PATCH/DELETE) userAgent: 'my-plugin/1.0',})Option TitreoEpayClientConfig |
Type | Défaut | Rôle |
|---|---|---|---|
apiKey |
string |
— (requis) | Bearer token. tep_live_… ou tep_test_…. Le constructeur lève si absent. |
baseUrl |
string |
— (requis) | Racine de l’API. Le slash final est retiré automatiquement. |
fetch |
typeof fetch |
globalThis.fetch |
Implémentation fetch injectable (tests, runtime sans fetch global). |
timeoutMs |
number |
30000 |
Délai d’abandon par requête (via AbortController). |
maxRetries |
number |
3 |
Nombre max de retentatives sur 5xx / erreurs réseau. |
autoIdempotency |
boolean |
true |
Génère un Idempotency-Key (UUID v7) si non fourni, sur les méthodes mutantes. |
userAgent |
string |
@titreo-epay/sdk-js |
Valeur du header User-Agent. |
Usage : créer une session, ajouter un gift leg, soumettre
Section intitulée « Usage : créer une session, ajouter un gift leg, soumettre »Le parcours type combine une carte cadeau (gift leg) et un moyen de paiement classique (psp leg) dans une seule session, puis déclenche l’encaissement avec submit.
import { TitreoEpayClient } from '@titreo-epay/sdk'
const client = new TitreoEpayClient({ apiKey: process.env.TEP_API_KEY!, baseUrl: 'https://api.titreo-epay.io',})
// 0. Vérifier la clé et ses scopesconst me = await client.me.get()console.log(me.merchantId, me.scopes)
// 1. Créer la session (montants en plus petite unité, ex. centimes)const session = await client.sessions.create({ reference: 'ORDER-12345', totalAmount: 5000, currency: 'EUR',})// session.remainingAmount === 5000, session.status === 'CREATED'
// 2. Ajouter une carte cadeau (gift leg)await client.sessions.addGiftLeg(session.id, { cardToken: 'tok_xxx', cardLast4: '1234', emitter: 'titreo', availableBalance: 2000,})
// 3. Désigner le PSP qui couvre le reste (psp leg)await client.sessions.setPspLeg(session.id, { providerType: 'stripe', paymentMethod: 'card',})
// 4. Soumettre — clé d'idempotence explicite recommandéeconst result = await client.sessions.submit(session.id, { idempotencyKey: `submit-${session.id}`,})console.log(result.status, result.outboxId)Toutes les méthodes mutantes acceptent un second argument { idempotencyKey }. En l’absence de clé explicite et avec autoIdempotency: true (défaut), le SDK en génère une par requête. Pour un plugin CMS « retry-aware » (qui peut rejouer le même appel après un crash), fournir une clé stable et déterministe (par ex. dérivée de la référence de commande) est fortement recommandé. Voir Idempotence.
sequenceDiagram
participant P as "Plugin / Console"
participant S as "TitreoEpayClient.sessions"
participant A as "API /v1/*"
P->>S: "create({ reference, totalAmount, currency })"
S->>A: "POST /v1/sessions (Idempotency-Key)"
A-->>S: "201 Session (status CREATED)"
P->>S: "addGiftLeg(id, { cardToken, ... })"
S->>A: "POST /v1/sessions/:id/legs/gift"
A-->>S: "200 Session (+ leg gift_card)"
P->>S: "setPspLeg(id, { providerType, paymentMethod })"
S->>A: "POST /v1/sessions/:id/legs/psp"
A-->>S: "200 Session (+ leg psp)"
P->>S: "submit(id, { idempotencyKey })"
S->>A: "POST /v1/sessions/:id/submit"
A-->>S: "200 SubmitResult { status, outboxId }"
Carte des fichiers
Section intitulée « Carte des fichiers »Tous les chemins sont relatifs à sdk-js/.
| Fichier | Rôle | Exports clés |
|---|---|---|
src/index.ts |
Barrel d’export public du package. | TitreoEpayClient, generateIdempotencyKey, les classes d’erreur, les types (Session, Leg, Me, Page, SubmitResult…) et les types d’entrée (CreateSessionInput, AddGiftLegInput…). |
src/client.ts |
La classe TitreoEpayClient et ses ressources internes. |
TitreoEpayClient, TitreoEpayClientConfig, CreateSessionInput, ListSessionsQuery, AddGiftLegInput, SetPspLegInput, UpdateMerchantInput, CreateApiKeyInput, UpsertPspConfigInput, ListAuditLogsQuery, CreateMerchantInput, ListMerchantsQuery. |
src/http.ts |
Le moteur transport : fetch, bearer, idempotence, retry, timeout, mapping des erreurs. |
HttpClient, HttpClientConfig, RequestOptions, generateIdempotencyKey. |
src/types.ts |
Types de domaine côté SDK (réponses et énumérations). Écrits à la main en Phase 1. | Session, Leg, LegType, SessionStatus, Currency, Scope, Me, Merchant, ApiKey, CreatedApiKey, PspConfig, AuditLog, Page, SubmitResult, RefundResult, BootstrapManifest. |
src/errors.ts |
Hiérarchie d’erreurs typées. | TitreoEpayError, ApiError, NetworkError, IdempotencyConflictError. |
src/webhooks.ts |
Sous-module @titreo-epay/sdk/webhooks : vérification HMAC et parsing d’événements. |
verifyMerchantWebhook, parseWebhookEvent, WebhookVerifyOptions, WebhookVerificationResult, MerchantWebhookEvent. |
src/generated/openapi.ts |
Types paths/components générés depuis l’openapi.json de l’API (référence de contrat). |
paths, webhooks, components, operations. |
scripts/gen.ts |
Script CLI (pnpm gen) qui régénère src/generated/openapi.ts. |
— |
tests/client.test.ts |
Tests du client : auth, idempotence, query params, mapping d’erreurs, 204. | — |
tests/webhooks.test.ts |
Tests de la signature webhook (valide, falsifiée, expirée, secret erroné). | — |
Ressources exposées par TitreoEpayClient
Section intitulée « Ressources exposées par TitreoEpayClient »L’objet client expose des sous-ressources (instanciées dans le constructeur), chacune mappant un groupe d’endpoints /v1/*.
| Accès | Méthodes | Endpoints |
|---|---|---|
client.me |
get() |
GET /v1/me |
client.sessions |
create, get, list, addGiftLeg, setPspLeg, removeLeg, submit, refund, bootstrap |
/v1/sessions, /v1/sessions/:id, …/legs/gift, …/legs/psp, …/legs/:legId, …/submit, …/refund, …/bootstrap |
client.merchants.me |
get, update |
GET / PUT /v1/merchants/me |
client.apiKeys |
create, list, revoke |
/v1/merchants/me/api-keys, …/api-keys/:id |
client.pspConfigs |
list, get, upsert, delete |
/v1/merchants/me/psp-configs, …/psp-configs/:provider |
client.auditLogs |
list |
GET /v1/audit-logs |
client.admin.merchants |
create, list, get, update |
/v1/admin/merchants, …/merchants/:id (scope admin) |
Les méthodes de liste (sessions.list, auditLogs.list, admin.merchants.list) renvoient un Page<T> avec pagination par curseur ({ data, nextCursor, hasMore }). Les paramètres from / to acceptent indifféremment une string ISO ou un Date (converti via toISOString()).
HttpClient : fetch, bearer, idempotence, retry
Section intitulée « HttpClient : fetch, bearer, idempotence, retry »HttpClient (dans src/http.ts) est le cœur transport. Chaque méthode de ressource appelle http.request<T>(...). Le client n’utilise que fetch (natif ou injecté), sans dépendance HTTP externe.
Pour chaque requête, HttpClient :
- Construit l’URL via
URL+searchParams; les valeursundefinedsont ignorées et les tableaux génèrent des paramètres répétés (ex.status=COMPLETED&status=FAILED). - Pose les headers :
Authorization: Bearer <apiKey>,Accept: application/json,User-Agent, etContent-Type: application/jsonsi un body est présent. - Gère l’idempotence : sur les méthodes non sûres (tout sauf
GET/HEAD/OPTIONS), pose un headerIdempotency-Key— soit celui fourni, soit un UUID v7 auto-généré siautoIdempotencyest actif. - Applique un timeout : un
AbortControllerannule la requête au-delà detimeoutMs. - Retente sur erreur : voir la stratégie ci-dessous.
- Désérialise :
204 No Contentou corps vide →undefined; sinonJSON.parsetypé enT.
Stratégie de retry
Section intitulée « Stratégie de retry »Le backoff est exponentiel avec jitter : min(5000, 200 · 2^tentative) + aléa(0..100) ms. Les conditions de retry sont prudentes pour ne pas créer de doublons de paiement :
- les erreurs réseau (timeout, DNS…) sont retentées jusqu’à
maxRetries, puis converties enNetworkError; - les réponses 5xx sont retentées sauf sur méthode mutante dépourvue d’
Idempotency-Key— dans ce cas l’erreur est levée immédiatement (un POST sans clé ne peut pas être rejoué sans risque de double encaissement) ; - les réponses 4xx ne sont jamais retentées : l’erreur est mappée et levée tout de suite.
flowchart TD
A["request()"] --> B["fetch (timeout via AbortController)"]
B -->|"réseau KO"| R{"attempt < maxRetries ?"}
R -->|"oui"| W["backoff + jitter, retry"] --> B
R -->|"non"| NE["throw NetworkError"]
B -->|"réponse"| C{"response.ok ?"}
C -->|"oui"| OK["204/vide → undefined, sinon JSON.parse(T)"]
C -->|"non"| E["toApiError(response)"]
E --> S{"status >= 500 ?"}
S -->|"non (4xx)"| TH["throw ApiError / IdempotencyConflictError"]
S -->|"oui"| K{"mutant sans Idempotency-Key ?"}
K -->|"oui"| TH
K -->|"non"| R
Erreurs typées
Section intitulée « Erreurs typées »Toutes les erreurs dérivent de TitreoEpayError. Le mapping se fait dans HttpClient.toApiError à partir du corps { code, message, details } renvoyé par l’API.
| Classe | Quand | Champs notables |
|---|---|---|
ApiError |
Réponse HTTP non 2xx | status, code, message, details, getters isClientError / isServerError |
IdempotencyConflictError |
Réponse code === 'IDEMPOTENCY_KEY_CONFLICT' (HTTP 422) |
hérite d’ApiError (même clé d’idempotence, body différent) |
NetworkError |
Échec transport après retries épuisés | message, cause |
TitreoEpayError |
Classe de base | — |
import { ApiError, IdempotencyConflictError, NetworkError } from '@titreo-epay/sdk'
try { await client.sessions.submit(sessionId, { idempotencyKey: key })} catch (err) { if (err instanceof IdempotencyConflictError) { // même clé, corps différent → aligner le body ou changer de clé } else if (err instanceof ApiError) { console.error(err.status, err.code, err.message) if (err.isClientError) { /* 4xx : input à corriger */ } if (err.isServerError) { /* 5xx : retries déjà épuisés */ } } else if (err instanceof NetworkError) { // timeout / DNS — retries déjà épuisés }}Vérification de webhook entrant
Section intitulée « Vérification de webhook entrant »L’API Titreo E-Pay notifie le marchand des transitions de session (paiement complété, échoué, expiré, remboursé) via des webhooks signés. Le sous-module @titreo-epay/sdk/webhooks fournit la vérification de signature côté récepteur (le serveur du plugin / de la boutique). Ce module utilise node:crypto : il est destiné à un environnement serveur.
Schéma de signature attendu :
- header
t=<unix_ts>,v1=<hex>(transmis dansx-titreo-signature) ; - HMAC-SHA256 de la chaîne
`${timestamp}.${rawBody}`avec le webhook secret du marchand, comparé en temps constant (timingSafeEqual) ; - protection anti-rejeu via une tolérance temporelle (
toleranceSeconds, défaut 300 s).
verifyMerchantWebhook(rawBody, signatureHeader, secret, options?) renvoie un résultat discriminé :
{ valid: true, timestamp };{ valid: false, reason }avecreason ∈ { 'malformed_header', 'expired', 'invalid_signature' }.
import { verifyMerchantWebhook, parseWebhookEvent } from '@titreo-epay/sdk/webhooks'
// rawBody DOIT être le corps brut non parsé (pas l'objet JSON déjà désérialisé)async function handleTitreoWebhook(rawBody: string, sig: string | undefined, secret: string) { const result = verifyMerchantWebhook(rawBody, sig, secret, { toleranceSeconds: 300 }) if (!result.valid) { throw new Error(`webhook rejeté : ${result.reason}`) } const event = parseWebhookEvent(rawBody) switch (event.event) { case 'session.completed': // marquer la commande payée côté CMS break case 'session.failed': // marquer l'échec break case 'session.refunded': case 'session.partially_refunded': // ajuster côté CMS break case 'session.expired': // libérer le panier break }}Les types d’événement reconnus par MerchantWebhookEvent.event : session.completed, session.failed, session.expired, session.refunded, session.partially_refunded.
Génération des types depuis openapi.json
Section intitulée « Génération des types depuis openapi.json »La source de vérité du contrat HTTP est l’openapi.json publié par l’API. Le fichier src/generated/openapi.ts contient les types paths générés à partir de ce spec (via openapi-typescript). Il sert de référence de contrat vérifiable ; en Phase 1 les types métier réellement exposés (src/types.ts) restent écrits à la main pour une ergonomie plus directe.
Régénération :
pnpm gen # source par défaut : ../api/openapi.json (workspace)pnpm gen https://api.titreo-epay.io/openapi.json # depuis l'URL CI publiéeLe script scripts/gen.ts (lancé via tsx) accepte un chemin local ou une URL : il lit/fetch le spec, le passe à openapiTS, et écrit le résultat dans src/generated/openapi.ts. Sans argument, il pointe sur ../api/openapi.json (l’API voisine dans le monorepo de dev).
flowchart LR A["openapi.json (API)"] -->|"pnpm gen path-ou-url"| B["openapi-typescript"] B --> C["src/generated/openapi.ts (paths, components)"] C -. référence de contrat .-> D["src/types.ts (hand-written, Phase 1)"]
Scripts npm et outillage
Section intitulée « Scripts npm et outillage »Gestionnaire de paquets : pnpm (pnpm@10.16.0). Bundler : tsup (entrées src/index.ts + src/webhooks.ts, formats ESM/CJS, dts, cible es2022).
| Script | Commande réelle | Rôle |
|---|---|---|
pnpm build |
tsup |
Build dual ESM/CJS + types dans dist/. |
pnpm typecheck |
tsc --noEmit |
Vérification de types stricte. |
pnpm lint |
biome check . |
Lint + format (Biome). |
pnpm lint:fix |
biome check --write . |
Corrige automatiquement. |
pnpm test |
vitest run |
Lance la suite de tests. |
pnpm test:watch |
vitest |
Tests en mode watch. |
pnpm gen |
tsx scripts/gen.ts |
Régénère les types OpenAPI. |
Scopes et permissions
Section intitulée « Scopes et permissions »La clé d’API porte des scopes qui conditionnent l’accès aux ressources. Le type Scope énumère : sessions:read, sessions:write, psp:read, psp:write, webhooks:write, admin.
| Scope | Couvre |
|---|---|
sessions:read |
GET sessions, bootstrap |
sessions:write |
create / addGiftLeg / setPspLeg / removeLeg / submit / refund |
psp:read |
pspConfigs.list / pspConfigs.get |
psp:write |
pspConfigs.upsert / pspConfigs.delete |
webhooks:write |
merchants.me.update |
admin |
console interne Titreo : client.admin.*, couvre l’ensemble des scopes |
Un appel hors scope renvoie une ApiError (4xx). Pour la gestion des clés et la rotation, voir Sécurité.
Liens utiles
Section intitulée « Liens utiles »- Vue d’ensemble de l’écosystème — où se situe le SDK
- Backend — l’API que ce SDK consomme
- Contrats inter-briques — le contrat
/v1/*etopenapi.json - Idempotence — pourquoi et comment fournir une clé stable
- Sécurité — clés d’API, scopes, signature webhook
- checkout-sdk — la couche UI (hors périmètre de ce SDK)
- Environnement de dev local — lancer l’API en local pour tester le SDK