DocsQuickstart: your first successful job

Quickstart: your first successful job

Sign up, add funds, and run your first job with curl, from request to result.

This page takes you from no account to a finished job. Every command here is plain curl and runs in any shell.

Veralens turns images, PDFs, office files and text into JSON that matches your schema, or into Markdown or plain text. Every request creates a job. You create it, then you fetch the result when it is done (or get a webhook).

Want to see output before you write code? Browse three real documents with their schema, output and cost.

1. Create an account and an API key

  1. Sign up at veralens.ai.
  2. Open API Keys in the dashboard and create a key.
  3. Copy the secret. It starts with vl_ and is shown once. Store it somewhere safe.

2. Add funds

There is no free balance. A job you create with an empty balance is rejected with insufficient_balance, and the response includes a topup_url to add funds.

Open Usage in the dashboard and add funds, from $5, €5 or £5 up to 1,000 at a time. Funds do not expire. A failed job costs nothing. See Pricing & Funds.

3. Set your key and the base URL

bash
export VERALENS_API_KEY="vl_your_secret_key"
export BASE="https://veralens.ai/api/v1"

Every request sends the key as a Bearer token:

http
Authorization: Bearer vl_your_secret_key

4. Create a job from text

Send one or more sources and a schema that describes the JSON you want back.

bash
curl -s -X POST "$BASE/jobs/create" \
  -H "Authorization: Bearer $VERALENS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "sources": [
      { "type": "text", "data": "Invoice A-1029 from Acme Corp. Total due: 412.50 USD by 1 March 2026." }
    ],
    "schema": {
      "type": "object",
      "properties": {
        "invoiceNumber": { "type": "string", "description": "Invoice number exactly as printed." },
        "vendor":        { "type": "string", "description": "Company that sent the invoice." },
        "total":         { "type": "number", "description": "Total amount due." },
        "currency":      { "type": "string", "format": "currency" },
        "dueDate":       { "type": "string", "format": "date" }
      },
      "required": ["invoiceNumber", "total"]
    }
  }'

You get 201 Created and the job, already in pending. usage.cost is the hold: an estimate kept aside from your balance until the job ends. output and the token counts arrive when it finishes.

json
{
  "status": "success",
  "data": {
    "id": "V1StGXR8_Z5jdHi6Bx",
    "status": "pending",
    "createdAt": "2026-10-06T10:15:00.000Z",
    "source": "key",
    "target": "json",
    "sources": { "images": 0, "texts": 1, "documents": 0 },
    "model": "vera-1.0-high",
    "usage": { "cost": 0.0021, "inputTokens": null, "outputTokens": null },
    "output": null,
    "durationMs": null
  }
}

Save the id:

bash
export JOB_ID="V1StGXR8_Z5jdHi6Bx"

5. Or create a job from a PDF

Files go in as a base64 data URL. A whole file is too long for the command line, so write the body to a file first and send it with --data-binary.

bash
printf '{"sources":[{"type":"document","data":"data:application/pdf;base64,%s"}],"target":"markdown"}' \
  "$(base64 -w0 invoice.pdf)" > body.json

curl -s -X POST "$BASE/jobs/create" \
  -H "Authorization: Bearer $VERALENS_API_KEY" \
  -H "Content-Type: application/json" \
  --data-binary @body.json

On macOS, use base64 -i invoice.pdf instead of base64 -w0 invoice.pdf. The same body works for images (data:image/png;base64,...) and office files. See sources for every accepted type and limit.

6. Get the result

Fetch the job every 2 to 3 seconds until status is completed or failed. Polling faster runs into the rate limit of 30 requests a minute.

bash
curl -s "$BASE/jobs/$JOB_ID/get" \
  -H "Authorization: Bearer $VERALENS_API_KEY"

A finished job looks like this:

json
{
  "status": "success",
  "data": {
    "id": "V1StGXR8_Z5jdHi6Bx",
    "status": "completed",
    "createdAt": "2026-10-06T10:15:00.000Z",
    "source": "key",
    "target": "json",
    "sources": { "images": 0, "texts": 1, "documents": 0 },
    "model": "vera-1.0-high",
    "usage": { "cost": 0.0019, "inputTokens": 212, "outputTokens": 41 },
    "output": "{\"invoiceNumber\":\"A-1029\",\"vendor\":\"Acme Corp\",\"total\":412.5,\"currency\":\"USD\",\"dueDate\":\"2026-03-01\"}",
    "durationMs": 1840
  }
}

output is always a JSON string, so parse it on your side. This holds for every target: a markdown job gives you the markdown text once you parse it. usage.cost is now the real charge in USD. It can be higher or lower than the hold. A failed job costs nothing: the hold goes back to your balance.

A wait loop in shell (needs jq):

bash
while true; do
  JOB=$(curl -s "$BASE/jobs/$JOB_ID/get" -H "Authorization: Bearer $VERALENS_API_KEY")
  STATUS=$(echo "$JOB" | jq -r .data.status)
  [ "$STATUS" != "pending" ] && break
  sleep 3
done
echo "$JOB" | jq -r '.data.output | fromjson'

The same flow in Python, with requests:

python
import json, os, time
import requests

BASE = "https://veralens.ai/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['VERALENS_API_KEY']}"}

body = {
    "sources": [{"type": "text", "data": "Invoice A-1029 from Acme Corp. Total due: 412.50 USD."}],
    "schema": {
        "type": "object",
        "properties": {
            "invoiceNumber": {"type": "string"},
            "total": {"type": "number"},
        },
    },
}
job = requests.post(f"{BASE}/jobs/create", json=body, headers=HEADERS).json()["data"]

while job["status"] == "pending":
    time.sleep(3)
    job = requests.get(f"{BASE}/jobs/{job['id']}/get", headers=HEADERS).json()["data"]

if job["status"] == "failed":
    raise RuntimeError(f"job {job['id']} failed")
print(json.loads(job["output"]))

And in Node 18 or later, with the built-in fetch:

js
const BASE = "https://veralens.ai/api/v1"
const headers = {
  Authorization: `Bearer ${process.env.VERALENS_API_KEY}`,
  "Content-Type": "application/json",
}

const body = {
  "sources": [{ "type": "text", "data": "Invoice A-1029 from Acme Corp. Total due: 412.50 USD." }],
  "schema": {
    "type": "object",
    "properties": {
      "invoiceNumber": { "type": "string" },
      "total": { "type": "number" },
    },
  },
}
let res = await fetch(`${BASE}/jobs/create`, { method: "POST", headers, body: JSON.stringify(body) })
let job = (await res.json()).data

while (job.status === "pending") {
  await new Promise((r) => setTimeout(r, 3000))
  res = await fetch(`${BASE}/jobs/${job.id}/get`, { headers })
  job = (await res.json()).data
}

if (job.status === "failed") throw new Error(`job ${job.id} failed`)
console.log(JSON.parse(job.output))

7. Or get a webhook

Add "webhook_url": "https://yourapp.com/hooks/veralens" to the create body and Veralens posts the result to that URL when the job ends. Webhooks need an API key; they don't work from the dashboard. Your handler must:

  • check the x-veralens-signature header (below),
  • reply with a 2xx within 10 seconds, then do the work,
  • skip a job_id it has already handled, because a retry can send the same event twice.

The signature is t=<unix seconds>,v1=<hex>. v1 is an HMAC SHA-256 of <t>.<raw body>, keyed with the secret of the API key that created the job. In Node:

js
import { createHmac, timingSafeEqual } from "node:crypto"

//rawBody: the request body exactly as received, before JSON.parse
export function isVeralensWebhook(rawBody, signatureHeader, apiKey) {
  const parts = Object.fromEntries(
    signatureHeader.split(",").map((part) => part.split("=")),
  )
  const expected = createHmac("sha256", apiKey)
    .update(`${parts.t}.${rawBody}`)
    .digest("hex")
  const received = Buffer.from(parts.v1 ?? "")
  if (received.length !== expected.length) return false
  if (!timingSafeEqual(received, Buffer.from(expected))) return false
  //refuse old deliveries so a captured request can't be replayed later
  return Math.abs(Date.now() / 1000 - Number(parts.t)) < 300
}

See Jobs & Queue for the payloads and the retry schedule.

Troubleshooting

You getWhat to do
You get401 unauthorizedWhat to doThe key is missing, mistyped or revoked. Send Authorization: Bearer vl_....
You get400 insufficient_balanceWhat to doYour balance can't cover the hold. Add funds.
You get400 invalid_inputWhat to doThe body failed validation. Send schema, target or both. A schema needs target json or no target. webhook_url must be a full URL and needs an API key.
You get400 validation_json_malformedWhat to doThe body is not valid JSON. Check the quotes, or send the body from a file.
You get400 job_document_* or job_image_mime_unsupportedWhat to doThe file type, size or page count is not accepted. See sources.
You get429 rate_limit_exceededWhat to doWait the number of seconds in the Retry-After header, then retry.
You getJob ends failedWhat to doNothing was charged. Retry the job. If the same file fails again, send a smaller file or ask for fewer fields.

Next steps