Direct API integration
Call the API with plain HTTP — no SDK, any language. The exact anonymous and authenticated request/response shapes, with curl examples.
No SDK, any language — this is the exact HTTP contract for calling the API directly. If you’re using JavaScript, the SDK wraps all of this (submit, poll, error handling) for you — see Plain webpage integration or Choose your integration path if you haven’t picked a guide yet.
Every example below shows the real API base URL for this environment — copy it exactly as written, with no extra path segment appended after it.
Two different endpoints, not one URL with two auth modes
These are two distinct HTTP endpoints. They also return different response shapes for the same tool — know which one you’re building against.
Anonymous — POST /tools/:slug/run + GET /tools/runs/:ticketId
No credentials required. Uses the tool slug. Response fields are normalized across every tool
to result.text (plus token/latency metadata) so the public Tool Portal can render any tool with
one generic renderer. Rate-limited per IP; see Errors below for the anonymous-path
error table (in particular, this path requires a real browser — see the note on 403).
curl -X POST "https://api.quravin.com/tools/translate/run" \
-H "Content-Type: application/json" \
-d '{
"inputs": { "text": "Hello", "target_language": "de" }
}'
Response (202):
{ "ticket_id": "01J...ULID", "status": "QUEUED" }
Poll:
curl "https://api.quravin.com/tools/runs/01J...ULID"
Response once done:
{
"ticket_id": "01J...ULID",
"status": "DONE",
"mode": "pipeline",
"pipeline_id": "translate-string",
"result": { "text": "Hallo", "model": "gpt-4o-mini", "latency_ms": 812 },
"created_at": "2026-07-10T12:00:00.000Z",
"updated_at": "2026-07-10T12:00:02.000Z"
}
Note result.text, not result.translation — the anonymous path always normalizes to text.
Authenticated — POST /tickets + GET /tickets/:id
Requires x-api-key: <key> or Authorization: Bearer <jwt> (see Choose your auth
mode for how to get either one). Uses the pipeline_id, and
returns each pipeline’s raw, un-normalized output shape — for translate-string that’s
translation, not text. See Tool reference for every tool’s
pipeline_id and its real output fields.
With a static API key:
curl -X POST "https://api.quravin.com/tickets" \
-H "Content-Type: application/json" \
-H "x-api-key: <your-api-key>" \
-d '{
"mode": "pipeline",
"pipeline_id": "translate-string",
"inputs": { "text": "Hello", "target_language": "de" }
}'
With a session JWT (minted via POST /auth/token — see Choose your auth
mode) — identical body, only the header changes:
curl -X POST "https://api.quravin.com/tickets" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <your-jwt>" \
-d '{
"mode": "pipeline",
"pipeline_id": "translate-string",
"inputs": { "text": "Hello", "target_language": "de" }
}'
Both return the same response (202):
{ "ticket_id": "01J...ULID", "status": "QUEUED" }
Poll — with an API key:
curl "https://api.quravin.com/tickets/01J...ULID" \
-H "x-api-key: <your-api-key>"
Poll — with a session JWT:
curl "https://api.quravin.com/tickets/01J...ULID" \
-H "Authorization: Bearer <your-jwt>"
Response once done:
{
"ticket_id": "01J...ULID",
"status": "DONE",
"mode": "pipeline",
"pipeline_id": "translate-string",
"pipeline_version": "v3",
"result": { "translation": "Hallo", "applied_terms": [] },
"created_at": "2026-07-10T12:00:00.000Z",
"updated_at": "2026-07-10T12:00:02.000Z"
}
If it fails instead:
{
"ticket_id": "01J...ULID",
"status": "FAILED",
"mode": "pipeline",
"pipeline_id": "translate-string",
"error": "Ticket failed: <reason>",
"created_at": "2026-07-10T12:00:00.000Z",
"updated_at": "2026-07-10T12:00:02.000Z"
}
Canceling a ticket — DELETE /tickets/:id
Requests cancellation (by writing a cancel.flag). You can only cancel a ticket your own
credentials created — an API key with operator scope can cancel any ticket, a JWT/webtoken caller
only its own (else 403).
curl -X DELETE "https://api.quravin.com/tickets/01J...ULID" \
-H "x-api-key: <your-api-key>"
# or with a session JWT:
curl -X DELETE "https://api.quravin.com/tickets/01J...ULID" \
-H "Authorization: Bearer <your-jwt>"
Response:
{ "ticket_id": "01J...ULID", "status": "CANCELED" }
A polled GET /tickets/:id on a canceled ticket returns the same shape as DONE/FAILED above,
just with status: "CANCELED".
Listing your tickets — GET /tickets
Returns tickets scoped to your own identity — a JWT/webtoken caller always gets its own tickets
back; only an operator-scoped API key may pass ?user= / ?app= to look up an arbitrary
user/app’s tickets.
curl "https://api.quravin.com/tickets?limit=20" \
-H "Authorization: Bearer <your-jwt>"
Query parameters:
| Param | Default | Notes |
|---|---|---|
limit | 50 | Max 1000. |
hydrate | true | false returns just [{ ticket_id }] — cheap, skips fetching each ticket’s full state. |
user | caller’s own id | Only an operator-scoped API key may pass a different user’s id. |
app | caller’s own app | Only an operator-scoped API key may pass a different app’s id. |
Response:
{ "tickets": [ { "ticket_id": "01J...ULID", "status": "DONE", "...": "..." } ] }
The important takeaway if you’re graduating from the public “try it” widget to a real
integration: the output field name changes from result.text (anonymous) to the
pipeline-specific field (authenticated) — for translate, that’s result.translation.
Errors
Which errors you can get back depends on how you’re calling — anonymous and authenticated calls are validated differently. Don’t assume one path’s error behavior applies to the other.
Calling anonymously (no API key)
All published tools, rate-limited per IP.
| Status | Error code | Meaning |
|---|---|---|
| 404 | — | Unknown tool slug. |
| 403 | — | Bot check failed. This path requires a real browser context that can execute a Cloudflare Turnstile challenge — it always 403s from a server, curl, or a headless script with no Turnstile token. Use an authenticated path (API key or session JWT) for server-to-server calls. |
| 429 | guest_quota_exceeded | You’ve hit the anonymous per-IP or global daily request limit. |
| 429 | guest_cost_exceeded | You’ve hit the anonymous per-IP or global daily cost limit (expensive tools use up this budget faster). |
| 202 | — | Success — { ticket_id, status: "QUEUED" }. Poll for the result the same way as any other call. |
Important: on this path, your inputs are not checked against each tool’s schema before the
call is accepted — a missing required field or an invalid value doesn’t get you a 400. The run
itself will fail instead, which you’ll see when you poll for the result. Once you have an API key,
switch to the authenticated path below, which validates upfront.
Calling with an API key or session token
Any tool you have access to.
| Status | Error code | Meaning |
|---|---|---|
| 401 | — | Missing, invalid, or expired credentials. |
| 403 | app_disabled | Your application has been disabled. |
| 403 | org_suspended | Your organization has been suspended. |
| 403 | org_pending_deletion | Your organization is scheduled for deletion. |
| 403 | — | This tool isn’t in your credentials’ allowed pipeline list. |
| 403 | origin_not_allowed | Your Application has allowed_origins configured and the request’s Origin header doesn’t exactly match one of them. Only applies if you (or your org admin) opted into this in the Console — apps that never configure it are unaffected. |
| 400 | — | Your inputs don’t match the tool’s requirements (missing required field, invalid value, or the total input is too large). This path does validate upfront, unlike the anonymous one. |
| 429 | rate_limited | You’re sending requests faster than your per-minute limit — a fixed one-minute window per org, not a rolling window. Because it’s fixed rather than sliding, a burst can briefly exceed the nominal cap by up to ~2x right at a minute boundary (e.g. a burst at 12:00:59 plus another at 12:01:00); back off and retry rather than assuming an exact cap. |
| 429 | quota_exceeded | You’ve hit your monthly request quota. |
| 429 | daily_cost_exceeded | You’ve hit a daily cost cap — a velocity limit, distinct from running out of balance below. |
| 402 | insufficient_credits | Your organization’s credit balance is too low to cover this call. Top up and retry. Note: a bare x-api-key has no org wallet at all, so a billable call on wallet-mode billing returns this even with funds available — switch to a session JWT (see Choose your auth mode). |
| 202 | — | Success — { ticket_id, status: "QUEUED" }. Poll for the result. |
For how credentials and the 401/403/429 auth decisions work in more depth, see Choose your auth mode. For every tool’s inputs, valid values, and output shape, see Tool reference.