Integration Guide

Everything you need to integrate Synthya OCR into your application β€” with ready-to-use code examples in Python, JavaScript, Java, cURL, Go, and C#.

Table of Contents

  1. API Overview
  2. Endpoint Reference
  3. Request Format
  4. Response Format
  5. OCR Modes
  6. Code Examples
  7. Error Handling
  8. Auto-Batching Behavior
  9. Best Practices

1. API Overview

The Synthya OCR API provides a single, simple endpoint for all OCR operations. You send a base64-encoded image and receive structured JSON results. The server automatically batches concurrent requests for optimal throughput.

πŸ’‘ Simple by design: One endpoint, one image per request. No need to manage batches β€” the server handles it automatically. Send as many concurrent requests as you'd like.

2. Endpoint Reference

POST /api/ocr

Content-Type: application/json

Max request size: 30 MB

Timeout: 5 minutes per request

3. Request Format

FieldTypeRequiredDescription
imagestringβœ… Yes Base64-encoded image data (no data: prefix)
imageTypestringNo Image format: png, jpg, jpeg, gif, tif. Default: png
modestringNo OCR mode: receipt, full, targeted, template, survey, tree. Default: full
promptstringNo Custom prompt (only for full mode)
queryobjectNo Structured query (only for targeted mode). See Modes section.
⚠️ Important: Send raw base64 β€” do not include the data:image/...;base64, prefix. The server adds the correct MIME type based on imageType.

4. Response Format

All responses are JSON with the following structure:

JSON
{
  "id":   "uuid-of-the-request",
  "mode": "full",
  "data": {
    // Mode-specific result β€” see below
  }
}

On error:

JSON
{
  "id":    "uuid-of-the-request",
  "mode":  "full",
  "error": "description of the error"
}

5. OCR Modes

full (default)

Extracts all visible text from the image. Returns {"text": "...", "mode": "full"}. Optionally accepts a prompt for custom instructions.

receipt

Extracts structured receipt data: store name, items, prices, totals, discounts. Returns a verified receipt object with suspicious item detection.

targeted

Extracts specific data using a structured query object. The query field supports:

FieldDescription
documentTypeType of document (e.g. "invoice", "report")
columnsArray of column names to extract
itemsArray of specific items to look for
freeTextAdditional instructions in natural language

template

Specialized for NH 착과수 μ‘°μ‚¬ν‘œ (fruit survey) forms. Returns structured JSON with validation.

survey

Specialized for NH ν˜„μ§€μ‘°μ‚¬μ„œ (field survey) forms. Returns structured JSON with validation.

tree

Specialized for NH λ‚˜λ¬΄μ‘°μ‚¬ (tree investigation) forms. Returns structured JSON with validation.

6. Code Examples

Python
JavaScript
Java
cURL
Go
C#

Basic Usage β€” Full OCR

Python
import base64
import requests

API_URL = "http://localhost:6800/api/ocr"

# Read and encode the image
with open("document.png", "rb") as f:
    image_b64 = base64.b64encode(f.read()).decode("utf-8")

# Send the request
response = requests.post(API_URL, json={
    "image": image_b64,
    "imageType": "png",
    "mode": "full"
})

result = response.json()
print(result["data"]["text"])

Receipt Mode

Python
import base64
import requests

API_URL = "http://localhost:6800/api/ocr"

with open("receipt.jpg", "rb") as f:
    image_b64 = base64.b64encode(f.read()).decode("utf-8")

response = requests.post(API_URL, json={
    "image": image_b64,
    "imageType": "jpg",
    "mode": "receipt"
})

receipt = response.json()["data"]
print(f"Store: {receipt['storeName']}")
print(f"Total: {receipt['totalAfterTax']}")
for item in receipt["items"]:
    flag = " ⚠️" if item["isSuspicious"] else ""
    print(f"  {item['name']}: {item['totalPrice']}{flag}")

Targeted Mode

Python
import base64
import requests

API_URL = "http://localhost:6800/api/ocr"

with open("invoice.png", "rb") as f:
    image_b64 = base64.b64encode(f.read()).decode("utf-8")

response = requests.post(API_URL, json={
    "image": image_b64,
    "imageType": "png",
    "mode": "targeted",
    "query": {
        "documentType": "invoice",
        "columns": ["ν’ˆλͺ…", "μˆ˜λŸ‰", "단가", "κΈˆμ•‘"],
        "items": ["사과", "λ°°", "감"],
        "freeText": "합계도 μΆ”μΆœν•΄ μ£Όμ„Έμš”"
    }
})

data = response.json()["data"]
for item in data["json"]["items"]:
    print(f"{item['name']}: {item['values']}")

NH Template Modes (template / survey / tree)

Python
import base64
import requests
import json

API_URL = "http://localhost:6800/api/ocr"

def process_nh_form(image_path, mode):
    """Process an NH agricultural form.
    
    Args:
        image_path: Path to the scanned form image
        mode: One of 'template' (착과수), 'survey' (ν˜„μ§€μ‘°μ‚¬μ„œ), 'tree' (λ‚˜λ¬΄μ‘°μ‚¬)
    """
    with open(image_path, "rb") as f:
        image_b64 = base64.b64encode(f.read()).decode("utf-8")

    ext = image_path.rsplit(".", 1)[-1].lower()

    response = requests.post(API_URL, json={
        "image": image_b64,
        "imageType": ext,
        "mode": mode
    }, timeout=300)

    result = response.json()
    if "error" in result:
        print(f"Error: {result['error']}")
        return None

    data = result["data"]
    parsed = data.get("json")
    validation = data.get("validation")

    if validation:
        trusted = validation.get("trusted", validation.get("valid", False))
        issues = validation.get("issues") or []
        print(f"Validation: {'βœ… Trusted' if trusted else '⚠️ Issues found'}")
        for issue in issues:
            print(f"  - {issue}")

    return parsed

# Example: Process a 착과수 μ‘°μ‚¬ν‘œ (fruit survey)
fruit_data = process_nh_form("fruit_survey_scan.jpg", "template")
if fruit_data:
    print(json.dumps(fruit_data, ensure_ascii=False, indent=2))

# Example: Process a ν˜„μ§€μ‘°μ‚¬μ„œ (field survey)
field_data = process_nh_form("field_survey_scan.png", "survey")

# Example: Process a λ‚˜λ¬΄μ‘°μ‚¬ (tree investigation)
tree_data = process_nh_form("tree_investigation_scan.tif", "tree")

Concurrent Processing (async)

Python
import asyncio
import base64
import aiohttp
from pathlib import Path

API_URL = "http://localhost:6800/api/ocr"

async def process_image(session, filepath):
    """Process a single image β€” the server auto-batches concurrent requests."""
    data = Path(filepath).read_bytes()
    ext = Path(filepath).suffix.lstrip(".").lower()
    if ext == "jpg":
        ext = "jpeg"

    async with session.post(API_URL, json={
        "image": base64.b64encode(data).decode(),
        "imageType": ext,
        "mode": "receipt"
    }) as resp:
        return await resp.json()

async def main():
    images = list(Path("receipts/").glob("*.jpg"))
    print(f"Processing {len(images)} images...")

    async with aiohttp.ClientSession() as session:
        tasks = [process_image(session, img) for img in images]
        results = await asyncio.gather(*tasks)

    for img, result in zip(images, results):
        if "error" in result:
            print(f"❌ {img.name}: {result['error']}")
        else:
            total = result["data"].get("totalAfterTax", "N/A")
            print(f"βœ… {img.name}: total = {total}")

asyncio.run(main())

Node.js β€” Basic Usage

JavaScript (Node.js)
const fs = require('fs');
const path = require('path');

const API_URL = 'http://localhost:6800/api/ocr';

async function ocrImage(filePath, mode = 'full') {
  const imageBuffer = fs.readFileSync(filePath);
  const imageB64 = imageBuffer.toString('base64');
  const ext = path.extname(filePath).slice(1).toLowerCase();

  const response = await fetch(API_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      image: imageB64,
      imageType: ext === 'jpg' ? 'jpeg' : ext,
      mode: mode,
    }),
  });

  return response.json();
}

// Usage
const result = await ocrImage('document.png', 'full');
console.log(result.data.text);

Browser β€” File Input

JavaScript (Browser)
async function processFile(file) {
  // Convert File to base64
  const buffer = await file.arrayBuffer();
  const base64 = btoa(
    new Uint8Array(buffer).reduce((s, b) => s + String.fromCharCode(b), '')
  );

  // Determine image type from file name
  const ext = file.name.split('.').pop().toLowerCase();

  const response = await fetch('/api/ocr', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      image: base64,
      imageType: ext === 'jpg' ? 'jpeg' : ext,
      mode: 'receipt',
    }),
  });

  return response.json();
}

// With a file input
document.getElementById('fileInput').addEventListener('change', async (e) => {
  const file = e.target.files[0];
  if (!file) return;

  const result = await processFile(file);
  console.log('OCR result:', result);
});

NH Template Modes (template / survey / tree)

JavaScript (Node.js)
const fs = require('fs');
const path = require('path');

const API_URL = 'http://localhost:6800/api/ocr';

/**
 * Process an NH agricultural form.
 * @param {string} filePath - Path to the scanned form image
 * @param {'template'|'survey'|'tree'} mode - NH form type
 */
async function processNHForm(filePath, mode) {
  const buffer = fs.readFileSync(filePath);
  const ext = path.extname(filePath).slice(1).toLowerCase();

  const response = await fetch(API_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      image: buffer.toString('base64'),
      imageType: ext === 'jpg' ? 'jpeg' : ext,
      mode: mode,
    }),
  });

  const result = await response.json();
  if (result.error) {
    console.error(`Error: ${result.error}`);
    return null;
  }

  const { json: parsed, validation } = result.data;

  if (validation) {
    const trusted = validation.trusted ?? validation.valid ?? false;
    console.log(`Validation: ${trusted ? 'βœ… Trusted' : '⚠️ Issues'}`);
    (validation.issues || []).forEach(i => console.log(`  - ${i}`));
  }

  return parsed;
}

// Usage
const fruitData = await processNHForm('fruit_survey.jpg', 'template');
const fieldData = await processNHForm('field_survey.png', 'survey');
const treeData  = await processNHForm('tree_form.tif', 'tree');

console.log(JSON.stringify(fruitData, null, 2));

Concurrent Processing (Node.js)

JavaScript (Node.js)
const fs = require('fs');
const path = require('path');
const { glob } = require('fs/promises');

const API_URL = 'http://localhost:6800/api/ocr';

async function processImage(filePath) {
  const buffer = fs.readFileSync(filePath);
  const ext = path.extname(filePath).slice(1).toLowerCase();

  const resp = await fetch(API_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      image: buffer.toString('base64'),
      imageType: ext,
      mode: 'receipt',
    }),
  });
  return { file: path.basename(filePath), result: await resp.json() };
}

// Process all JPGs concurrently β€” server auto-batches them
const files = fs.readdirSync('receipts/')
  .filter(f => /\.(jpg|jpeg|png)$/i.test(f))
  .map(f => path.join('receipts/', f));

const results = await Promise.all(files.map(processImage));

for (const { file, result } of results) {
  console.log(`${file}: ${result.data?.totalAfterTax ?? result.error}`);
}

Java 11+ β€” HttpClient

Java
import java.net.URI;
import java.net.http.*;
import java.nio.file.*;
import java.util.Base64;

public class SynthyaOCR {
    private static final String API_URL = "http://localhost:6800/api/ocr";

    public static String ocr(String imagePath, String mode) throws Exception {
        // Read and encode the image
        byte[] imageBytes = Files.readAllBytes(Path.of(imagePath));
        String imageB64 = Base64.getEncoder().encodeToString(imageBytes);

        // Determine image type from extension
        String ext = imagePath.substring(imagePath.lastIndexOf('.') + 1).toLowerCase();
        if (ext.equals("jpg")) ext = "jpeg";

        // Build JSON payload
        String json = String.format("""
            {
                "image": "%s",
                "imageType": "%s",
                "mode": "%s"
            }
            """, imageB64, ext, mode);

        // Send the request
        HttpClient client = HttpClient.newHttpClient();
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(API_URL))
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(json))
            .build();

        HttpResponse<String> response = client.send(
            request, HttpResponse.BodyHandlers.ofString()
        );

        return response.body();
    }

    public static void main(String[] args) throws Exception {
        String result = ocr("document.png", "full");
        System.out.println(result);
    }
}

Targeted Mode with Jackson

Java
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.node.*;
import java.net.URI;
import java.net.http.*;
import java.nio.file.*;
import java.util.Base64;

public class TargetedOCR {
    private static final String API_URL = "http://localhost:6800/api/ocr";
    private static final ObjectMapper mapper = new ObjectMapper();

    public static JsonNode targetedOcr(String imagePath, String docType,
            String[] columns, String[] items) throws Exception {

        byte[] imageBytes = Files.readAllBytes(Path.of(imagePath));

        ObjectNode payload = mapper.createObjectNode();
        payload.put("image", Base64.getEncoder().encodeToString(imageBytes));
        payload.put("imageType", "png");
        payload.put("mode", "targeted");

        ObjectNode query = mapper.createObjectNode();
        query.put("documentType", docType);
        ArrayNode cols = query.putArray("columns");
        for (String c : columns) cols.add(c);
        ArrayNode itms = query.putArray("items");
        for (String i : items) itms.add(i);
        payload.set("query", query);

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(API_URL))
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(mapper.writeValueAsString(payload)))
            .build();

        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());

        return mapper.readTree(response.body());
    }

    public static void main(String[] args) throws Exception {
        JsonNode result = targetedOcr("invoice.png", "invoice",
            new String[]{"ν’ˆλͺ…", "μˆ˜λŸ‰", "단가"},
            new String[]{"사과", "λ°°"});
        System.out.println(result.toPrettyString());
    }
}

NH Template Modes (template / survey / tree)

Java
import com.fasterxml.jackson.databind.*;
import java.net.URI;
import java.net.http.*;
import java.nio.file.*;
import java.util.Base64;

public class NHFormOCR {
    private static final String API_URL = "http://localhost:6800/api/ocr";
    private static final ObjectMapper mapper = new ObjectMapper();
    private static final HttpClient client = HttpClient.newHttpClient();

    /**
     * Process an NH agricultural form.
     * @param imagePath path to the scanned image
     * @param mode "template" (착과수), "survey" (ν˜„μ§€μ‘°μ‚¬μ„œ), or "tree" (λ‚˜λ¬΄μ‘°μ‚¬)
     */
    public static JsonNode processForm(String imagePath, String mode) throws Exception {
        byte[] bytes = Files.readAllBytes(Path.of(imagePath));
        String ext = imagePath.substring(imagePath.lastIndexOf('.') + 1).toLowerCase();
        if (ext.equals("jpg")) ext = "jpeg";

        String json = String.format("""
            {
                "image": "%s",
                "imageType": "%s",
                "mode": "%s"
            }
            """, Base64.getEncoder().encodeToString(bytes), ext, mode);

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(API_URL))
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(json))
            .build();

        HttpResponse<String> response = client.send(
            request, HttpResponse.BodyHandlers.ofString());

        JsonNode root = mapper.readTree(response.body());

        // Check validation results
        JsonNode validation = root.at("/data/validation");
        if (!validation.isMissingNode()) {
            boolean trusted = validation.path("trusted").asBoolean(
                validation.path("valid").asBoolean(false));
            System.out.println("Validation: " + (trusted ? "βœ… Trusted" : "⚠️ Issues"));
            validation.path("issues").forEach(
                issue -> System.out.println("  - " + issue.asText()));
        }

        return root.at("/data/json");
    }

    public static void main(String[] args) throws Exception {
        // 착과수 μ‘°μ‚¬ν‘œ
        JsonNode fruit = processForm("fruit_survey.jpg", "template");
        System.out.println(fruit.toPrettyString());

        // ν˜„μ§€μ‘°μ‚¬μ„œ
        JsonNode field = processForm("field_survey.png", "survey");

        // λ‚˜λ¬΄μ‘°μ‚¬
        JsonNode tree = processForm("tree_form.tif", "tree");
    }
}

Concurrent Processing with CompletableFuture

Java
import java.net.URI;
import java.net.http.*;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.*;

public class BatchOCR {
    private static final String API_URL = "http://localhost:6800/api/ocr";
    private static final HttpClient client = HttpClient.newHttpClient();

    public static CompletableFuture<String> processAsync(Path imagePath) {
        try {
            byte[] bytes = Files.readAllBytes(imagePath);
            String ext = imagePath.toString().substring(
                imagePath.toString().lastIndexOf('.') + 1);
            String json = String.format("""
                {"image":"%s","imageType":"%s","mode":"receipt"}
                """, Base64.getEncoder().encodeToString(bytes), ext);

            HttpRequest req = HttpRequest.newBuilder()
                .uri(URI.create(API_URL))
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(json))
                .build();

            return client.sendAsync(req, HttpResponse.BodyHandlers.ofString())
                .thenApply(HttpResponse::body);
        } catch (Exception e) {
            return CompletableFuture.failedFuture(e);
        }
    }

    public static void main(String[] args) throws Exception {
        List<Path> images = Files.list(Path.of("receipts/"))
            .filter(p -> p.toString().matches(".*\\.(jpg|png|jpeg)$"))
            .collect(Collectors.toList());

        // All requests are sent concurrently β€” server auto-batches them
        List<CompletableFuture<String>> futures = images.stream()
            .map(BatchOCR::processAsync)
            .collect(Collectors.toList());

        CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();

        for (int i = 0; i < images.size(); i++) {
            System.out.printf("%s: %s%n",
                images.get(i).getFileName(), futures.get(i).get());
        }
    }
}

Full OCR

Bash / cURL
# Encode the image to base64
IMAGE_B64=$(base64 -w 0 document.png)

# Send the request
curl -s -X POST http://localhost:6800/api/ocr \
  -H "Content-Type: application/json" \
  -d "{
    \"image\": \"$IMAGE_B64\",
    \"imageType\": \"png\",
    \"mode\": \"full\"
  }" | jq .

Receipt Mode

Bash / cURL
# One-liner: encode + send receipt for processing
curl -s -X POST http://localhost:6800/api/ocr \
  -H "Content-Type: application/json" \
  -d "$(jq -n --arg img "$(base64 -w 0 receipt.jpg)" '{
    image: $img,
    imageType: "jpg",
    mode: "receipt"
  }')" | jq '.data | {store: .storeName, total: .totalAfterTax}'

Targeted Mode

Bash / cURL
curl -s -X POST http://localhost:6800/api/ocr \
  -H "Content-Type: application/json" \
  -d "$(jq -n --arg img "$(base64 -w 0 invoice.png)" '{
    image: $img,
    imageType: "png",
    mode: "targeted",
    query: {
      documentType: "invoice",
      columns: ["ν’ˆλͺ…", "μˆ˜λŸ‰", "단가", "κΈˆμ•‘"],
      items: ["사과", "λ°°"]
    }
  }')" | jq '.data.json.items'

NH Template Modes (template / survey / tree)

Bash / cURL
# 착과수 μ‘°μ‚¬ν‘œ (fruit survey) β€” mode: template
curl -s -X POST http://localhost:6800/api/ocr \
  -H "Content-Type: application/json" \
  -d "$(jq -n --arg img "$(base64 -w 0 fruit_survey.jpg)" '{
    image: $img,
    imageType: "jpg",
    mode: "template"
  }')" | jq '.data | {json, validation}'

# ν˜„μ§€μ‘°μ‚¬μ„œ (field survey) β€” mode: survey
curl -s -X POST http://localhost:6800/api/ocr \
  -H "Content-Type: application/json" \
  -d "$(jq -n --arg img "$(base64 -w 0 field_survey.png)" '{
    image: $img,
    imageType: "png",
    mode: "survey"
  }')" | jq '.data.validation'

# λ‚˜λ¬΄μ‘°μ‚¬ (tree investigation) β€” mode: tree
curl -s -X POST http://localhost:6800/api/ocr \
  -H "Content-Type: application/json" \
  -d "$(jq -n --arg img "$(base64 -w 0 tree_form.tif)" '{
    image: $img,
    imageType: "tif",
    mode: "tree"
  }')" | jq '.data.json'

Batch Processing with xargs

Bash
# Process all images in parallel (4 at a time)
# Server auto-batches concurrent requests for optimal throughput
ls receipts/*.jpg | xargs -P 4 -I {} bash -c '
  FILE="{}"
  B64=$(base64 -w 0 "$FILE")
  RESULT=$(curl -s -X POST http://localhost:6800/api/ocr \
    -H "Content-Type: application/json" \
    -d "{\"image\":\"$B64\",\"imageType\":\"jpg\",\"mode\":\"receipt\"}")
  TOTAL=$(echo "$RESULT" | jq -r ".data.totalAfterTax // \"error\"")
  echo "$FILE: $TOTAL"
'

Basic Usage

Go
package main

import (
	"bytes"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"path/filepath"
	"strings"
)

const apiURL = "http://localhost:6800/api/ocr"

type OCRRequest struct {
	Image     string      `json:"image"`
	ImageType string      `json:"imageType"`
	Mode      string      `json:"mode"`
	Prompt    string      `json:"prompt,omitempty"`
	Query     interface{} `json:"query,omitempty"`
}

type OCRResponse struct {
	ID    string                 `json:"id"`
	Mode  string                 `json:"mode"`
	Error string                 `json:"error,omitempty"`
	Data  map[string]interface{} `json:"data,omitempty"`
}

func ocrImage(filePath, mode string) (*OCRResponse, error) {
	data, err := os.ReadFile(filePath)
	if err != nil {
		return nil, err
	}

	ext := strings.TrimPrefix(filepath.Ext(filePath), ".")
	if ext == "jpg" {
		ext = "jpeg"
	}

	payload, _ := json.Marshal(OCRRequest{
		Image:     base64.StdEncoding.EncodeToString(data),
		ImageType: ext,
		Mode:      mode,
	})

	resp, err := http.Post(apiURL, "application/json", bytes.NewReader(payload))
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	var result OCRResponse
	json.NewDecoder(resp.Body).Decode(&result)
	return &result, nil
}

func main() {
	result, err := ocrImage("document.png", "full")
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error: %v\n", err)
		os.Exit(1)
	}
	if result.Error != "" {
		fmt.Fprintf(os.Stderr, "OCR error: %s\n", result.Error)
		os.Exit(1)
	}
	fmt.Println(result.Data["text"])
}

NH Template Modes (template / survey / tree)

Go
package main

import (
	"bytes"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"path/filepath"
	"strings"
)

const apiURL = "http://localhost:6800/api/ocr"

// processNHForm sends a scanned NH form for OCR.
// mode: "template" (착과수), "survey" (ν˜„μ§€μ‘°μ‚¬μ„œ), "tree" (λ‚˜λ¬΄μ‘°μ‚¬)
func processNHForm(filePath, mode string) (map[string]interface{}, error) {
	data, err := os.ReadFile(filePath)
	if err != nil {
		return nil, err
	}

	ext := strings.TrimPrefix(filepath.Ext(filePath), ".")
	if ext == "jpg" {
		ext = "jpeg"
	}

	payload, _ := json.Marshal(map[string]string{
		"image":     base64.StdEncoding.EncodeToString(data),
		"imageType": ext,
		"mode":      mode,
	})

	resp, err := http.Post(apiURL, "application/json", bytes.NewReader(payload))
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	var result map[string]interface{}
	json.NewDecoder(resp.Body).Decode(&result)

	if errMsg, ok := result["error"].(string); ok {
		return nil, fmt.Errorf("OCR error: %s", errMsg)
	}

	// Extract data and check validation
	resultData, _ := result["data"].(map[string]interface{})
	if validation, ok := resultData["validation"].(map[string]interface{}); ok {
		trusted, _ := validation["trusted"].(bool)
		if !trusted {
			if valid, ok := validation["valid"].(bool); ok {
				trusted = valid
			}
		}
		if trusted {
			fmt.Println("βœ… Validation passed")
		} else {
			fmt.Println("⚠️ Validation issues found")
			if issues, ok := validation["issues"].([]interface{}); ok {
				for _, issue := range issues {
					fmt.Printf("  - %v\n", issue)
				}
			}
		}
	}

	parsed, _ := resultData["json"].(map[string]interface{})
	return parsed, nil
}

func main() {
	// 착과수 μ‘°μ‚¬ν‘œ
	fruit, err := processNHForm("fruit_survey.jpg", "template")
	if err != nil {
		fmt.Println(err)
	} else {
		out, _ := json.MarshalIndent(fruit, "", "  ")
		fmt.Println(string(out))
	}

	// ν˜„μ§€μ‘°μ‚¬μ„œ
	processNHForm("field_survey.png", "survey")

	// λ‚˜λ¬΄μ‘°μ‚¬
	processNHForm("tree_form.tif", "tree")
}

Concurrent Processing

Go
package main

import (
	"bytes"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"sync"
)

func main() {
	files, _ := filepath.Glob("receipts/*.jpg")
	fmt.Printf("Processing %d images...\n", len(files))

	var wg sync.WaitGroup
	for _, f := range files {
		wg.Add(1)
		go func(path string) {
			defer wg.Done()

			data, _ := os.ReadFile(path)
			ext := strings.TrimPrefix(filepath.Ext(path), ".")

			payload, _ := json.Marshal(map[string]string{
				"image":     base64.StdEncoding.EncodeToString(data),
				"imageType": ext,
				"mode":      "receipt",
			})

			resp, err := http.Post("http://localhost:6800/api/ocr",
				"application/json", bytes.NewReader(payload))
			if err != nil {
				fmt.Printf("❌ %s: %v\n", filepath.Base(path), err)
				return
			}
			defer resp.Body.Close()

			var result map[string]interface{}
			json.NewDecoder(resp.Body).Decode(&result)
			fmt.Printf("βœ… %s: %v\n", filepath.Base(path), result["data"])
		}(f)
	}
	wg.Wait()
}

Basic Usage (.NET 6+)

C#
using System.Net.Http.Json;
using System.Text.Json;

var apiUrl = "http://localhost:6800/api/ocr";
var client = new HttpClient();

async Task<JsonDocument> OcrImageAsync(string filePath, string mode = "full")
{
    var imageBytes = await File.ReadAllBytesAsync(filePath);
    var ext = Path.GetExtension(filePath).TrimStart('.').ToLower();
    if (ext == "jpg") ext = "jpeg";

    var payload = new {
        image = Convert.ToBase64String(imageBytes),
        imageType = ext,
        mode = mode
    };

    var response = await client.PostAsJsonAsync(apiUrl, payload);
    var json = await response.Content.ReadAsStringAsync();
    return JsonDocument.Parse(json);
}

// Usage
var result = await OcrImageAsync("document.png", "full");
Console.WriteLine(result.RootElement
    .GetProperty("data")
    .GetProperty("text")
    .GetString());

NH Template Modes (template / survey / tree)

C#
using System.Net.Http.Json;
using System.Text.Json;

var apiUrl = "http://localhost:6800/api/ocr";
var client = new HttpClient { Timeout = TimeSpan.FromMinutes(5) };

/// <summary>
/// Process an NH agricultural form.
/// mode: "template" (착과수), "survey" (ν˜„μ§€μ‘°μ‚¬μ„œ), "tree" (λ‚˜λ¬΄μ‘°μ‚¬)
/// </summary>
async Task<JsonElement?> ProcessNHFormAsync(string filePath, string mode)
{
    var bytes = await File.ReadAllBytesAsync(filePath);
    var ext = Path.GetExtension(filePath).TrimStart('.').ToLower();
    if (ext == "jpg") ext = "jpeg";

    var payload = new {
        image = Convert.ToBase64String(bytes),
        imageType = ext,
        mode = mode
    };

    var response = await client.PostAsJsonAsync(apiUrl, payload);
    var json = await response.Content.ReadAsStringAsync();
    var doc = JsonDocument.Parse(json);
    var root = doc.RootElement;

    // Check for errors
    if (root.TryGetProperty("error", out var error))
    {
        Console.WriteLine($"Error: {error.GetString()}");
        return null;
    }

    // Check validation
    var data = root.GetProperty("data");
    if (data.TryGetProperty("validation", out var validation))
    {
        var trusted = validation.TryGetProperty("trusted", out var t)
            ? t.GetBoolean()
            : validation.TryGetProperty("valid", out var v) && v.GetBoolean();

        Console.WriteLine(trusted ? "βœ… Validation passed" : "⚠️ Issues found");
        if (validation.TryGetProperty("issues", out var issues))
        {
            foreach (var issue in issues.EnumerateArray())
                Console.WriteLine($"  - {issue.GetString()}");
        }
    }

    return data.TryGetProperty("json", out var parsed) ? parsed : null;
}

// Usage
var fruitData = await ProcessNHFormAsync("fruit_survey.jpg", "template");
Console.WriteLine(fruitData?.ToString());

var fieldData = await ProcessNHFormAsync("field_survey.png", "survey");
var treeData  = await ProcessNHFormAsync("tree_form.tif", "tree");

Concurrent Processing

C#
using System.Net.Http.Json;
using System.Text.Json;

var apiUrl = "http://localhost:6800/api/ocr";
var client = new HttpClient { Timeout = TimeSpan.FromMinutes(5) };

var images = Directory.GetFiles("receipts/", "*.jpg");
Console.WriteLine($"Processing {images.Length} images...");

// All requests are sent concurrently β€” server auto-batches
var tasks = images.Select(async file =>
{
    var bytes = await File.ReadAllBytesAsync(file);
    var payload = new {
        image = Convert.ToBase64String(bytes),
        imageType = "jpg",
        mode = "receipt"
    };

    var resp = await client.PostAsJsonAsync(apiUrl, payload);
    var json = await resp.Content.ReadAsStringAsync();
    return (File: Path.GetFileName(file), Result: json);
}).ToList();

var results = await Task.WhenAll(tasks);
foreach (var (file, result) in results)
{
    Console.WriteLine($"{file}: {result[..Math.Min(result.Length, 120)]}...");
}

7. Error Handling

The API returns standard HTTP status codes:

StatusMeaning
200Success β€” check the response data field
400Bad request β€” invalid JSON, missing image, unsupported imageType or mode
405Method not allowed β€” only POST is accepted
413Request too large β€” max 30 MB
500Internal error β€” VLM processing failure
429Rate limited β€” too many requests (if rate limiter is enabled)
πŸ’‘ Note: A VLM processing error (e.g. model timeout) returns HTTP 200 with an "error" field in the JSON body. Always check for result.error in your response handling.

Error handling example (Python)

Python
import requests

def safe_ocr(image_b64, image_type="png", mode="full"):
    try:
        resp = requests.post("http://localhost:6800/api/ocr", json={
            "image": image_b64,
            "imageType": image_type,
            "mode": mode,
        }, timeout=300)

        # HTTP-level error
        resp.raise_for_status()

        result = resp.json()

        # Application-level error
        if "error" in result:
            print(f"OCR error: {result['error']}")
            return None

        return result["data"]

    except requests.Timeout:
        print("Request timed out (5 min limit)")
        return None
    except requests.RequestException as e:
        print(f"Request failed: {e}")
        return None

8. Auto-Batching Behavior

The server transparently batches incoming requests for optimal GPU utilization:

πŸ’‘ What this means for you: Just send requests whenever you need to. If you're processing many images, send them concurrently β€” the server will automatically batch and process them efficiently. No special batching logic needed on your side.

9. Best Practices