Skip to content

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.

$0.0005one credit, one converted page
$25smallest pack — 50,000 pages
neverpaid and bonus credits expire
$0billed for any 4xx, 5xx or timeout

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.

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.

PriceBase creditsBonusTotal creditsEffective per 1,000 pages
$2550,000—50,000$0.5000
$100200,000+10%220,000$0.4545
$5001,000,000+15%1,150,000$0.4348
$2,5005,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.

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.

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 itemRateApplies toIn this release
page_convert1 credit per billable pagePOST /v1/convertYes
toc_outline1 credit per requestPOST /v1/tocYes
page_probe1 credit per 100 pages, rounded upPOST /v1/inspect with scan: "full"No — /v1/inspect is not enabled
toc_infer1 credit per billable pagePOST /v1/toc, inference pathNo — 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.

The cost function, in full
billable_pages = pages_selected
− { p : some warning w has severity "critical", scope "page", and p in w.pages }
cost_credits = |billable_pages| × 1

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

OutcomeBilled
200Pages converted, minus every page in usage.pages_not_billed
Any 4xx, including 402 and 4290
Any 5xx, any timeout, any converter crash0
A page carrying a critical page-scope warning0 for that page; the rest of the request bills normally
Cache hitFull rate — you received the product

This is contractual, and it is machine-checkable from the response alone:

Every page appearing in a critical page-scope warning appears in usage.pages_not_billed, and every page in usage.pages_not_billed appears in a critical page-scope warning.

Assert both in your CI. We do.

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:

A 19-page selection where pages 7, 8 and 12 are image-only
{
"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.

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 sendHeld
A bounded pages expression such as 1-5 or 1-5,9Exactly the selected count
pages omitted, null, or unbounded such as 10-last200 credits, the synchronous ceiling
POST /v1/toc1 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.

Three places, for three different questions.

GET /v1/account — free, needs account:read
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.

Every conversion carries the money in headers, so pacing needs no extra call:

x-credits-charged: 2
x-credits-balance: 49998
x-credits-unit: page

x-credits-unit is page on every endpoint — one credit is one page, everywhere.

The authoritative credit history
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.

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:

402 insufficient_credits
{
"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.

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.

TierEntryMax single top-upMax per 24 hMax balance
1 NewFirst settled payment$100$100400,000 credits
2 Funded≥ 7 days since the first settled payment, no risk events$500$1,0004,000,000 credits
3 Seasoned≥ 30 days, ≥ $250 lifetime settled, no risk events$2,500$5,00020,000,000 credits
4 ContractManual 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.

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.

Deleting the free tier reads as hostile unless the replacement is stated in the same breath, so here it is.

What you are actually askingHow 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.

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.