BotifyOCR API Reference
Base URL
https://studio.botifynow.com
Authentication
Every request must include:
Authorization: Bearer botify_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxGenerate 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:
queuedrunningcompletedfailed
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
donePython
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 key404: Unknown job ID500: Internal processing error
Legacy Endpoints
These remain unchanged for existing clients:
POST /api/ocr/process/raw/asyncGET /api/ocr/process/raw/async/{jobId}
Auth for legacy flow uses X-User-Name and X-License-Code headers.