Vai al contenuto principale

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.

Lascia che Sentra scriva il tuo codice di integrazione per te.
Usa il nostro assistente AI in VS Code, Cursor o Windsurf per generare codice SDK/API, gestori di webhook, concessioni di crediti e altro — semplicemente descrivendo ciò che desideri.
Prova Sentra: Integrazione potenziata da AI →
In questo tutorial, creerai NeuralAPI — una piattaforma AI a livelli in cui ogni piano di abbonamento include un credito mensile di token, i clienti possono acquistare pacchetti di ricarica quando sono a corto di crediti, e il tuo backend detrae automaticamente i crediti mentre le richieste vengono elaborate da OpenAI.
Questo tutorial utilizza Node.js/Express + l’SDK OpenAI. I concetti di Dodo Payments (crediti, misuratori, webhook) si applicano a qualsiasi framework o fornitore AI — adatta liberamente.
Alla fine di questo tutorial, saprai come:
  • Creare un diritto di credito personalizzato (token) e un misuratore che detrae automaticamente da esso
  • Collegare i crediti ai piani di abbonamento (con e senza sforamento) e a un prodotto di ricarica una tantum
  • Collegare un endpoint di completamento OpenAI reale che fattura i token tramite Dodo Payments
  • Interrogare il saldo del credito live di un cliente tramite l’SDK
  • Verificare le firme del webhook e indirizzare gli eventi di credito di Dodo Payments

Cosa Stiamo Costruendo

Ecco il modello di pricing per NeuralAPI:
ProdottoPrezzoTokenSforamento
Piano Iniziale$29/mese10,000,000 token/cicloBloccato a zero
Piano Pro$99/mese40,000,000 token/ciclo$0.005 per 1K token
Pacchetto di Ricarica Token$19 una tantum+5,000,000 token
Prima di iniziare, assicurati di avere:
  • Un account Dodo Payments (va bene la modalità di test)
  • Una chiave API OpenAI
  • Node.js 18+
  • Conoscenza di base di TypeScript/Node.js

Passaggio 1: Crea il Tuo Diritto di Credito Token

Per prima cosa, crea il diritto di credito che condivideranno entrambi i piani di abbonamento e il pacchetto di ricarica. Pensa a questo come alla definizione dell’unità “token” che utilizza la tua piattaforma.
Pagina di elenco dei crediti che mostra i diritti di credito creati
1

Navigate to Credits

  1. Accedi alla tua dashboard di Dodo Payments
  2. Clicca su Prodotti nella barra laterale sinistra
  3. Seleziona la scheda Crediti
  4. Clicca su Crea Credito
2

Configure the credit unit

Compila i dettagli di base per il tuo credito token:Nome Credito: API TokensTipo di Credito: Seleziona Unità PersonalizzataNome Unità: tokenPrecisione: 0 (i token sono sempre numeri interi)Scadenza Credito: 30 days (i crediti si azzerano a ogni ciclo di fatturazione)
La precisione non può essere modificata dopo la creazione di un credito. Per i conteggi dei token, 0 (numeri interi) è quasi sempre corretto.
3

Skip overage at the credit level

Lascia disabilitato lo sforamento qui — lo configurerai per piano quando aggancerai il credito ai prodotti. Ciò consente al piano Iniziale di bloccare l’uso a zero mentre il piano Pro consente lo sforamento.
Le impostazioni di sforamento configurate qui sono predefinite. Ogni collegamento del prodotto può sovrascriverle — ed è esattamente ciò che faremo nel Passaggio 3.
4

Save and copy the credit ID

Clicca su Crea Credito. Una volta salvato, apri il credito e copia il suo ID — sembra cent_xxxxxxxxxxxx.
Il tuo diritto di credito API Tokens è pronto. Successivamente, crea un misuratore affinché gli eventi di utilizzo possano guidare le detrazioni automaticamente.

Passaggio 2: Crea un Misuratore per l’Uso dei Token

Un misuratore aggrega gli eventi di utilizzo in arrivo e li converte in detrazioni di credito. Ti serve questo prima di creare i prodotti del piano, poiché lo aggancerai durante la creazione del prodotto nel Passaggio 3.
1

Open the Meters section

  1. Nella barra laterale della dashboard, vai su Prodotti → Misuratori
  2. Clicca su Crea Misuratore
2

Configure the meter

Compila:Nome Misuratore: Token Usage MeterNome Evento: api.tokens_used (questo deve corrispondere esattamente a ciò che invia la tua app)Tipo di Aggregazione: Sum — sommiamo il conteggio dei token da ogni eventoSopra la Proprietà: tokens — la chiave dei metadati su ciascun evento il cui valore verrà sommatoUnità di Misura: tokens
I nomi degli eventi sono case-sensitive. api.tokens_usedApi.Tokens.Used — scegline uno e attieniti a esso.
Salva il misuratore e copia il suo ID — lo citerai quando lo agganci ai prodotti.
Il misuratore è creato. Ora possiamo collegarlo al credito quando configuriamo i prodotti.

Passaggio 3: Crea i Prodotti del Piano

Entrambi i piani devono essere prodotti Usage Based Billing, non semplici Abbonamenti — i misuratori possono essere agganciati solo ai prodotti UBB, e hai bisogno del misuratore per detrarre automaticamente i crediti quando i clienti chiamano la tua API. I prodotti UBB supportano ancora una tariffa base ricorrente (la $29 / $99); l’uso al di sopra di tale soglia viene fatturato in crediti.
Configurazione della tariffazione Basata sull'Uso

Piano Iniziale ($29/mese — 10M token, nessuno sforamento)

1

Create the Starter UBB product

  1. Vai su Prodotti → Crea Prodotto
  2. Seleziona Usage Based Billing come tipo di tariffazione
  3. Compila:
Nome Prodotto: NeuralAPI StarterDescrizione: 10 million API tokens per month. Perfect for individual developers and small projects.Prezzo Fisso: 29.00 (la tariffa base ricorrente — fatturata mensilmente anche prima di qualsiasi utilizzo)Ciclo di Fatturazione: MonthlyValuta: USD
2

Attach the meter

Nella sezione Seleziona Misuratore, clicca su + e aggiungi Token Usage Meter. Poi sul misuratore:
  1. Attiva Fattura utilizzo in Crediti
  2. Diritto di Credito: seleziona API Tokens
  3. Unità di Misura per Credito: 1 — ogni token nell’evento mappa a 1 credito detratto
  4. Soglia Gratuita: 0 — la stessa allocazione del credito è il “livello gratuito” del cliente; non abbiamo bisogno di una banda gratuita extra
Misuratore con usare il fatturato nei crediti abilitati e token API selezionati
Questo è il collegamento che fa sì che gli eventi api.tokens_used in arrivo detraggano effettivamente dal saldo del cliente.
3

Configure credit issuance for Starter

Ancora sul prodotto, scorri alla sezione di configurazione del credito che appare una volta agganciato un misuratore fatturato in crediti:Crediti emessi per ciclo di fatturazione: 10000000Consentire Sforamento: Disabilitato — i clienti Iniziali sono bloccati quando i token si esaurisconoImporta Impostazioni di Credito Predefinite: Abilitato — usa la scadenza di 30 giorni dal diritto di credito
Modulo di configurazione dei crediti con importo per ciclo e impostazioni di sforamento
Clicca Salva e copia l’ID del prodotto.
Piano Iniziale: tariffa base di $29/mese, 10M token/ciclo, bloccato a zero, detrazioni automatiche tramite misuratore.

Piano Pro ($99/mese — 40M token, sforamento abilitato)

1

Create the Pro UBB product

Stesso flusso del Piano Iniziale, con numeri maggiori:Nome Prodotto: NeuralAPI ProDescrizione: 40 million API tokens per month with overage. Built for production applications.Prezzo Fisso: 99.00Ciclo di Fatturazione: MonthlyValuta: USD
2

Attach the meter

Identico al Piano Iniziale: aggiungi Token Usage Meter, attiva Fattura utilizzo in Crediti, seleziona API Tokens, Unità di Misura per Credito 1, Soglia Gratuita 0.
3

Configure credit issuance with overage

Configura l’emissione del credito, questa volta abilitando lo sforamento:Crediti emessi per ciclo di fatturazione: 40000000Importa Impostazioni di Credito Predefinite: Disabilitato — dobbiamo personalizzare le impostazioni di sforamento per prodottoConsenti Sforamento: AbilitatoPrezzo per Unità: 0.000005 USD per token (cioè, 0.005per1Ktoken,o0.005 per 1K token, o 5 per 1M token — sopra la tariffa effettiva per token del piano per scoraggiare gli spurghi)Comportamento dello Sforamento: Bill overage at billing — lo sforamento viene addebitato sulla fattura successiva, quindi il saldo si azzeraSalva il prodotto e copia l’ID del prodotto.
Piano Pro: tariffa base di 99/mese,40Mtoken/ciclo,sforamentoa99/mese, 40M token/ciclo, sforamento a 0.005/1K token, detrazioni automatiche tramite misuratore.

Passaggio 4: Crea il Pacchetto di Ricarica Token

Il pacchetto di ricarica è un acquisto una tantum che aggiunge 5,000,000 token al saldo di un cliente esistente.
Sezione dei prezzi del prodotto con Pagamento Singolo selezionata
1

Create a one-time product

  1. Vai su Prodotti → Crea Prodotto
  2. Seleziona Pagamento Singolo come tipo di tariffazione
  3. Compila:
Nome Prodotto: Token Top-Up PackDescrizione: Instantly add 5 million tokens to your NeuralAPI balance.Prezzo: 19.00Valuta: USD
2

Attach the token credit

  1. Nella sezione Diritti, clicca su Collega accanto a Crediti
  2. Seleziona API Tokens
  3. Imposta Crediti emessi: 5000000
  4. Disabilita Importa Impostazioni di Credito Predefinite — vogliamo sovrascrivere la scadenza predefinita di 30 giorni
  5. Imposta Scadenza Credito: 365 days
  6. Salva il prodotto
Copia l’ID del prodotto.
Perché una scadenza più lunga sulle ricariche? I crediti di abbonamento si azzerano ogni 30 giorni perché è il ciclo. Le ricariche sono acquisti prepagati — il cliente ha pagato $19 in anticipo e si aspetta ragionevolmente che quei token durino più di un mese. 365 giorni corrispondono a come funzionano davvero i crediti prepagati su OpenAI, AWS e Anthropic, pur limitando la tua responsabilità in modo che i clienti non possano accumulare indefinitamente.
Pacchetto di Ricarica configurato — l’acquisto concede 5,000,000 token che rimangono validi per 365 giorni.

Passaggio 5: Costruisci il Backend

Ora costruiamo il server Express che gestisce il checkout dell’abbonamento, il checkout della ricarica, i completamenti OpenAI reali con fatturazione dei token, le query del saldo e gli eventi di webhook del credito.
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
Crea un tsconfig.json:
tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "commonjs",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  }
}
Aggiorna package.json scripts:
package.json
{
  "scripts": {
    "dev": "tsx watch src/server.ts",
    "build": "tsc",
    "start": "node dist/server.js"
  }
}
2

Set up environment variables

Crea .env con le tue credenziali e gli ID dai passaggi precedenti:
.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
Non commettere mai .env nel controllo della versione. Aggiungilo immediatamente a .gitignore.
Compilerai DODO_PAYMENTS_WEBHOOK_KEY nel Passaggio 7 dopo aver registrato il tuo endpoint di webhook.
3

Implement the server

Crea 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 completato: checkout dell’abbonamento, checkout della ricarica, completamento OpenAI con fatturazione dei token misurati, query del saldo e un gestore del webhook verificato.
@dodopayments/ingestion-blueprints fornisce tracker drop-in che automatizzano la chiamata usageEvents.ingest per te — incluso il LLM Blueprint, API gateway, object storage, streams, e time-range usage.
4

A note on how deductions actually happen

Potresti aver notato che non c’è alcuna chiamata esplicita “detrarre N crediti”. Questo è voluto:
  1. Il tuo gestore chiama OpenAI e riceve usage.total_tokens (es. 1532).
  2. Inserisci un singolo evento di utilizzo: event_name: api.tokens_used, metadata: { tokens: 1532 }.
  3. Token Usage Meter aggrega gli eventi per cliente.
  4. Poiché il misuratore è collegato al credito API Tokens con Fattura utilizzo in Crediti, Dodo Payments detrae 1532 crediti dal grant non scaduto più vecchio del cliente (FIFO).
  5. Se lo sforamento è abilitato e il cliente va sotto zero, il deficit viene tracciato e fatturato sulla fattura successiva.
Il misuratore gestisce tutto ciò. Il tuo codice inserisce solo eventi.

Passaggio 6: Aggiungi un Frontend Demo

Crea public/index.html per testare tutti i flussi nel tuo browser. Persistiamo l’ID cliente su localStorage in modo che abbonamento → generazione → ricarica condividano tutti la stessa identità, imitando un’app con accesso:
<!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>

Passaggio 7: Collegare il Webhook

I webhook consentono al tuo server di reagire ai cambiamenti di saldo — li utilizzerai per inviare email di “saldo in esaurimento” prima che i clienti raggiungano lo zero.
1

Expose your local server

I webhook necessitano di un URL pubblico. Per lo sviluppo locale, usa ngrok o qualsiasi tunnel:
ngrok http 3000
Copia l’URL https://...ngrok-free.app.
2

Register the webhook in Dodo Payments

  1. Nella dashboard, vai su Sviluppatori → Webhook → Aggiungi Endpoint
  2. URL: https://your-tunnel.ngrok-free.app/webhooks/dodo
  3. Iscriviti a (al minimo):
    • credit.added
    • credit.deducted
    • credit.overage_charged
  4. Salva e copia il Segreto della Firma
  5. Incollalo in .env come DODO_PAYMENTS_WEBHOOK_KEY, quindi riavvia npm run dev
L’SDK dodo.webhooks.unwrap() valida i valori webhook-id, webhook-timestamp e webhook-signature usando il tuo segreto per la firma. Non devi realizzare un HMAC a mano — e non dovresti, perché Dodo Payments utilizza Standard Webhooks, che firma id.timestamp.body invece del solo body.

Passaggio 8: Testa il Flusso Completo

1

Subscribe a test customer

  1. Esegui npm run dev
  2. Apri http://localhost:3000
  3. Scegli Piano Pro, inserisci un’email e un nome di test, clicca Ottieni Link per il Checkout, completa il checkout con dettagli della carta di test
  4. Nella dashboard, vai su Clienti → il più recente e copia l’ID cus_...
  5. Incollalo nel campo “ID cliente loggato” nel demo e clicca Salva
Il cliente dovrebbe avere 40,000,000 token. Clicca Aggiorna Saldo per confermare.
2

Generate a real AI response

Digita un prompt e clicca Genera. Il server chiama OpenAI, ottiene indietro il valore effettivo total_tokens, inserisce un evento di utilizzo e restituisce la risposta.
Gli eventi di utilizzo vengono elaborati da un lavoratore in background ogni ~minuto. Il saldo non calerà istantaneamente — attendi 30–90 secondi e clicca di nuovo su Aggiorna Saldo. Non concludere che sia rotto se il primo aggiornamento non mostra movimento.
3

Test the top-up flow

Clicca Acquista 5M Token — $19 e completa il checkout. Dopo che il pagamento è avvenuto, aggiorna il saldo — dovrebbe aumentare di 5,000,000 token. Il registro del server dovrebbe mostrare un evento credit.added.

Risoluzione dei Problemi

Cause Possibili:
  • Il nome dell’evento del misuratore non corrisponde a event_name che stai inviando (api.tokens_used è case-sensitive)
  • Il misuratore non è collegato al credito API Tokens sul prodotto — vai alla configurazione del misuratore del prodotto e conferma che Fattura utilizzo in Crediti è attivato
  • La chiave metadata.tokens non corrisponde al campo “Sopra la Proprietà” del misuratore
  • Il grant del cliente è scaduto (controlla la cronologia dei crediti del cliente)
Cosa Controllare:
  1. Prodotti → Misuratori: apri il misuratore e conferma che mostra il nome del credito collegato sul collegamento del prodotto
  2. La scheda Eventi sul misuratore — gli eventi inseriti dovrebbero apparire lì anche prima della detrazione
  3. Clienti → [Cliente] → Crediti: le voci del registro dovrebbero apparire entro un minuto o due
Cause Possibili:
  • Il cliente non ha ancora completato il checkout — i crediti vengono emessi solo dopo un pagamento riuscito
  • Stai interrogando con il customer_id sbagliato (usa l’ID cus_... dalla dashboard, non il tuo ID DB)
  • Il CREDIT_ENTITLEMENT_ID in .env non corrisponde al credito attaccato al prodotto
Cosa Controllare: Apri Clienti → [Cliente] → Crediti. Se non appaiono crediti lì, il diritto del prodotto non è stato attaccato o il pagamento non è stato completato.
Cause Possibili:
  • Lo sforamento non è stato abilitato sul collegamento del credito del prodotto Pro (l’impostazione a livello di credito è solo un predefinito)
  • Il cliente è effettivamente sul Piano Iniziale, non sul Pro
  • Il limite di sforamento è stato impostato a 0
Cosa Controllare: Modifica Pro → Diritti → Crediti → conferma che Consenti Sforamento sia attivato e che Prezzo per Unità sia 0.000005 (= $5 per milione di token; ricontrolla gli zeri principali — il campo accetta il prezzo per token, non per 1K).
Cause Possibili:
  • Ordine di parsing del corpo: express.json() è stato applicato a /webhooks/dodo prima di express.raw() — l’SDK ha bisogno dei byte grezzi della richiesta, non del JSON parsato
  • Segreto di firma errato in DODO_PAYMENTS_WEBHOOK_KEY
  • Il proxy inverso sta riscrivendo le intestazioni
Cosa Controllare: Conferma che la linea app.use('/webhooks/dodo', express.raw(...)) venga prima di app.use(express.json()) in server.ts.

Hai Bisogno di Aiuto?

Congratulazioni! Hai Costruito la Fatturazione Basata su Crediti per NeuralAPI

La tua piattaforma ora ha un sistema di fatturazione a crediti completo e pronto per la produzione:

Token Credit Entitlement

Un credito API Tokens riutilizzabile con scadenza di 30 giorni, condiviso tra tutti i piani e il pacchetto di ricarica

Tiered Plans, One Credit

Piano Iniziale (10M, limite rigido) e Pro (40M + sforamento) configurati per prodotto senza duplicazione del credito

One-Time Top-Up Pack

I clienti aggiungono 5M token per $19 senza cambiare il loro abbonamento

Auto-Deduction via Meter

I conteggi reali dei token OpenAI vengono inseriti come eventi; il misuratore detrae i crediti FIFO senza tracciamento manuale

Live Balance API

Saldo in tempo reale tramite l’SDK per regolare l’accesso, visualizzare l’uso o avvisare i clienti nell’app

Verified Webhook Pipeline

Eventi del registro crediti (credit.added, credit.deducted, credit.overage_charged) instradati tramite un gestore verificato dal SDK
Vai in produzione? Stringi queste:
  • Autenticazione su /credits/:customerId e /api/generate — attualmente chiunque può colpire questi con qualsiasi ID cliente. Autentica gli utenti e cerca il loro ID cliente lato server.
  • Stable event_ids — l’esempio utilizza Date.now() + random. In produzione, usa il tuo ID richiesta in modo che i tentativi siano idempotenti (Dodo Payments deduplica per event_id).
  • Mantieni la mappatura cliente↔utente — memorizza customer_id nel tuo DB dopo il primo checkout in modo da non avere bisogno di un passaggio di copia manuale.
  • Decidi cosa succede quando un abbonamento termina. I crediti del piano rimangono nel registro del cliente fino alla loro scadenza naturale (30 giorni dall’emissione) e i crediti di ricarica restano validi per 365 giorni — ma il cookbook /api/generate controlla solo saldo, non stato dell’abbonamento. Quindi un cliente annullato può ancora consumare i token rimanenti. Questo è il default amichevole per il consumatore. Se desideri un controllo dell’accesso più rigoroso, o (a) ascolta il webhook subscription.cancelled e regola /api/generate in base allo stato dell’abbonamento, o (b) chiama l’API del registro di Dodo per addebitare i crediti inutilizzati del piano in caso di cancellazione lasciando intatti i crediti di ricarica.
  • Monitora il dashboard Usage Billing per rilevare precocemente anomalie di misurazione.

Credit-Based Billing Reference

Documentazione completa CBB: rollover, modalità di sforamento, gestione del registro, tutti gli endpoint API.

Credit Webhook Events

Schemi dei payload per ogni evento di credito che il tuo server potrebbe ricevere.
Last modified on May 14, 2026