Passer au contenu principal

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.

Laissez Sentra écrire votre code d’intégration pour vous.
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.
Essayez Sentra : Intégration pilotée par IA →
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.
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.
À 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 :
ProduitPrixJetonsSurconsommation
Plan Starter29 $/mois10,000,000 jetons/cycleBloqué à zéro
Plan Pro99 $/mois40,000,000 jetons/cycle0,005 $ par 1 000 jetons
Pack de recharge de jetons19 $ une fois+5,000,000 jetons
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

É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.
Page de liste des crédits montrant les droits de crédits créés
1

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
2

Configure the credit unit

Remplissez les détails de base pour votre crédit de jetons :Nom du crédit : API TokensType de crédit : Sélectionnez Unité personnaliséeNom de l’unité : tokenPré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)
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.
3

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

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

É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.
1

Open the Meters section

  1. Dans la barre latérale du tableau de bord, allez à Produits → Compteurs
  2. Cliquez sur Créer un compteur
2

Configure the meter

Remplissez :Nom du compteur : Token Usage MeterNom 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énementSur propriété : tokens — la clé de métadonnées sur chaque événement dont la valeur sera additionnéeUnité de mesure : tokens
Les noms d’événements sont sensibles à la casse. api.tokens_usedApi.Tokens.Used — choisissez-en un et tenez-vous y.
Enregistrez le compteur et copiez son identifiant — vous le référencerez lors de l’attachement aux produits.
Le compteur est créé. Maintenant, nous pouvons le connecter au crédit lorsque nous configurons les produits.

É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.
Configuration de tarification basée sur l'utilisation

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

1

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 StarterDescription : 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 : MonthlyDevise : USD
2

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
Compteur avec Facturer l'utilisation en crédits activé et jetons API selectionnés
C’est le câblage qui fait que les événements api.tokens_used entrants déduisent réellement du solde du client.
3

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 : 10000000Permettre la surconsommation : Désactivé — les clients Starter sont bloqués lorsque les jetons sont épuisésImporter les paramètres de crédit par défaut : Activé — utilisez l’expiration de 30 jours du droit de crédit
Formulaire de configuration de crédits avec montant par cycle et réglages de surconsommation
Cliquez sur Enregistrer et copiez l’identifiant du produit.
Plan Starter : frais de base de 29 $/mois, 10M de jetons/cycle, bloqué à zéro, déduction automatique via le compteur.

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

1

Create the Pro UBB product

Même flux que Starter, avec des chiffres plus grands :Nom du produit : NeuralAPI ProDescription : 40 million API tokens per month with overage. Built for production applications.Prix fixe : 99.00Cycle de facturation : MonthlyDevise : USD
2

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

Configure credit issuance with overage

Configurez l’émission de crédits, cette fois en activant la surconsommation :Crédits émis par cycle de facturation : 40000000Importer les paramètres de crédit par défaut : Désactivé — nous devons personnaliser les paramètres de surconsommation par produitPermettre la surconsommation : ActivéPrix par unité : 0.000005 USD par jeton (c’est-à-dire, 0,005 par1000jetons,ou5par 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.
Plan Pro : frais de base de 99 /mois,40Mdejetons/cycle,surconsommationaˋ0,005/mois, 40M de jetons/cycle, surconsommation à 0,005 /1K jetons, déduction automatique via le compteur.

É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.
Section de tarification des produits avec Paiement Unique sélectionné
1

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 PackDescription : Instantly add 5 million tokens to your NeuralAPI balance.Prix : 19.00Devise : USD
2

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.
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.
Pack de recharge configuré — le fait de l’acheter accorde 5,000,000 de jetons valables pendant 365 jours.

É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.
1

Set up your project

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 :
tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "commonjs",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  }
}
Mettez à jour les scripts package.json :
package.json
{
  "scripts": {
    "dev": "tsx watch src/server.ts",
    "build": "tsc",
    "start": "node dist/server.js"
  }
}
2

Set up environment variables

Créez .env avec vos identifiants et ID des étapes précédentes :
.env
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
Ne jamais commettre .env au contrôle de version. Ajoutez-le à .gitignore immédiatement.
Vous remplirez DODO_PAYMENTS_WEBHOOK_KEY à l’étape 7 après l’enregistrement de votre point de terminaison de webhook.
3

Implement the server

Créez src/server.ts :
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');
});
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é.
@dodopayments/ingestion-blueprints fournit des trackers prêts à l’emploi qui automatisent l’appel usageEvents.ingest pour vous — y compris le LLM Blueprint, API gateway, object storage, streams, et time-range usage.
4

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.

É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 :
<!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>

É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.
1

Expose your local server

Les webhooks nécessitent une URL publique. Pour le développement local, utilisez ngrok ou tout autre tunnel :
ngrok http 3000
Copiez l’URL https://...ngrok-free.app.
2

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
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, qui signe id.timestamp.body plutôt que juste le corps.

Étape 8 : Testez le flux complet

1

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
  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
Le client devrait avoir 40,000,000 de jetons. Cliquez sur Actualiser le solde pour confirmer.
2

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

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.

Dépannage

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

Besoin d’aide?

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 :

Token Credit Entitlement

Un crédit réutilisable API Tokens avec une expiration de 30 jours, partagé sur tous les plans et le pack de recharge

Tiered Plans, One Credit

Starter (10M, limite stricte) et Pro (40M + surconsommation) configurés par produit sans dupliquer le crédit

One-Time Top-Up Pack

Les clients ajoutent 5M de jetons pour 19 $ sans changer leur abonnement

Auto-Deduction via Meter

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

Live Balance API

Solde en temps réel via le SDK pour limiter l’accès, afficher l’utilisation ou avertir les clients dans l’application

Verified Webhook Pipeline

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

Credit-Based Billing Reference

Documentation complète de la CBB : reports, modes de surconsommation, gestion du registre, tous les points de terminaison de l’API.

Credit Webhook Events

Schémas de charge pour chaque événement de crédit que votre serveur pourrait recevoir.
Last modified on May 14, 2026