{
  "openapi": "3.1.0",
  "info": {
    "title": "Veralens API",
    "version": "1.0.0",
    "summary": "Extract structured data from text, images and documents.",
    "description": "Send text, images or documents with an optional JSON Schema and get structured JSON, Markdown or plain text back. Jobs run asynchronously: create one, then receive the result on a webhook or poll the job. Every response uses the same envelope: `{ \"status\": \"success\", \"data\": ... }` or `{ \"status\": \"error\", \"error_code\": ... }`. Branch on the HTTP status and `error_code`. Full guides: https://veralens.ai/docs",
    "termsOfService": "https://veralens.ai/terms",
    "contact": {
      "name": "Veralens support",
      "email": "support@veralens.ai",
      "url": "https://veralens.ai/docs"
    }
  },
  "externalDocs": {
    "description": "API Reference",
    "url": "https://veralens.ai/docs/api-reference"
  },
  "servers": [{ "url": "https://veralens.ai/api/v1" }],
  "security": [{ "apiKey": [] }],
  "tags": [
    {
      "name": "Jobs",
      "description": "Create extraction jobs and read their results.",
      "externalDocs": { "url": "https://veralens.ai/docs/jobs" }
    }
  ],
  "paths": {
    "/jobs/create": {
      "post": {
        "operationId": "createJob",
        "tags": ["Jobs"],
        "summary": "Create an extraction job",
        "description": "Returns at once with the job in `pending`; the work runs asynchronously. The estimated cost is held on your balance until the job settles. Send at least one of `schema` or `target`.",
        "security": [{ "apiKey": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateJobRequest"
              },
              "example": {
                "sources": [
                  {
                    "type": "image",
                    "data": "data:image/png;base64,iVBORw0KGgo..."
                  }
                ],
                "engine": "vera-1.0-high",
                "schema": {
                  "type": "object",
                  "properties": {
                    "merchant": {
                      "type": "string",
                      "description": "Store or merchant name."
                    },
                    "total": {
                      "type": "number",
                      "description": "Grand total paid."
                    }
                  },
                  "required": ["merchant", "total"]
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The job, already in `pending`. `model`, `target` and the hold (`usage.cost`) are set; `output` and token usage fill in once the job runs.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/JobResponse" }
              }
            }
          },
          "400": {
            "description": "The body failed validation (`invalid_input`, `validation_json_malformed`, a `job_*` source error, `engine_invalid_request`) or the balance does not cover the hold (`insufficient_balance`, with `message` and `topup_url`).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    { "$ref": "#/components/schemas/Error" },
                    {
                      "$ref": "#/components/schemas/InsufficientBalanceError"
                    }
                  ]
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" },
          "500": { "$ref": "#/components/responses/InternalServerError" },
          "502": {
            "description": "The engine is temporarily unavailable (`engine_service_unavailable`). Nothing was held. Retry shortly.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "503": {
            "description": "The engine is busy (`engine_unavailable`). Nothing was held. Retry shortly.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "504": {
            "description": "Sizing the job took too long (`engine_timeout`). Nothing was held. Retry shortly.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          }
        }
      }
    },
    "/jobs/{jobId}/get": {
      "get": {
        "operationId": "getJob",
        "tags": ["Jobs"],
        "summary": "Get a job",
        "description": "Fetch one of your jobs by id. Poll every 2 to 3 seconds until `status` is no longer `pending`, or use a webhook instead.",
        "security": [{ "apiKey": [] }],
        "parameters": [
          {
            "name": "jobId",
            "in": "path",
            "required": true,
            "description": "Job id, as returned by `POST /jobs/create`.",
            "schema": { "type": "string", "minLength": 1 }
          }
        ],
        "responses": {
          "200": {
            "description": "The job.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/JobResponse" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": {
            "description": "No job with that id for your account (`job_not_found`).",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "429": { "$ref": "#/components/responses/TooManyRequests" },
          "500": { "$ref": "#/components/responses/InternalServerError" }
        }
      }
    },
    "/jobs/list": {
      "get": {
        "operationId": "listJobs",
        "tags": ["Jobs"],
        "summary": "List jobs",
        "description": "List your jobs, most recent first.",
        "security": [{ "apiKey": [] }],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "description": "Returns jobs whose id contains this text. It does not search inside jobs.",
            "schema": { "type": "string" }
          },
          {
            "name": "status",
            "in": "query",
            "description": "`finished` returns completed jobs.",
            "schema": {
              "type": "string",
              "enum": ["all", "finished", "pending", "failed"],
              "default": "all"
            }
          },
          {
            "name": "source",
            "in": "query",
            "description": "Filter by origin: `key` (API) or `playground` (dashboard).",
            "schema": { "$ref": "#/components/schemas/JobOrigin" }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Page size.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 100
            }
          },
          {
            "name": "offset",
            "in": "query",
            "description": "Rows to skip.",
            "schema": { "type": "integer", "minimum": 0, "default": 0 }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of jobs and the total count for the filter.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobListResponse"
                }
              }
            }
          },
          "400": {
            "description": "A query parameter is not valid (`invalid_input`).",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" },
          "500": { "$ref": "#/components/responses/InternalServerError" }
        }
      }
    }
  },
  "webhooks": {
    "jobFinished": {
      "post": {
        "summary": "Job finished",
        "description": "Sent to the `webhook_url` of a job created with an API key, once the job ends. Verify `x-veralens-signature` before you trust the body: `v1` is the hex HMAC SHA-256 of `<t>.<raw body>`, keyed with the secret of the API key that created the job. Reply `2xx` within 10 seconds; failed deliveries are retried up to 6 more times, so dedupe on `job_id`.",
        "externalDocs": { "url": "https://veralens.ai/docs/jobs" },
        "parameters": [
          {
            "name": "x-veralens-signature",
            "in": "header",
            "required": true,
            "description": "`t=<unix seconds>,v1=<hex>`.",
            "schema": { "type": "string" }
          },
          {
            "name": "x-veralens-timestamp",
            "in": "header",
            "required": true,
            "description": "The same `t`, on its own.",
            "schema": { "type": "string" }
          },
          {
            "name": "x-veralens-delivery-attempt",
            "in": "header",
            "required": true,
            "description": "The attempt number for this delivery.",
            "schema": { "type": "string" }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "object",
                    "required": ["status", "job_id", "data"],
                    "properties": {
                      "status": { "const": "success" },
                      "job_id": { "type": "string" },
                      "data": {
                        "description": "The parsed result: an object for `json` targets, a string for `markdown` and `plaintext`."
                      }
                    }
                  },
                  {
                    "type": "object",
                    "required": ["status", "job_id"],
                    "description": "The job ended `failed` and its hold was refunded.",
                    "properties": {
                      "status": { "const": "error" },
                      "job_id": { "type": "string" }
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Acknowledge the delivery with any `2xx`."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "An API key from **API Keys** in the dashboard, sent as `Authorization: Bearer vl_...`. The secret starts with `vl_` and is shown once."
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing, wrong or revoked API key (`unauthorized`).",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" }
          }
        }
      },
      "Forbidden": {
        "description": "The account is blocked (`user_banned`). Contact support@veralens.ai.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" }
          }
        }
      },
      "TooManyRequests": {
        "description": "Over a rate limit (`rate_limit_exceeded`), or too many extractions at once (`engine_rate_limited`). Limits apply per API key and per endpoint.",
        "headers": {
          "Retry-After": {
            "description": "Seconds to wait before you retry.",
            "schema": { "type": "integer" }
          },
          "RateLimit-Limit": {
            "description": "The limit of the window you hit.",
            "schema": { "type": "integer" }
          },
          "RateLimit-Remaining": {
            "description": "`0`.",
            "schema": { "type": "integer" }
          },
          "RateLimit-Reset": {
            "description": "Seconds until the window resets.",
            "schema": { "type": "integer" }
          }
        },
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" }
          }
        }
      },
      "InternalServerError": {
        "description": "Unexpected error (`internal_server_error`). Retry.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" }
          }
        }
      }
    },
    "schemas": {
      "ErrorCode": {
        "type": "string",
        "description": "Stable error code. Branch on it; human-readable text is advisory only.",
        "enum": [
          "unauthorized",
          "user_banned",
          "invalid_input",
          "validation_json_malformed",
          "insufficient_balance",
          "job_not_found",
          "job_image_mime_unsupported",
          "job_document_mime_unsupported",
          "job_document_too_large",
          "job_document_too_many_pages",
          "job_document_content_too_large",
          "endpoint_not_found",
          "rate_limit_exceeded",
          "engine_rate_limited",
          "engine_unavailable",
          "engine_service_unavailable",
          "engine_timeout",
          "engine_invalid_request",
          "internal_server_error"
        ]
      },
      "Error": {
        "type": "object",
        "required": ["status", "error_code"],
        "properties": {
          "status": { "const": "error" },
          "error_code": { "$ref": "#/components/schemas/ErrorCode" },
          "request_id": {
            "type": "string",
            "description": "Sent with internal_server_error. Include it when you report the problem. Every response also carries it in the x-request-id header."
          }
        }
      },
      "InsufficientBalanceError": {
        "type": "object",
        "required": ["status", "error_code", "message", "topup_url"],
        "properties": {
          "status": { "const": "error" },
          "error_code": { "const": "insufficient_balance" },
          "message": { "type": "string" },
          "topup_url": {
            "type": "string",
            "format": "uri",
            "description": "Link to add funds in the dashboard."
          }
        }
      },
      "Source": {
        "type": "object",
        "required": ["type", "data"],
        "properties": {
          "type": {
            "type": "string",
            "enum": ["text", "image", "document"]
          },
          "data": {
            "type": "string",
            "minLength": 1,
            "description": "For `text`, the raw text. For `image` and `document`, a base64 data URL such as `data:application/pdf;base64,...`. Images: any raster format (SVG is rejected). Documents: PDF, Word, Excel, CSV, plain text or Markdown; 20 MB per file, 200 pages per PDF, 800,000 characters of text per other file."
          }
        }
      },
      "CreateJobRequest": {
        "type": "object",
        "required": ["sources"],
        "properties": {
          "sources": {
            "type": "array",
            "minItems": 1,
            "items": { "$ref": "#/components/schemas/Source" }
          },
          "schema": {
            "type": "object",
            "description": "A JSON Schema describing the output object. When present, `target` must be `json` or omitted. See https://veralens.ai/docs/schemas"
          },
          "target": {
            "$ref": "#/components/schemas/JobTarget",
            "description": "Output format. Required when no `schema` is given; defaults to `json` when a schema is present."
          },
          "global_prompt": {
            "type": "string",
            "minLength": 1,
            "description": "Natural-language instructions applied to the whole extraction."
          },
          "engine": {
            "$ref": "#/components/schemas/Engine",
            "default": "vera-1.0-high"
          },
          "webhook_url": {
            "type": "string",
            "format": "uri",
            "description": "HTTPS URL that receives the result when the job finishes (see the `jobFinished` webhook)."
          }
        },
        "anyOf": [{ "required": ["schema"] }, { "required": ["target"] }]
      },
      "JobStatus": {
        "type": "string",
        "enum": ["pending", "completed", "failed"]
      },
      "JobOrigin": {
        "type": "string",
        "enum": ["key", "playground"]
      },
      "JobTarget": {
        "type": "string",
        "enum": ["json", "markdown", "plaintext"]
      },
      "Engine": {
        "type": "string",
        "enum": ["vera-1.0-high", "vera-1.0-low"]
      },
      "JobListItem": {
        "type": "object",
        "required": [
          "id",
          "status",
          "createdAt",
          "source",
          "target",
          "sources",
          "model",
          "usage"
        ],
        "properties": {
          "id": { "type": "string" },
          "status": { "$ref": "#/components/schemas/JobStatus" },
          "createdAt": { "type": "string", "format": "date-time" },
          "source": { "$ref": "#/components/schemas/JobOrigin" },
          "target": { "$ref": "#/components/schemas/JobTarget" },
          "sources": {
            "type": "object",
            "description": "Count of inputs by type.",
            "required": ["images", "texts", "documents"],
            "properties": {
              "images": { "type": "integer", "minimum": 0 },
              "texts": { "type": "integer", "minimum": 0 },
              "documents": { "type": "integer", "minimum": 0 }
            }
          },
          "model": {
            "description": "Engine for this job, set when the job is created.",
            "oneOf": [
              { "$ref": "#/components/schemas/Engine" },
              { "type": "null" }
            ]
          },
          "usage": {
            "type": "object",
            "description": "While `pending`, `cost` is the hold and the token counts are `null`. Once `completed`, `cost` is the real charge in USD.",
            "required": ["cost", "inputTokens", "outputTokens"],
            "properties": {
              "cost": { "type": ["number", "null"] },
              "inputTokens": { "type": ["integer", "null"] },
              "outputTokens": { "type": ["integer", "null"] }
            }
          }
        }
      },
      "Job": {
        "allOf": [
          { "$ref": "#/components/schemas/JobListItem" },
          {
            "type": "object",
            "required": ["output", "durationMs"],
            "properties": {
              "output": {
                "type": ["string", "null"],
                "description": "The result, encoded as a JSON string for every target: `JSON.parse` it. `null` until completed."
              },
              "durationMs": {
                "type": ["integer", "null"],
                "description": "Wall-clock runtime; `null` while `pending`."
              }
            }
          }
        ]
      },
      "JobResponse": {
        "type": "object",
        "required": ["status", "data"],
        "properties": {
          "status": { "const": "success" },
          "data": { "$ref": "#/components/schemas/Job" }
        }
      },
      "JobListResponse": {
        "type": "object",
        "required": ["status", "data"],
        "properties": {
          "status": { "const": "success" },
          "data": {
            "type": "object",
            "required": ["jobs", "total"],
            "properties": {
              "jobs": {
                "type": "array",
                "items": { "$ref": "#/components/schemas/JobListItem" }
              },
              "total": { "type": "integer", "minimum": 0 }
            }
          }
        }
      }
    }
  }
}
