Extraction Schemas
A schema describes the fields you want back. Create it once, version it, label the version your callers get, and test it before you rely on it.
Snippet setup
The Python and TypeScript snippets on this page set BASE_URL to the host of the base URL and read your key from AI_2KW_API_KEY. Python: import os, requests, BASE_URL = "https://api.2kw.ai", api_key = os.environ["AI_2KW_API_KEY"] and headers = {"Authorization": f"Bearer {api_key}"}. TypeScript: const BASE_URL = "https://api.2kw.ai" and const apiKey = process.env.AI_2KW_API_KEY.
Schema Management
Before you can extract anything, you need a schema. Schemas are organization-scoped and support full version control.
Creating a Schema
A schema takes two calls: create the schema with a name and an optional description, then commit its JSON Schema definition as the first version.
POST /v1/schemas
curl -X POST https://api.2kw.ai/v1/schemas \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk_your_api_key" \
-d '{
"name": "invoice",
"description": "Supplier invoices"
}'
The response carries the schema's id. The JSON Schema is not part of this call: the API reference lists a content field on the schema, but a value sent there is not stored.
POST /v1/schemas/{schemaId}/versions
{
"jsonSchema": {
"type": "object",
"properties": {
"company": { "type": "string" },
"invoice_number": { "type": "string" },
"total": { "type": "number" },
"line_items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"description": { "type": "string" },
"quantity": { "type": "integer" },
"price": { "type": "number" }
}
}
}
},
"required": ["company", "invoice_number"]
},
"changeDescription": "First version"
}
jsonSchema is the JSON Schema definition; one that is not a valid JSON Schema is refused with 400. changeDescription says what changed. Every field: API reference.
The response (201) is the new version with its id, versionNumber and jsonSchema, and the latest label now points to it. Every later change is another call to this endpoint.
Version Control
Every time you change your schema, you create a new version. Versions are immutable —this gives you a full audit trail of every change.
You can:
- Commit new versions with a change description
- Deactivate versions you don't want used anymore
- Re-activate a historical version (creates a new version as a copy, preserving the audit trail)
- Pin extractions to a specific version for reproducibility
Each new version auto-increments the version number and updates the latest label.
Re-activation creates a copy
Re-activating version 3 doesn't revert —it creates version N+1 with the same content. The original stays untouched.
Labels
Labels are named pointers to specific schema versions. They decouple your application code from version numbers, enabling controlled rollouts.
How Labels Work
Every schema gets a latest label automatically, updated on each new version. You create custom labels for deployment stages:
| Label | Points to | Purpose |
|---|---|---|
latest | v5 (auto) | Always the newest version |
production | v3 | What your API consumers use |
staging | v5 | What you're testing |
Your application resolves schemas by ID + label —when you're ready to promote, just move the label pointer.
Managing Labels
POST /v1/schemas/{schemaId}/labels
Create a label:
{
"name": "production",
"schemaVersionId": "version-uuid"
}
Update a label to point to a different version:
PUT /v1/schemas/{schemaId}/labels/{labelName}
{
"schemaVersionId": "new-version-uuid"
}
Label names must be lowercase alphanumeric with hyphens (e.g., production, staging, v2-rollback). The system latest label cannot be deleted.
Resolving Schemas
GET /v1/schemas/{schemaId}/resolve
Resolve a schema by ID and optional label to get the JSON Schema definition for a specific version:
Request
curl "https://api.2kw.ai/v1/schemas/{schemaId}/resolve?label=production" \
-H "Authorization: Bearer sk_your_api_key"
The label parameter names the label to resolve and defaults to latest.
Response:
{
"schemaId": "uuid",
"schemaName": "invoice-schema",
"versionId": "uuid",
"versionNumber": 3,
"jsonSchema": {
"type": "object",
"properties": {
"company": { "type": "string" },
"total": { "type": "number" }
}
},
"label": "production"
}
Validate Before Committing
POST /v1/schemas/{schemaId}/validate
Check if a schema is valid before you commit it. Returns structured errors and warnings:
{
"jsonSchema": {
"type": "object",
"properties": {
"name": { "type": "string" }
}
}
}
The response has three fields: valid, errors and warnings. Errors mean the schema document itself cannot be used. The same checks run when you commit a version, so POST /v1/schemas/{schemaId}/versions answers 400 for a document that fails them. The typical errors are:
JSON Schema must be a JSON objectJSON Schema must contain at least one schema keyword (e.g., type, properties, $schema)Invalid JSON Schema: <detail from the validator>
Check the JSON syntax, make sure the document is an object with at least one JSON Schema keyword, and validate it against the JSON Schema 2020-12 specification.
Errors are about the schema, never about extraction output. An extraction whose output does not match its schema does not fail: there is no failed status and no retry. The mismatches are recorded on the result instead, see Schema Validation.
Schema-safety warnings
Warnings are advisory and never block saving a version: valid stays true. The validate endpoint returns them on request, and the schema editor in the console lints live while you type. Each warning names a stable rule id, a JSON pointer to the exact location in your schema, a message and, where one applies, a suggested fix. The editor offers a one-click fix for the first three rules.
| Rule | Trigger | Why it is risky | Fix |
|---|---|---|---|
required-non-nullable | A required field declares a type that does not include null | The model must emit some value even when the document has none | Allow null ("type": ["string", "null"]) or remove the field from required |
required-enum | A required field has an enum that contains neither "insufficient_evidence" nor null | The model must pick a listed value even when the document contains none | Add "insufficient_evidence" or null to the enum, or remove the field from required |
min-items | minItems greater than 0 on any array | Forces the model to invent entries for documents that have none | Remove minItems and check cardinality after extraction instead, for example with a business rule |
provider-rewrite | The schema uses constraints your provider does not enforce | Constraints look enforced but are ignored at generation time, or the schema is rejected | Informational, no automatic fix. See Provider enforcement below |
A required field that carries an enum is reported as required-enum, not required-non-nullable. A field without a type keyword, or one whose anyOf/oneOf includes a {"type": "null"} branch, is already nullable and is not flagged.
The pointer targets the property for required-non-nullable and required-enum, and the offending keyword for min-items and provider-rewrite. A provider-rewrite warning about prompt-only providers uses the empty pointer, which means the whole schema.
Example. This schema requires two fields, one of them an enum without an escape value:
{
"jsonSchema": {
"type": "object",
"properties": {
"invoice_number": { "type": "string" },
"status": { "type": "string", "enum": ["paid", "unpaid"] }
},
"required": ["invoice_number", "status"]
}
}
With only the platform's default provider in play, the response is:
{
"valid": true,
"errors": [],
"warnings": [
{
"rule": "required-non-nullable",
"pointer": "/properties/invoice_number",
"message": "Required field 'invoice_number' cannot be null. When the document does not contain this value, models fabricate one rather than fail.",
"suggestion": "Allow null via \"type\": [\"string\", \"null\"], or remove 'invoice_number' from \"required\"."
},
{
"rule": "required-enum",
"pointer": "/properties/status",
"message": "Required enum field 'status' forces the model to pick a listed value even when the document contains none — the highest-risk fabrication construct.",
"suggestion": "Add an escape value \"insufficient_evidence\" to the enum, or remove 'status' from \"required\"."
}
]
}
An organization that has an Anthropic or Mistral provider configured also gets a provider-rewrite warning at the empty pointer.
Escape values
Make fields optional and nullable by default. Require a field only when a document without it should count as unprocessable. For enums, include the escape value insufficient_evidence so the model has a legal way to say "not in the document". A null entry in the enum counts as an escape too. An escape value reads as a deliberate absence; a fabricated value looks exactly like a real one.
Provider enforcement
Extraction runs on the provider behind the model you choose, and providers differ in what they enforce while generating:
- Anthropic and Mistral receive the schema as prompt text, without constrained decoding. Required fields, enums and every other constraint are advisory.
- OpenAI, Azure OpenAI and xAI receive the schema as a structured-output
json_schema. Constraint keywords outside the enforced subset are ignored, and Azure OpenAI may reject a schema that uses them. The flagged keywords arepattern,format,minLength,maxLength,minimum,maximum,exclusiveMinimum,exclusiveMaximum,multipleOf,maxItems,uniqueItems,minPropertiesandmaxProperties.minItemsis not enforced either; it is reported by themin-itemsrule, which carries the fix. - Vertex AI and Ollama are not checked, so they produce no
provider-rewritewarnings.
The linter checks your schema against every provider configured for your organization, plus Azure OpenAI, which serves the platform's built-in models.
2kw.ai does not enforce these keywords after extraction either. It does check every output against its schema and records the violations, for enforced and advisory keywords alike, see Schema Validation.
Test Against Sample Text
POST /v1/schemas/{schemaId}/test
Run an extraction against sample text without persisting the result. Great for iterating on your schema:
Request
curl -X POST https://api.2kw.ai/v1/schemas/{schemaId}/test \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk_your_api_key" \
-d '{
"jsonSchema": {
"type": "object",
"properties": {
"name": { "type": "string" },
"email": { "type": "string" }
}
},
"sampleText": "Contact Jane Smith at jane@acme.com",
"model": "gpt-5.1"
}'
The response includes token usage and processing time so you can estimate costs:
{
"success": true,
"extractedData": { "name": "Jane Smith", "email": "jane@acme.com" },
"inputTokens": 85,
"outputTokens": 22,
"processingDurationMs": 890,
"modelUsed": "gpt-5.1"
}
All Schema Endpoints
The endpoints in one place. Every field: API reference.
| Method | Endpoint | What it does |
|---|---|---|
GET | /v1/schemas?search=invoice&page=0&size=20 | List schemas, paginated, optionally filtered by search |
POST | /v1/schemas | Create a schema (name, description) |
GET | /v1/schemas/{id} | Get a schema |
PUT | /v1/schemas/{id} | Update a schema's name or description |
DELETE | /v1/schemas/{id} | Delete a schema and all its versions. Needs the admin role |
POST | /v1/schemas/{schemaId}/versions | Commit a new version (jsonSchema, changeDescription) |
GET | /v1/schemas/{schemaId}/versions?page=0&size=20 | List versions, paginated |
GET | /v1/schemas/{schemaId}/versions/{versionId} | Get one version |
GET | /v1/schemas/{schemaId}/versions/latest | The latest version; 404 while there is none. Deprecated: use resolve |
PUT | /v1/schemas/{schemaId}/versions/{versionId}/activate | Re-activate a historical version as a new copy |
DELETE | /v1/schemas/{schemaId}/versions/{versionId} | Deactivate a version (soft delete); answers 204. Needs the admin role |
GET | /v1/schemas/{schemaId}/labels | List labels |
POST | /v1/schemas/{schemaId}/labels | Create a label (name, schemaVersionId) |
PUT | /v1/schemas/{schemaId}/labels/{labelName} | Point a label at another version (schemaVersionId) |
DELETE | /v1/schemas/{schemaId}/labels/{labelName} | Delete a label; latest cannot be deleted. Needs the admin role |
GET | /v1/schemas/{schemaId}/resolve?label=production | Resolve a label (default latest) to its version |
POST | /v1/schemas/{schemaId}/validate | Validate a JSON Schema without saving it |
POST | /v1/schemas/{schemaId}/test | Extract from sample text without saving the result |