Skip to Content
API Reference

BotifyOCR API Reference

Base URL

https://studio.botifynow.com

Authentication

Every request must include:

Authorization: Bearer botify_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Generate the key in Studio API Keys.

Submit OCR Job

POST /api/v1/ocr

Request type: multipart/form-data

  • file: PDF, TIFF, PNG, or JPG (max 50 MB)
  • mode: fast | vision | auto

Response:

{ "jobId": "p8001-9f59c624-..." }

Poll OCR Job

GET /api/v1/ocr/{jobId}

Poll every ~2 seconds until status is terminal:

  • queued
  • running
  • completed
  • failed

End-to-End Implementations

cURL

#!/usr/bin/env bash set -euo pipefail BASE_URL="https://studio.botifynow.com" API_KEY="botify_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" FILE_PATH="/absolute/path/to/document.tif" SUBMIT_JSON="$(curl -sS -X POST "${BASE_URL}/api/v1/ocr" \ -H "Authorization: Bearer ${API_KEY}" \ -F "file=@${FILE_PATH}" \ -F "mode=vision")" JOB_ID="$(printf '%s' "${SUBMIT_JSON}" | jq -r '.jobId')" echo "jobId=${JOB_ID}" while true; do POLL_JSON="$(curl -sS "${BASE_URL}/api/v1/ocr/${JOB_ID}" \ -H "Authorization: Bearer ${API_KEY}")" STATUS="$(printf '%s' "${POLL_JSON}" | jq -r '.status')" echo "status=${STATUS}" if [[ "${STATUS}" == "completed" || "${STATUS}" == "failed" ]]; then printf '%s\n' "${POLL_JSON}" > ocr_result.json break fi sleep 2 done

Python

import time import requests BASE_URL = "https://studio.botifynow.com" API_KEY = "botify_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" FILE_PATH = "document.tif" headers = {"Authorization": f"Bearer {API_KEY}"} with open(FILE_PATH, "rb") as f: submit = requests.post( f"{BASE_URL}/api/v1/ocr", headers=headers, files={"file": f}, data={"mode": "vision"}, timeout=120, ) submit.raise_for_status() job_id = submit.json()["jobId"] while True: poll = requests.get( f"{BASE_URL}/api/v1/ocr/{job_id}", headers=headers, timeout=120, ) poll.raise_for_status() payload = poll.json() status = payload.get("status") if status in ("completed", "failed"): print(payload) break time.sleep(2)

TypeScript

import fs from "node:fs"; import FormData from "form-data"; const baseUrl = "https://studio.botifynow.com"; const apiKey = "botify_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; const form = new FormData(); form.append("file", fs.createReadStream("document.tif")); form.append("mode", "vision"); const submit = await fetch(`${baseUrl}/api/v1/ocr`, { method: "POST", headers: { Authorization: `Bearer ${apiKey}` }, body: form as any, }); if (!submit.ok) throw new Error(await submit.text()); const { jobId } = await submit.json() as { jobId: string }; while (true) { const poll = await fetch(`${baseUrl}/api/v1/ocr/${jobId}`, { headers: { Authorization: `Bearer ${apiKey}` }, }); if (!poll.ok) throw new Error(await poll.text()); const data = await poll.json() as any; if (data.status === "completed" || data.status === "failed") { console.log(data); break; } await new Promise((r) => setTimeout(r, 2000)); }

C#

using System.Net.Http.Headers; using System.Text.Json; var baseUrl = "https://studio.botifynow.com"; var apiKey = "botify_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; using var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); using var form = new MultipartFormDataContent(); form.Add(new ByteArrayContent(await File.ReadAllBytesAsync("document.tif")), "file", "document.tif"); form.Add(new StringContent("vision"), "mode"); var submitResp = await client.PostAsync($"{baseUrl}/api/v1/ocr", form); submitResp.EnsureSuccessStatusCode(); var jobId = JsonDocument.Parse(await submitResp.Content.ReadAsStringAsync()) .RootElement.GetProperty("jobId").GetString(); while (true) { var pollResp = await client.GetAsync($"{baseUrl}/api/v1/ocr/{jobId}"); pollResp.EnsureSuccessStatusCode(); var body = await pollResp.Content.ReadAsStringAsync(); var root = JsonDocument.Parse(body).RootElement; var status = root.GetProperty("status").GetString(); if (status == "completed" || status == "failed") { Console.WriteLine(body); break; } await Task.Delay(2000); }

Java

HttpClient client = HttpClient.newHttpClient(); String baseUrl = "https://studio.botifynow.com"; String apiKey = "botify_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; Path filePath = Path.of("document.tif"); String boundary = "----BotifyBoundary"; byte[] fileBytes = Files.readAllBytes(filePath); var body = new java.io.ByteArrayOutputStream(); body.write(("--" + boundary + "\r\n").getBytes()); body.write(("Content-Disposition: form-data; name=\"file\"; filename=\"document.tif\"\r\n").getBytes()); body.write(("Content-Type: image/tiff\r\n\r\n").getBytes()); body.write(fileBytes); body.write(("\r\n--" + boundary + "\r\n").getBytes()); body.write(("Content-Disposition: form-data; name=\"mode\"\r\n\r\nvision\r\n").getBytes()); body.write(("--" + boundary + "--\r\n").getBytes()); HttpRequest submit = HttpRequest.newBuilder() .uri(URI.create(baseUrl + "/api/v1/ocr")) .header("Authorization", "Bearer " + apiKey) .header("Content-Type", "multipart/form-data; boundary=" + boundary) .POST(HttpRequest.BodyPublishers.ofByteArray(body.toByteArray())) .build(); String submitBody = client.send(submit, HttpResponse.BodyHandlers.ofString()).body(); java.util.regex.Matcher m = java.util.regex.Pattern .compile("\"jobId\"\\s*:\\s*\"([^\"]+)\"") .matcher(submitBody); if (!m.find()) throw new RuntimeException("jobId missing: " + submitBody); String jobId = m.group(1); while (true) { HttpRequest poll = HttpRequest.newBuilder() .uri(URI.create(baseUrl + "/api/v1/ocr/" + jobId)) .header("Authorization", "Bearer " + apiKey) .GET() .build(); String pollBody = client.send(poll, HttpResponse.BodyHandlers.ofString()).body(); if (pollBody.contains("\"status\":\"completed\"") || pollBody.contains("\"status\":\"failed\"")) { System.out.println(pollBody); break; } Thread.sleep(2000); }

Go

body := &bytes.Buffer{} writer := multipart.NewWriter(body) part, _ := writer.CreateFormFile("file", "document.tif") f, _ := os.Open("document.tif") io.Copy(part, f) f.Close() writer.WriteField("mode", "vision") writer.Close() baseURL := "https://studio.botifynow.com" apiKey := "botify_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" submitReq, _ := http.NewRequest("POST", baseURL+"/api/v1/ocr", body) submitReq.Header.Set("Authorization", "Bearer "+apiKey) submitReq.Header.Set("Content-Type", writer.FormDataContentType()) submitResp, _ := http.DefaultClient.Do(submitReq) submitRaw, _ := io.ReadAll(submitResp.Body) submitResp.Body.Close() var submit map[string]any json.Unmarshal(submitRaw, &submit) jobID, _ := submit["jobId"].(string) for { pollReq, _ := http.NewRequest("GET", baseURL+"/api/v1/ocr/"+jobID, nil) pollReq.Header.Set("Authorization", "Bearer "+apiKey) pollResp, _ := http.DefaultClient.Do(pollReq) pollRaw, _ := io.ReadAll(pollResp.Body) pollResp.Body.Close() var payload map[string]any json.Unmarshal(pollRaw, &payload) status, _ := payload["status"].(string) if status == "completed" || status == "failed" { fmt.Println(string(pollRaw)) break } time.Sleep(2 * time.Second) }

Error Codes

  • 400: Invalid request (file/mode missing or invalid)
  • 401: Invalid/revoked/missing API key
  • 404: Unknown job ID
  • 500: Internal processing error

Legacy Endpoints

These remain unchanged for existing clients:

  • POST /api/ocr/process/raw/async
  • GET /api/ocr/process/raw/async/{jobId}

Auth for legacy flow uses X-User-Name and X-License-Code headers.