Quickstart
Four steps, in this order: create an account, put credits on it, mint a key, convert a document. All of it is self-serve — there is no sales call and no waiting list.
-
Create an account
Section titled “Create an account”Go to kaho.ai/signup and give us an email address. We send a sign-in link; clicking it creates the account and signs you in.
There is no password. Authentication is a magic link, so there is nothing for you to store, nothing to reset, and nothing for anyone to stuff. The same link works whether you are signing up or signing back in, so kaho.ai/login and the signup page are interchangeable.
You now have an account with zero credits and no API key. Both of the next two steps are things you do; neither happens automatically.
-
Buy credits
Section titled “Buy credits”Open Buy credits at kaho.ai/app/billing and pick a pack. Checkout is hosted by Stripe, so no card details ever touch us, and the card form asks for strong customer authentication.
Price Credits Bonus Effective per 1,000 pages $25 50,000 — $0.5000 $100 220,000 +10% $0.4545 $500 1,150,000 +15% $0.4348 $2,500 6,000,000 +20% $0.4167 One credit converts one page. Paid and bonus credits never expire, there is no subscription to cancel, and there is no minimum monthly spend. The $25 floor exists because card fees are 9% of a $5 charge against 4.1% of a $25 one, and because removing sub-$5 charges removes the cheap card-testing probe entirely.
Credits are granted when the payment settles, from Stripe’s own webhook — not when the browser comes back. The return page polls until they land, which is usually seconds.
-
Mint an API key
Section titled “Mint an API key”Open API keys at kaho.ai/app/keys and create one.
A name is required, not optional: one form field is the difference between an incident where you know which key leaked and one where you do not. Names are 1 to 60 characters, and an account holds at most 20 active keys at a time.
The plaintext key is shown exactly once, on the page that creates it. We store only
HMAC-SHA-256(pepper, key), so there is no reveal button — there is nothing to reveal. If you lose it, revoke it and mint another.Put the key where your shell can see it export KAHO_API_KEY="kaho_sk_live_………"Confirm it works.
GET /v1/accountcosts nothing and needs only theaccount:readscope, which every console-issued key carries.Check the key and read the account curl -sS https://api.kaho.ai/v1/account \-H "Authorization: Bearer $KAHO_API_KEY"200 — a funded account that has not converted anything yet {"object": "account","account_id": "org_01M08YB4Q7WQ0S6TS8R2Q0N6JD","status": "active","env": "live","credit_balance": 50000,"credits_held": 0,"debt_credits": 0,"credit_unit": "page","lifetime_credits_charged": 0,"trust_tier": 1,"rate_card_version": 1,"test_pages_used": 0,"test_pages_limit": 5000,"limits": {"requests_per_min": 120,"pages_per_min": 12000,"concurrency": 4,"upload_bytes_per_min": 536870912,"inspect_per_min": 30,"toc_per_min": 60,"inspect_sample_pages_per_day": 2000,"unbilled_vcpu_ms_per_hour": 60000,"burn_cap_credits_per_day": 0,"max_upload_bytes": 33554432,"max_selected_pages": 200}}trust_tieris an integer,0to4. It rises with account history and it is what sets your purchase ceilings — see Trust tiers. -
Convert your first PDF
Section titled “Convert your first PDF”The shortest call there is: raw PDF bytes in, Markdown out.
One curl, Markdown on stdout curl -sS "https://api.kaho.ai/v1/convert?pages=1-2" \-H "Authorization: Bearer $KAHO_API_KEY" \-H "Accept: text/markdown" \-H "Content-Type: application/pdf" \--data-binary @report.pdfWith
Accept: text/markdownthe body is the Markdown and nothing else; every number from the JSON response is still there, inx-kaho-*andx-credits-*response headers:Response headers on that call x-request-id: req_01M09AXCT0SKR13Z2Z6CN6MQ64x-kaho-engine: kaho-md-1.0.0x-kaho-page-count: 42x-kaho-pages-processed: 2x-kaho-pages-billed: 2x-kaho-warning-count: 2x-kaho-warning-summary: metadata_unavailable=1,engine_alias_floating=1x-credits-charged: 2x-credits-balance: 49998x-credits-unit: pageDrop the
Acceptheader — JSON is the default — and you get the whole object instead:The full JSON response curl -sS "https://api.kaho.ai/v1/convert?pages=1-2" \-H "Authorization: Bearer $KAHO_API_KEY" \-H "Idempotency-Key: $(uuidgen)" \-H "Content-Type: application/pdf" \--data-binary @report.pdf200 application/json {"document": {"page_count": 42,"pages_selected": "1-2","pages_selected_count": 2,"content_sha256": "9d0f2c1b7a4e5638ab90c2d41f7e6b58c3a90d21e4f5b6c7d8e9f0a1b2c3d4e5","quality": { "text_layer_coverage": 1.0000 }},"markdown": "Notice of annual general meeting\n\nThe meeting will be held at …","pages": null,"links": null,"warnings": [{"code": "engine_alias_floating","severity": "info","scope": "document","message": "This request named a floating engine alias. The resolved version is echoed in `engine`.","pages": [],"billed": true,"doc_url": "https://docs.kaho.ai/warnings/engine_alias_floating"},{"code": "metadata_unavailable","severity": "info","scope": "document","message": "Document metadata was not read by this engine build, so the document block carries only what was measured.","pages": [],"billed": true,"doc_url": "https://docs.kaho.ai/warnings/metadata_unavailable"}],"warning_summary": { "metadata_unavailable": 1, "engine_alias_floating": 1 },"timings_ms": { "parse": 18, "convert": 96, "encode": 3, "total": 117 },"id": "conv_01M09AXCT0SKR13Z2Z6CN6MQ64","object": "conversion","engine": "kaho-md-1.0.0","created_at": "2026-08-18T02:23:54Z","usage": {"pages_processed": 2,"pages_billed": 2,"pages_not_billed": [],"credits_charged": 2,"credit_balance_after": 49998,"line_items": [{ "kind": "page_convert", "quantity": 2, "credits": 2 }],"cache": "miss"}}Two pages, two credits, a tenth of a cent. There is no
statusfield on a conversion: a200means we produced a result, and everything that was less than perfect about it is inwarnings.
Reading that response
Section titled “Reading that response”Five fields carry almost all of the meaning.
| Field | What it tells you |
|---|---|
usage.credits_charged | What this request cost. Always 0 on any 4xx or 5xx |
usage.pages_not_billed | Pages we produced nothing useful for and did not charge you for |
warnings | Every degradation, at page granularity. A 200 is never a claim of perfection |
document.content_sha256 | SHA-256 of exactly the markdown bytes you were served |
engine | The resolved engine, always a pinned version, even if you asked for the alias |
engine_alias_floating appears on every request that does not pin an engine, which is every
request that leaves engine at its default of kaho-md-1. It is telling you that the bytes you
just received are reproducible only if you pin: pass engine=kaho-md-1.0.0 and
document.content_sha256 becomes an assertion you can put in CI.
metadata_unavailable also appears on every conversion today. It means the engine did not read
the document’s metadata dictionary, so the response reports only what it measured — no title, no
language, no tagged flag. Absent rather than guessed is the rule: we do not report a fact about
your document that we did not look up.
Three ways to supply the PDF
Section titled “Three ways to supply the PDF”Exactly three encodings, and they may not be mixed. There are no precedence rules, by
construction — precedence rules are where contracts rot. Query parameters alongside a multipart or
JSON body are 400 query_options_not_allowed; anything else is 415 unsupported_content_type.
The simplest, and the right choice for a one-shot conversion. Scalar options only, in the
query string — nested options such as normalize.dehyphenate need the multipart encoding.
A document password cannot be sent this way.
curl -sS "https://api.kaho.ai/v1/convert?pages=1-5,9&page_markers=rule" \ -H "Authorization: Bearer $KAHO_API_KEY" \ -H "Content-Type: application/pdf" \ --data-binary @report.pdfThe general case. The whole options object goes in an options part typed as
application/json, and a document password goes in its own part — never a query parameter
and never a header, both of which are routinely logged by proxies.
curl -sS https://api.kaho.ai/v1/convert \ -H "Authorization: Bearer $KAHO_API_KEY" \ -F "password=hunter2" \ -F 'options={ "pages": "1-5,9", "headers_footers": "strip", "normalize": { "dehyphenate": true, "unicode": "nfc" } };type=application/json'A JSON body names a file you uploaded earlier:
{"source":{"type":"file_id","file_id":"…"}}.
There is no way to upload a file in v1. The endpoint that would create a file_id
is not part of this release, so this encoding cannot be used yet. It is documented because
the shape is fixed and because a JSON body with any other source.type is a typed refusal
you may hit: base64 and url are both 400 unsupported_source_type.
Uploads are capped at 32 MiB, and a request body must arrive within 20 seconds of its first byte.
Make retries safe
Section titled “Make retries safe”Send an Idempotency-Key on every POST. It is optional and strongly recommended, and it is the
difference between a network blip costing you nothing and costing you a second conversion.
curl -sS https://api.kaho.ai/v1/convert \ -H "Authorization: Bearer $KAHO_API_KEY" \ -H "Idempotency-Key: invoice-2026-Q2-84120" \The key is 1 to 255 printable ASCII characters and is scoped to your organization rather than to
the API key, so rotating a key in the middle of a retry cannot double-bill you. A replay of a
completed request returns the original response with Idempotency-Replayed: true and is not
billed again. A retry that arrives while the first attempt is still in flight is
409 idempotency_in_progress with Retry-After: 1. The same key with a different request body is
409 idempotency_conflict. Records live for 25 hours.
Test keys
Section titled “Test keys”Every account can also mint kaho_sk_test_ keys. A test key performs a real conversion against
separate infrastructure, bills 0 credits, and is capped at 5 selected pages per request and
5,000 pages per organization per month. GET /v1/account reports test_pages_used and
test_pages_limit so you can see where you are.
Test keys honour X-Kaho-Simulate, so you can exercise your error handling without constructing a
hostile PDF:
curl -sS https://api.kaho.ai/v1/convert \ -H "Authorization: Bearer $KAHO_TEST_KEY" \ -H "X-Kaho-Simulate: insufficient_credits" \ -H "Content-Type: application/pdf" \ --data-binary @small.pdfOn a live key the header is ignored rather than refused, so a live key can never zero its own pages by accident. The full list of codes it honours is in Authentication.
What to read next
Section titled “What to read next”- Endpoints — every route, with a pasteable call and a real response for each.
- Authentication — key format, scopes, and what to do when a key leaks.
- Pricing and credits — what bills, what does not, and what happens at zero.
- Page ranges — the
pagesgrammar, and how a selection turns into a bill. - Rate limits — the meters, the headers, and how to pace against them.
- Errors — the envelope, the taxonomy, and what
retryableactually means.