Skip to content

Credits & top-ups — client integration ​

Everything a customer spends is tokens, held in one wallet. Plans add tokens every cycle, top-ups add tokens once, and every generation — text, image, video, audio, on any model — spends tokens at what that call actually cost us.

A top-up is built by the customer, like a telecom "make your own pack": they choose how many credits they want (and, where an admin has switched them on, things like images or videos) and the server prices it. The client sends only the selection. It never computes or sends a price or a token count.

The model in one paragraph ​

A token is a fixed slice of provider cost (admin-set; ৳18 per 1M tokens by default), and a credit is a bundle of tokens (10,000 by default). The admin defines the components a top-up can be built from, their ranges and prices, and rules that add bonuses or discounts (volume bonuses, weekend extras, a first top-up bonus). The server applies all of it and never lets promotions take profit below a floor. A generation reserves its estimated cost in tokens and is trued up to the real cost afterwards.

No package is required. A customer with tokens can generate on any available model. New accounts get the Free Trial once, automatically (see GET /v1/plans, features.signupDefault).

Endpoints ​

MethodPathAuthUse it for
GET/v1/topup/optionspublicBuild the top-up screen: components, ranges, unit prices, offers, channel rules
POST/v1/topup/quoteoptionalPrice a selection as the customer changes it
POST/v1/topup/ordersuserBuy: price again and open the checkout
GET/v1/topup/orders/:iduserPoll after the gateway redirect
GET/v1/topup/ordersuserTop-up history
GET/v1/credits/balanceuserWallet balance and lifetime totals
GET/v1/credits/transactionsuserStatement, newest first, cursor-paged
GET/v1/credits/ratespublicToken price of every model — cost hints beside Generate
POST/v1/credits/estimatepublicTokens one generation will reserve

All money is BDT poisha (22500 = ৳225). Token counts are integers.

1. The builder screen ​

bash
curl http://localhost:4040/v1/topup/options
json
{
  "enabled": true,
  "currency": "BDT",
  "tokensPerCredit": 10000,
  "minOrderCents": 2000,
  "maxOrderCents": 1000000,
  "roundToCents": 100,
  "charsPer300Tokens": 1000,
  "components": [
    {
      "key": "credits", "name": "Credits", "description": "Spend on any model — images, video, audio and text",
      "kind": "CREDITS", "unit": "credit",
      "minUnits": 50, "maxUnits": 20000, "step": 10, "defaultUnits": 500,
      "tokensPerUnit": 10000,
      "pricing": { "mode": "formula", "unitPriceCents": 45 }
    }
  ],
  "offers": [
    { "id": "…", "name": "Volume bonus: 500 credits", "description": "Buy 500 credits, get 50 more", "type": "BONUS",
      "conditions": { "audience": "ALL", "minUnits": { "component": "credits", "units": 500 } },
      "effect": { "type": "BONUS_UNITS", "component": "credits", "units": 50 }, "endsAt": null }
  ],
  "channels": [
    { "slug": "sslcommerz", "name": "SSLCommerz", "enabled": true, "minAmountCents": null, "maxAmountCents": null, "amountsCents": [] },
    { "slug": "dcb", "name": "Carrier billing", "enabled": true, "minAmountCents": null, "maxAmountCents": null, "amountsCents": [9900, 49900] }
  ]
}
  • Hide the screen when enabled is false.
  • One control per component — a slider or stepper from minUnits to maxUnits in steps, starting at defaultUnits. 0 leaves a component out; any other value must be in range. Label with unit ("500 credits", "20 images").
  • pricing is for display only ("৳0.45 a credit", or the tiers table). Always show the price from the quote: rounding, bonuses and discounts only exist there.
  • offers are the rules running now, to advertise ("Buy 2,000 credits, get 300 more"). Whether one applies is decided by the quote.
  • kind: "MODEL_USAGE" components ("images") grant the tokens that much usage costs. The tokens are spendable on anything — say so ("≈ 20 images' worth").

2. Pricing a selection ​

Call on every change (debounce ~250 ms). Send the bearer token if the customer is signed in: first-purchase and new-account offers depend on who they are.

bash
curl -X POST http://localhost:4040/v1/topup/quote -H 'Content-Type: application/json' \
  -d '{ "selection": { "credits": 500 } }'
json
{
  "valid": true,
  "issues": [],
  "currency": "BDT",
  "lines": [
    { "key": "credits", "name": "Credits", "kind": "CREDITS", "unit": "credit", "units": 500,
      "tokensPerUnit": 10000, "tokens": 5000000, "unitPriceCents": 45, "priceCents": 22500, "tierMinUnits": null }
  ],
  "subtotalCents": 22500,
  "discountCents": 0,
  "amountCents": 22500,
  "tokens": 5000000,
  "bonusTokens": 500000,
  "totalTokens": 5500000,
  "credits": 500,
  "bonusCredits": 50,
  "totalCredits": 550,
  "applied": [
    { "id": "…", "name": "Volume bonus: 500 credits", "type": "BONUS",
      "effect": { "type": "BONUS_UNITS", "component": "credits", "units": 50 }, "discountCents": 0, "bonusTokens": 500000 }
  ],
  "offers": [
    { "id": "…", "name": "First top-up bonus", "type": "PROMOTION", "requirement": "first top-up only — sign in to check" }
  ],
  "payableWith": ["sslcommerz", "bkash"],
  "unavailableChannels": [{ "slug": "dcb", "reason": "only takes ৳99, ৳499" }],
  "usage": { "items": [ { "displayName": "Qwen Image 3", "unit": "image", "units": 6 }, "…" ], "tokensSpent": 0, "tokensLeft": 0 }
}

(The bonus shown is one of the volume bonus templates, switched on for the example. All rule templates ship switched off.)

  • Pay: amountCents. Show subtotalCents crossed out when discountCents > 0.
  • Get: totalCredits (and totalTokens); show bonusCredits as "+50 bonus".
  • issues — show under the matching control (field is the component key, or amount for order limits). An invalid selection still answers 200, with valid: false, and cannot be bought.
  • offers — nudges: "add 100 credits for +50 bonus", "pay with bKash for ৳10 off", "sign in". Rules that cannot apply for other reasons are not listed.
  • payableWith — offer only these channels (intersect with GET /v1/payments/channels for logos and required fields). Show unavailableChannels[].reason beside a greyed-out one. Carrier billing only takes registered amounts.
  • usage — "enough for 6 images · 1 video · …", split across the cheapest model of each kind.

Send channel once the customer picks one: channel-only promotions then apply.

3. Buying ​

bash
curl -X POST http://localhost:4040/v1/topup/orders \
  -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' \
  -d '{ "selection": { "credits": 500 }, "provider": "bkash", "expectedAmountCents": 22500 }'
# → { "orderId": "…", "paymentId": "…", "redirectUrl": "https://…",
#     "amountCents": 22500, "tokens": 5000000, "bonusTokens": 500000, "totalTokens": 5500000 }

The server prices the selection again from scratch and charges that. Send expectedAmountCents (the amount you showed): if it no longer matches — a promotion ended, a rule changed — the order is refused so nobody pays a price they did not see:

json
{ "error": { "code": "PRICE_CHANGED", "message": "the price is now ৳250 — please confirm", "details": { "amountCents": 25000, "totalTokens": 5000000 } } }

HTTP 409. Show the new price and let them confirm (send the new amount).

Other refusals (400, message safe to show): an invalid selection (details.issues), a channel that cannot take the amount, top-ups switched off, a couponCode (promotions are automatic).

  • Carrier billing (dcb) — extra.msisdn and extra.paymentProvider as for a plan. A top-up is always one charge.
  • The tokens and bonus are fixed on the order when it is placed and granted exactly once when the gateway confirms.

4. After the redirect ​

The gateway sends the customer back to your page, but the webhook decides. Poll with the orderId:

bash
curl http://localhost:4040/v1/topup/orders/ord_… -H "Authorization: Bearer $TOK"
json
{
  "id": "…", "status": "SUCCEEDED",
  "selection": { "credits": 500 }, "lines": [ "…" ], "appliedRules": [ "…" ],
  "subtotalCents": 22500, "discountCents": 0, "amountCents": 22500, "currency": "BDT",
  "tokens": 5000000, "bonusTokens": 500000, "totalTokens": 5500000, "channel": "bkash",
  "payment": { "id": "…", "status": "SUCCEEDED", "provider": "BKASH", "paidAt": "2026-09-15T10:03:40.000Z" },
  "tokensGranted": 5500000,
  "createdAt": "2026-09-15T10:02:11.000Z"
}
statusShow
PENDING"Confirming your payment…" — poll every 2–3 s for up to a minute, then offer to check later
SUCCEEDED"N credits added" (tokensGranted / tokensPerCredit), refresh the balance
FAILEDPayment failed — offer to try again (a new order)
CANCELLEDThe customer backed out — nothing was charged
EXPIREDCheckout was never completed (a day passed)
REFUNDEDRefunded; the tokens were taken back

GET /v1/topup/orders lists the customer's orders (cursor-paged). Someone else's order id answers 404.

5. Balance and statement ​

bash
curl http://localhost:4040/v1/credits/balance -H "Authorization: Bearer $TOK"
json
{
  "tokens": 5500000,
  "credits": 550,
  "tokensPerCredit": 10000,
  "updatedAt": "2026-09-15T10:03:40.000Z",
  "totals": { "purchased": 5000000, "bonus": 500000, "granted": 1000000, "converted": 0, "adjusted": 0, "spent": 1000000 }
}

Show credits (tokens / tokensPerCredit, rounded down to 2 decimals). Both come with the balance, so there's no need to read /v1/topup/options for the ratio. GET /v1/subscriptions/credits/balance returns the same fields without totals.

bash
curl 'http://localhost:4040/v1/credits/transactions?limit=20' -H "Authorization: Bearer $TOK"
kindSignMeaning
GRANT+Plan allowance for the cycle, or a support grant
PURCHASE+A top-up (paymentId set)
PROMO+A top-up's bonus (same paymentId as its purchase), or a promotion
USAGE−A generation. Reserved before the call; a second USAGE row appears if it ran over
REFUND+A failed generation, or a reservation that came in under the real cost
ADJUSTMENT− / +A refunded top-up taken back (paymentId set), or a correction
CONVERSION+The pre-token balance, converted once

Group a USAGE with an immediately following REFUND of the same generation and show the net; show a PROMO under the PURCHASE with the same paymentId.

6. Cost hints before generating ​

bash
curl http://localhost:4040/v1/credits/rates
KindEstimate
Imagerates.image × count
Videorates.second × durationSeconds
Speechrates.1k_chars × chars / 1000, or rates.minute × minutes
Musicrates.generation
Text(inputTokens × rates.1m_input_tokens + maxTokens × rates.1m_output_tokens) / 1e6

Or ask the server, which uses the estimator the gateway reserves with:

bash
curl -X POST http://localhost:4040/v1/credits/estimate -H 'Content-Type: application/json' \
  -d '{ "model": "gemini/veo-3.1-lite-generate-preview", "durationSeconds": 8 }'

7. Running out ​

A generation the balance cannot cover is refused before it runs — HTTP 429 QUOTA_EXCEEDED with details.shortfallTokens. Deep-link to the builder with the credits preselected: ceil(shortfallTokens / tokensPerCredit), raised to the component's minUnits and rounded up to its step.

Migrating ​

  • GET /v1/credits/pricing, GET /v1/credits/quote, GET /v1/credits/packs and checkout's topUpCents / packId are gone. Use /v1/topup.
  • GET /v1/subscriptions/credits/packs returns [].
  • From the bucket wallet: read tokens (the wallet) instead of textRequests, imageCredits, videoCredits; existing balances converted automatically (one CONVERSION row).

kikori.ai — internal documentation