Pricing and credits
1 credit = 1 converted page = $0.0005. Credits are prepaid and spent one page at a time. No subscription, no per-request fee, no minimum charge per document, and no page-count rounding on a conversion.
Every credit quantity in the API is a non-negative integer. There are no fractional credits anywhere, ever, and no floating-point number touches the billing path — money is tracked in integers end to end.
The four packs
Section titled “The four packs”Buy from Buy credits at kaho.ai/app/billing. Checkout is hosted by Stripe; no card details reach us. The bonus is real credits, not a discount code.
| Price | Base credits | Bonus | Total credits | Effective per 1,000 pages |
|---|---|---|---|---|
| $25 | 50,000 | — | 50,000 | $0.5000 |
| $100 | 200,000 | +10% | 220,000 | $0.4545 |
| $500 | 1,000,000 | +15% | 1,150,000 | $0.4348 |
| $2,500 | 5,000,000 | +20% | 6,000,000 | $0.4167 |
There is no arbitrary-amount field, and $25 is the floor. Stripe’s flat 30¢ is 9.0% of a $5 charge and 4.1% of a $25 one, and sub-$5 charges are exactly the range in which stolen cards get tested; a $25 minimum with no free-form amount removes that probe entirely. $2,500 is the self-serve ceiling. Above it is a contract invoice, never self-serve checkout.
Credits are granted from Stripe’s own webhook after the payment settles — never from the browser coming back, and never from anything the customer’s request can influence. Purchases in a currency other than USD are automatically refunded rather than converted at a guessed rate.
Why credits and not dollars
Section titled “Why credits and not dollars”A credit is an abstract unit whose standard-page price happens to be 1. Pricing in credits rather than micro-dollars means a price cut cannot silently make an existing balance buy more pages than it was sold to buy, the bonus ladder lands on exact integers with no dust, and no response ever has to perform a division — and divisions are where rounding bugs live.
The rate card
Section titled “The rate card”Rate card version 1 is immutable, and it is pinned into every credit hold, so the price you
were quoted is the price you are charged. Your account’s version is rate_card_version in
GET /v1/account.
| Line item | Rate | Applies to | In this release |
|---|---|---|---|
page_convert | 1 credit per billable page | POST /v1/convert | Yes |
toc_outline | 1 credit per request | POST /v1/toc | Yes |
page_probe | 1 credit per 100 pages, rounded up | POST /v1/inspect with scan: "full" | No — /v1/inspect is not enabled |
toc_infer | 1 credit per billable page | POST /v1/toc, inference path | No — the inference path is not enabled |
page_convert and toc_infer are never rounded: one page, one credit. page_probe and
toc_outline are the only two line items that round, neither is a conversion charge, and the
“no page-count rounding” promise is scoped to conversion for exactly that reason.
What a conversion costs
Section titled “What a conversion costs”billable_pages = pages_selected − { p : some warning w has severity "critical", scope "page", and p in w.pages }
cost_credits = |billable_pages| × 1Not input bytes, not output bytes, not wall clock — pages, because “no page-count surprise” is a promise and the other three are not things you can predict before you send the request. The billable set is computed by the converter from the document; nothing in your request can influence it.
Each unique page bills exactly once regardless of how many ranges cover it: 1-5,3-7 selects seven
pages and bills seven credits.
What bills and what does not
Section titled “What bills and what does not”| Outcome | Billed |
|---|---|
200 | Pages converted, minus every page in usage.pages_not_billed |
Any 4xx, including 402 and 429 | 0 |
Any 5xx, any timeout, any converter crash | 0 |
A page carrying a critical page-scope warning | 0 for that page; the rest of the request bills normally |
| Cache hit | Full rate — you received the product |
This is contractual, and it is machine-checkable from the response alone:
Every page appearing in a
criticalpage-scope warning appears inusage.pages_not_billed, and every page inusage.pages_not_billedappears in acriticalpage-scope warning.
Assert both in your CI. We do.
Pages with no text layer never bill
Section titled “Pages with no text layer never bill”The three critical page-scope codes are page_has_no_text_layer, page_render_failed and
page_budget_exceeded. The one you will actually meet is the first: a page that yields no
extractable characters — a scan, a photograph, a page that is one big image — produced nothing of
value, so it is removed from the bill and the response says which pages and why.
There is no OCR in v1, so a scanned document converts to nothing and costs nothing:
{ "warnings": [ { "code": "page_has_no_text_layer", "severity": "critical", "scope": "page", "message": "This page carries no extractable text layer, so it produced no content and was not billed.", "pages": [7], "billed": false, "doc_url": "https://docs.kaho.ai/warnings/page_has_no_text_layer" } ], "usage": { "pages_processed": 19, "pages_billed": 16, "pages_not_billed": [7, 8, 12], "credits_charged": 16, "credit_balance_after": 49982, "line_items": [{ "kind": "page_convert", "quantity": 16, "credits": 16 }], "cache": "miss" }}One warning object per code and page, which is what makes warning_summary a useful count; the
array above is abridged to one of the three.
If no page in the document has extractable text, the request fails outright with
422 document_has_no_text_layer and bills nothing at all.
Holds and settlement
Section titled “Holds and settlement”We cannot know what a conversion costs until the PDF is parsed, so a request takes a credit hold on admission and settles the real figure when it completes. The hold is never larger than it has to be:
| What you send | Held |
|---|---|
A bounded pages expression such as 1-5 or 1-5,9 | Exactly the selected count |
pages omitted, null, or unbounded such as 10-last | 200 credits, the synchronous ceiling |
POST /v1/toc | 1 credit |
The 200-credit ceiling is a ten-cent hold released within one round trip when the request settles. Charged is always less than or equal to held, by construction and by a database constraint. Any hold left by a request that does not bill is released within 60 seconds.
You are never charged the hold. You are charged the settled figure, and it is in
usage.credits_charged on the response and in the x-credits-charged header.
Checking your balance
Section titled “Checking your balance”Three places, for three different questions.
Before you call
Section titled “Before you call”curl -sS https://api.kaho.ai/v1/account \ -H "Authorization: Bearer $KAHO_API_KEY"credit_balance is what you can spend, credits_held is what in-flight requests have reserved,
and debt_credits is what a reversed payment left owing. limits in that response is your
account’s limits, not the published defaults. The full body is in
Endpoints.
On every response, for free
Section titled “On every response, for free”Every conversion carries the money in headers, so pacing needs no extra call:
x-credits-charged: 2x-credits-balance: 49998x-credits-unit: pagex-credits-unit is page on every endpoint — one credit is one page, everywhere.
Afterwards, authoritatively
Section titled “Afterwards, authoritatively”curl -sS -G https://api.kaho.ai/v1/credits/ledger \ -H "Authorization: Bearer $KAHO_API_KEY" \ --data-urlencode "limit=100"GET /v1/credits/ledger reads the account’s ledger directly and is the authoritative record of
every credit movement: it is append-only and hash-chained, so entry_hash lets you verify the
sequence has not been rewritten. GET /v1/usage and GET /v1/usage/records are indexed reporting
copies — faster to query, groupable by endpoint, and able to lag the ledger slightly. Reconcile
money against the ledger; analyse behaviour with usage.
The console shows the same figures at kaho.ai/app, read from the reporting copy so a console page can never contend with the money path.
Running out of credits
Section titled “Running out of credits”At zero, a conversion is refused before any work is done, and the refusal tells you exactly how short you are and where to fix it:
{ "error": { "type": "payment_required_error", "code": "insufficient_credits", "message": "This account does not have enough credits for this request.", "param": null, "retryable": false, "retry_after_ms": null, "doc_url": "https://docs.kaho.ai/errors/insufficient_credits", "request_id": "req_01M09AXCT0SKR13Z2Z6CN6MQ64", "details": { "credits_required": 200, "credits_available": 43, "top_up_url": "https://kaho.ai/app/billing" } }}Three things are true of that response and worth planning around.
It bills zero, like every other 4xx. Running out costs nothing.
credits_required is the hold, not the true cost. If you sent an unbounded page range it is
the 200-credit ceiling, even for a three-page document. Send a bounded pages expression and the
requirement becomes exact.
It is not retryable. Retrying an identical request against an unchanged balance produces an identical refusal. Top up, then retry.
The other 402 is account_delinquent: a reversed payment left the account owing credits, and
spending is frozen until that clears. A top-up smaller than the debt is refused at checkout naming
the amount required, so you are never told to pay without being told how much.
Trust tiers
Section titled “Trust tiers”Purchase limits rise with account history. The tier is an integer in GET /v1/account as
trust_tier, and it is computed from settled payments, never from anything you can assert.
| Tier | Entry | Max single top-up | Max per 24 h | Max balance |
|---|---|---|---|---|
| 1 New | First settled payment | $100 | $100 | 400,000 credits |
| 2 Funded | ≥ 7 days since the first settled payment, no risk events | $500 | $1,000 | 4,000,000 credits |
| 3 Seasoned | ≥ 30 days, ≥ $250 lifetime settled, no risk events | $2,500 | $5,000 | 20,000,000 credits |
| 4 Contract | Manual approval; invoice, wire or ACH | — | — | Negotiated |
A refund, a dispute or a fraud warning in the trailing 90 days returns the account to tier 1 immediately — there is no grace period and no hysteresis, because those are precisely the events after which a larger balance is a worse idea.
Tier 0 is an account with no settled payment. GET /v1/account also reports
limits.burn_cap_credits_per_day, a ceiling on credits spent per 24 hours regardless of balance;
when it is 0, no burn cap applies to your account. If one does apply and you need more on day
one, ask support.
Credit lifetime and refunds
Section titled “Credit lifetime and refunds”Paid and bonus credits never expire. There is no monthly reset and no expiry cliff. Goodwill credits, if we ever issue you any, expire after 12 months and are the only expiring source.
Unspent credits from a purchase are refundable to the original payment method within 30 days, twice per account lifetime, and the refund is proportional to what is left: spend half the pack and half the money comes back. Beyond 30 days it is discretionary and we default to yes. Write to support with the purchase; there is no self-serve refund button in the console yet.
A top-up is characterised in the terms of service as a prepayment for a specific service — not stored value, not a gift card, not a general-purpose balance. That is why there is no cash-out.
Evaluating without a free tier
Section titled “Evaluating without a free tier”Deleting the free tier reads as hostile unless the replacement is stated in the same breath, so here it is.
| What you are actually asking | How to answer it |
|---|---|
| “Does the integration work — auth, errors, retries, idempotency?” | A kaho_sk_test_ key, minted at signup, free. Every endpoint, every error shape, every header, 5,000 pages a month, and X-Kaho-Simulate to force failures on demand |
| “Is the output any good on my documents?” | The $25 pack — 50,000 pages, which is a large evaluation — with unspent credits refundable for 30 days. An evaluation that does not work out costs you the processing fee and nothing else |
| “Will there be a bill surprise?” | There is no per-request fee, no minimum charge and no rounding on conversion, and every response carries usage.credits_charged and credit_balance_after. A bounded pages expression makes the cost exactly the number of pages you named |
Reinstating a free tier is a cron job and a table. Reversing a terminated merchant account is not available at any price, which is why the decision went the way it did.
The house eats the rounding
Section titled “The house eats the rounding”Where a calculation could land between two integers, it lands in your favour: grants round up, bonus credits round up, refunded cash rounds up, credit clawback rounds down. The most the house can be wrong by on any single event is one credit — half a thousandth of a cent.