Tasks
Call a ready-made task, such as sentiment or receipt extraction, and get JSON that matches its schema.
A task is a named job with a fixed input and a fixed output: you send the input, and the answer
is JSON that has been checked against the task's output schema. We write and version each task's
prompt, models and schema, so you do not write a prompt or a response_format yourself.
curl https://api.opinference.com/v1/tasks/sentiment \
-H "Authorization: Bearer $OP_API_KEY" \
-H "Content-Type: application/json" \
-d '{"text": "The battery lasts all day, but the screen scratches easily."}'{
"id": "gen_...",
"object": "task.result",
"task": "sentiment@1",
"model": "openai/gpt-oss-120b",
"output": {"label": "mixed", "score": 0.1},
"usage": {"input_tokens": 212, "output_tokens": 18},
"op": {"generations": ["gen_..."]}
}Routes
| Route | What it does |
|---|---|
GET /v1/tasks | Lists every task your key may call, with its input and output schema and an estimated price |
POST /v1/tasks/{name} | Runs the task's latest version. The body is the input object itself |
POST /v1/tasks/{name}@{n} | Runs version n. A version never changes once published, so pin one when you depend on its output |
A key may call a task only if it may use every model the task lists. GET /v1/tasks shows only
those tasks, and POST refuses the others with 403 forbidden.
How an answer is checked
A task request is served as a non-streaming chat completion with the task's prompt and its output
schema as a strict response_format. The gateway then checks the answer:
- A refusal from the model ends the request with
502 output_invalidandreason: "refusal". - An answer cut off at the task's output limit ends it with
reason: "length". - An answer that is not JSON, or does not match the output schema, is sent back to the model once
with the validator's message. If the second answer also fails, the request ends with
reason: "schema"andparamnames the failing JSON pointer.
output is always the checked answer, never the provider's raw reply.
Billing
Each generation is billed as a chat completion of the model that answered. A repaired request is two
generations and bills twice; op.generations lists both, in order, and usage is their sum. Your
budget must cover two generations before the request is admitted, and what the repair did not use is
released.
Idempotency-Key covers every generation of a request. A retry with the same key after any
generation was billed returns 409 idempotency_conflict rather than running again, so send a new key
to retry an output_invalid.
Accuracy targets
For the extraction tasks, we score answers against a committed corpus of at least 50 cases each, counting every field: precision, recall and their F1. The figures below are targets, not measurements. A measured figure is published here only from a capped run against live providers, with its date, model, corpus version and case count.
| Task | Target field F1 | Measured |
|---|---|---|
receipt-extract | ≥ 0.90 (target) | Not yet measured |
entities | ≥ 0.90 (target) | Not yet measured |
keywords | ≥ 0.80 (target) | Not yet measured |
resume-extract | ≥ 0.90 (target) | Not yet measured |
resume-extract is scored on synthetic résumés: every person, employer and school in its corpus is
invented, so no real person's data is involved. The task is served once its models pass the file
check.
The tasks
Each example shows a request body and an answer of the shape you get back. The answers are illustrations, not recorded output.
sentiment
{"text": "The battery lasts all day, but the screen scratches easily."}{"label": "mixed", "score": 0.1}label is positive, neutral, negative or mixed; score runs from -1 to 1.
classify
{"text": "My card was charged twice for one order.", "labels": ["billing", "shipping", "account"]}{"labels": [{"label": "billing", "score": 0.97}]}Set multi_label to true to allow more than one label.
entities
{"text": "Ada Lovelace met Charles Babbage in London in June 1833.", "types": ["person", "location", "date"]}{
"entities": [
{"text": "Ada Lovelace", "type": "person"},
{"text": "Charles Babbage", "type": "person"},
{"text": "London", "type": "location"},
{"text": "June 1833", "type": "date"}
]
}Types are person, organization, location, date, money, product, event and other.
Without types, every type is returned.
keywords
{"text": "Solar panels convert sunlight into electricity using photovoltaic cells...", "max_keywords": 3}{"keywords": ["solar panels", "photovoltaic cells", "electricity"]}max_keywords is 1 to 20, and 10 when absent.
summarize
{"text": "...", "max_words": 60, "style": "bullets"}{"summary": "- The council approved the new budget.\n- Spending on parks rises by 4%."}style is paragraph (the default) or bullets. max_words is 10 to 500, and 120 when absent.
translate
{"text": "Where is the train station?", "target_language": "de"}{"translation": "Wo ist der Bahnhof?", "source_language": "en"}target_language is a language name or an ISO 639-1 code. source_language is detected when absent.
grammar
{"text": "Their going to the park tomorow."}{
"corrected": "They're going to the park tomorrow.",
"edits": [
{"original": "Their", "corrected": "They're"},
{"original": "tomorow", "corrected": "tomorrow"}
]
}prompt-optimize
{"prompt": "write about dogs", "goal": "A short blog intro for new dog owners"}{
"prompt": "Write a friendly 120-word introduction for a blog aimed at first-time dog owners...",
"changes": ["Named the audience", "Set a length", "Set the tone"]
}workflow-draft
{
"description": "Caption an image, then write a one-line alt text from the caption.",
"models": [{"id": "openai/gpt-5-mini", "modality": "text"}]
}{"definition": "{\"schema\": \"op.workflow/1\", \"slug\": \"caption-alt-text\", ...}"}definition is a workflow in the op.workflow/1 format, as JSON
text, for you to review and save. The task checks only that it is text: it is not checked against the
workflow rules or the catalog, so it can name a model that does not exist or break a rule a save
enforces. Saving it is what checks it. description is 1 to 4,000 characters. models lists at most
500 model ids the draft may use, each with its modality; without it, the draft names models from what
the model knows, which may not be in our catalog.
receipt-extract
image is an https URL or a data: URI of an image. The task runs only on models that accept
images.
{"image": "https://example.com/receipt.png"}{
"document_type": "receipt",
"merchant_name": "Quayside Fish Bar",
"merchant_address": "8 Mill Lane, Lakeview",
"merchant_tax_id": null,
"invoice_number": "R-60040",
"date": "2026-10-24",
"time": "08:35",
"currency": "EUR",
"line_items": [
{"description": "Garden salad", "quantity": 4, "unit_price": 9.59, "total": 38.36}
],
"subtotal": 38.36,
"tax": 3.07,
"tip": null,
"total": 41.43
}A field the document does not show is null. Dates are YYYY-MM-DD and times 24-hour.
Tasks that read a PDF
A task that reads a PDF takes it in one input field as data:application/pdf;base64,..., under the
rules for a PDF part. The PDF is sent to the
model as a file, not as text. A field that holds anything else, such as an image or a URL, is
refused with 400 invalid_request, and param names the field. Such a task runs only on endpoints
that have passed our PDF check. resume-extract will be the first of these tasks.
Errors
| Status | Code | When |
|---|---|---|
| 400 | invalid_request | The input does not match the task's input schema. param names the first failing field |
| 413 | payload_too_large | A PDF input is over 10 MiB. param names the field |
| 403 | forbidden | Your key may not use a model the task lists |
| 404 | model_not_found | No such task, or no such version |
| 404 | no_endpoint_available | No endpoint of the task's models can serve it now |
| 409 | idempotency_conflict | The key was used and a generation was already billed |
| 502 | output_invalid | The answer could not be used. reason is schema, refusal or length |
See Errors for the envelope.