Skip to content

What's new — client developer changelog ​

Only what changes the client: new endpoints, new fields, new error codes, and behaviour that differs from what you may have already built against.

Almost everything here is additive — a new field or a new route — so existing calls keep working. Two entries are not, and both are worth reading before you upgrade:

  • 2026-08-17, the catalog rebuild — retires model slugs. A hardcoded slug will 503.
  • 2026-08-19 — capabilities.controls stops advertising controls a model never honoured, so a settings panel built from it renders fewer inputs. The fields are still accepted; they just did nothing before.
  • 2026-09-15, the top-up builder — GET /v1/credits/pricing, GET /v1/credits/quote and checkout's topUpCents are gone; top-ups are built and bought under /v1/topup.

Newest first.


2026-09-16 — Plans include models by price; full lists and count on model groups ​

A plan's models are no longer a fixed hand-picked list. Each plan includes the models it is sold with plus every model its token allowance can pay for a sensible amount of, so the lists grow as models are priced and switched on.

  • GET /v1/plans — features.modelAccess[] is now computed per request. Each group gains count and models lists every model of that kind the plan includes and can serve today (hundreds of text models on the larger plans; the response is ~400 KB), named by their real display name, each with provider, unit and tokensPerUnit. The old note ("served by …") is no longer sent. allowedModelIds and servableModelIds are larger (hundreds for the bigger plans).
  • New: GET /v1/plans/:slug/models — every model the plan includes, grouped by kind, with its token cost per unit. Public. 404 for unknown, archived and internal plans.

What to do: expect long models arrays; count equals their length. GET /v1/models/me and the allowed flag already reflect the larger grants.


2026-09-16 — Many more models in /v1/models ​

Models that can be sold (callable, priced) now switch on automatically instead of waiting for an operator, so GET /v1/models lists hundreds of models rather than a few dozen. Nothing in the response shape changed. What a signed-in customer on a package may use is still decided by the package: read allowed, or use GET /v1/models/me. A picker built on the assumption of a short list should search or group by type and provider.

Internal accounts may hold a plan that is not in GET /v1/plans (all-access). GET /v1/subscriptions still returns it with its slug and name; don't assume the plan appears in the public list.


2026-09-16 — A provider refusing one model is a 400, not a 503 ​

When an AI provider answers 403 for a specific model — OpenAI's "your organization must be verified to use the model", a project without access to a model — generation endpoints now return 400 BAD_REQUEST with the provider's message. Before, the same request returned 503 PROVIDER_UNAVAILABLE, and the platform also took the provider's key out of service, so every other model on that provider disappeared from /v1/models until an operator restored it.

What to do: show the error message; don't retry the same model. Other models are unaffected. A 403 that says the key itself is invalid still returns 503.

Also: blocked provider keys now recover on their own within about 10 minutes once they work again, so models that vanished from /v1/models for that reason come back without anyone acting.


2026-09-16 — Model rows say what a model can do ​

Models are discovered from the providers now instead of being listed in the platform's code, so GET /v1/models carries more about each one. Everything here is additive.

  • features — capability tags: TEXT, VISION, REASONING, TOOLS, IMAGE_GENERATION, IMAGE_EDIT, VIDEO_GENERATION, TTS, MUSIC, STT, EMBEDDING. Use these when type is too coarse — an image model that can edit advertises IMAGE_EDIT, one that can only generate does not.
  • inputModalities / outputModalities, maxOutputTokens, description.
  • availability — AVAILABLE, or RETIRING when the provider has announced a shutdown date. A retiring model still works; warn rather than hide it.
  • New filter: GET /v1/models?capability=VISION.

Nothing was removed, and no slug changed. The catalogue a customer sees is still only what an admin has put on sale.


2026-09-15 — ৳6 daily plan; archived plans refused at checkout ​

GET /v1/plans has a new plan, daily: interval: "DAILY", priceCents: 600, 60,000 tokens, text and voice models, payable on every channel. Carrier billing and bKash auto-renew charge it each day until cancelled. Render DAILY as "/day".

POST /v1/payments/checkout now answers 404 plan for an archived plan, like plan switch and bKash auto-renew already did. A client holding an old plan id gets a clear refusal instead of a charge for a plan that is no longer sold.


2026-09-15 — balances carry credits ​

GET /v1/credits/balance and GET /v1/subscriptions/credits/balance now also return credits (tokens / tokensPerCredit, rounded down to 2 decimals) and tokensPerCredit (the admin setting). Additive: tokens is unchanged.

Show the balance as credits. The old per-bucket fields (imageCredits, videoCredits, textRequests) are not part of the balance any more — a client still summing them shows 0 while the wallet is full.


2026-09-15 — the top-up builder; payment status you can poll ​

Customers build their own top-up ​

Like a telecom "make your own pack": the customer picks how much of each component they want and the server prices it. Nothing on the client computes a price.

  • New: GET /v1/topup/options — the components (credits, and any others switched on, such as images), each with its range, step, unit, tokens per unit and price per unit or volume tiers; the offers running; order limits; per-channel amount rules.
  • New: POST /v1/topup/quote — send { selection: { credits: 500 } } (and channel once chosen). Returns amountCents, tokens, bonusTokens, credits, the rules that applied, offers one step away ("add 100 credits", "sign in"), payableWith, unavailableChannels and usage. An invalid selection answers 200 with valid: false and issues. Optional bearer token: with one, first-purchase and new-account offers are applied.
  • New: POST /v1/topup/orders — { selection, provider, extra?, expectedAmountCents? } → { orderId, paymentId, redirectUrl }. If the price moved since the customer saw it: 409 PRICE_CHANGED with details.amountCents. Coupons do not apply; promotions are automatic.
  • New: GET /v1/topup/orders, GET /v1/topup/orders/:id — history and status (PENDING, SUCCEEDED, FAILED, CANCELLED, EXPIRED, REFUNDED) with tokensGranted.
  • Removed: GET /v1/credits/pricing, GET /v1/credits/quote, checkout's topUpCents (and the short-lived fixed packs: GET /v1/credits/packs, checkout's packId). None was used by a released client. GET /v1/subscriptions/credits/packs returns [].
  • A top-up on DCB is always one charge, and only at an amount registered for carrier billing — payableWith already accounts for that.

GET /v1/payments/:id ​

The caller's own payment: status, kind (plan or topup), topUpOrderId, tokensGranted. Another user's payment is 404.

CANCELLED and EXPIRED are new payment statuses: a customer who backed out on bKash is now CANCELLED rather than FAILED, and a checkout nobody completed becomes EXPIRED after a day.

Wallet totals and two ledger kinds ​

  • GET /v1/credits/balance adds totals: purchased, bonus, granted, converted, adjusted, spent.
  • The ledger shows PROMO (a top-up's bonus, same paymentId as its PURCHASE) and ADJUSTMENT (a refunded top-up taken back).

Settling is exactly-once ​

A gateway callback that repeats, arrives late, or reports a different amount no longer grants twice, un-pays a payment, or fulfils the wrong amount.

See Credits & top-ups.


Music generation works ​

gemini/lyria-3-clip-preview (30s) and gemini/lyria-3-pro-preview (full song) are live. They were previously unusable for a reason nothing in the response revealed: Gemini answers the audio endpoint in two encodings — TTS returns raw PCM, Lyria returns a finished MP3 — and the platform wrapped both in a WAV header, producing a file nothing could decode.

Music takes no voice. Send a description of the track — genre, instruments, tempo — not a script. Lyria decides its own length; there is no duration parameter, so do not render a length control for it.

capabilities.controls is now per-model, and honest ​

Every AUDIO model used to advertise the same two controls. Music and sound effects offered a voice picker that does nothing, and Gemini TTS offered a format it silently ignores (it always returns WAV).

Now the block reflects what the model actually accepts. Build your settings panel from it rather than from the model type — that is what it is for. Nothing is advertised that the request schema cannot carry.

Provider failures explain themselves ​

PROVIDER_UNAVAILABLE used to arrive with details: null, so "not on your plan", "no such model" and "the provider is down" were indistinguishable. It now carries the upstream reason:

jsonc
{ "error": { "code": "PROVIDER_UNAVAILABLE", "details": {
  "upstream": { "code": "QUOTA", "status": 429, "message": "…" },
  "accountsTried": 2, "accountsOverBudget": 0 } } }

The prompt enhancer no longer reads stage directions aloud ​

target: "audio" asked the model for "tone, pacing and delivery notes", which produced bracketed tags ([Tone: Warm]) that no TTS model in the catalog understands — so they were spoken. A 1.35s line became 12.5s of a voice reciting them, billed per character.

There are now three targets: audio.speech (the words to say, tags forbidden), audio.music (genre, instruments, BPM) and audio.sfx (one sound described). Bare audio still works and means audio.speech, but it is the wrong instruction for a music prompt — send the specific one.

Every plan reaches every model ​

Plans no longer differ by which models they grant. They differ by how much you may use, which is what limits has always described. Practically: allowed is now true on every row for any signed-in user, and availableOn is empty because there is no cheaper plan that unlocks anything.

Gate your UI on quota, not on the model list. A user who runs out sees QUOTA_EXCEEDED at generate time, and that is the message to design for.

Six more models are live ​

Qwen Image 3 and 3 Pro plus Qwen3 TTS (Alibaba), Grok Imagine image and video (xAI), and two OpenRouter text models. Two things worth knowing before you wire them up:

They are slow. Qwen Image measured 64–89 seconds and Grok Imagine 29–76, against 2–24 for the Gemini image models. If a proxy sits between you and this API, its timeout needs to exceed that — a request killed in transit still generates and still bills.

xAI ignores exact pixel sizes. It accepts an aspect ratio from a fixed menu, so width/height are snapped to the nearest one. Ask for 1536×2048 and you get 3:4 framing, not those pixels.

BL never finished: it creates the subscription, texts the subscriber a five-digit OTP valid for five minutes, and waits. There was nowhere to submit that PIN, so checkout simply stopped — no charge, no subscription, no error.

New endpoint: POST /v1/payments/dcb/verify-pin with { paymentId, pinCode }. A wrong or expired PIN returns 400 and can be retried once the customer requests another.

Only some carriers need this. GET /v1/payments/channels marks them: the paymentProvider field's options now carry needsPin. Grameenphone and Robi complete on the redirect and need no PIN screen. The carrier list itself is admin-configurable now, so read it rather than hardcoding operators.

A plan can restrict which gateways buy it ​

Plan.allowedChannels limits a plan to particular payment channels — a carrier-billed daily pack is registered with the operator at one amount and cannot be charged another way. Empty means all, which is what every existing plan carries, so nothing changes unless it is configured.

Checkout with a disallowed gateway returns 400 naming what is allowed:

jsonc
{ "error": { "code": "BAD_REQUEST",
  "message": "Plan dcb-daily cannot be paid for with bkash. Allowed: dcb",
  "details": { "requested": "bkash", "allowedChannels": ["dcb"] } } }

bKash one-off and bKash auto-renew count as one channel, so allowing either allows both — nobody is refused for ticking "renew automatically".


2026-08-17 — the model lineup was rebuilt ​

This one does change what you already built against. The catalog was re-picked model by model against a direct-vendor vs OpenRouter cost comparison, and most of what was there before is gone.

Read the catalog, do not hardcode slugs ​

Twelve providers were retired: OpenAI, Anthropic, DeepSeek, Moonshot Kimi, Replicate, Stability, Kling, Ideogram, Luma, Runware, ElevenLabs and Tavus. Every model under them is now DISABLED — dropped from GET /v1/models, and refused by the gateway with MODEL_UNAVAILABLE (503) rather than generating.

If you hardcoded a slug anywhere — openai/gpt-4o-mini, fal/flux-schnell, replicate/sdxl — it will 503. Build pickers from GET /v1/models, which is what it is for. Nothing was deleted, so past generations, usage records and invoices still resolve their model.

The lineup is now nine providers: Google Gemini, fal.ai, Black Forest Labs, MiniMax, Runway, OpenRouter, BytePlus ModelArk, Alibaba DashScope and xAI.

Four of those cannot serve traffic yet ​

OpenRouter, BytePlus, Alibaba and xAI are registered with their models and pricing, but no adapter has been written for them, so they ship DISABLED. Their models are not in GET /v1/models and will not appear until the integration lands. Nothing to do on your side — this is here so the gap is not a surprise when you read the admin catalog and see rows you cannot call.

What you can call today: Gemini text and multimodal, Nano Banana 2 Lite / 2 / Pro for image, Veo 3.1 Lite and Fast for video with audio, Gemini 3.1 Flash TTS, Lyria 3 for music, Seedream 4.5 on fal, FLUX.2 Klein 4B, MiniMax H3 and Runway Gen-4.5.

The slug is the vendor's API id, not the marketing name ​

Match on id or slug from GET /v1/models and render displayName — do not try to construct a slug from a product name. "Nano Banana 2 Lite" is gemini/gemini-3.1-flash-lite-image; "Nano Banana Pro" is gemini/gemini-3-pro-image. Same for Seedance, whose BytePlus ids carry a dated build tag (byteplus/dreamina-seedance-2-5-260628). Every id in the catalog is now checked against the provider's own live model list by npm run catalog:verify-ids.

Chat still works, on Gemini ​

The cost comparison this lineup came from covered generative media only and listed no text model at all. Gemini's text and multimodal models were kept deliberately so /v1/ai/text and /v1/chat keep working — gemini/gemma-4-31b-it is the cheap default and gemini/gemini-flash-latest the general one.

Plan grants were repaired, not emptied ​

Every plan's allowedModelIds had the retired ids stripped out. The free and starter tiers were re-resolved onto the new lineup rather than left pointing at models that no longer exist, so servableModelIds on GET /v1/plans is accurate again and no plan grants zero models.


2026-08-15 — fixes from the frontend integration pass ​

Everything in the 2026-08-13 findings report, in the order it was raised. Two of them were taking your users' money for things they did not receive.

Verification status. The contract changes below — the error enum, the async flag, handle on login, cost and the pricing unit — were read back off a running build's /docs-json and are confirmed. The behavioural ones — the count top-up and its refund, the async job actually running, the 503 for a retired model, availableOn filtering — have tests but had no live run when this was written, because the database was unreachable from where it was built. Worth one pass on staging before you rely on them.

POST /v1/ai/image can run as a job now — async: true ​

Your proxy was right and the API was wrong. The OpenAI image models take 40–55s; most proxies, CDNs and edge runtimes hang up well before that, and the customer saw a 500 while the generation completed and billed anyway.

jsonc
POST /v1/ai/image  { "model": "openai/gpt-image-2", "prompt": "…", "async": true }
// -> { "jobId": "job_…", "status": "QUEUED" }        immediately

Then poll GET /v1/jobs/{id} or subscribe to GET /v1/jobs/{id}/stream — the same job API video already uses, so if you can render a video job you can render this. On success the row carries output: { images, usage }.

One difference worth knowing: the response has already returned by the time credits are checked, so a credit or model problem arrives on the job (status: "FAILED", errorCode: "QUOTA_EXCEEDED") rather than as an HTTP status. A bad or retired model slug still fails synchronously.

Sync is still the default and still right for Gemini (~8s).

count is honoured everywhere — and you are only charged for what arrives ​

The Gemini image models return one image per call whatever you ask for. Two things were wrong: you got one image, and the wallet was debited for two.

Now the extra images are fetched concurrently until the count is met, and the charge is for images delivered, not requested. If a top-up call fails you get the ones that worked and the rest is refunded.

The first time a model under-delivers, capabilities.maxBatch is recorded on it, so the studio can cap the control instead of the customer discovering the limit. Models that batch properly are unaffected — still one upstream call.

Pricing carries its unit ​

pricing was our provider cost, in USD cents on some rows and BDT poisha on others, distinguishable only by whether currency happened to be present. Your workaround read it as credits, so a ৳5.07 model was advertised at "507 credits". Both readings were wrong, and that is our fault for shipping the field bare.

Now every row is normalised to one currency and says what it is:

jsonc
"pricing": { "perImage": 507, "currency": "BDT", "unit": "PROVIDER_COST_MINOR",
             "note": "what the model costs us … Not the customer price — see `cost`." },
"cost":    { "requests": 1, "imageCredits": "one per image requested" }

Render cost, never pricing. An image is one image credit whichever model draws it — the per-model number is our cost of goods, not a price list.

availableOn no longer names the plan the caller is already on ​

That is what made every locked row read "Pay As You Go". It is filtered out now.

The zero price is not a bug, though: Pay As You Go grants model access with no included allowance, and the customer buys credit packs instead of subscribing. So it genuinely is the cheapest route to a model, and the honest rendering is "included with Pay As You Go — buy credits" rather than an upgrade button. If a priceCents: 0 entry still leads after the filter, that is the real answer.

A retired model answers 503 MODEL_UNAVAILABLE, not 404 ​

You had it right: 404 reads as your bug. 404 now means "no such slug" and nothing else. A slug that exists but cannot be served — retired, disabled, or its provider has no working credential — is MODEL_UNAVAILABLE, which is also what a recipe naming openai/dall-e-3 will now return.

That gives you the staging case you asked for: openai/dall-e-3 is pinned DISABLED on this deployment, so it is a live example to exercise your "pick another model" branch against.

MODEL_UNAVAILABLE is also in the published error enum now, so generated types can represent it.

recipe.params is populated — with one caveat on the post you tested ​

New generations already stored width/height/count, so the recipe returns them. It now also falls back to the output media's own dimensions and derives aspectRatio from them (768x1024 → 3:4), omitted rather than guessed when nothing standard is within 4%.

The specific post you quoted will still come back empty, and it is worth knowing why. Pulling it apart during verification showed it is seeded demo data whose output carries no dimensions either:

jsonc
"images": [{ "url": "https://picsum.photos/seed/nook/768/1024", "mimeType": "image/jpeg" }]

The size only exists inside that URL as text, which we will not parse. The seeder now records params and dimensions like a real generation does, so this stops happening on freshly seeded environments — but the rows already sitting in a deployed database keep their empty params until it is reseeded. Keep your own default for an empty params: the recipe omits what it cannot know rather than inventing a shape.

Login returns handle ​

POST /v1/auth/login, /signup and the Google routes all return user.handle and user.avatarUrl now — no follow-up GET /v1/users/me to link to the signed-in user's own profile.


2026-08-11 — server-side model instructions ​

Every model is now given an instruction by the backend before your prompt reaches it, and the token defaults are resolved server-side. Nothing you send today breaks; this mostly means you can stop sending things you used to have to.

You no longer need to send a system message ​

changelog.mdPOST /v1/ai/text used to forward messages verbatim, so a caller that sent no system message got the raw model's default behaviour — restating the question, opening with "Certainly! Here is…", answering in the wrong language. Each client worked around that separately, which is why output differed between the storefront and the playground.

The platform now prepends a short instruction (~42 tokens) per model type. Your own system message is still honoured and is placed after ours, so where they conflict the more specific one is what the model read last. Send nothing and you get sensible behaviour; send your own and it wins.

An admin can override the instruction per model, so output quality improves without a client release.

A default output cap: maxTokens 2048 ​

When you do not pass maxTokens, text and multimodal calls now default to 2048. Billing is on actual output, so this only bites runaway answers — but if you have a long-form feature (an article, a full translation), pass maxTokens explicitly and nothing is applied on top of it.

Per-model defaults can differ; GET /v1/models does not expose them, since the only thing a client needs to know is "pass your own if you need more".

Chat history is trimmed by token budget ​

/v1/chat replayed up to 40 turns regardless of length. It now replays as much recent history as fits a per-model token budget (a quarter of the context window, capped at 24k tokens). Oldest turns fall out first; the newest message and the system instruction always survive.

Practical effect: a long thread stays cheap and stops silently hitting the context limit. If your product needs the model to remember something from 200 turns ago, put it in the conversation's system field rather than relying on replay — that is never trimmed.

Image and video get a default negative prompt ​

When you send no negativePrompt, image and video models are given a short default (blurry, low quality, watermark…). Send your own and it is used instead — the default never overrides you. Diffusion models get no prose instruction: a system paragraph would end up rendered into the picture.

One finding worth knowing ​

Not every model is improved by an instruction. On gemini/gemma-4-31b-it the instruction was restated instead of followed — and cost 74 input tokens against 6 without it, so the output was worse at 12× the price. That family is sent nothing by default. If a model behaves oddly, that is the first thing to check with us.


2026-08-11 — the social layer ​

The community went from "a feed you can like" to a loop: publish → people share and watch → someone makes their own version → back to the studio. Six new endpoints, four new fields on every post.

Full reference: Publish, share, remix.

New endpoints ​

MethodPathAuthWhat for
POST/v1/community/posts/:id/shareoptionalCount a share and get ready-made per-network links
GET/v1/community/posts/:id/analyticsauthor or adminPer-post totals + a daily series
GET/v1/community/me/analyticsrequiredCreator dashboard across every post
GET/v1/community/posts/:id/recipeoptionalWhat a post was made with, ready to prefill the studio
GET/v1/community/posts/:id/remixesoptionalThe posts made from this one
GET/v1/community/feed/recent?tag=optionalHashtag page — the recent feed filtered to one tag

New fields on every post ​

shareCount, remixCount, and remixOf (the post this was made from, or null) now come back from feed/recent, feed/trending, feed/following, users/:handle/posts, posts/:id, and posts/:id/remixes.

On the /works gallery the same numbers arrive inside _magic, plus remixable — false for imported (Kling) works, which have no recipe of ours to hand back, so the remix button has to be hidden on those cards rather than fail on tap.

POST /v1/community/posts takes remixOfPostId ​

Pass it when the generation came from another post's recipe. The original gets credit and its remixCount goes up — once, however many times you re-publish. Publishing the same generationId twice still updates the existing post rather than creating a second card, so a retry after a dropped response is safe.

Every account now has a handle ​

78 of 82 existing accounts had none, which meant their profile had no address and they could not be followed. All backfilled; new signups get one automatically. Read it from GET /v1/users/me → user.handle (still editable through PATCH /v1/account/profile).

trendingScore is now (likes*3 + comments*4 + shares*6 + remixes*8 + views*0.5) / hours_since^1.5, recomputed every 15 minutes. If you sort or badge on this value, expect the ordering to shift — shares and remixes now outrank likes.

Analytics caveat worth surfacing in the UI ​

Daily view history starts 2026-08-11. Days before that report 0 views while the all-time totals.views is correct — the response carries a note saying so. Don't let a flat start read as a dead post.


2026-08-10 — model gating, image sizes, carrier billing ​

availableOn on every model row ​

GET /v1/models rows already had allowed (does the caller's plan include this?). That says to lock a row but not what to offer instead. Each row now also carries availableOn: the sellable plans that include the model, cheapest first, so availableOn[0] is the upgrade to name.

jsonc
{
  "slug": "openai/gpt-image-1", "displayName": "GPT Image", "type": "IMAGE",
  "allowed": false,                       // null when anonymous
  "availableOn": [                        // cheapest first; [] if no plan carries it
    { "slug": "starter-weekly", "name": "Starter", "priceCents": 19900 },
    { "slug": "pro-monthly", "name": "Pro", "priceCents": 99900 }
  ]
}

Build the picker from this instead of hardcoding the plan ladder — an admin editing a plan would silently drift a hardcoded one. The internal playground plan is never named: it grants everything and is not for sale.

For a picker that only shows what the user can already use, GET /v1/models/me is unchanged.

New error code: MODEL_UNAVAILABLE (503) ​

Being on the plan is not the same as being usable. A model the admin disabled, a provider that lost its credential, or one the upstream retired is now refused before credits are reserved:

StatusCodeMeansWhat to show
403MODEL_NOT_IN_PLANTheir plan doesn't include itUpsell — name availableOn[0]
503MODEL_UNAVAILABLEThe model itself is not usable right now"Pick another model" — not an upgrade prompt

Keep them apart in the UI. With MODEL_UNAVAILABLE the customer did nothing wrong and upgrading would not help.

Image sizes are snapped, not rejected ​

POST /v1/ai/image with a width/height an OpenAI-family model does not accept no longer fails with the provider's wording (Invalid size '768x1024'). The size is snapped to the closest aspect ratio the model offers, so a portrait request never comes back landscape:

Model familyAccepted
gpt-image-*1024×1024 · 1024×1536 · 1536×1024
dall-e-31024×1024 · 1024×1792 · 1792×1024
dall-e-2256×256 · 512×512 · 1024×1024

Anything else (other providers, proxies, fine-tunes) is passed through unchanged. Since the size you send may be snapped, don't lay out assuming exact pixels — some providers report width/height per image, OpenAI does not.

Carrier billing (dcb) only offers what it can actually bill ​

GET /v1/payments/channels returns the dcb carrier options from what this merchant has registered — pick from that list, don't hardcode GP/BL/ROBI/ROBI_WAP/BKASH. A carrier without a registered plan refuses every request, so listing it hands the customer a dead end.

Amounts are registered per carrier too, and that is a fact about the amount, not a gateway having a bad minute. It now answers 400 with this amount cannot be charged to a mobile balance — please choose another payment method instead of a 503 that invites a retry which will never work.

Per-checkout return URLs are honoured on the way back ​

If you pass your own success/cancel/fail URLs to POST /v1/payments/checkout, they are now stored and used when the gateway returns. They used to be accepted, forwarded, then ignored — a cancel page carrying ?item=…&type=credits lost its query and the customer landed on the generic page.

Carrier billing never sees your URLs (Hullor validates and refuses one it cannot reach); it goes through our own callback and then on to your page.

Four new plans to sell ​

SlugNameIntervalPriceNotes
dcb-dailyDaily PassDAILY৳5Priced to what carrier billing can charge
mini-weeklyMiniWEEKLY৳29Minimal access tier
tokens-monthlyTokensMONTHLY৳99Text-heavy: 600k tokens, no image/video credits
dcb-monthlyMonthly PassMONTHLY৳150Carrier-billable monthly

Read them from GET /v1/plans as always — never hardcode prices or the ladder.


Earlier ​

Plan switching (prorated upgrades, prepaid downgrades), coupons, payment channels, bKash recurring and agreements, and the grace period are all covered in Billing — client integration.

kikori.ai — internal documentation