Skip to content

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.

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

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

    PriceCreditsBonusEffective per 1,000 pages
    $2550,000—$0.5000
    $100220,000+10%$0.4545
    $5001,150,000+15%$0.4348
    $2,5006,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.

  3. 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/account costs nothing and needs only the account:read scope, 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_tier is an integer, 0 to 4. It rises with account history and it is what sets your purchase ceilings — see Trust tiers.

  4. 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.pdf

    With Accept: text/markdown the body is the Markdown and nothing else; every number from the JSON response is still there, in x-kaho-* and x-credits-* response headers:

    Response headers on that call
    x-request-id: req_01M09AXCT0SKR13Z2Z6CN6MQ64
    x-kaho-engine: kaho-md-1.0.0
    x-kaho-page-count: 42
    x-kaho-pages-processed: 2
    x-kaho-pages-billed: 2
    x-kaho-warning-count: 2
    x-kaho-warning-summary: metadata_unavailable=1,engine_alias_floating=1
    x-credits-charged: 2
    x-credits-balance: 49998
    x-credits-unit: page

    Drop the Accept header — 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.pdf
    200 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 status field on a conversion: a 200 means we produced a result, and everything that was less than perfect about it is in warnings.

Five fields carry almost all of the meaning.

FieldWhat it tells you
usage.credits_chargedWhat this request cost. Always 0 on any 4xx or 5xx
usage.pages_not_billedPages we produced nothing useful for and did not charge you for
warningsEvery degradation, at page granularity. A 200 is never a claim of perfection
document.content_sha256SHA-256 of exactly the markdown bytes you were served
engineThe 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.

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.

Content-Type: application/pdf
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.pdf

Uploads are capped at 32 MiB, and a request body must arrive within 20 seconds of its first byte.

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.

A stable business key beats a random one
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.

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:

Force a 402 without spending anything
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.pdf

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

  • 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 pages grammar, 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 retryable actually means.