OCR — Advanced & Handwriting v1.0 60 credits / call
Read printed or handwritten text off an image — full transcription, or named fields off a form, with illegible marks flagged rather than guessed.
- Base path
- /api/v1/ocr
- Methods
- GET or POST — parameters are read identically from the query string or a JSON body
- Auth
- X-Api-Key header (never Authorization: Bearer)
- Price
- 60 credits per call · about $0.0600 at 1,000 credits/USD
- Free tier
- No per-API allowance; calls draw on your account balance
- Test mode
- Not supported; this API requires a live key
- Rate limit
- 60 calls/minute per key by default, raisable per key
- Pricing mode
- Dynamic — the price depends on the request, so it is estimated up front and the difference refunded once the work is done
OCR — Advanced & Handwriting
Send a picture, get the text. Handles printed documents, screenshots, photographed pages, and handwriting — the case classical OCR engines are worst at.
POST /api/v1/ocr
X-Api-Key: ksty_live_...
Content-Type: application/json
{"image": "iVBORw0KGgoAAAANSUhEUg..."}
{
"text": "Meeting notes — 14 March\nRevenue down 12% [?] to 4.1M\nFreeze hiring until Q1",
"lines": ["Meeting notes — 14 March", "Revenue down 12% [?] to 4.1M", "Freeze hiring until Q1"],
"language": "en",
"handwritten": true,
"legibility": "partial",
"unreadable": ["One word between '12%' and 'to' on line 2 — heavily overwritten"],
"usage": { "input_tokens": 1620, "output_tokens": 180 }
}
The thing to understand before you trust it
Classical OCR fails visibly: it returns Rev3nue d0wn and you can see it went wrong. A model
reading bad handwriting fails invisibly — it returns a fluent, sensible word that simply is not
what was on the page, and nothing downstream can distinguish that from a correct read.
This API is built to make that failure visible instead. Three signals, in every response:
| Signal | Read it as |
|---|---|
[?] in the text |
a mark at this position could not be read |
unreadable |
one short note per such spot, describing where |
legibility |
clear, partial or poor for the page overall |
A poor page with four unreadable notes is the API doing its job, not failing at it. Route on
legibility rather than treating every response as equally good — that one branch is the
difference between an automated pipeline you can trust and one that quietly corrupts records.
Endpoints
Transcribe — POST /api/v1/ocr
The whole page, in reading order, with original spelling, punctuation and line breaks preserved.
Nothing is corrected, expanded or translated — a transcription that improves on the original has
stopped being a transcription. lines is the same text split per visual line, which is usually
what you want for tables and forms.
Named fields — POST /api/v1/ocr.fields
Give it the values you actually want and skip the transcription:
{"image": "...", "fields": ["invoice_no", "date", "total", "vat_number"]}
Every field comes back with found and value. Values are copied verbatim — £1,240.00
stays as written rather than becoming 1240.0, because you can normalise a faithful value and
cannot recover a mangled one. A field that isn't on the document, or can't be read, answers
found: false with an empty value rather than a plausible-looking invention.
This endpoint is also cheaper: it holds and spends far less than a full transcription, since it only ever produces a handful of short strings.
Images
PNG, JPEG, GIF or WebP, sent as base64 in image — a bare base64 string or a data: URL both
work. Up to 5 MB decoded, which is roughly 6.7 MB of base64 once encoded.
PDFs are not accepted here. Convert the pages to images first. A scanned PDF is the obvious next thing this API should take, and it is on the list — it is left out of v1 because per-page pricing is a real decision rather than a plumbing change.
Resolution is worth thinking about: text needs to be legible to a human at 100% zoom for the model to read it reliably, but a 12-megapixel phone photo of a receipt costs several times what a sensible crop does, for no accuracy gain. Crop to the document and downscale so the smallest text is still comfortably readable.
hint is optional and helps on ambiguous documents — "a UK VAT invoice" or "a handwritten prescription" gives the model context for what the marks are likely to be, without telling it what
they say.
Pricing
Per token, like text-ai, because that is how the model bills — see the rate table on that page.
An image is charged as input tokens by its pixel area, not its file size, so cropping a photo
cuts the cost where compressing it does not.
The gateway pre-charges an estimate and refunds the difference once the call finishes. That estimate
assumes the full output ceiling, so the hold on a transcription is substantially larger than what a
typical page actually costs — expect the refund, and don't budget against the hold. X-Credits-Charged
is the real figure.
Live keys only, no free allowance
No sandbox and no monthly grant, for the same reason as text-ai: every call spends real money on a
metered model upstream. A ksty_test_ key gets 403 sandbox_disabled. Calls draw on your normal
balance, including the credits granted at signup.
When to use it
- Digitising handwritten forms, notes, or delivery dockets that a classical OCR engine mangles.
- Pulling a fixed set of values off receipts or invoices, with
found: falsetelling you which documents need a human instead of silently producing a wrong total. - Reading screenshots and photographed pages where layout and line breaks matter.
Endpoints
Transcribe all text in an image, printed or handwritten
Preserves the original spelling, punctuation, casing and line breaks. Illegible marks become [?] in the text with a matching note in unreadable — the model is instructed never to substitute a plausible word for one it cannot read.
curl -X POST "https://ksty.ch/api/v1/ocr" \
-H "X-Api-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"image":"iVBORw0KGgoAAAANSUhEUgAA...","hint":"example"}'
<?php
$ch = curl_init('https://ksty.ch/api/v1/ocr');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'X-Api-Key: YOUR_API_KEY',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => '{ "image": "iVBORw0KGgoAAAANSUhEUgAA...", "hint": "example"}',
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
const response = await fetch('https://ksty.ch/api/v1/ocr', {
method: 'POST',
headers: {
'X-Api-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
"image": "iVBORw0KGgoAAAANSUhEUgAA...",
"hint": "example"
})
});
const data = await response.json();
import requests
response = requests.post(
'https://ksty.ch/api/v1/ocr',
headers={'X-Api-Key': 'YOUR_API_KEY'},
json={
"image": "iVBORw0KGgoAAAANSUhEUgAA...",
"hint": "example"
},
)
data = response.json()
{
"text": "Meeting notes — 14 March\nRevenue down 12% [?] to 4.1M",
"lines": [
"Meeting notes — 14 March",
"Revenue down 12% [?] to 4.1M"
],
"language": "en",
"handwritten": true,
"legibility": "partial",
"unreadable": [
"One word on line 2 — heavily overwritten"
],
"usage": {
"input_tokens": 1620,
"output_tokens": 180
}
}
[?] marks a spot that could not be read.
Read named values off a form, receipt or invoice
Send the values you want in fields and get back one {found, value} per name — nothing else. Values are copied verbatim rather than normalised, and a field that is absent or illegible answers found: false instead of an invention.
Cheaper than a full transcription, because it only ever produces a handful of short strings.
curl -X POST "https://ksty.ch/api/v1/ocr.fields" \
-H "X-Api-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"image":"iVBORw0KGgoAAAANSUhEUgAA...","fields":["invoice_no","date","total"],"hint":"example"}'
<?php
$ch = curl_init('https://ksty.ch/api/v1/ocr.fields');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'X-Api-Key: YOUR_API_KEY',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => '{ "image": "iVBORw0KGgoAAAANSUhEUgAA...", "fields": [ "invoice_no", "date", "total" ], "hint": "example"}',
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
const response = await fetch('https://ksty.ch/api/v1/ocr.fields', {
method: 'POST',
headers: {
'X-Api-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
"image": "iVBORw0KGgoAAAANSUhEUgAA...",
"fields": [
"invoice_no",
"date",
"total"
],
"hint": "example"
})
});
const data = await response.json();
import requests
response = requests.post(
'https://ksty.ch/api/v1/ocr.fields',
headers={'X-Api-Key': 'YOUR_API_KEY'},
json={
"image": "iVBORw0KGgoAAAANSUhEUgAA...",
"fields": [
"invoice_no",
"date",
"total"
],
"hint": "example"
},
)
data = response.json()
{
"fields": {
"invoice_no": {
"found": true,
"value": "INV-2291"
},
"date": {
"found": true,
"value": "14/03/2026"
},
"total": {
"found": true,
"value": "£1,240.00"
},
"vat_number": {
"found": false,
"value": ""
}
},
"handwritten": false,
"legibility": "clear",
"usage": {
"input_tokens": 1450,
"output_tokens": 64
}
}
Errors
Errors are JSON with a stable error.code — branch on the code, never on
the message. Everything below carries the X-Request-Id of the failed call.
Requests rejected by this API are not charged, and a 502 after a
charge is refunded automatically.
data: URL is accepted too. For ocr.fields, send a non-empty list of field names.
Response headers
Every successful call reports its own cost, so you never have to guess what a request spent or reconcile it later.