← OCR LabAPI
Your token

Driving OCR Lab from your own code

Recognition in this app is local and free: the PP-OCRv6 models ship with the page, run in the browser, and no image is ever uploaded. There is no API for that part, and there does not need to be — the weights are the publisher's unmodified Apache-2.0 files, so you can run exactly the same ONNX models yourself.

What this API exposes is the second half: the metered pass that takes recognised lines and returns a repaired, reordered, optionally structured document. If you already have OCR output from anywhere, you can send it here.

Base URL and envelope

Everything lives under https://api.skillsafe.ai/v1/app-api. Every response is one of two shapes:

{ "data": { ... } }                                  // success
{ "error": { "code": "VALIDATION_ERROR", "message": "...", "details": { ... } } }

Check for error first; the HTTP status mirrors it.

Error codes

CodeHTTPWhat to do
UNAUTHORIZED401Missing or expired token. Mint a new one on the token page.
PAYMENT_REQUIRED402The balance is below min_credits. Nothing ran and nothing was charged.
VALIDATION_ERROR400The input did not match the shape below — usually ocr.lines missing or empty.
NOT_FOUND404Unknown job id, record id, or collection.
RATE_LIMITED429Too many requests. /similar is the tightest at 30/min per IP.
INTERNAL500Platform-side failure. A failed run refunds its whole hold.

1. Get a token

Every call carries Authorization: Bearer <token>. The app already holds one, and the token page shows it, copies it, and prints a shell export — you never need to open a developer console. Two kinds exist:

Recognition itself needs no token and no account. It fetches the bundled PP-OCRv6 weights from this origin once and then runs entirely in the tab — no image is ever uploaded, and nothing on the recognition path is billed. This API covers the repair pass only.

2. Check the session and balance

GET /me tells you who the token belongs to and what it can afford. Do this before a run: comparing credits against the hold from step 3 is what stops a 402 from happening after you have already built the payload.

curl -sS -X GET 'https://api.skillsafe.ai/v1/app-api/me' \
  -H 'Authorization: Bearer YOUR_TOKEN'

3. Price the run

POST /estimate is free, creates no job, and charges nothing. It returns the model actually bound to the app, the markup, and two numbers that matter: hold_credits (what gets reserved, priced against the full output cap) and min_credits (the floor below which the run is refused). The real charge is almost always well under the hold.

{
  "model": "gpt-5.6-terra",
  "model_alias": "gpt-terra",
  "markup_bps": 1000,
  "hold_credits": 1703,
  "min_credits": 215,
  "sponsor_enabled": false
}
curl -sS -X POST 'https://api.skillsafe.ai/v1/app-api/estimate' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
  "task": "clean",
  "target": "markdown",
  "language_hint": "mixed",
  "translate_to": "",
  "instructions": "keep the table columns",
  "image": {
    "width": 720,
    "height": 300
  },
  "ocr": {
    "engine": "PP-OCRv6_tiny · onnxruntime-web wasm",
    "line_count": 2,
    "mean_confidence": 0.9962,
    "low_confidence_threshold": 0.75,
    "low_confidence_lines": [],
    "clip_note": "",
    "lines": [
      {
        "i": 1,
        "text": "浏览器端文字识别",
        "conf": 0.999,
        "box": [
          38,
          29,
          311,
          66
        ]
      },
      {
        "i": 2,
        "text": "PP-OCRv6 tiny 6MB",
        "conf": 0.994,
        "box": [
          38,
          100,
          314,
          135
        ]
      }
    ]
  }
}'

4. Run it

The input is the recogniser's output plus what you want done with it. Every field:

FieldTypeMeaning
taskstring"clean" — the only task this app sends.
targetstringtext, markdown, fields or translate. Decides the shape of TEXT and whether FIELDS is populated.
language_hintstringauto, zh, en or mixed.
translate_tostringTarget language. Only read when target is translate.
instructionsstringFree-text note, truncated to 300 characters.
imageobject{width, height} in pixels — the frame the boxes are in.
ocr.enginestringWhich recogniser produced the lines.
ocr.line_countnumberHow many lines were recognised in total, before any clipping.
ocr.mean_confidencenumber0–1, averaged over lines.
ocr.low_confidence_thresholdnumberBelow this a line is treated as suspect. The app sends 0.75.
ocr.low_confidence_linesnumber[]1-based indices of the suspect lines.
ocr.clip_notestringNon-empty when the page was too long for the budget and middle lines were elided.
ocr.lines[]array{i, text, conf, box}. box is [x0,y0,x1,y1], or null for an elision marker.
$modelstringOptional per-run model alias override. Omit to use the app's own.

Pass an Idempotency-Key derived from the content. A retry with the same key returns the original job instead of billing a second time — this is what makes a network blip safe.

# 1. submit — Idempotency-Key makes a retry safe
JOB=$(curl -sS -X POST 'https://api.skillsafe.ai/v1/app-api/run' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: clean-9f2ab41c:0' \
  -d @input.json | python3 -c 'import json,sys;print(json.load(sys.stdin)["data"]["job_id"])')

# 2. poll until status is succeeded or failed
while :; do
  BODY=$(curl -sS "https://api.skillsafe.ai/v1/app-api/jobs/$JOB" -H 'Authorization: Bearer YOUR_TOKEN')
  STATUS=$(printf '%s' "$BODY" | python3 -c 'import json,sys;print(json.load(sys.stdin)["data"]["status"])')
  [ "$STATUS" = "succeeded" ] || [ "$STATUS" = "failed" ] && break
  sleep 2
done
printf '%s' "$BODY" | python3 -c 'import json,sys;print(json.load(sys.stdin)["data"]["output"]["output"])'

5. Stream it instead

A page of text takes long enough that you usually want it as it arrives. POST /run-stream is the same input and the same billing over server-sent events: delta carries text, job fires once the job row exists, and done carries charged_credits and truncated.

truncated: true means the balance sat between min_credits and hold_credits, so the run executed with a reduced output cap. Treat the answer as incomplete rather than final.

curl -N -X POST 'https://api.skillsafe.ai/v1/app-api/run-stream' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: clean-9f2ab41c:0' \
  -d @input.json

# Server-sent events. `delta` carries output as it generates; `job` fires once the
# job row exists; `done` carries the settled charge.
# event: delta
# data: {"text":"VERDICT>>> clean\n"}
# event: done
# data: {"job_id":"job_...","charged_credits":214,"truncated":false}

6. Parse the output contract

The reply is marker-delimited, not JSON — a transcription full of quotes and newlines survives markers far more reliably than it survives a model's attempt at valid JSON.

VERDICT>>> noisy
SUMMARY>>> A Chinese-language product screenshot with one mis-read currency amount.
TEXT>>>
## 浏览器端文字识别

PP-OCRv6 tiny 6MB
<<<TEXT
FIELDS>>>
{ }
<<<FIELDS
FLAGS>>>
- L4 (conf 0.52) "1O0.5O" -> "100.50": zero/letter-O confusion in an amount
<<<FLAGS
NOTES>>>
None.
<<<NOTES

The app also cross-checks TEXT against the recogniser's own characters and reports anything the model introduced. If you drive this API yourself, do the same: it is the cheapest hallucination check available, and it costs nothing.

7. Read past scans

Scans the app saved live in the scans collection, scoped to the calling subject. query filters and sorts; similar is vector search over the embedded title and preview fields. Note that every where entry must be an operator object — a bare value is rejected — and that the sort key is sort, not order_by.

curl -sS -X POST 'https://api.skillsafe.ai/v1/app-api/collections/scans/query' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
  "where": {
    "mean_conf": {
      "lt": 0.9
    }
  },
  "sort": {
    "field": "ran_at",
    "dir": "desc"
  },
  "limit": 10
}'

Rates and limits worth knowing