> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dodopayments.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Construire une plateforme d'API IA avec facturation basée sur des crédits

> Créez un service d'API IA en niveaux avec des crédits de jetons — émettez des crédits via des plans d'abonnement et des packs de recharge ponctuels, et déduisez automatiquement lorsque les clients appellent votre API.

<Tip>
  <strong>Laissez Sentra écrire votre code d'intégration pour vous.</strong><br />
  Utilisez notre assistant IA dans VS Code, Cursor ou Windsurf pour générer du code SDK/API, des gestionnaires de webhooks, l'octroi de crédits, et plus — simplement en décrivant ce que vous voulez.

  <a href="https://dodopayments.com/sentra" target="_blank" rel="noopener noreferrer">
    Essayez Sentra : Intégration pilotée par IA →
  </a>
</Tip>

Dans ce tutoriel, vous construirez **NeuralAPI** — une plateforme IA en niveaux où chaque plan d'abonnement dispose d'une allocation mensuelle de crédits de jetons, les clients peuvent acheter des packs de recharge lorsqu'ils sont à court, et votre backend déduit automatiquement les crédits à mesure que les demandes sont traitées par OpenAI.

<Note>
  Ce tutoriel utilise Node.js/Express + le SDK OpenAI. Les concepts de Dodo Payments (crédits, compteurs, webhooks) s'appliquent à tout framework ou fournisseur IA — adaptez librement.
</Note>

À la fin de ce tutoriel, vous saurez comment :

* Créer un droit de crédit personnalisé (jetons) et un compteur qui déduit automatiquement
* Attacher des crédits à des plans d'abonnements (avec et sans surconsommation) et un produit de recharge ponctuel
* Connecter un véritable point de terminaison de complétion OpenAI qui facture les jetons via Dodo Payments
* Interroger le solde de crédits en direct d'un client via le SDK
* Vérifier les signatures des webhooks et router les événements de crédits de Dodo Payments

## Ce que nous construisons

Voici le modèle de tarification pour NeuralAPI :

| Produit                    | Prix           | Jetons                  | Surconsommation           |
| -------------------------- | -------------- | ----------------------- | ------------------------- |
| Plan Starter               | 29 \$/mois     | 10,000,000 jetons/cycle | Bloqué à zéro             |
| Plan Pro                   | 99 \$/mois     | 40,000,000 jetons/cycle | 0,005 \$ par 1 000 jetons |
| Pack de recharge de jetons | 19 \$ une fois | +5,000,000 jetons       | —                         |

<Info>
  Avant de commencer, assurez-vous d'avoir :

  * Un compte Dodo Payments (mode test suffisant)
  * Une clé API OpenAI
  * Node.js 18+
  * Une connaissance de base de TypeScript/Node.js
</Info>

## Étape 1 : Créez votre droit de crédit de jetons

Tout d'abord, créez le droit de crédit que les deux plans d'abonnement et le pack de recharge partageront. Considérez cela comme la définition de l'unité "jeton" utilisée par votre plateforme.

<Frame caption="The Credits tab under Products shows all your credit entitlements.">
  <img src="https://mintcdn.com/dodopayments/eU6ZCQ885P3550bK/images/CBB/Desktop%20-%20Cookbook%20-%20NeuralAPI%20-%20Credit.png?fit=max&auto=format&n=eU6ZCQ885P3550bK&q=85&s=6c34ed3755c78534dcd6012680e98e40" alt="Page de liste des crédits montrant les droits de crédits créés" style={{ maxHeight: '500px', width: 'auto' }} width="2931" height="1665" data-path="images/CBB/Desktop - Cookbook - NeuralAPI - Credit.png" />
</Frame>

<Steps>
  <Step title="Navigate to Credits">
    1. Connectez-vous à votre tableau de bord Dodo Payments
    2. Cliquez sur **Produits** dans la barre latérale gauche
    3. Sélectionnez l'onglet **Crédits**
    4. Cliquez sur **Créer un crédit**
  </Step>

  <Step title="Configure the credit unit">
    Remplissez les détails de base pour votre crédit de jetons :

    **Nom du crédit** : `API Tokens`

    **Type de crédit** : Sélectionnez **Unité personnalisée**

    **Nom de l'unité** : `token`

    **Précision** : `0` (les jetons sont toujours des nombres entiers)

    **Expiration des crédits** : `30 days` (les crédits se réinitialisent à chaque cycle de facturation)

    <Warning>
      La précision ne peut pas être modifiée une fois qu'un crédit est créé. Pour les comptes de jetons, `0` (nombres entiers) est presque toujours correct.
    </Warning>
  </Step>

  <Step title="Skip overage at the credit level">
    Laissez la surconsommation **désactivée** ici — vous la configurerez par plan lors de l'attachement du crédit aux produits. Cela permet au plan Starter de bloquer l'utilisation à zéro tandis que le plan Pro permet la surconsommation.

    <Tip>
      Les paramètres de surconsommation configurés ici sont des *valeurs par défaut*. Chaque attachement de produit peut les remplacer — ce que nous ferons exactement à l'étape 3.
    </Tip>
  </Step>

  <Step title="Save and copy the credit ID">
    Cliquez sur **Créer le crédit**. Une fois enregistré, ouvrez le crédit et copiez son identifiant — il ressemble à `cent_xxxxxxxxxxxx`.

    <Check>
      Votre droit de crédit `API Tokens` est prêt. Ensuite, créez un compteur afin que les événements d'utilisation puissent entraîner des déductions automatiquement.
    </Check>
  </Step>
</Steps>

## Étape 2 : Créez un compteur pour l'utilisation des jetons

Un compteur agrège les événements d'utilisation entrants et les convertit en déductions de crédits. Vous en avez besoin *avant* de créer les produits de planification, car vous l'attacherez lors de la création du produit à l'étape 3.

<Steps>
  <Step title="Open the Meters section">
    1. Dans la barre latérale du tableau de bord, allez à **Produits → Compteurs**
    2. Cliquez sur **Créer un compteur**
  </Step>

  <Step title="Configure the meter">
    Remplissez :

    **Nom du compteur** : `Token Usage Meter`

    **Nom de l'événement** : `api.tokens_used` *(cela doit correspondre exactement à ce que votre application envoie)*

    **Type d'agrégation** : `Sum` — nous additionnons le nombre de jetons de chaque événement

    **Sur propriété** : `tokens` — la clé de métadonnées sur chaque événement dont la valeur sera additionnée

    **Unité de mesure** : `tokens`

    <Warning>
      Les noms d'événements sont sensibles à la casse. `api.tokens_used` ≠ `Api.Tokens.Used` — choisissez-en un et tenez-vous y.
    </Warning>

    Enregistrez le compteur et copiez son identifiant — vous le référencerez lors de l'attachement aux produits.

    <Check>
      Le compteur est créé. Maintenant, nous pouvons le connecter au crédit lorsque nous configurons les produits.
    </Check>
  </Step>
</Steps>

## Étape 3 : Créez les produits de planification

Les deux plans doivent être des produits de **facturation basée sur l'utilisation**, pas de simples abonnements — les compteurs ne peuvent être attachés qu'aux produits UBB, et vous avez besoin du compteur pour déduire automatiquement les crédits à mesure que les clients appellent votre API. Les produits UBB prennent toujours en charge des frais de base récurrents (la `$29` / `$99`); l'utilisation au-delà de cela est facturée en crédits.

<Frame caption="Usage Based Billing pricing type with meter configuration.">
  <img src="https://mintcdn.com/dodopayments/ibNfoFRyCIGyt3pO/images/CBB/Desktop%20-%20Attach%20Credit%20-%20UBB.jpg?fit=max&auto=format&n=ibNfoFRyCIGyt3pO&q=85&s=41b2862c12d126e7843098307e27e137" alt="Configuration de tarification basée sur l'utilisation" style={{ maxHeight: '500px', width: 'auto' }} width="2880" height="1920" data-path="images/CBB/Desktop - Attach Credit - UBB.jpg" />
</Frame>

### Plan Starter (29 \$/mois — 10M de jetons, pas de surconsommation)

<Steps>
  <Step title="Create the Starter UBB product">
    1. Allez à **Produits → Créer un produit**
    2. Sélectionnez **Facturation basée sur l'utilisation** comme type de tarification
    3. Remplissez :

    **Nom du produit** : `NeuralAPI Starter`

    **Description** : `10 million API tokens per month. Perfect for individual developers and small projects.`

    **Prix fixe** : `29.00` (les frais de base récurrents — facturés mensuellement même avant toute utilisation)

    **Cycle de facturation** : `Monthly`

    **Devise** : `USD`
  </Step>

  <Step title="Attach the meter">
    Dans la section **Sélectionner le compteur**, cliquez sur **+** et ajoutez `Token Usage Meter`. Puis sur le compteur :

    1. Basculer **Facturer l'utilisation en crédits** sur on
    2. **Droit de crédit** : sélectionnez `API Tokens`
    3. **Unités de compteur par crédit** : `1` — chaque jeton de l'événement correspond à 1 crédit déduit
    4. **Seuil gratuit** : `0` — l'allocation de crédits elle-même est le "niveau gratuit" du client ; nous n'avons pas besoin d'une bande gratuite supplémentaire

    <Frame caption="Toggle 'Bill usage in Credits' on the meter and pick the credit entitlement.">
      <img src="https://mintcdn.com/dodopayments/ibNfoFRyCIGyt3pO/images/CBB/Desktop%20-%20Attach%20Credit%20-%20UBB-5.jpg?fit=max&auto=format&n=ibNfoFRyCIGyt3pO&q=85&s=b4ef2fe5079cbf3bb39eb3814f101cbd" alt="Compteur avec Facturer l'utilisation en crédits activé et jetons API selectionnés" style={{ maxHeight: '500px', width: 'auto' }} width="2880" height="2282" data-path="images/CBB/Desktop - Attach Credit - UBB-5.jpg" />
    </Frame>

    C'est le câblage qui fait que les événements `api.tokens_used` entrants déduisent réellement du solde du client.
  </Step>

  <Step title="Configure credit issuance for Starter">
    Toujours sur le produit, faites défiler la section de configuration des crédits qui apparaît une fois un compteur facturé en crédits attaché :

    **Crédits émis par cycle de facturation** : `10000000`

    **Permettre la surconsommation** : **Désactivé** — les clients Starter sont bloqués lorsque les jetons sont épuisés

    **Importer les paramètres de crédit par défaut** : Activé — utilisez l'expiration de 30 jours du droit de crédit

    <Frame caption="Configure credit issuance per cycle on the UBB product.">
      <img src="https://mintcdn.com/dodopayments/ibNfoFRyCIGyt3pO/images/CBB/Desktop%20-%20Attach%20Credit%20-%20UBB-6.jpg?fit=max&auto=format&n=ibNfoFRyCIGyt3pO&q=85&s=22e99c54f11305a24d63c77e09a4650c" alt="Formulaire de configuration de crédits avec montant par cycle et réglages de surconsommation" style={{ maxHeight: '500px', width: 'auto' }} width="2880" height="1920" data-path="images/CBB/Desktop - Attach Credit - UBB-6.jpg" />
    </Frame>

    Cliquez sur **Enregistrer** et copiez l'identifiant du produit.

    <Check>
      Plan Starter : frais de base de 29 \$/mois, 10M de jetons/cycle, bloqué à zéro, déduction automatique via le compteur.
    </Check>
  </Step>
</Steps>

### Plan Pro (99 \$/mois — 40M de jetons, surconsommation activée)

<Steps>
  <Step title="Create the Pro UBB product">
    Même flux que Starter, avec des chiffres plus grands :

    **Nom du produit** : `NeuralAPI Pro`

    **Description** : `40 million API tokens per month with overage. Built for production applications.`

    **Prix fixe** : `99.00`

    **Cycle de facturation** : `Monthly`

    **Devise** : `USD`
  </Step>

  <Step title="Attach the meter">
    Identique à Starter : ajoutez `Token Usage Meter`, activez **Facturer l'utilisation en crédits**, sélectionnez `API Tokens`, **Unités de compteur par crédit** `1`, **Seuil gratuit** `0`.
  </Step>

  <Step title="Configure credit issuance with overage">
    Configurez l'émission de crédits, cette fois en activant la surconsommation :

    **Crédits émis par cycle de facturation** : `40000000`

    **Importer les paramètres de crédit par défaut** : **Désactivé** — nous devons personnaliser les paramètres de surconsommation par produit

    **Permettre la surconsommation** : **Activé**

    **Prix par unité** : `0.000005` USD par jeton (c'est-à-dire, 0,005 $par 1 000 jetons, ou 5$ par 1M de jetons — au-dessus du taux effectif par jeton du plan pour décourager les débordements)

    **Comportement de surconsommation** : `Bill overage at billing` — la surconsommation est facturée sur la prochaine facture, puis le solde est réinitialisé

    Enregistrez le produit et copiez l'identifiant du produit.

    <Check>
      Plan Pro : frais de base de 99 $/mois, 40M de jetons/cycle, surconsommation à 0,005 $/1K jetons, déduction automatique via le compteur.
    </Check>
  </Step>
</Steps>

## Étape 4 : Créez le pack de recharge de jetons

Le pack de recharge est un achat unique qui accorde 5,000,000 de jetons au solde d'un client existant.

<Frame caption="Single Payment pricing selected for a one-time credit product.">
  <img src="https://mintcdn.com/dodopayments/ibNfoFRyCIGyt3pO/images/CBB/Desktop%20-%20Attach%20Credit%20-%20OTP.jpg?fit=max&auto=format&n=ibNfoFRyCIGyt3pO&q=85&s=1743cb3e515952f9d4b1b2782cebac8b" alt="Section de tarification des produits avec Paiement Unique sélectionné" style={{ maxHeight: '500px', width: 'auto' }} width="2880" height="1920" data-path="images/CBB/Desktop - Attach Credit - OTP.jpg" />
</Frame>

<Steps>
  <Step title="Create a one-time product">
    1. Allez à **Produits → Créer un produit**
    2. Sélectionnez **Paiement unique** comme type de tarification
    3. Remplissez :

    **Nom du produit** : `Token Top-Up Pack`

    **Description** : `Instantly add 5 million tokens to your NeuralAPI balance.`

    **Prix** : `19.00`

    **Devise** : `USD`
  </Step>

  <Step title="Attach the token credit">
    1. Dans la section **Droits**, cliquez sur **Attacher** à côté de **Crédits**
    2. Sélectionnez `API Tokens`
    3. Définissez **Crédits émis** : `5000000`
    4. **Désactiver** *Importer les paramètres de crédit par défaut* — nous voulons remplacer l'expiration par défaut de 30 jours
    5. Définissez **Expiration des crédits** : `365 days`
    6. Enregistrez le produit

    Copiez l'identifiant du produit.

    <Tip>
      Pourquoi une expiration plus longue sur les recharges ? Les crédits d'abonnement se réinitialisent tous les 30 jours car c'est le cycle. Les recharges sont des *achats prépayés* — le client a payé 19 \$ à l'avance et s'attend raisonnablement à ce que ces jetons durent plus d'un mois. 365 jours correspond à la manière dont fonctionnent réellement les crédits prépayés chez OpenAI, AWS et Anthropic, tout en limitant votre responsabilité pour que les clients ne puissent pas stocker indéfiniment.
    </Tip>

    <Check>
      Pack de recharge configuré — le fait de l'acheter accorde 5,000,000 de jetons valables pendant 365 jours.
    </Check>
  </Step>
</Steps>

## Étape 5 : Construire le backend

Construisons maintenant le serveur Express qui gère le paiement de l'abonnement, le paiement de la recharge, les complétions réelles d'OpenAI avec facturation par jetons, les requêtes de solde, et les événements webhooks de crédit.

<Steps>
  <Step title="Set up your project">
    ```bash theme={null}
    mkdir neural-api-billing
    cd neural-api-billing
    npm init -y
    npm install dodopayments openai express dotenv
    npm install -D @types/node @types/express typescript tsx
    ```

    Créez un `tsconfig.json` :

    ```json tsconfig.json theme={null}
    {
      "compilerOptions": {
        "target": "ES2022",
        "module": "commonjs",
        "outDir": "./dist",
        "rootDir": "./src",
        "strict": true,
        "esModuleInterop": true,
        "skipLibCheck": true
      }
    }
    ```

    Mettez à jour les scripts `package.json` :

    ```json package.json theme={null}
    {
      "scripts": {
        "dev": "tsx watch src/server.ts",
        "build": "tsc",
        "start": "node dist/server.js"
      }
    }
    ```
  </Step>

  <Step title="Set up environment variables">
    Créez `.env` avec vos identifiants et ID des étapes précédentes :

    ```bash .env theme={null}
    DODO_PAYMENTS_API_KEY=your_dodo_api_key_here
    DODO_PAYMENTS_WEBHOOK_KEY=your_webhook_signing_secret_here
    DODO_ENVIRONMENT=test_mode
    OPENAI_API_KEY=sk-...
    CREDIT_ENTITLEMENT_ID=cent_xxxxxxxxxxxx
    STARTER_PLAN_PRODUCT_ID=pdt_xxxxxxxxxxxx
    PRO_PLAN_PRODUCT_ID=pdt_xxxxxxxxxxxx
    TOPUP_PRODUCT_ID=pdt_xxxxxxxxxxxx
    BASE_URL=http://localhost:3000
    ```

    <Warning>
      Ne jamais commettre `.env` au contrôle de version. Ajoutez-le à `.gitignore` immédiatement.
    </Warning>

    Vous remplirez `DODO_PAYMENTS_WEBHOOK_KEY` à l'étape 7 après l'enregistrement de votre point de terminaison de webhook.
  </Step>

  <Step title="Implement the server">
    Créez `src/server.ts` :

    <CodeGroup>
      ```typescript src/server.ts expandable theme={null}
      import 'dotenv/config';
      import DodoPayments from 'dodopayments';
      import OpenAI from 'openai';
      import express, { Request, Response } from 'express';

      const app = express();

      // IMPORTANT: webhook route needs the raw body for signature verification.
      // We register the raw parser ONLY on /webhooks/dodo, then JSON for everything else.
      app.use('/webhooks/dodo', express.raw({ type: 'application/json' }));
      app.use(express.json());
      app.use(express.static('public'));

      const dodo = new DodoPayments({
        bearerToken: process.env.DODO_PAYMENTS_API_KEY!,
        webhookKey: process.env.DODO_PAYMENTS_WEBHOOK_KEY,
        environment: (process.env.DODO_ENVIRONMENT as 'test_mode' | 'live_mode') ?? 'test_mode',
      });

      const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY! });

      const CREDIT_ENTITLEMENT_ID = process.env.CREDIT_ENTITLEMENT_ID!;
      const BASE_URL = process.env.BASE_URL!;
      const PLAN_PRODUCTS: Record<string, string> = {
        starter: process.env.STARTER_PLAN_PRODUCT_ID!,
        pro: process.env.PRO_PLAN_PRODUCT_ID!,
      };

      // ────────────────────────────────────────────────────────────────────────────
      // Subscription checkout
      // Body: { plan: 'starter' | 'pro', email: string, name: string }
      // ────────────────────────────────────────────────────────────────────────────
      app.post('/checkout/subscribe', async (req: Request, res: Response) => {
        const { plan, email, name } = req.body;
        if (!PLAN_PRODUCTS[plan]) {
          return res.status(400).json({ error: `Unknown plan: ${plan}` });
        }
        try {
          const session = await dodo.checkoutSessions.create({
            product_cart: [{ product_id: PLAN_PRODUCTS[plan], quantity: 1 }],
            customer: { email, name },
            return_url: `${BASE_URL}/?subscribed=1`,
          });
          res.json({ checkout_url: session.checkout_url, session_id: session.session_id });
        } catch (err) {
          console.error('Subscription checkout error:', err);
          res.status(500).json({ error: 'Failed to create subscription checkout' });
        }
      });

      // ────────────────────────────────────────────────────────────────────────────
      // Top-up checkout — buyer must already be a customer
      // Body: { customer_id: string }
      // ────────────────────────────────────────────────────────────────────────────
      app.post('/checkout/topup', async (req: Request, res: Response) => {
        const { customer_id } = req.body;
        if (!customer_id) return res.status(400).json({ error: 'customer_id required' });
        try {
          const session = await dodo.checkoutSessions.create({
            product_cart: [{ product_id: process.env.TOPUP_PRODUCT_ID!, quantity: 1 }],
            customer: { customer_id },
            return_url: `${BASE_URL}/?topup=1`,
          });
          res.json({ checkout_url: session.checkout_url });
        } catch (err) {
          console.error('Top-up checkout error:', err);
          res.status(500).json({ error: 'Failed to create top-up checkout' });
        }
      });

      // ────────────────────────────────────────────────────────────────────────────
      // Live token balance for a customer
      // ────────────────────────────────────────────────────────────────────────────
      app.get('/credits/:customerId', async (req: Request, res: Response) => {
        try {
          const result = await dodo.creditEntitlements.balances.retrieve(req.params.customerId, {
            credit_entitlement_id: CREDIT_ENTITLEMENT_ID,
          });
          res.json({
            balance: result.balance,
            overage: result.overage,
            last_transaction_at: result.last_transaction_at,
          });
        } catch (err) {
          console.error('Balance fetch error:', err);
          res.status(500).json({ error: 'Failed to fetch credit balance' });
        }
      });

      // ────────────────────────────────────────────────────────────────────────────
      // AI completion — calls OpenAI, then ingests a usage event with the real
      // token count. The meter aggregates these and deducts credits automatically.
      // Body: { customer_id: string, prompt: string }
      // ────────────────────────────────────────────────────────────────────────────
      app.post('/api/generate', async (req: Request, res: Response) => {
        const { customer_id, prompt } = req.body;
        if (!customer_id || !prompt) {
          return res.status(400).json({ error: 'customer_id and prompt required' });
        }

        // Best-effort balance gate for Starter (no overage). Note: balance updates
        // are eventually consistent (~1 min lag from event ingestion), so a Starter
        // customer can technically squeeze through a few extra requests right after
        // running out. Use a stricter rate-limiter on top if you need hard cutoffs.
        try {
          const balance = await dodo.creditEntitlements.balances.retrieve(customer_id, {
            credit_entitlement_id: CREDIT_ENTITLEMENT_ID,
          });
          if (Number(balance.balance) <= 0 && Number(balance.overage) <= 0) {
            return res.status(402).json({
              error: 'Out of tokens. Top up or upgrade to continue.',
            });
          }
        } catch {
          // Fall through — if the balance lookup fails, don't block; rely on metering.
        }

        let completion;
        try {
          completion = await openai.chat.completions.create({
            model: 'gpt-5-mini',
            messages: [{ role: 'user', content: prompt }],
          });
        } catch (err) {
          console.error('OpenAI error:', err);
          return res.status(502).json({ error: 'Upstream AI provider failed' });
        }

        const tokensUsed = completion.usage?.total_tokens ?? 0;

        // Fire-and-forget — don't block the response on metering.
        ingestTokenUsage(customer_id, tokensUsed, completion.model).catch((err) =>
          console.error('Usage ingest failed:', err),
        );

        res.json({
          text: completion.choices[0]?.message?.content ?? '',
          tokens_used: tokensUsed,
          model: completion.model,
        });
      });

      async function ingestTokenUsage(customerId: string, tokens: number, model: string) {
        await dodo.usageEvents.ingest({
          events: [
            {
              // event_id is the idempotency key. Use a stable, unique value per request.
              event_id: `req_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`,
              customer_id: customerId,
              event_name: 'api.tokens_used',
              timestamp: new Date().toISOString(),
              metadata: { tokens, model },
            },
          ],
        });
      }

      // ────────────────────────────────────────────────────────────────────────────
      // Webhook handler — verifies signature using the SDK, then routes events.
      // ────────────────────────────────────────────────────────────────────────────
      app.post('/webhooks/dodo', async (req: Request, res: Response) => {
        const rawBody = (req.body as Buffer).toString('utf8');
        const headers = {
          'webhook-id': req.header('webhook-id') ?? '',
          'webhook-signature': req.header('webhook-signature') ?? '',
          'webhook-timestamp': req.header('webhook-timestamp') ?? '',
        };

        let event: { type: string; data: any };
        try {
          event = dodo.webhooks.unwrap(rawBody, { headers }) as any;
        } catch (err) {
          console.error('Webhook verification failed:', err);
          return res.status(401).json({ error: 'Invalid signature' });
        }

        switch (event.type) {
          case 'credit.added':
            console.log(`[credit.added] customer=${event.data.customer_id} amount=${event.data.amount}`);
            break;
          case 'credit.deducted':
            console.log(`[credit.deducted] customer=${event.data.customer_id} amount=${event.data.amount}`);
            break;
          case 'credit.overage_charged':
            console.log(`[credit.overage_charged] customer=${event.data.customer_id}`);
            break;
          default:
            // Ignore other event types
            break;
        }

        res.json({ received: true });
      });

      app.listen(3000, () => {
        console.log('NeuralAPI billing server running on http://localhost:3000');
      });
      ```

      ```json package.json theme={null}
      {
        "name": "neural-api-billing",
        "version": "1.0.0",
        "scripts": {
          "dev": "tsx watch src/server.ts",
          "build": "tsc",
          "start": "node dist/server.js"
        },
        "dependencies": {
          "dodopayments": "latest",
          "openai": "^4.0.0",
          "express": "^4.18.0",
          "dotenv": "^16.0.0"
        },
        "devDependencies": {
          "@types/node": "^20.0.0",
          "@types/express": "^4.17.0",
          "typescript": "^5.0.0",
          "tsx": "^4.0.0"
        }
      }
      ```
    </CodeGroup>

    <Check>
      Backend terminé: paiement de l'abonnement, paiement de la recharge, complétion OpenAI avec facturation par jetons, requête de solde, et un gestionnaire de webhooks vérifié.
    </Check>

    <Tip>
      [`@dodopayments/ingestion-blueprints`](/features/usage-based-billing/ingestion-blueprints) fournit des trackers prêts à l'emploi qui automatisent l'appel `usageEvents.ingest` pour vous — y compris le [LLM Blueprint](/developer-resources/ingestion-blueprints/llm), [API gateway](/developer-resources/ingestion-blueprints/api-gateway), [object storage](/developer-resources/ingestion-blueprints/object-storage), [streams](/developer-resources/ingestion-blueprints/stream), et [time-range](/developer-resources/ingestion-blueprints/time-range) usage.
    </Tip>
  </Step>

  <Step title="A note on how deductions actually happen">
    Vous avez peut-être remarqué qu'il n'y a pas d'appel explicite "deduct N credits". C'est par design :

    1. Votre gestionnaire appelle OpenAI et reçoit `usage.total_tokens` (par exemple, 1532).
    2. Vous ingérez un seul événement d'utilisation : `event_name: api.tokens_used`, `metadata: { tokens: 1532 }`.
    3. Le `Token Usage Meter` agrège des événements par client.
    4. Parce que le compteur est lié au crédit `API Tokens` avec **Facturer l'utilisation en crédits**, Dodo Payments déduit 1532 crédits de la subvention non expirée la plus ancienne du client (FIFO).
    5. Si la surconsommation est activée et que le client passe en dessous de zéro, le déficit est suivi et facturé sur la prochaine facture.

    Le compteur gère tout cela. Votre code ne fait qu'ingérer des événements.
  </Step>
</Steps>

## Étape 6 : Ajoutez une interface de démonstration

Créez `public/index.html` pour tester tous les flux dans votre navigateur. Nous conservons l'ID client dans `localStorage` afin que l'abonnement → génération → recharge partagent tous la même identité, imitant une application connectée :

<CodeGroup>
  ```html public/index.html expandable theme={null}
  <!DOCTYPE html>
  <html>
  <head>
    <title>NeuralAPI Demo</title>
    <style>
      body { font-family: system-ui, sans-serif; max-width: 760px; margin: 40px auto; padding: 20px; color: #1a1a2e; }
      h1 { font-size: 24px; }
      h2 { margin-top: 36px; border-bottom: 1px solid #eee; padding-bottom: 8px; font-size: 18px; }
      .panel { padding: 16px; background: #fafafe; border: 1px solid #e6e6f0; border-radius: 8px; margin: 12px 0; }
      .form-group { margin: 12px 0; }
      label { display: block; margin-bottom: 4px; font-weight: 600; font-size: 13px; }
      input, select, textarea { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 6px; box-sizing: border-box; font-family: inherit; font-size: 14px; }
      textarea { min-height: 80px; resize: vertical; }
      button { background: #6366f1; color: white; padding: 10px 18px; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 600; }
      button:hover { background: #4f46e5; }
      button:disabled { background: #c7c7d4; cursor: not-allowed; }
      .balance { font-size: 32px; font-weight: 700; color: #6366f1; }
      .muted { color: #777; font-size: 13px; margin-top: 4px; }
      .result { margin-top: 12px; padding: 12px; background: #fff; border: 1px solid #e6e6f0; border-radius: 6px; font-size: 14px; white-space: pre-wrap; }
      .row { display: flex; gap: 12px; align-items: center; }
      .row > * { flex: 1; }
    </style>
  </head>
  <body>
    <h1>NeuralAPI Demo</h1>

    <div class="panel">
      <label>Logged-in customer ID (paste once after subscribing)</label>
      <div class="row">
        <input id="customerId" placeholder="cus_xxxxxxxxxxxx" />
        <button onclick="saveCustomerId()" style="flex:0">Save</button>
      </div>
      <div class="muted">After completing checkout, copy the customer ID from your Dodo Payments dashboard (Customers → most recent) and paste here.</div>
    </div>

    <h2>1. Subscribe to a Plan</h2>
    <div class="form-group"><label>Plan</label>
      <select id="plan">
        <option value="starter">Starter — $29/mo, 10M tokens</option>
        <option value="pro">Pro — $99/mo, 40M tokens + overage</option>
      </select>
    </div>
    <div class="form-group"><label>Email</label><input type="email" id="email" placeholder="you@example.com" /></div>
    <div class="form-group"><label>Name</label><input id="name" placeholder="Your name" /></div>
    <button onclick="subscribe(event)">Get Checkout Link</button>
    <div id="subscribeResult" class="result" style="display:none"></div>

    <h2>2. Generate AI Response (deducts tokens)</h2>
    <div class="form-group"><label>Prompt</label><textarea id="prompt" placeholder="Explain quantum computing in one sentence"></textarea></div>
    <button onclick="generate(event)">Generate</button>
    <div id="generateResult" class="result" style="display:none"></div>

    <h2>3. Live Token Balance</h2>
    <button onclick="checkBalance(event)">Refresh Balance</button>
    <div id="balanceResult" class="result" style="display:none"></div>

    <h2>4. Buy a Top-Up Pack</h2>
    <button onclick="topup(event)">Buy 5M Tokens — $19</button>
    <div id="topupResult" class="result" style="display:none"></div>

    <script>
      const $ = (id) => document.getElementById(id);
      document.addEventListener('DOMContentLoaded', () => {
        $('customerId').value = localStorage.getItem('customerId') || '';
      });

      function getCustomerId() {
        const id = $('customerId').value.trim();
        if (!id) { alert('Save a customer ID first'); throw new Error('no customer'); }
        return id;
      }

      function saveCustomerId() {
        localStorage.setItem('customerId', $('customerId').value.trim());
        alert('Saved');
      }

      async function withLoading(btn, loadingLabel, fn) {
        const original = btn.textContent;
        btn.disabled = true;
        btn.textContent = loadingLabel;
        try { await fn(); } finally {
          btn.disabled = false;
          btn.textContent = original;
        }
      }

      async function subscribe(ev) {
        await withLoading(ev.target, 'Loading…', async () => {
          const res = await fetch('/checkout/subscribe', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ plan: $('plan').value, email: $('email').value, name: $('name').value }),
          });
          const data = await res.json();
          const el = $('subscribeResult');
          el.style.display = 'block';
          el.innerHTML = res.ok
            ? `<a href="${data.checkout_url}" target="_blank">Open Checkout →</a>`
            : `Error: ${data.error}`;
        });
      }

      async function generate(ev) {
        const customer_id = getCustomerId();
        await withLoading(ev.target, 'Generating…', async () => {
          const res = await fetch('/api/generate', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ customer_id, prompt: $('prompt').value }),
          });
          const data = await res.json();
          const el = $('generateResult');
          el.style.display = 'block';
          el.innerHTML = res.ok
            ? `<strong>Response:</strong>\n${data.text}\n\n<em>Tokens used: ${data.tokens_used} (${data.model})</em>`
            : `Error: ${data.error}`;
          if (res.ok) refreshBalanceSilently();
        });
      }

      async function checkBalance(ev) {
        const customer_id = getCustomerId();
        await withLoading(ev.target, 'Refreshing…', async () => {
          const res = await fetch('/credits/' + customer_id);
          const data = await res.json();
          const el = $('balanceResult');
          el.style.display = 'block';
          el.innerHTML = res.ok
            ? `<div class="balance">${Number(data.balance).toLocaleString()} tokens</div>
               <div class="muted">Overage used: ${Number(data.overage).toLocaleString()} · Last activity: ${data.last_transaction_at ?? 'never'}</div>`
            : `Error: ${data.error}`;
        });
      }

      async function refreshBalanceSilently() {
        const customer_id = $('customerId').value.trim();
        if (!customer_id) return;
        const res = await fetch('/credits/' + customer_id);
        const data = await res.json();
        const el = $('balanceResult');
        el.style.display = 'block';
        el.innerHTML = res.ok
          ? `<div class="balance">${Number(data.balance).toLocaleString()} tokens</div>
             <div class="muted">Overage used: ${Number(data.overage).toLocaleString()} · Last activity: ${data.last_transaction_at ?? 'never'}</div>`
          : `Error: ${data.error}`;
      }

      async function topup(ev) {
        const customer_id = getCustomerId();
        await withLoading(ev.target, 'Loading…', async () => {
          const res = await fetch('/checkout/topup', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ customer_id }),
          });
          const data = await res.json();
          const el = $('topupResult');
          el.style.display = 'block';
          el.innerHTML = res.ok
            ? `<a href="${data.checkout_url}" target="_blank">Open Top-Up Checkout →</a>`
            : `Error: ${data.error}`;
        });
      }
    </script>
  </body>
  </html>
  ```
</CodeGroup>

## Étape 7 : Connectez le Webhook

Les webhooks permettent à votre serveur de réagir aux changements de solde — vous les utiliserez pour envoyer des emails "en cours d'épuisement" avant que les clients atteignent zéro.

<Steps>
  <Step title="Expose your local server">
    Les webhooks nécessitent une URL publique. Pour le développement local, utilisez [ngrok](https://ngrok.com) ou tout autre tunnel :

    ```bash theme={null}
    ngrok http 3000
    ```

    Copiez l'URL `https://...ngrok-free.app`.
  </Step>

  <Step title="Register the webhook in Dodo Payments">
    1. Dans le tableau de bord, allez à **Développeurs → Webhooks → Ajouter un point de terminaison**
    2. **URL** : `https://your-tunnel.ngrok-free.app/webhooks/dodo`
    3. Abonnez-vous à (au minimum) :
       * `credit.added`
       * `credit.deducted`
       * `credit.overage_charged`
    4. Enregistrez et copiez le **Secret de signature**
    5. Collez-le dans `.env` comme `DODO_PAYMENTS_WEBHOOK_KEY`, puis redémarrez `npm run dev`

    <Tip>
      Le `dodo.webhooks.unwrap()` du SDK valide les en-têtes `webhook-id`, `webhook-timestamp`, et `webhook-signature` à l'aide de votre secret de signature. Vous n'avez pas besoin de vérifier les HMAC vous-même — et vous ne devriez pas, car Dodo Payments utilise [Webhooks Standards](https://www.standardwebhooks.com/), qui signe `id.timestamp.body` plutôt que juste le corps.
    </Tip>
  </Step>
</Steps>

## Étape 8 : Testez le flux complet

<Steps>
  <Step title="Subscribe a test customer">
    1. Exécutez `npm run dev`
    2. Ouvrez `http://localhost:3000`
    3. Choisissez **Plan Pro**, entrez un email de test + nom, cliquez sur **Obtenir un lien de paiement**, complétez le paiement avec [détails carte test](/miscellaneous/testing-process)
    4. Dans le tableau de bord, allez à **Clients → plus récent** et copiez l'ID `cus_...`
    5. Collez-le dans le champ "ID client connecté" sur la démo et cliquez sur **Enregistrer**

    <Check>
      Le client devrait avoir 40,000,000 de jetons. Cliquez sur **Actualiser le solde** pour confirmer.
    </Check>
  </Step>

  <Step title="Generate a real AI response">
    Tapez une invite et cliquez sur **Générer**. Le serveur appelle OpenAI, obtient `total_tokens`, ingère un événement d'utilisation, et retourne la réponse.

    <Info>
      Les événements d'utilisation sont traités par un travailleur en arrière-plan toutes les \~minutes. Le solde ne diminuera pas instantanément — attendez 30 à 90 secondes et cliquez sur **Actualiser le solde** à nouveau. Ne concluez pas que c'est cassé si la première actualisation ne montre aucun mouvement.
    </Info>
  </Step>

  <Step title="Test the top-up flow">
    Cliquez sur **Acheter 5M de jetons — 19 \$** et complétez le paiement. Après le paiement réussi, actualisez le solde — il devrait augmenter de 5,000,000 de jetons. Votre journal serveur devrait montrer un événement `credit.added`.
  </Step>
</Steps>

## Dépannage

<AccordionGroup>
  <Accordion title="Credits not deducting after usage events">
    **Causes possibles:**

    * Le nom de l'événement du compteur ne correspond pas au `event_name` que vous envoyez (`api.tokens_used` est sensible à la casse)
    * Le compteur n'est pas lié au crédit `API Tokens` sur le produit — allez à la configuration du compteur du produit et confirmez que **Facturer l'utilisation en crédits** est activé
    * La clé `metadata.tokens` ne correspond pas au champ "Sur propriété" du compteur
    * La subvention du client a expiré (vérifiez l'historique des crédits du client)

    **Que vérifier :**

    1. **Produits → Compteurs** : ouvrez le compteur et confirmez qu'il montre le nom du crédit lié sur l'attachement du produit
    2. L'onglet **Événements** sur le compteur — les événements ingérés devraient y apparaître même avant la déduction
    3. **Clients → \[Client] → Crédits** : les entrées du registre devraient apparaître dans une minute ou deux
  </Accordion>

  <Accordion title="Balance always shows 0 or 'customer not found'">
    **Causes possibles:**

    * Le client n'a pas encore terminé le paiement — les crédits ne sont alloués qu'après un paiement réussi
    * Vous interrogez avec le mauvais `customer_id` (utilisez l'ID `cus_...` du tableau de bord, pas votre propre ID DB)
    * Le `CREDIT_ENTITLEMENT_ID` dans `.env` ne correspond pas au crédit attaché au produit

    **Que vérifier :**
    Ouvrez **Clients → \[Client] → Crédits**. Si aucun crédit n'apparaît là-bas, le droit de produit n'était pas attaché ou le paiement n'a pas été complété.
  </Accordion>

  <Accordion title="Overage not working for Pro plan customers">
    **Causes possibles:**

    * La surconsommation n'a pas été activée sur **l'attachement de crédit du produit Pro** (le paramètre de niveau de crédit est juste une valeur par défaut)
    * Le client est en fait sur Starter, pas Pro
    * La limite de surconsommation était définie à 0

    **Que vérifier :**
    Modifier Pro → Droits → Crédits → confirmer que **Permettre la surconsommation** est activé et **Prix par unité** est `0.000005` (= 5 \$ par million de jetons; vérifiez bien les zéros devant — le champ prend le prix par jeton, pas par 1K).
  </Accordion>

  <Accordion title="`Webhook verification failed` in logs">
    **Causes possibles:**

    * Ordre d'analyse du corps: `express.json()` a été appliqué à `/webhooks/dodo` avant `express.raw()` — le SDK a besoin des **octets bruts** de la requête, pas JSON analysé
    * Mauvais secret de signature dans `DODO_PAYMENTS_WEBHOOK_KEY`
    * Le proxy inverse réécrit les en-têtes

    **Que vérifier :**
    Confirmez que la ligne `app.use('/webhooks/dodo', express.raw(...))` vient *avant* `app.use(express.json())` dans `server.ts`.
  </Accordion>
</AccordionGroup>

## Besoin d'aide?

* [Communauté Discord](https://discord.gg/bYqAp4ayYh)
* [support@dodopayments.com](mailto:support@dodopayments.com)

## Félicitations ! Vous avez construit une facturation basée sur des crédits pour NeuralAPI

Votre plateforme dispose désormais d'un système de facturation de crédits complet et prêt pour la production :

<CardGroup cols={2}>
  <Card title="Token Credit Entitlement" icon="coins">
    Un crédit réutilisable `API Tokens` avec une expiration de 30 jours, partagé sur tous les plans et le pack de recharge
  </Card>

  <Card title="Tiered Plans, One Credit" icon="layer-group">
    Starter (10M, limite stricte) et Pro (40M + surconsommation) configurés par produit sans dupliquer le crédit
  </Card>

  <Card title="One-Time Top-Up Pack" icon="circle-plus">
    Les clients ajoutent 5M de jetons pour 19 \$ sans changer leur abonnement
  </Card>

  <Card title="Auto-Deduction via Meter" icon="bolt">
    Les compteurs de jetons OpenAI réels ingérés en tant qu'événements ; le compteur déduit les crédits FIFO sans suivi manuel
  </Card>

  <Card title="Live Balance API" icon="gauge">
    Solde en temps réel via le SDK pour limiter l'accès, afficher l'utilisation ou avertir les clients dans l'application
  </Card>

  <Card title="Verified Webhook Pipeline" icon="bell">
    Événements de registre des crédits (`credit.added`, `credit.deducted`, `credit.overage_charged`) routés via un gestionnaire vérifié par signature utilisant l'assistant de webhooks standards du SDK
  </Card>
</CardGroup>

<Info>
  **Vous allez en production?** Resserrez ceci :

  * **Auth sur `/credits/:customerId` et `/api/generate`** — actuellement, n'importe qui peut les atteindre avec n'importe quel ID client. Authentifiez les utilisateurs et recherchez *leur* ID client côté serveur.
  * **`event_id` stables** — l'exemple utilise `Date.now() + random`. En production, utilisez votre ID de requête pour que les réessais soient idempotents (Dodo Payments élimine les doublons par `event_id`).
  * **Persister le mapping client↔utilisateur** — enregistrez `customer_id` dans votre DB après le premier paiement pour éviter un copier-coller manuel.
  * **Décider de ce qui se passe lorsque l'abonnement se termine.** Les crédits de plan restent dans le registre du client jusqu'à leur expiration naturelle (30 jours après l'émission) et les crédits de recharge restent valables pendant 365 jours — mais la cuisine de `/api/generate` vérifie uniquement *le solde*, pas l'état de l'abonnement. Ainsi, un client annulé peut toujours consommer ses jetons restants. C'est le choix axé sur le client. Si vous souhaitez un contrôle d'accès plus strict, soit (a) écoutez le webhook `subscription.cancelled` et limitez `/api/generate` selon l'état de l'abonnement, soit (b) appelez l'API du registre de Dodo pour débiter les crédits de plan inutilisés lors de l'annulation tout en laissant les crédits de recharge intacts.
  * **Surveillez le tableau de bord de la facturation à l'utilisation** pour détecter les anomalies de mesure tôt.
</Info>

<CardGroup cols={2}>
  <Card title="Credit-Based Billing Reference" icon="book" href="/features/credit-based-billing">
    Documentation complète de la CBB : reports, modes de surconsommation, gestion du registre, tous les points de terminaison de l'API.
  </Card>

  <Card title="Credit Webhook Events" icon="bell" href="/developer-resources/webhooks/intents/credit">
    Schémas de charge pour chaque événement de crédit que votre serveur pourrait recevoir.
  </Card>
</CardGroup>
