Skip to content
laya /api

API docs

Your request is queued, a Laya worker picks it up, and you get typed JSON back. Everything below uses the base URL https://laya-api.de/api/v1.

Try it

Paste an API key, edit the request and send it to the live API. No key yet? Get a free beta key.

Uses your key against the live API. Counts toward your usage.

Quick start

  1. Create a free account.
  2. Open Dashboard → API keys and create a key. You can reveal and copy it there at any time.
  3. Send your first request and wait up to 10 seconds for the answer:
shell
curl -X POST "https://laya-api.de/api/v1/systemone?wait=10" \
  -H "Authorization: Bearer $LAYA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "state": "Help! My payouts have been failing for 3 days.",
    "model": "laya-latest",
    "questions": {
      "is_urgent": {
        "type": "noul",
        "instructions": "Does this convey urgency?",
        "criteria": {
          "true": "Explicitly time-sensitive",
          "false": "No urgency expressed"
        }
      }
    }
  }'

If the answer is ready within 10 seconds you get 200 and the finished job:

200 OK
{
  "id": "01J9Z3K6V8X2N4QH7R5T0WBC1D",
  "object": "job",
  "status": "completed",
  "model": "laya-latest",
  "metadata": null,
  "created_at": "2026-09-28T09:14:02Z",
  "started_at": "2026-09-28T09:14:02Z",
  "completed_at": "2026-09-28T09:14:03Z",
  "result": {
    "model": "laya-1.0.0",
    "answers": {
      "is_urgent": { "type": "noul", "noul": 0.95 }
    },
    "usage": { "input_tokens": 296, "output_tokens": 20 }
  },
  "error": null
}

Authentication

Send your key as a bearer token on every request. Keys start with laya_sk_. You can create several keys and revoke them one by one in the dashboard. Keep keys on your server; never ship them in browser or mobile code.

Authorization: Bearer laya_sk_…

Create a request

POST /api/v1/systemone

The body uses the same fields as Jev.

FieldTypeDescription
state required string | object | array The content to evaluate: plain text, or structured data such as a support ticket or a list of search results.
model required string laya-latest or laya-1.
questions required map<id, Question> 1 to 32 named questions. Each id matches ^[A-Za-z0-9_-]{1,64}$ and becomes the key of its answer.
metadata object Anything you want to attach, such as your own ticket id. It is echoed back on the job.

Optional query parameter: wait, 0 to 30 seconds. See Waiting and polling. Optional header: Idempotency-Key. See Idempotency.

Questions

Every question has a type, instructions (string, object or array) and criteria. The criteria define every answer the model is allowed to give.

noul: yes or no

Criteria must have exactly the keys true and false.

"is_urgent": {
  "type": "noul",
  "instructions": "Does this convey urgency?",
  "criteria": {
    "true": "Explicitly time-sensitive",
    "false": "No urgency expressed"
  }
}

choice: one option out of many

Criteria map each option key to a description. 2 to 50 options.

"team": {
  "type": "choice",
  "instructions": "Which team should handle this ticket?",
  "criteria": {
    "billing": "Payments, invoices, refunds",
    "bug": "Something in the product is broken",
    "feature_request": "The customer wants something new"
  }
}

score: a rating on ordered levels

Criteria are a list of level descriptions, lowest first. 2 to 11 levels.

"politeness": {
  "type": "score",
  "instructions": "How polite is the agent's reply?",
  "criteria": ["Rude", "Neutral", "Friendly"]
}

Waiting and polling

Every request becomes a job. Without wait, or when the job isn't finished in time, you get 202 Accepted with the job in status queued or processing. When the job is completed or failed within the wait, you get 200 OK.

To check on a job later, fetch it by id:

GET /api/v1/jobs/{id}

shell
curl "https://laya-api.de/api/v1/jobs/01J9Z3K6V8X2N4QH7R5T0WBC1D" \
  -H "Authorization: Bearer $LAYA_API_KEY"

Jobs are kept, but their content is not: 7 days after a job completes or fails, its metadata, result and error come back as null. Store the answers you need on your side.

A small polling loop in JavaScript:

javascript
const headers = {
  Authorization: `Bearer ${process.env.LAYA_API_KEY}`,
  "Content-Type": "application/json",
};

let job = await fetch("https://laya-api.de/api/v1/systemone?wait=10", {
  method: "POST",
  headers,
  body: JSON.stringify({ state, model: "laya-latest", questions }),
}).then((r) => r.json());

while (job.status === "queued" || job.status === "processing") {
  await new Promise((resolve) => setTimeout(resolve, 1000));
  job = await fetch(`https://laya-api.de/api/v1/jobs/${job.id}`, { headers }).then((r) => r.json());
}

if (job.status === "completed") {
  console.log(job.result.answers);
}

The same in Python:

python
import os, time, requests

BASE = "https://laya-api.de/api/v1"
headers = {"Authorization": f"Bearer {os.environ['LAYA_API_KEY']}"}

job = requests.post(
    f"{BASE}/systemone",
    params={"wait": 10},
    headers=headers,
    json={"state": state, "model": "laya-latest", "questions": questions},
).json()

while job["status"] in ("queued", "processing"):
    time.sleep(1)
    job = requests.get(f"{BASE}/jobs/{job['id']}", headers=headers).json()

print(job["result"]["answers"] if job["status"] == "completed" else job["error"])

The job object

{
  "id": "01J9Z3K6V8X2N4QH7R5T0WBC1D",   // ULID
  "object": "job",
  "status": "queued | processing | completed | failed",
  "model": "laya-latest",               // the model you asked for
  "metadata": { … } | null,              // echoed from your request
  "created_at": "2026-09-28T09:14:02Z",
  "started_at": "…" | null,
  "completed_at": "…" | null,
  "result": {                            // set when completed
    "model": "laya-1.0.0",               // the exact model version that ran
    "answers": { … },
    "usage": { "input_tokens": 296, "output_tokens": 20 }
  } | null,
  "error": { "type": "…", "message": "…" } | null   // set when failed
}

Answers

result.answers has one entry per question id, in the same format as Jev.

"answers": {
  "is_urgent": { "type": "noul", "noul": 0.95 },

  "team": {
    "type": "choice",
    "choice": "billing",
    "probabilities": { "billing": 0.81, "bug": 0.12, "feature_request": 0.07 },
    "confidence": 0.92
  },

  "politeness": {
    "type": "score",
    "score": 1.5,
    "probabilities": { "0": 0.1, "1": 0.3, "2": 0.6 },
    "legend": { "0": "Rude", "1": "Neutral", "2": "Friendly" },
    "confidence": 0.9
  }
}
  • noul: the probability that the answer is yes, from 0 to 1.
  • choice: the most likely option, the probability of every option, and how confident the model is overall.
  • score: the probability-weighted level (so 1.5 sits between level 1 and level 2), the probability of each level, and a legend mapping level numbers back to your descriptions.

Confidence is not the same as probability. A choice can have a clear winner and still low confidence when the state is ambiguous. A common pattern: act automatically above a confidence threshold, and send everything else to a human.

List jobs

GET /api/v1/jobs

Newest first. Optional query parameters: status (queued, processing, completed or failed), limit (1 to 100) and cursor. Pass next_cursor from one page as cursor to get the next one. It is null on the last page.

{
  "data": [ { "id": "01J9Z3K6V8X2N4QH7R5T0WBC1D", "object": "job", … } ],
  "next_cursor": "eyJpZCI6IjAxSjla…" | null
}

Idempotency

Networks fail. To retry a POST safely, send an Idempotency-Key header (any string up to 255 characters, such as a UUID). Sending the same key again returns the original job instead of creating a second one.

Idempotency-Key: 9f1c2b8e-3a4d-4c1e-9b7a-2f6d8e0c5a11

Usage and tokens

Each completed job reports usage.input_tokens and usage.output_tokens. Only input tokens count toward your usage. Output tokens are free. Your dashboard shows input tokens per day and per month. During the open beta, all usage is free.

Errors and limits

StatusMeaning
401The key is missing, wrong or revoked.
404No job with that id exists on your account.
422The request body didn't validate. The response names each field that failed.
429Rate limit reached: 60 requests per minute per key during the beta. Wait for the number of seconds in the Retry-After header, then retry with exponential backoff.
422 Unprocessable Entity
{
  "message": "The questions.team.criteria field must have at least 2 items.",
  "errors": {
    "questions.team.criteria": ["The questions.team.criteria field must have at least 2 items."]
  }
}

A job with status failed is not an HTTP error: the request was fine, but the job couldn't be processed. Read error.type and error.message for the reason, and send a new request if you want to try again.