File Processing

Submit PDFs for OCR processing and stream real-time progress via the REST API.

Upload and process a file

The recommended way to send a file through the pipeline. Handles upload and dispatch in a single request.

POST /api/file-processing/tenant-upload-and-process

Headers

HeaderRequiredValue
x-api-keyYesYour tenant API key
x-user-idYesUser to associate with this file
Content-TypeYesmultipart/form-data

Body — multipart form with a single file field (PDF, max 64 MB).

Response 200 OK

{
  "success": true,
  "fileId": "file_abc123"
}

Use fileId to poll progress or query records.

Example (cURL)

curl -X POST https://nexa-ed.com/api/file-processing/tenant-upload-and-process \
  -H "x-api-key: nxa_live_..." \
  -H "x-user-id: user_abc123" \
  -F "file=@/path/to/term2-results.pdf"

Example (Python)

import requests
 
with open("term2-results.pdf", "rb") as f:
    response = requests.post(
        "https://nexa-ed.com/api/file-processing/tenant-upload-and-process",
        headers={
            "x-api-key": "nxa_live_...",
            "x-user-id": "user_abc123",
        },
        files={"file": ("term2-results.pdf", f, "application/pdf")},
    )
file_id = response.json()["fileId"]

Info

Browser upload? Use the two-phase flow below to avoid the file passing through your server. The /tenant-upload-and-process endpoint above is ideal for server-to-server or CLI usage.


Browser-direct upload (two-phase)

For browser clients where you don't want the file passing through your server:

Phase 1 — Request an upload token

POST /api/file-processing/upload-token

Headers

HeaderRequiredValue
x-api-keyYesYour tenant API key (server-side only)
x-user-idYesUser to associate with this file
Content-TypeYesapplication/json

Body

{
  "name": "term2-results.pdf",
  "size": 2048576,
  "type": "application/pdf"
}

Response 200 OK

{
  "token": "eyJ...",
  "uploadUrl": "https://nexa-ed.com/api/file-processing/tenant-upload-and-process"
}

The token is valid for 2 minutes.

Phase 2 — Upload directly from the browser

POST {uploadUrl}

Headers

HeaderRequiredValue
x-upload-tokenYesToken from Phase 1
Content-TypeYesmultipart/form-data

Body — same as above: multipart file field, PDF only, max 64 MB.

Response 200 OK — same shape as the single-step endpoint: { success: true, fileId }.

Example (browser JavaScript)

// Phase 1 — run server-side (your API route, not the browser)
const { token, uploadUrl } = await fetch("/api/your-prepare-route", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: file.name, size: file.size, type: file.type }),
}).then(r => r.json());
 
// Phase 2 — run in the browser
const form = new FormData();
form.append("file", file);
const { fileId } = await fetch(uploadUrl, {
  method: "POST",
  headers: { "x-upload-token": token },
  body: form,
}).then(r => r.json());

Info

Using Next.js? The @nexa-ed/next SDK handles Phase 1 automatically via the /api/nexa/prepare route. The useUploadFile hook and <UploadZone /> component implement the full two-phase flow out of the box.


Submit a file for processing

Kick off the OCR pipeline for an already-uploaded file. Returns immediately with a jobId — processing happens asynchronously.

POST /api/file-processing/extract

Warning

Rate limit: 20 requests / minute per tenant. This endpoint calls Google Document AI and Gemini — it is the most expensive route on the platform. Exceeding the limit returns HTTP 429 with a Retry-After header. See Rate limits for retry guidance.

Headers

HeaderRequiredValue
x-api-keyYesYour tenant API key
x-user-idNoUser to associate with this file
Content-TypeYesapplication/json

Body

{
  "fileUrl": "https://uploadthing.com/f/abc123.pdf",
  "fileId": "file_abc123",
  "fileName": "term2-results.pdf",
  "extractionType": "document_ai"
}
FieldTypeRequiredDescription
fileUrlstringYesPublicly accessible URL of the uploaded PDF
fileIdstringYesYour identifier for this file
fileNamestringNoHuman-readable name
extractionTypestringNo"document_ai" (default) or "tesseract"

Response 202 Accepted

{
  "success": true,
  "status": "pending",
  "jobId": "job_xyz789"
}

Example (cURL)

curl -X POST https://nexa-ed.com/api/file-processing/extract \
  -H "x-api-key: nxa_live_..." \
  -H "x-user-id: user_abc123" \
  -H "Content-Type: application/json" \
  -d '{
    "fileUrl": "https://uploadthing.com/f/abc123.pdf",
    "fileId": "file_abc123",
    "fileName": "term2-results.pdf"
  }'

Example (Python)

import requests
 
response = requests.post(
    "https://nexa-ed.com/api/file-processing/extract",
    headers={
        "x-api-key": "nxa_live_...",
        "x-user-id": "user_abc123",
    },
    json={
        "fileUrl": "https://uploadthing.com/f/abc123.pdf",
        "fileId": "file_abc123",
        "fileName": "term2-results.pdf",
    },
)
data = response.json()
job_id = data["jobId"]

Stream processing progress

Subscribe to real-time SSE events for a file. The stream closes automatically when processing completes or fails.

GET /api/file-processing/progress/:fileId

Headers

HeaderRequiredValue
x-api-keyYesYour tenant API key
AcceptYestext/event-stream

SSE events

Each event is a JSON object in the data: field:

data: {"stage":"ocr","progress":45,"message":"Extracting page 3 of 8"}

data: {"stage":"analysis","progress":80,"message":"Running LLM refinement"}

data: {"stage":"complete","progress":100,"fileId":"file_abc123","recordCount":34}
FieldTypeDescription
stagestring"queued" | "ocr" | "analysis" | "complete" | "failed"
progressnumber0–100
messagestringHuman-readable status
fileIdstringEchoed on completion
recordCountnumberNumber of student records extracted (on complete)
errorstringError message (on failed)

Example (Python with sseclient)

import sseclient, requests
 
with requests.get(
    f"https://nexa-ed.com/api/file-processing/progress/{file_id}",
    headers={"x-api-key": "nxa_live_...", "Accept": "text/event-stream"},
    stream=True,
) as resp:
    client = sseclient.SSEClient(resp)
    for event in client.events():
        import json
        data = json.loads(event.data)
        print(f"{data['stage']}: {data['progress']}%")
        if data["stage"] in ("complete", "failed"):
            break

Example (Node.js EventSource)

const { EventSource } = require("eventsource");
 
const es = new EventSource(
  `https://nexa-ed.com/api/file-processing/progress/${fileId}`,
  { headers: { "x-api-key": process.env.NEXA_API_KEY } },
);
 
es.onmessage = (e) => {
  const data = JSON.parse(e.data);
  console.log(`${data.stage}: ${data.progress}%`);
  if (data.stage === "complete" || data.stage === "failed") es.close();
};

Check file status

Lightweight polling alternative to SSE.

GET /api/file-processing/status?fileId=file_abc123

Response 200 OK

{
  "fileId": "file_abc123",
  "status": "processing",
  "progress": 45,
  "stage": "ocr"
}

List processed files

GET /api/file-processing/history

Query params

ParamTypeDefaultDescription
userIdstring—Filter by user
limitnumber20Max results
offsetnumber0Pagination offset

Response

{
  "files": [
    {
      "fileId": "file_abc123",
      "fileName": "term2-results.pdf",
      "status": "complete",
      "recordCount": 34,
      "createdAt": "2026-03-15T10:22:00Z"
    }
  ],
  "total": 1
}

Webhook — file completed

When processing finishes, Nexa sends a POST to your configured webhook URL.

Payload

{
  "event": "file.completed",
  "fileId": "file_abc123",
  "userId": "user_abc123",
  "tenantId": "loretto_00123",
  "recordCount": 34,
  "timestamp": 1742000000
}

See REST API Overview → Webhook verification for how to verify the signature.

On this page