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

# Membangun Platform API AI dengan Penagihan Berbasis Kredit

> Bangun layanan API AI bertingkat dengan kredit token — berikan kredit melalui paket langganan dan paket top-up satu kali, lalu kurangi secara otomatis saat pelanggan memanggil API Anda.

<Tip>
  <strong>Biarkan Sentra menulis kode integrasi untuk Anda.</strong><br />
  Gunakan asisten AI kami di VS Code, Cursor, atau Windsurf untuk membuat kode SDK/API, handler webhook, pemberian kredit, dan lainnya — cukup dengan menjelaskan apa yang Anda inginkan.

  <a href="https://dodopayments.com/sentra" target="_blank" rel="noopener noreferrer">
    Coba Sentra: Integrasi Bertenaga AI →
  </a>
</Tip>

Dalam tutorial ini, Anda akan membangun **NeuralAPI** — platform AI bertingkat di mana setiap paket langganan memiliki alokasi kredit token bulanan, pelanggan dapat membeli paket top-up saat kredit mereka hampir habis, dan backend Anda secara otomatis mengurangi kredit saat permintaan diproses oleh OpenAI.

<Note>
  Tutorial ini menggunakan Node.js/Express + OpenAI SDK. Konsep Dodo Payments (kredit, meter, webhook) berlaku untuk framework atau penyedia AI apa pun — sesuaikan dengan bebas.
</Note>

Di akhir tutorial ini, Anda akan mengetahui cara:

* Membuat entitlement kredit khusus (token) dan meter yang secara otomatis mengurangi kredit darinya
* Mengaitkan kredit ke paket langganan (dengan dan tanpa kelebihan pemakaian) serta produk top-up satu kali
* Menghubungkan endpoint completion OpenAI nyata yang menagihkan token melalui Dodo Payments
* Meminta saldo kredit pelanggan secara real-time melalui SDK
* Memverifikasi signature webhook dan merutekan event kredit Dodo Payments

## Yang Akan Kita Bangun

Berikut model harga untuk NeuralAPI:

| Produk             | Harga          | Token                   | Kelebihan Pemakaian  |
| ------------------ | -------------- | ----------------------- | -------------------- |
| Paket Starter      | \$29/bulan     | 10.000.000 token/siklus | Diblokir saat nol    |
| Paket Pro          | \$99/bulan     | 40.000.000 token/siklus | \$0.005 per 1K token |
| Paket Top-Up Token | \$19 satu kali | +5.000.000 token        | —                    |

<Info>
  Sebelum memulai, pastikan Anda memiliki:

  * Akun Dodo Payments (mode pengujian dapat digunakan)
  * API key OpenAI
  * Node.js 18+
  * Pemahaman dasar tentang TypeScript/Node.js
</Info>

## Langkah 1: Buat Entitlement Kredit Token

Pertama, buat entitlement kredit yang akan digunakan bersama oleh kedua paket langganan dan paket top-up. Anggap ini sebagai definisi unit "token" yang digunakan platform Anda.

<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="Halaman daftar kredit yang menampilkan entitlement kredit yang telah dibuat" 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. Masuk ke dashboard Dodo Payments
    2. Klik **Products** di sidebar kiri
    3. Pilih tab **Credits**
    4. Klik **Create Credit**
  </Step>

  <Step title="Configure the credit unit">
    Isi detail dasar untuk kredit token Anda:

    **Nama Kredit**: `API Tokens`

    **Jenis Kredit**: Pilih **Custom Unit**

    **Nama Unit**: `token`

    **Presisi**: `0` (token selalu berupa bilangan bulat)

    **Kedaluwarsa Kredit**: `30 days` (kredit direset setiap siklus penagihan)

    <Warning>
      Presisi tidak dapat diubah setelah kredit dibuat. Untuk jumlah token, `0` (bilangan bulat) hampir selalu merupakan pilihan yang tepat.
    </Warning>
  </Step>

  <Step title="Skip overage at the credit level">
    Biarkan kelebihan pemakaian **dinonaktifkan** di sini — Anda akan mengaturnya per paket saat mengaitkan kredit ke produk. Dengan begitu, paket Starter memblokir penggunaan saat saldo nol, sedangkan paket Pro mengizinkan kelebihan pemakaian.

    <Tip>
      Pengaturan kelebihan pemakaian yang dikonfigurasi di sini adalah *default*. Setiap pengaitan produk dapat menimpanya — dan itulah yang akan kita lakukan di Langkah 3.
    </Tip>
  </Step>

  <Step title="Save and copy the credit ID">
    Klik **Create Credit**. Setelah tersimpan, buka kredit tersebut dan salin ID-nya — formatnya seperti `cent_xxxxxxxxxxxx`.

    <Check>
      Entitlement kredit `API Tokens` Anda siap digunakan. Selanjutnya, buat meter agar event penggunaan dapat memicu pengurangan secara otomatis.
    </Check>
  </Step>
</Steps>

## Langkah 2: Buat Meter untuk Penggunaan Token

Meter mengagregasi event penggunaan yang masuk dan mengubahnya menjadi pengurangan kredit. Anda memerlukannya *sebelum* membuat produk paket, karena meter akan dikaitkan saat pembuatan produk pada Langkah 3.

<Steps>
  <Step title="Open the Meters section">
    1. Di sidebar dashboard, buka **Products** → **Meters**
    2. Klik **Create Meter**
  </Step>

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

    **Nama Meter**: `Token Usage Meter`

    **Nama Event**: `api.tokens_used` *(harus sama persis dengan yang dikirim aplikasi Anda)*

    **Jenis Agregasi**: `Sum` — kami menjumlahkan jumlah token dari setiap event

    **Over Property**: `tokens` — kunci metadata pada setiap event yang nilainya akan dijumlahkan

    **Unit Pengukuran**: `tokens`

    <Warning>
      Nama event peka huruf besar-kecil. `api.tokens_used` ≠ `Api.Tokens.Used` — pilih salah satu dan gunakan secara konsisten.
    </Warning>

    Simpan meter dan salin ID-nya — Anda akan merujuknya saat mengaitkannya ke produk.

    <Check>
      Meter telah dibuat. Sekarang kita dapat menghubungkannya ke kredit saat mengonfigurasi produk.
    </Check>
  </Step>
</Steps>

## Langkah 3: Buat Produk Paket

Kedua paket harus berupa produk **Usage Based Billing**, bukan Subscription biasa — meter hanya dapat dikaitkan ke produk UBB, dan Anda memerlukan meter untuk mengurangi kredit secara otomatis saat pelanggan memanggil API. Produk UBB tetap mendukung biaya dasar berulang (`$29` / `$99`); penggunaan di luar itu akan ditagihkan dalam kredit.

<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="Konfigurasi harga Usage Based Billing" style={{ maxHeight: '500px', width: 'auto' }} width="2880" height="1920" data-path="images/CBB/Desktop - Attach Credit - UBB.jpg" />
</Frame>

### Paket Starter (\$29/bulan — 10 juta token, tanpa kelebihan pemakaian)

<Steps>
  <Step title="Create the Starter UBB product">
    1. Buka **Products → Create Product**
    2. Pilih **Usage Based Billing** sebagai jenis harga
    3. Isi:

    **Nama Produk**: `NeuralAPI Starter`

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

    **Harga Tetap**: `29.00` (biaya dasar berulang — ditagihkan setiap bulan bahkan sebelum ada penggunaan)

    **Siklus Penagihan**: `Monthly`

    **Mata Uang**: `USD`
  </Step>

  <Step title="Attach the meter">
    Di bagian **Select meter**, klik **+** dan tambahkan `Token Usage Meter`. Kemudian pada meter tersebut:

    1. Aktifkan **Bill usage in Credits**
    2. **Credit Entitlement**: pilih `API Tokens`
    3. **Meter units per credit**: `1` — setiap token dalam event dipetakan ke 1 kredit yang dikurangi
    4. **Free Threshold**: `0` — alokasi kredit itu sendiri merupakan "free tier" pelanggan; kita tidak memerlukan tingkat gratis tambahan

    <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="Meter dengan Bill usage in Credits yang diaktifkan dan API Tokens yang dipilih" style={{ maxHeight: '500px', width: 'auto' }} width="2880" height="2282" data-path="images/CBB/Desktop - Attach Credit - UBB-5.jpg" />
    </Frame>

    Inilah pengaturan yang membuat event `api.tokens_used` yang masuk benar-benar mengurangi saldo pelanggan.
  </Step>

  <Step title="Configure credit issuance for Starter">
    Masih di produk tersebut, gulir ke bagian konfigurasi kredit yang muncul setelah meter yang ditagihkan dalam kredit dikaitkan:

    **Kredit yang diberikan per siklus penagihan**: `10000000`

    **Allow Overage**: **Disabled** — pelanggan Starter diblokir saat token habis

    **Import Default Credit Settings**: Enabled — gunakan kedaluwarsa 30 hari dari entitlement kredit

    <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="Formulir konfigurasi kredit dengan jumlah per siklus dan pengaturan kelebihan pemakaian" style={{ maxHeight: '500px', width: 'auto' }} width="2880" height="1920" data-path="images/CBB/Desktop - Attach Credit - UBB-6.jpg" />
    </Frame>

    Klik **Save** dan salin ID produk.

    <Check>
      Paket Starter: biaya dasar \$29/bulan, 10 juta token/siklus, diblokir saat nol, dikurangi otomatis melalui meter.
    </Check>
  </Step>
</Steps>

### Paket Pro (\$99/bulan — 40 juta token, kelebihan pemakaian diaktifkan)

<Steps>
  <Step title="Create the Pro UBB product">
    Alurnya sama seperti Starter, dengan angka yang lebih besar:

    **Nama Produk**: `NeuralAPI Pro`

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

    **Harga Tetap**: `99.00`

    **Siklus Penagihan**: `Monthly`

    **Mata Uang**: `USD`
  </Step>

  <Step title="Attach the meter">
    Sama seperti Starter: tambahkan `Token Usage Meter`, aktifkan **Bill usage in Credits**, pilih `API Tokens`, **Meter units per credit** `1`, dan **Free Threshold** `0`.
  </Step>

  <Step title="Configure credit issuance with overage">
    Konfigurasikan pemberian kredit, kali ini dengan mengaktifkan kelebihan pemakaian:

    **Kredit yang diberikan per siklus penagihan**: `40000000`

    **Import Default Credit Settings**: **Disable** — kita perlu menyesuaikan pengaturan kelebihan pemakaian per produk

    **Allow Overage**: **Enabled**

    **Price Per Unit**: `0.000005` USD per token (yaitu $0.005 per 1K token, atau $5 per 1 juta token — di atas tarif efektif per token paket untuk mencegah penggunaan berlebih)

    **Overage Behavior**: `Bill overage at billing` — kelebihan pemakaian ditagihkan pada invoice berikutnya, lalu saldo direset

    Simpan produk dan salin ID produk.

    <Check>
      Paket Pro: biaya dasar $99/bulan, 40 juta token/siklus, kelebihan pemakaian sebesar $0.005/1K token, dikurangi otomatis melalui meter.
    </Check>
  </Step>
</Steps>

## Langkah 4: Buat Paket Top-Up Token

Paket top-up adalah pembelian satu kali yang memberikan 5.000.000 token ke saldo pelanggan yang sudah ada.

<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="Bagian harga produk dengan Single Payment yang dipilih" 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. Buka **Products → Create Product**
    2. Pilih **Single Payment** sebagai jenis harga
    3. Isi:

    **Nama Produk**: `Token Top-Up Pack`

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

    **Harga**: `19.00`

    **Mata Uang**: `USD`
  </Step>

  <Step title="Attach the token credit">
    1. Di bagian **Entitlements**, klik **Attach** di samping **Credits**
    2. Pilih `API Tokens`
    3. Atur **Credits issued**: `5000000`
    4. **Nonaktifkan** *Import Default Credit Settings* — kita ingin menimpa kedaluwarsa default 30 hari
    5. Atur **Credit Expiry**: `365 days`
    6. Simpan produk

    Salin ID produk.

    <Tip>
      Mengapa kedaluwarsa top-up lebih lama? Kredit langganan direset setiap 30 hari karena itulah siklusnya. Top-up adalah *pembelian prabayar* — pelanggan membayar \$19 di muka dan sewajarnya mengharapkan token tersebut berlaku lebih dari sebulan. 365 hari sesuai dengan cara kerja kredit prabayar di OpenAI, AWS, dan Anthropic, sekaligus membatasi kewajiban Anda agar pelanggan tidak dapat menimbun kredit tanpa batas.
    </Tip>

    <Check>
      Paket Top-Up telah dikonfigurasi — pembeliannya memberikan 5.000.000 token yang tetap berlaku selama 365 hari.
    </Check>
  </Step>
</Steps>

## Langkah 5: Bangun Backend

Sekarang mari kita bangun server Express yang menangani checkout langganan, checkout top-up, completion OpenAI nyata dengan penagihan token, permintaan saldo, dan event webhook kredit.

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

    Buat `tsconfig.json`:

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

    Perbarui script `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">
    Buat `.env` dengan kredensial dan ID dari langkah sebelumnya:

    ```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>
      Jangan pernah commit `.env` ke version control. Segera tambahkan ke `.gitignore`.
    </Warning>

    Anda akan mengisi `DODO_PAYMENTS_WEBHOOK_KEY` pada Langkah 7 setelah mendaftarkan endpoint webhook Anda.
  </Step>

  <Step title="Implement the server">
    Buat `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 selesai: checkout langganan, checkout top-up, completion OpenAI dengan penagihan token bermeter, permintaan saldo, dan handler webhook terverifikasi.
    </Check>

    <Tip>
      [`@dodopayments/ingestion-blueprints`](/features/usage-based-billing/ingestion-blueprints) menyediakan tracker siap pakai yang mengotomatiskan panggilan `usageEvents.ingest` untuk Anda — termasuk penggunaan [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), dan [time-range](/developer-resources/ingestion-blueprints/time-range).
    </Tip>
  </Step>

  <Step title="A note on how deductions actually happen">
    Anda mungkin menyadari bahwa tidak ada panggilan eksplisit "kurangi N kredit". Itu memang dirancang demikian:

    1. Handler Anda memanggil OpenAI dan menerima `usage.total_tokens` (misalnya, 1532).
    2. Anda memasukkan satu event penggunaan: `event_name: api.tokens_used`, `metadata: { tokens: 1532 }`.
    3. `Token Usage Meter` mengagregasi event berdasarkan pelanggan.
    4. Karena meter terhubung ke kredit `API Tokens` dengan **Bill usage in Credits**, Dodo Payments mengurangi 1532 kredit dari grant pelanggan yang paling lama dan belum kedaluwarsa (FIFO).
    5. Jika kelebihan pemakaian diaktifkan dan saldo pelanggan berada di bawah nol, defisit dilacak dan ditagihkan pada invoice berikutnya.

    Meter menangani semua proses tersebut. Kode Anda hanya perlu memasukkan event.
  </Step>
</Steps>

## Langkah 6: Tambahkan Frontend Demo

Buat `public/index.html` untuk menguji semua alur di browser Anda. Kami menyimpan ID pelanggan ke `localStorage` agar subscribe → generate → top-up semuanya menggunakan identitas yang sama, meniru aplikasi yang penggunanya telah login:

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

## Langkah 7: Hubungkan Webhook

Webhook memungkinkan server Anda merespons perubahan saldo — Anda akan menggunakannya untuk mengirim email "saldo hampir habis" sebelum pelanggan mencapai nol.

<Steps>
  <Step title="Expose your local server">
    Webhook memerlukan URL publik. Untuk pengembangan lokal, gunakan [ngrok](https://ngrok.com) atau tunnel apa pun:

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

    Salin URL `https://...ngrok-free.app`.
  </Step>

  <Step title="Register the webhook in Dodo Payments">
    1. Di dashboard, buka **Developers → Webhooks → Add Endpoint**
    2. **URL**: `https://your-tunnel.ngrok-free.app/webhooks/dodo`
    3. Berlangganan minimal ke:
       * `credit.added`
       * `credit.deducted`
       * `credit.overage_charged`
    4. Simpan dan salin **Signing Secret**
    5. Tempelkan ke `.env` sebagai `DODO_PAYMENTS_WEBHOOK_KEY`, lalu mulai ulang `npm run dev`

    <Tip>
      `dodo.webhooks.unwrap()` SDK memvalidasi header `webhook-id`, `webhook-timestamp`, dan `webhook-signature` menggunakan signing secret Anda. Anda tidak perlu membuat verifikasi HMAC sendiri — dan sebaiknya tidak melakukannya, karena Dodo Payments menggunakan [Standard Webhooks](https://www.standardwebhooks.com/), yang menandatangani `id.timestamp.body`, bukan hanya body.
    </Tip>
  </Step>
</Steps>

## Langkah 8: Uji Alur Lengkap

<Steps>
  <Step title="Subscribe a test customer">
    1. Jalankan `npm run dev`
    2. Buka `http://localhost:3000`
    3. Pilih **Paket Pro**, masukkan email + nama pengujian, klik **Get Checkout Link**, lalu selesaikan checkout menggunakan [detail kartu pengujian](/miscellaneous/testing-process)
    4. Di dashboard, buka **Customers → most recent** dan salin ID `cus_...`
    5. Tempelkan ke kolom "Logged-in customer ID" pada demo dan klik **Save**

    <Check>
      Pelanggan seharusnya memiliki 40.000.000 token. Klik **Refresh Balance** untuk mengonfirmasi.
    </Check>
  </Step>

  <Step title="Generate a real AI response">
    Ketik prompt dan klik **Generate**. Server memanggil OpenAI, menerima `total_tokens` aktual, memasukkan event penggunaan, lalu mengembalikan respons.

    <Info>
      Event penggunaan diproses oleh background worker setiap sekitar satu menit. Saldo tidak akan langsung berkurang — tunggu 30–90 detik dan klik **Refresh Balance** lagi. Jangan menganggap sistem rusak jika refresh pertama belum menunjukkan perubahan.
    </Info>
  </Step>

  <Step title="Test the top-up flow">
    Klik **Buy 5M Tokens — \$19** dan selesaikan checkout. Setelah pembayaran berhasil, refresh saldo — saldo seharusnya bertambah 5.000.000 token. Log server Anda seharusnya menampilkan event `credit.added`.
  </Step>
</Steps>

## Pemecahan Masalah

<AccordionGroup>
  <Accordion title="Credits not deducting after usage events">
    **Kemungkinan penyebab:**

    * Nama event meter tidak sama dengan `event_name` yang Anda kirim (`api.tokens_used` peka huruf besar-kecil)
    * Meter tidak ditautkan ke kredit `API Tokens` pada produk — buka konfigurasi meter produk dan pastikan **Bill usage in Credits** aktif
    * Kunci `metadata.tokens` tidak sama dengan kolom "Over Property" pada meter
    * Grant pelanggan telah kedaluwarsa (periksa riwayat kredit pelanggan)

    **Yang perlu diperiksa:**

    1. **Products → Meters**: buka meter dan pastikan nama kredit yang ditautkan terlihat pada pengaitan produk
    2. Tab **Events** pada meter — event yang dimasukkan akan muncul di sana bahkan sebelum pengurangan dilakukan
    3. **Customers → \[Customer] → Credits**: entri ledger seharusnya muncul dalam satu atau dua menit
  </Accordion>

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

    * Pelanggan belum menyelesaikan checkout — kredit hanya diberikan setelah pembayaran berhasil
    * Anda melakukan permintaan dengan `customer_id` yang salah (gunakan ID `cus_...` dari dashboard, bukan ID DB Anda sendiri)
    * `CREDIT_ENTITLEMENT_ID` di `.env` tidak sama dengan kredit yang dikaitkan ke produk

    **Yang perlu diperiksa:**
    Buka **Customers → \[Customer] → Credits**. Jika tidak ada kredit yang muncul, entitlement produk belum dikaitkan atau pembayaran belum selesai.
  </Accordion>

  <Accordion title="Overage not working for Pro plan customers">
    **Kemungkinan penyebab:**

    * Kelebihan pemakaian tidak diaktifkan pada **pengaitan kredit produk Pro** (pengaturan tingkat kredit hanya merupakan default)
    * Pelanggan sebenarnya menggunakan Starter, bukan Pro
    * Batas kelebihan pemakaian ditetapkan ke 0

    **Yang perlu diperiksa:**
    Edit Pro → Entitlements → Credits → pastikan **Allow Overage** aktif dan **Price Per Unit** adalah `0.000005` (= \$5 per satu juta token; periksa kembali angka nol di depan — kolom ini menerima harga per token, bukan per 1K).
  </Accordion>

  <Accordion title="`Webhook verification failed` in logs">
    **Kemungkinan penyebab:**

    * Urutan parsing body: `express.json()` diterapkan ke `/webhooks/dodo` sebelum `express.raw()` — SDK memerlukan **raw bytes** dari request, bukan JSON yang telah diparsing
    * Signing secret yang salah di `DODO_PAYMENTS_WEBHOOK_KEY`
    * Reverse proxy menulis ulang header

    **Yang perlu diperiksa:**
    Pastikan baris `app.use('/webhooks/dodo', express.raw(...))` berada *sebelum* `app.use(express.json())` di `server.ts`.
  </Accordion>
</AccordionGroup>

## Butuh bantuan?

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

## Selamat! Anda Telah Membangun Penagihan Berbasis Kredit untuk NeuralAPI

Platform Anda kini memiliki sistem penagihan kredit lengkap yang siap digunakan di production:

<CardGroup cols={2}>
  <Card title="Token Credit Entitlement" icon="coins">
    Kredit `API Tokens` yang dapat digunakan kembali dengan kedaluwarsa 30 hari, digunakan bersama oleh semua paket dan paket top-up
  </Card>

  <Card title="Tiered Plans, One Credit" icon="layer-group">
    Starter (10 juta, batas keras) dan Pro (40 juta + kelebihan pemakaian) dikonfigurasi per produk tanpa menduplikasi kredit
  </Card>

  <Card title="One-Time Top-Up Pack" icon="circle-plus">
    Pelanggan dapat menambahkan 5 juta token seharga \$19 tanpa mengubah langganan mereka
  </Card>

  <Card title="Auto-Deduction via Meter" icon="bolt">
    Jumlah token OpenAI nyata dimasukkan sebagai event; meter mengurangi kredit secara FIFO tanpa pelacakan manual
  </Card>

  <Card title="Live Balance API" icon="gauge">
    Saldo real-time melalui SDK untuk membatasi akses, menampilkan penggunaan, atau memperingatkan pelanggan di aplikasi
  </Card>

  <Card title="Verified Webhook Pipeline" icon="bell">
    Event ledger kredit (`credit.added`, `credit.deducted`, `credit.overage_charged`) dirutekan melalui handler terverifikasi signature menggunakan helper Standard Webhooks milik SDK
  </Card>
</CardGroup>

<Info>
  **Akan digunakan di production?** Perketat hal-hal berikut:

  * **Auth pada `/credits/:customerId` dan `/api/generate`** — saat ini siapa pun dapat memanggilnya dengan ID pelanggan apa pun. Autentikasi pengguna dan cari ID pelanggan *mereka* di sisi server.
  * **`event_id` yang stabil** — contoh ini menggunakan `Date.now() + random`. Dalam production, gunakan ID request Anda agar retry bersifat idempoten (Dodo Payments melakukan deduplikasi berdasarkan `event_id`).
  * **Simpan pemetaan pelanggan↔pengguna** — simpan `customer_id` di DB setelah checkout pertama agar Anda tidak memerlukan langkah penempelan manual.
  * **Tentukan apa yang terjadi saat langganan berakhir.** Kredit paket tetap berada di ledger pelanggan hingga kedaluwarsa secara alami (30 hari sejak diberikan), sedangkan kredit top-up tetap berlaku selama 365 hari — tetapi `/api/generate` dalam cookbook hanya memeriksa *saldo*, bukan status langganan. Jadi, pelanggan yang membatalkan langganan masih dapat menggunakan token yang tersisa. Ini adalah default yang ramah konsumen. Jika Anda menginginkan kontrol akses yang lebih ketat, (a) dengarkan webhook `subscription.cancelled` dan batasi `/api/generate` berdasarkan status langganan, atau (b) panggil API ledger milik Dodo untuk mendebit kredit paket yang belum digunakan saat pembatalan, sambil membiarkan kredit top-up tetap utuh.
  * **Pantau dashboard Usage Billing** untuk mendeteksi anomali metering sejak dini.
</Info>

<CardGroup cols={2}>
  <Card title="Credit-Based Billing Reference" icon="book" href="/features/credit-based-billing">
    Dokumentasi CBB lengkap: rollover, mode kelebihan pemakaian, pengelolaan ledger, dan semua endpoint API.
  </Card>

  <Card title="Credit Webhook Events" icon="bell" href="/developer-resources/webhooks/intents/credit">
    Skema payload untuk setiap event kredit yang mungkin diterima server Anda.
  </Card>
</CardGroup>
