Aller au contenu

sdk-js (JavaScript/TypeScript)

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
Fenêtre de terminal
pnpm add @titreo-epay/sdk
# ou
npm install @titreo-epay/sdk

Le 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 scopes
const 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ée
const 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 }"

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é).

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 (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 :

  1. Construit l’URL via URL + searchParams ; les valeurs undefined sont ignorées et les tableaux génèrent des paramètres répétés (ex. status=COMPLETED&status=FAILED).
  2. Pose les headers : Authorization: Bearer <apiKey>, Accept: application/json, User-Agent, et Content-Type: application/json si un body est présent.
  3. Gère l’idempotence : sur les méthodes non sûres (tout sauf GET/HEAD/OPTIONS), pose un header Idempotency-Key — soit celui fourni, soit un UUID v7 auto-généré si autoIdempotency est actif.
  4. Applique un timeout : un AbortController annule la requête au-delà de timeoutMs.
  5. Retente sur erreur : voir la stratégie ci-dessous.
  6. Désérialise : 204 No Content ou corps vide → undefined ; sinon JSON.parse typé en T.

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 en NetworkError ;
  • 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

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
}
}

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 dans x-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 } avec reason ∈ { '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.

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 :

Fenêtre de terminal
pnpm gen # source par défaut : ../api/openapi.json (workspace)
pnpm gen https://api.titreo-epay.io/openapi.json # depuis l'URL CI publiée

Le 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)"]

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.

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é.