← Developer guides

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:

ParamDefaultNotes
limit50Max 1000.
hydratetruefalse returns just [{ ticket_id }] — cheap, skips fetching each ticket’s full state.
usercaller’s own idOnly an operator-scoped API key may pass a different user’s id.
appcaller’s own appOnly 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.

StatusError codeMeaning
404Unknown tool slug.
403Bot 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.
429guest_quota_exceededYou’ve hit the anonymous per-IP or global daily request limit.
429guest_cost_exceededYou’ve hit the anonymous per-IP or global daily cost limit (expensive tools use up this budget faster).
202Success — { 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.

StatusError codeMeaning
401Missing, invalid, or expired credentials.
403app_disabledYour application has been disabled.
403org_suspendedYour organization has been suspended.
403org_pending_deletionYour organization is scheduled for deletion.
403This tool isn’t in your credentials’ allowed pipeline list.
403origin_not_allowedYour 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.
400Your 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.
429rate_limitedYou’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.
429quota_exceededYou’ve hit your monthly request quota.
429daily_cost_exceededYou’ve hit a daily cost cap — a velocity limit, distinct from running out of balance below.
402insufficient_creditsYour 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).
202Success — { 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.