{
    "openapi": "3.0.3",
    "info": {
        "title": "OCR \u2014 Advanced & Handwriting \u2014 Ksty.ch",
        "version": "1.0",
        "description": "Read printed or handwritten text off an image \u2014 full transcription, or named fields off a form, with illegible marks flagged rather than guessed.\n\n# OCR \u2014 Advanced & Handwriting\n\nSend a picture, get the text. Handles printed documents, screenshots, photographed pages, and\n**handwriting** \u2014 the case classical OCR engines are worst at.\n\n```\nPOST /api/v1/ocr\nX-Api-Key: ksty_live_...\nContent-Type: application/json\n\n{\"image\": \"iVBORw0KGgoAAAANSUhEUg...\"}\n```\n\n```json\n{\n  \"text\": \"Meeting notes \u2014 14 March\\nRevenue down 12% [?] to 4.1M\\nFreeze hiring until Q1\",\n  \"lines\": [\"Meeting notes \u2014 14 March\", \"Revenue down 12% [?] to 4.1M\", \"Freeze hiring until Q1\"],\n  \"language\": \"en\",\n  \"handwritten\": true,\n  \"legibility\": \"partial\",\n  \"unreadable\": [\"One word between '12%' and 'to' on line 2 \u2014 heavily overwritten\"],\n  \"usage\": { \"input_tokens\": 1620, \"output_tokens\": 180 }\n}\n```\n\n## The thing to understand before you trust it\n\nClassical OCR fails **visibly**: it returns `Rev3nue d0wn` and you can see it went wrong. A model\nreading bad handwriting fails **invisibly** \u2014 it returns a fluent, sensible word that simply is not\nwhat was on the page, and nothing downstream can distinguish that from a correct read.\n\nThis API is built to make that failure visible instead. Three signals, in every response:\n\n| Signal | Read it as |\n|---|---|\n| `[?]` in the text | a mark at this position could not be read |\n| `unreadable` | one short note per such spot, describing where |\n| `legibility` | `clear`, `partial` or `poor` for the page overall |\n\nA `poor` page with four `unreadable` notes is the API doing its job, not failing at it. **Route on\n`legibility` rather than treating every response as equally good** \u2014 that one branch is the\ndifference between an automated pipeline you can trust and one that quietly corrupts records.\n\n## Endpoints\n\n### Transcribe \u2014 `POST /api/v1/ocr`\n\nThe whole page, in reading order, with original spelling, punctuation and line breaks preserved.\nNothing is corrected, expanded or translated \u2014 a transcription that improves on the original has\nstopped being a transcription. `lines` is the same text split per visual line, which is usually\nwhat you want for tables and forms.\n\n### Named fields \u2014 `POST /api/v1/ocr.fields`\n\nGive it the values you actually want and skip the transcription:\n\n```json\n{\"image\": \"...\", \"fields\": [\"invoice_no\", \"date\", \"total\", \"vat_number\"]}\n```\n\nEvery field comes back with `found` and `value`. Values are copied **verbatim** \u2014 `\u00a31,240.00`\nstays as written rather than becoming `1240.0`, because you can normalise a faithful value and\ncannot recover a mangled one. A field that isn't on the document, or can't be read, answers\n`found: false` with an empty value rather than a plausible-looking invention.\n\nThis endpoint is also cheaper: it holds and spends far less than a full transcription, since it\nonly ever produces a handful of short strings.\n\n## Images\n\nPNG, JPEG, GIF or WebP, sent as base64 in `image` \u2014 a bare base64 string or a `data:` URL both\nwork. Up to 5 MB decoded, which is roughly 6.7 MB of base64 once encoded.\n\n> **PDFs are not accepted here.** Convert the pages to images first. A scanned PDF is the obvious\n> next thing this API should take, and it is on the list \u2014 it is left out of v1 because per-page\n> pricing is a real decision rather than a plumbing change.\n\nResolution is worth thinking about: text needs to be legible to a human at 100% zoom for the model\nto read it reliably, but a 12-megapixel phone photo of a receipt costs several times what a sensible\ncrop does, for no accuracy gain. Crop to the document and downscale so the smallest text is still\ncomfortably readable.\n\n`hint` is optional and helps on ambiguous documents \u2014 `\"a UK VAT invoice\"` or `\"a handwritten\nprescription\"` gives the model context for what the marks are likely to be, without telling it what\nthey say.\n\n## Pricing\n\nPer token, like `text-ai`, because that is how the model bills \u2014 see the rate table on that page.\nAn image is charged as input tokens by its **pixel area**, not its file size, so cropping a photo\ncuts the cost where compressing it does not.\n\nThe gateway pre-charges an estimate and refunds the difference once the call finishes. That estimate\nassumes the full output ceiling, so the hold on a transcription is substantially larger than what a\ntypical page actually costs \u2014 expect the refund, and don't budget against the hold. `X-Credits-Charged`\nis the real figure.\n\n## Live keys only, no free allowance\n\nNo sandbox and no monthly grant, for the same reason as `text-ai`: every call spends real money on a\nmetered model upstream. A `ksty_test_` key gets `403 sandbox_disabled`. Calls draw on your normal\nbalance, including the credits granted at signup.\n\n## When to use it\n\n- Digitising handwritten forms, notes, or delivery dockets that a classical OCR engine mangles.\n- Pulling a fixed set of values off receipts or invoices, with `found: false` telling you which\n  documents need a human instead of silently producing a wrong total.\n- Reading screenshots and photographed pages where layout and line breaks matter.",
        "x-credit-cost": 60,
        "x-free-grant": {
            "type": "none",
            "amount": 0
        },
        "x-sandbox": false
    },
    "servers": [
        {
            "url": "https://ksty.ch/api/v1"
        }
    ],
    "security": [
        {
            "ApiKeyAuth": []
        }
    ],
    "paths": {
        "/ocr": {
            "post": {
                "operationId": "ocr_post",
                "summary": "Transcribe all text in an image, printed or handwritten",
                "x-credit-cost": 60,
                "responses": {
                    "200": {
                        "description": "Success",
                        "content": {
                            "application/json": {
                                "example": {
                                    "text": "Meeting notes \u2014 14 March\nRevenue down 12% [?] to 4.1M",
                                    "lines": [
                                        "Meeting notes \u2014 14 March",
                                        "Revenue down 12% [?] to 4.1M"
                                    ],
                                    "language": "en",
                                    "handwritten": true,
                                    "legibility": "partial",
                                    "unreadable": [
                                        "One word on line 2 \u2014 heavily overwritten"
                                    ],
                                    "usage": {
                                        "input_tokens": 1620,
                                        "output_tokens": 180
                                    }
                                },
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "text": {
                                            "type": "string",
                                            "description": "The full transcription with original line breaks. `[?]` marks a spot that could not be read."
                                        },
                                        "lines": {
                                            "type": "array",
                                            "description": "The same text, one entry per visual line \u2014 usually what you want for forms and tables."
                                        },
                                        "language": {
                                            "type": "string",
                                            "description": "Detected language of the text, or **unknown**."
                                        },
                                        "handwritten": {
                                            "type": "boolean",
                                            "description": "True when a substantial part of the content is handwritten."
                                        },
                                        "legibility": {
                                            "type": "string",
                                            "description": "clear, partial or poor. Branch on this rather than trusting every response equally."
                                        },
                                        "unreadable": {
                                            "type": "array",
                                            "description": "One short note per unreadable spot. Empty on a clean page."
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "400": {
                        "description": "invalid_request \u2014 No \"image\" was sent, it was not valid base64, it decoded to almost nothing, or \"fields\" was missing or unusable on the fields endpoint.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "Missing or invalid API key",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "402": {
                        "description": "Insufficient credits \u2014 nothing is charged and the API is never reached",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "The key is not scoped to this API, or a test key was used on an API without sandbox support",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "413": {
                        "description": "image_too_large \u2014 The decoded image exceeded 5 MB.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "415": {
                        "description": "unsupported_image \u2014 The bytes are not a PNG, JPEG, GIF or WebP.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "content_refused \u2014 The model declined to process the image.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "Rate limit exceeded for this key",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "502": {
                        "description": "provider_error \u2014 The model answered in a form this API could not read.\n\noutput_truncated \u2014 The transcription hit its output ceiling before it was complete.\n\nThe API failed after being charged; credits are refunded automatically",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "503": {
                        "description": "provider_busy \u2014 The upstream model was rate-limited, overloaded, or did not respond in time.\n\nprovider_unavailable \u2014 This deployment has no working model credentials configured.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                },
                "description": "Preserves the original spelling, punctuation, casing and line breaks. Illegible marks become `[?]` in the text with a matching note in **unreadable** \u2014 the model is instructed never to substitute a plausible word for one it cannot read.",
                "requestBody": {
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "required": [
                                    "image"
                                ],
                                "properties": {
                                    "image": {
                                        "type": "string",
                                        "description": "The picture as base64, or a data: URL. PNG, JPEG, GIF or WebP, up to 5 MB decoded.",
                                        "x-ui": "file",
                                        "x-accept": "image/png,image/jpeg,image/gif,image/webp",
                                        "example": "iVBORw0KGgoAAAANSUhEUgAA..."
                                    },
                                    "hint": {
                                        "type": "string",
                                        "description": "Optional context for what the document is, e.g. \"a handwritten delivery docket\".",
                                        "maxLength": 300
                                    }
                                }
                            }
                        }
                    }
                }
            }
        },
        "/ocr.fields": {
            "post": {
                "operationId": "ocr_fields_post",
                "summary": "Read named values off a form, receipt or invoice",
                "x-credit-cost": 60,
                "responses": {
                    "200": {
                        "description": "Success",
                        "content": {
                            "application/json": {
                                "example": {
                                    "fields": {
                                        "invoice_no": {
                                            "found": true,
                                            "value": "INV-2291"
                                        },
                                        "date": {
                                            "found": true,
                                            "value": "14/03/2026"
                                        },
                                        "total": {
                                            "found": true,
                                            "value": "\u00a31,240.00"
                                        },
                                        "vat_number": {
                                            "found": false,
                                            "value": ""
                                        }
                                    },
                                    "handwritten": false,
                                    "legibility": "clear",
                                    "usage": {
                                        "input_tokens": 1450,
                                        "output_tokens": 64
                                    }
                                },
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "fields": {
                                            "type": "object",
                                            "description": "One entry per name you asked for, in the same names you sent."
                                        },
                                        "handwritten": {
                                            "type": "boolean",
                                            "description": "True when a substantial part of the document is handwritten."
                                        },
                                        "legibility": {
                                            "type": "string",
                                            "description": "clear, partial or poor for the document overall."
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "400": {
                        "description": "invalid_request \u2014 No \"image\" was sent, it was not valid base64, it decoded to almost nothing, or \"fields\" was missing or unusable on the fields endpoint.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "Missing or invalid API key",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "402": {
                        "description": "Insufficient credits \u2014 nothing is charged and the API is never reached",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "The key is not scoped to this API, or a test key was used on an API without sandbox support",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "413": {
                        "description": "image_too_large \u2014 The decoded image exceeded 5 MB.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "415": {
                        "description": "unsupported_image \u2014 The bytes are not a PNG, JPEG, GIF or WebP.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "content_refused \u2014 The model declined to process the image.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "Rate limit exceeded for this key",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "502": {
                        "description": "provider_error \u2014 The model answered in a form this API could not read.\n\noutput_truncated \u2014 The transcription hit its output ceiling before it was complete.\n\nThe API failed after being charged; credits are refunded automatically",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "503": {
                        "description": "provider_busy \u2014 The upstream model was rate-limited, overloaded, or did not respond in time.\n\nprovider_unavailable \u2014 This deployment has no working model credentials configured.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                },
                "description": "Send the values you want in **fields** and get back one `{found, value}` per name \u2014 nothing else. Values are copied verbatim rather than normalised, and a field that is absent or illegible answers `found: false` instead of an invention.\n\nCheaper than a full transcription, because it only ever produces a handful of short strings.",
                "requestBody": {
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "required": [
                                    "image",
                                    "fields"
                                ],
                                "properties": {
                                    "image": {
                                        "type": "string",
                                        "description": "The picture as base64, or a data: URL.",
                                        "x-ui": "file",
                                        "x-accept": "image/png,image/jpeg,image/gif,image/webp",
                                        "example": "iVBORw0KGgoAAAANSUhEUgAA..."
                                    },
                                    "fields": {
                                        "type": "array",
                                        "description": "Field names to read, 1 to 25. Also accepts a comma-separated string.",
                                        "items": {
                                            "type": "string"
                                        },
                                        "example": [
                                            "invoice_no",
                                            "date",
                                            "total"
                                        ]
                                    },
                                    "hint": {
                                        "type": "string",
                                        "description": "Optional context for what the document is.",
                                        "maxLength": 300
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    },
    "components": {
        "securitySchemes": {
            "ApiKeyAuth": {
                "type": "apiKey",
                "in": "header",
                "name": "X-Api-Key"
            }
        },
        "schemas": {
            "Error": {
                "type": "object",
                "properties": {
                    "error": {
                        "type": "object",
                        "properties": {
                            "code": {
                                "type": "string",
                                "description": "Stable machine-readable code \u2014 branch on this, never on the message."
                            },
                            "message": {
                                "type": "string"
                            },
                            "request_id": {
                                "type": "string"
                            }
                        }
                    }
                }
            }
        }
    }
}