Skip to content
StraventaDocs
Now in Sandbox.
API Reference

OCR & Document Extraction API

Synchronous and asynchronous document extraction for Indonesian identity and financial documents, per-tenant field schemas, and API-key lifecycle

The OCR Service performs document OCR and structured field extraction for Indonesian identity and financial documents — KTP, NPWP, Kartu Keluarga, passport/TD3, bank statements and invoices. Your backend POSTs a document with an API key and receives confidence-scored fields back: synchronously for a single-page ID document, asynchronously for anything larger.

Every extracted field carries a confidence score and a provenance, and a field is auto-approvable only when both clear their thresholds. A field read by the recognizer alone is held to a higher floor than one confirmed by a checksum, and a failed checksum is never a low-confidence pass — it is a conflict that requires review.

Values are sealed

Extraction output is encrypted per record. Field values never appear in the audit log or in webhook payloads; those carry field names, confidences and outcomes only.

Base URLs

EnvironmentHostname
Sandboxhttps://sandbox.505pay.link
Productionhttps://api.505pay.link

Authentication

Authorization: Bearer ok_live_…      # production
Authorization: Bearer ok_test_…      # sandbox

The key is stored as a SHA-256 hash; only a 16-character lookup prefix is retained in clear for display. A revoked key, an unknown key, and a malformed or absent header all return the same 401, so the response cannot be used to enumerate valid prefixes.

Two scopes, and the asymmetry is the point

ScopeCan doMinted by
dataextraction and schema routesocrIssueApiKey / ocrRotateApiKey
adminthe API-key routes as wellthe ocr-keygen CLI only

Extraction endpoints take an ordinary data-scoped key — extracting is the product, so requiring an admin key there would force every integration to hold a key that can also mint successors. The api-key endpoints require admin scope; a data key authenticates and is then refused with 403 insufficient_scope.

Admin keys cannot be minted or rotated over HTTP

ocr-keygen authenticates with database access rather than with a key. That asymmetry is what makes revocation terminal: a leaked extraction key has no power to mint a successor, so revoking it ends the incident.

ocrIssueApiKey rejects a request for "admin" with 400 rather than silently downgrading it, so a caller never holds a key they believe is privileged and is not. ocrRotateApiKey refuses admin keys with 400 for the same class of reason — a leaked admin key allowed to rotate itself would leave the attacker holding the successor and the owner locked out by their own credential.

Two more properties of key issuance:

  • The environment is inherited from the calling key and cannot be set in the body — otherwise a sandbox admin key could mint a production credential.
  • The plaintext is returned once. Only the SHA-256 is stored, so a lost plaintext cannot be recovered — rotate instead. Rotation mints the replacement and revokes the original in a single transaction, so a crash can never leave two live keys or none.

Extraction

ocrGetJobResult returns exactly the body ocrExtractDocument returns for a synchronous call, so an integrator writes one decoder for both paths and never branches on which produced a result.

Synchronous — ocrExtractDocument

The document is sent base64-encoded in JSON, not as multipart: the inference sidecar's own contract is base64, so a multipart intake would decode and re-encode every document for no gain.

mime_type is advisory only — the sidecar sniffs the real type and ignores a wrong hint rather than trusting it.

The synchronous path holds a concurrency budget separate from the async path, so a batch cannot starve the interactive SLO. When that budget is exhausted the request is refused rather than queued.

Asynchronous — ocrCreateExtractionJob

Persists the document and returns 202 immediately with a job id to poll.

No staging backend means no async route

A deployment with no document-staging backend configured does not serve this route at all. The async path must store the bytes it will process later, and with nowhere to put them the honest answer is a 404 rather than a request that accepts a document and loses it.

ocrGetJob is the poll route and returns metadata only — never extracted values. It stays cheap on purpose: extracted values are sealed per record, and opening that seal is the separate, explicit ocrGetJobResult step. Poll until the job reports a terminal status, then read the result once.

A job belonging to another tenant is reported as 404, not 403 — a distinguishable 403 would confirm the id exists.

Records written before this service persisted tables come back with an empty tables object. That is the truth about those records rather than a failure to read them.


Schemas

Extraction schemas are per-tenant and database-authoritative — they live in ocr_schemas, keyed by (tenant_id, document_type, version) and protected by row-level security on tenant_id.

A tenant owns which fields it wants extracted. It does not own the classification of those fields.

Platform-owned keys are rejected, never stripped

compliance_bound, retention_class, default_sensitivity and the D33 sensitivity classes are projected from the platform registry on read and rejected on write with 400 platform_owned_field.

They are not silently stripped: a read-modify-write client must be told what to remove rather than believing its write took effect. The scan runs on the raw body before any typed decode, because decoding first would drop an unmodelled key on the floor and the fence would never see it.

The effect is that a tenant cannot move a KYC document off its retention floor by editing its own schema.

Versioning is append-only

ocrPutSchema writes the new row first and retires the previously active row afterwards. The reverse order would leave the tenant with no active schema at all for the window between the two statements — and permanently so if the create then failed.

Version numbering is prior.version + 1, or 1 when this is the first schema for that document type.

A new version is active on write

The new row is written active (ocr_schemas.active defaults to true), so a PUT takes effect for the next extraction. There is a brief, deliberate window in which two rows are active; reads resolve it deterministically with ORDER BY version DESC LIMIT 1. The overlap is harmless where the gap would not be.

ocrListSchemas returns only the active revision of each document type — one schema per type. Superseded revisions stay readable through the schema history, not here. So this is exactly the set an extraction submitted right now would be matched against.

An extraction for a document type with no active schema fails: there is no subject key to bind the record to.


Endpoint reference

Generated from the service's embedded OpenAPI spec (services/ocr-service/internal/http/openapi/openapi.yaml). The scope each group demands is stated in the group description above the cards.

Extraction

Submit a document for field extraction. These take an ordinary data-scoped key — extracting is the product, so an admin key is not required. Synchronous for a single-page ID, asynchronous for anything larger.

Jobs

Poll an asynchronous submission, then collect it once. The poll route returns metadata only; the result route opens the sealed record and returns the synchronous call's body shape.

Schemas

Per-tenant, DB-authoritative extraction schemas. A tenant owns WHICH fields it wants extracted, not their classification — platform-owned keys are rejected on write. Versioning is append-only and the new version is written active.

API keys

Key lifecycle. Every endpoint here requires an ADMIN-scoped key; a data key is refused with 403 insufficient_scope. Admin keys themselves can only be minted by the ocr-keygen CLI, which makes revocation of a leaked data key terminal.

Service metadata

Unauthenticated infrastructure routes. Liveness and readiness are for the orchestrator, not for callers; the spec and docs routes serve this service's own OpenAPI document.

Was this page helpful?

On this page