On this page
DEVELOPER REFERENCE
The Djev API.
Evaluate text, JSON, images and camera frames. Get a yes/no probability, a choice, or a score in a typed JSON response.
https://api.djev.devInvitations & API keys
Join once. Keep your account recovery separate from your application keys.
Access is invitation-only, with no email or OAuth. Every account has its own unique account ID. Open the playground, redeem your invitation, and save the recovery key shown when you join.
Save your recovery key or you will lose access to your account. It restores account management after signing out. There is no email or password reset. Use Access & keys to create named API keys for your applications.
There is one owner account, with 100 one-use invitations. Each claimed member account receives five invitations of its own. Invitations belong to the issuing account; claiming, expiring, or revoking one does not replenish that allowance.
Call the API with your API key
Send Authorization: Bearer <api_key> directly to POST /v1/request and saved-result retrieval. No login call, account ID, or token exchange is needed for an API request.
API keys and recovery keys are different credentials. Keep both private. API keys grant inference access; recovery keys restore account management. Never put keys in browser code, source control, URLs, logs, model input, or agent prompts.
The gateway briefly caches verified API-key access so repeated evaluations do not each query account storage. Revocation can take up to five minutes to reach an already cached credential. Invalid or expired access returns 401; retain the original request ID and bytes while restoring access.
Compatibility: short-lived access tokens
/v1/access/tokenOptional · 200 short-lived bearerExisting integrations may still exchange an API key with an empty JSON object. The response contains access_token, expires_at, expires_in: 300, and token_type: "Bearer". These tokens remain accepted until expiry. New integrations can send their API key directly.
The playground uses an HttpOnly account session and keeps its inference token in memory. Browser account routes under /auth/ are separate from API-key inference. Public docs and model metadata remain readable without credentials.
Usage & credits
One account balance. Charges based on actual input usage.
Prepaid billing is still being prepared. Open Usage & credits to see whether billing and purchases are enabled for the current service. GET /config also reports features.prepaid_billing; the rules below apply when it is true.
Input tokens only
The rate is $35 per billion input tokens, or $0.035 per million. Output tokens are free. All API keys and playground requests for an account share one prepaid balance; creating another key does not create another balance.
The billable count is the successful response’s usage.input_tokens: actual backend prompt tokens summed across physical reads. It includes rendered scaffolding, instructions and criteria, image-expanded tokens, and context repeated by independent questions, independent Score levels or samples. Deduplicated reads count once. It is not the number of characters or tokens in the raw user string, and cached prompt tokens still count. usage.output_tokens is reported for visibility but has no charge.
Holds and recovery
Before execution, Djev reserves a conservative token allowance from the account’s available balance. It settles the actual input usage when the canonical successful response is saved and charged, then releases the unused allowance. A successful canonical logical request is charged once. Polling and delivery of the same retained response do not add charges; internal failed attempts are not separately charged.
An unresolved hold continues to reduce available credit until settlement or a confirmed release. A disconnect or elapsed client timeout does not automatically refund it. Keep the same operation ID and exact request bytes during recovery. For a direct request, send X-Djev-Operation-Id; reuse its value as the durable Idempotency-Key when falling back. When prepaid billing is enabled, the client must save and supply an ID before either delivery mode: X-Djev-Operation-Id for direct requests and Idempotency-Key for durable requests. Missing IDs return 400 before reservation or inference. Billing-disabled deployments retain their existing optional-ID behavior.
402 insufficient_credits requires adding credit or resolving existing holds; do not automatically retry it. For temporary 503 billing_pending or billing_unavailable, preserve the ID and bytes, honor Retry-After, and retain unresolved work after a bounded waiting period. The API key already identifies the account: inference callers do not send a browser account ID or manage billing sessions.
Your first request
Send your state and questions with your API key.
/v1/request200 response · 202 pending# Bash, curl, jq and OpenSSL. One POST; queued work is polled by ID.
set -euo pipefail
BASE="https://api.djev.dev"
: "${DJEV_API_KEY:?Set DJEV_API_KEY in your server environment}"
umask 077
WORK=$(mktemp -d)
ID=$(openssl rand -hex 16)
printf '%s' "$ID" > "$WORK/id.txt"
cat > "$WORK/request.json" <<'JSON'
{
"model": "djev",
"state": "The checkout page is unavailable.",
"questions": {
"urgent": {
"type": "noul",
"instructions": "Does this describe an outage?"
}
},
"options": {
"seed": 0
}
}
JSON
SHA=$(openssl dgst -sha256 -r "$WORK/request.json" | cut -d ' ' -f 1)
echo "Keep recovery files: $WORK — request ID: $ID" >&2
if ! status=$(curl --max-time 30 -sS --output "$WORK/response.json" \
--dump-header "$WORK/headers.txt" --write-out '%{http_code}' \
-X POST "$BASE/v1/request" -H "Authorization: Bearer $DJEV_API_KEY" -H 'Content-Type: application/json' \
-H "Idempotency-Key: $ID" --data-binary "@$WORK/request.json"); then
echo "Submission uncertain. Keep the ID and exact request file to recover." >&2
exit 1
fi
if [[ "$status" == 200 ]]; then
tr -d '\r' < "$WORK/headers.txt" | grep -Fxi "x-djev-request-id: $ID" >/dev/null
tr -d '\r' < "$WORK/headers.txt" | grep -Fxi 'x-djev-durable: 1' >/dev/null
jq . "$WORK/response.json"
exit 0
fi
[[ "$status" == 202 ]] || { cat "$WORK/response.json" >&2; exit 1; }
jq -e --arg id "$ID" --arg sha "$SHA" \
'.id == $id and .request_sha256 == $sha' "$WORK/response.json" >/dev/null
deadline=$((SECONDS + 900))
while (( SECONDS < deadline )); do
status=$(curl --max-time 30 -sS --output "$WORK/response.json" \
--write-out '%{http_code}' -H "Authorization: Bearer $DJEV_API_KEY" "$BASE/v1/requests/$ID")
jq -e --arg id "$ID" --arg sha "$SHA" \
'.id == $id and .request_sha256 == $sha' "$WORK/response.json" >/dev/null
if [[ "$status" == 200 ]]; then
if jq -e '.status == "succeeded"' "$WORK/response.json" >/dev/null; then
jq .response "$WORK/response.json"
exit 0
fi
cat "$WORK/response.json" >&2
exit 1
fi
[[ "$status" == 202 ]] || { cat "$WORK/response.json" >&2; exit 1; }
sleep 3
done
echo "Stopped waiting; the accepted job is not cancelled. Resume by ID." >&2
exit 1import hashlib
import json
import os
import tempfile
import time
import uuid
from pathlib import Path
from urllib.request import Request, urlopen
BASE = "https://api.djev.dev"
API_KEY = os.environ["DJEV_API_KEY"] # Server-side secret, never browser code.
def api(path, data=None, headers=None):
request = Request(BASE + path, data=data,
headers={"Content-Type": "application/json", **(headers or {}),
"Authorization": "Bearer " + API_KEY})
with urlopen(request, timeout=30) as response:
return response.status, json.load(response), response.headers
def wait_for_result(request_id, request_sha256, seconds=900):
deadline = time.monotonic() + seconds
while time.monotonic() < deadline:
status, job, _ = api("/v1/requests/" + request_id)
if job.get("id") != request_id or job.get("request_sha256") != request_sha256:
raise RuntimeError("Receipt does not match this request")
if status == 200:
if job.get("status") == "succeeded":
return job["response"]
if job.get("status") == "failed":
raise RuntimeError(job["error"]["message"])
raise RuntimeError("Unexpected terminal response")
if status != 202:
raise RuntimeError("Unexpected polling status")
time.sleep(3)
raise TimeoutError("Stopped waiting; the accepted job is not cancelled. Resume by ID.")
def evaluate(payload):
# Freeze bytes once. Keep these files if a connection or polling attempt fails.
request_id = uuid.uuid4().hex
body = json.dumps(payload).encode("utf-8")
digest = hashlib.sha256(body).hexdigest()
saved = Path(tempfile.mkdtemp(prefix="djev-"))
(saved / "request.json").write_bytes(body)
(saved / "id.txt").write_text(request_id)
print("Recovery files:", saved, "Request ID:", request_id)
status, result, headers = api("/v1/request", body,
{"Idempotency-Key": request_id})
if status == 200:
if headers.get("x-djev-request-id") != request_id or headers.get("x-djev-durable") != "1":
raise RuntimeError("Missing durable acceptance receipt")
return result
if status != 202 or result.get("id") != request_id or result.get("request_sha256") != digest:
raise RuntimeError("Unexpected acceptance response; keep the recovery files")
return wait_for_result(request_id, digest)
payload = {'model': 'djev',
'state': 'The checkout page is unavailable.',
'questions': {'urgent': {'type': 'noul',
'instructions': 'Does this describe an '
'outage?'}},
'options': {'seed': 0}}
# Send once here. On an ambiguous failure, recover using the saved ID and bytes.
result = evaluate(payload)
print(result)const BASE = "https://api.djev.dev";
const API_KEY = process.env.DJEV_API_KEY; // Node.js, server-side only.
if (!API_KEY) throw new Error("Set DJEV_API_KEY in your server environment");
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
async function api(path, body, headers = {}, signal) {
const timeout = AbortSignal.timeout(30_000);
const response = await fetch(BASE + path, {
method: body === undefined ? "GET" : "POST",
headers: { "Content-Type": "application/json", ...headers, "Authorization": "Bearer " + API_KEY },
body, redirect: "error",
signal: signal ? AbortSignal.any([signal, timeout]) : timeout
});
const result = await response.json();
if (!response.ok) throw new Error(result.error?.message ?? "Request failed");
return { status: response.status, result, headers: response.headers };
}
async function waitForResult(id, digest, signal) {
const deadline = Date.now() + 900_000;
while (Date.now() < deadline) {
const { status, result: job } = await api("/v1/requests/" + id, undefined, {}, signal);
if (job.id !== id || job.request_sha256 !== digest)
throw new Error("Receipt does not match this request");
if (status === 200) {
if (job.status === "succeeded") return job.response;
if (job.status === "failed") throw new Error(job.error.message);
throw new Error("Unexpected terminal response");
}
if (status !== 202) throw new Error("Unexpected polling status");
await sleep(3000);
}
throw new Error("Stopped waiting; the accepted job is not cancelled. Resume by ID.");
}
async function evaluate(payload, signal) {
const id = crypto.randomUUID();
const requestBody = JSON.stringify(payload); // Freeze once; do not reserialize on retry.
const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(requestBody));
const digest = Array.from(new Uint8Array(hash), byte => byte.toString(16).padStart(2, "0")).join("");
// Save this object in your application before sending. Console output is not durable storage.
console.log("Keep privately for recovery:", { id, requestBody, digest });
const { status, result, headers } = await api("/v1/request", requestBody,
{ "Idempotency-Key": id }, signal);
if (status === 200) {
if (headers.get("x-djev-request-id") !== id || headers.get("x-djev-durable") !== "1")
throw new Error("Missing durable acceptance receipt");
return result;
}
if (status !== 202 || result.id !== id || result.request_sha256 !== digest)
throw new Error("Unexpected acceptance response; keep the recovery object");
return waitForResult(id, digest, signal);
}
const payload = {
"model": "djev",
"state": "The checkout page is unavailable.",
"questions": {
"urgent": {
"type": "noul",
"instructions": "Does this describe an outage?"
}
},
"options": {
"seed": 0
}
};
// This example submits once. Resume an ambiguous failure with the saved ID and bytes.
console.log(await evaluate(payload));Examples are copied as text. This page never sends an inference request. Open in playground loads the example input in a new tab after sign-in; it never runs it automatically. Question-only excerpts include example state. The shell example needs Bash, curl, jq and OpenSSL; Python uses the standard library, and JavaScript runs server-side in Node.js with native fetch and Web Crypto. Set DJEV_API_KEY in your server environment; all three examples use the API key directly and keep it out of recovery files. These examples submit once and stop on a connection error; keep the recovery ID and exact bytes. Console output is not durable application storage.
Durable requests
Keep a receipt, manage waiting, and retrieve the same saved outcome.
POST /v1/request now waits up to 20 seconds, then may return 202 Accepted. Existing clients must handle both 200 and 202. Use Prefer: respond-async to receive a receipt without waiting. Clients that expect an answer on every successful POST need a polling adapter.
| Response | Meaning |
|---|---|
POST · 200 | The existing typed model response, plus X-Djev-Request-Id and X-Djev-Durable: 1. |
POST or GET · 202 | A receipt with queued, running or retrying status. Respect Retry-After or poll about every three seconds. |
GET · 200 | A terminal envelope. Read status: succeeded contains response; failed contains error and response_status. HTTP 200 alone does not mean inference succeeded. |
GET · 404 | No retained job was found. Submission may not have been accepted, or its retention may have elapsed. |
/v1/requests/{id}202 pending · 200 terminalOne key per logical request
Generate 32–128 ASCII letters, digits, underscores or hyphens; a random UUID works. Save that key and the exact UTF-8 request bytes before sending. Reuse both after a lost acknowledgement or transient submission failure. The same key with different bytes returns 409, even if the JSON only differs in whitespace. A new key creates a new job.
Saved requests are scoped to the authenticated account. Use the same account when resuming; another account cannot retrieve or overwrite the job, even with the same ID. Keep IDs private and do not use predictable IDs. There is no public job listing. When prepaid billing is enabled, Idempotency-Key is required before durable acceptance; omission returns 400 before a credit hold or model work. With billing disabled, omitting it lets the server generate an ID, but cannot help you recover an acknowledgement whose ID you never received.
{
"id": "60ec093ff4c34c8791424e46294b6f27b",
"status": "queued",
"request_sha256": "ce91e5e3105e13ba80ccd56756909e77900f77a2c3e36fee635939cd1e5d02f2",
"created_at": 1790000000,
"status_url": "/v1/requests/60ec093ff4c34c8791424e46294b6f27b",
"retry_after": 3
}The sample ID and timings below are illustrative; generate your own ID. request_sha256 identifies the exact submitted bytes. The examples validate the receipt ID and hash, and construct the status path on the known API origin.
{
"id": "60ec093ff4c34c8791424e46294b6f27b",
"status": "succeeded",
"request_sha256": "ce91e5e3105e13ba80ccd56756909e77900f77a2c3e36fee635939cd1e5d02f2",
"created_at": 1790000000,
"status_url": "/v1/requests/60ec093ff4c34c8791424e46294b6f27b",
"retry_after": 0,
"request": {
"model": "djev",
"state": {
"message": "Checkout has been down for 20 minutes.",
"orders_blocked": true
},
"questions": {
"urgent": {
"type": "noul",
"instructions": "Does this describe an outage?",
"criteria": {
"true": "A service or feature is unavailable",
"false": "No service outage is described"
}
},
"team": {
"type": "choice",
"instructions": "Which team should handle this request?",
"criteria": {
"technical": "Broken features and outages",
"billing": "Charges and invoices",
"other": "Anything else"
}
},
"severity": {
"type": "score",
"instructions": "How severely is the business affected?",
"criteria": [
"No disruption",
"Some disruption; work can continue",
"A core activity is blocked"
]
}
},
"options": {
"isolation": "independent",
"score_mode": "categorical",
"samples": 1,
"steps": 1,
"seed": 0
}
},
"response": {
"model": "djev-0.1",
"answers": {
"urgent": {
"type": "noul",
"noul": 0.9
},
"team": {
"type": "choice",
"choice": "technical",
"probabilities": {
"technical": 0.8,
"billing": 0.1,
"other": 0.1
},
"confidence": 0.41832813428211324
},
"severity": {
"type": "score",
"score": 1.5999999999999999,
"legend": {
"0": "No disruption",
"1": "Some disruption; work can continue",
"2": "A core activity is blocked"
},
"probabilities": {
"0": 0.1,
"1": 0.2,
"2": 0.7
},
"confidence": 0.27015330083790245
}
},
"usage": {
"input_tokens": 500,
"output_tokens": 24
}
},
"timing": {
"queue_ms": 120,
"execution_ms": 65,
"model_ms": 58,
"headers": {
"x-djev-model-ms": "58.000"
}
}
}Recover without creating another job
After a connection error, first request GET /v1/requests/{your-saved-id}. If the submission was never confirmed and lookup returns 404 within your recovery window, resend the saved bytes with the same key. Do not regenerate a random key or rebuild the JSON. A known pending job only needs polling. Python and shell examples keep recovery files in the printed temporary directory; move them to your application’s protected storage if you need them across restarts or temporary-file cleanup.
What acceptance preserves
Acceptance is acknowledged only after the request and its pending record are persisted. Capacity keeps accepted jobs pending; it does not discard them. A worker or connection failure can cause the underlying inference to run again. The first persisted terminal result is canonical and is returned on subsequent lookups. This is not exactly-once computation.
Stopping local waiting does not cancel accepted server work. Closing the page, aborting a poll or reaching an example’s 15-minute waiting limit leaves the accepted job recoverable by ID. Status reads do not start model inference; already accepted work may restart it after interruption. Serving capacity is managed by the administrator; applications do not need a machine-start step.
Accepted request snapshots and terminal results use durable storage with seven days of inactivity retention. Pending entries are refreshed during recovery. Retention is not unlimited: a storage or recovery outage longer than seven days is outside this scope. Keep your own copy when longer retention is needed. A request rejected before durable acceptance, an invalid request, or bytes that never reached the service are not accepted jobs.
Low-latency delivery
All evaluation modes use POST /v1/request. Durable delivery remains the default, including requests without an idempotency key. When /config reports features.fast_requests: true, a request without an Idempotency-Key can opt into direct execution with Prefer: low-latency. An Idempotency-Key or the respond-async preference keeps the request on durable delivery, even when low latency is also requested.
POST /v1/request HTTP/1.1
Host: api.djev.dev
Authorization: Bearer <api_key>
Content-Type: application/json
Prefer: low-latency
X-Djev-Operation-Id: <saved-operation-id>
{
"model": "djev",
"state": "The checkout page is unavailable.",
"questions": {
"urgent": {
"type": "noul",
"instructions": "Does this describe an outage?"
}
},
"options": {
"seed": 0
}
}The submission URL stays the same for text, structured state, images, fast requests and durable requests. Use GET /v1/requests/{id} only to retrieve a saved durable request. Changing delivery mode does not change the request body or typed answer schema.
A direct response confirms the preference with Preference-Applied: low-latency. A successful 200 contains the normal typed answer; it has no server-saved receipt or retrieval key. If the preference is not confirmed, handle the ordinary durable response instead. The quickstarts above keep durable delivery.
For recoverable fast requests, save a random operation ID and the exact request bytes in your application before sending. Add X-Djev-Operation-Id: <id> to the direct attempt. When prepaid billing is enabled, this header is required for a direct request; omission returns 400 before a credit hold or model work. It does not select durable delivery. After a transient error, capacity response or uncertain connection outcome, use Idempotency-Key: <the-same-id> with those exact saved bytes, without the low-latency preference. If both ID headers are sent, their values must match. If recovering after a reload, check that ID first; an unconfirmed 404 can be followed by the keyed submission. Do not rebuild the JSON or change its model. When prepaid billing is enabled, the operation ID identifies its charge and canonical successful response. Durable fallback recovers an already charged response without another charge or model call. If execution remains unresolved, keep that ID and wait; do not create a new operation to escape the hold. When billing is disabled, direct execution has no saved canonical response and recovery can repeat inference.
Validate and retain a successful direct answer locally before retiring its recovery record. Stopping local waiting keeps recovery under your application’s control; it does not create a saved server job for a direct attempt.
Request & response
One shared context. Named questions. A predictable answer shape.
/v1/request200 answer · 202 receipt| Field | Type | Meaning |
|---|---|---|
staterequired | string · object · array | The context to evaluate. Structured JSON is accepted directly. An empty string is valid when an image supplies the context. |
questionsrequired | object | Map your question IDs to Noul, Choice, or Score definitions. The same IDs appear in answers. |
model | string | Default djev uses one step; aliases djev-latest and djev-0.1. Responses identify the canonical version as djev-0.1. |
images | array of data URLs | Optional. One validated static image; see image input. |
options | object | Optional sampling and question-isolation settings. See actual defaults. |
{
"model": "djev",
"state": "The checkout page is unavailable.",
"questions": {
"urgent": {
"type": "noul",
"instructions": "Does this describe an outage?"
}
},
"options": {
"seed": 0
}
}Text and structured state
Use plain text for a passage, or an object or array when your context already has structure. Instructions and criterion descriptions may also be strings, objects, arrays, or null.
{
"model": "djev",
"state": {
"message": "Checkout has been down for 20 minutes.",
"orders_blocked": true
},
"questions": {
"urgent": {
"type": "noul",
"instructions": "Does this describe an outage?",
"criteria": {
"true": "A service or feature is unavailable",
"false": "No service outage is described"
}
},
"team": {
"type": "choice",
"instructions": "Which team should handle this request?",
"criteria": {
"technical": "Broken features and outages",
"billing": "Charges and invoices",
"other": "Anything else"
}
},
"severity": {
"type": "score",
"instructions": "How severely is the business affected?",
"criteria": [
"No disruption",
"Some disruption; work can continue",
"A core activity is blocked"
]
}
},
"options": {
"isolation": "independent",
"score_mode": "categorical",
"samples": 1,
"steps": 1,
"seed": 0
}
}Read the response
These probabilities and token counts are illustrative, not recorded model outputs. They show the response shape.
{
"model": "djev-0.1",
"answers": {
"urgent": {
"type": "noul",
"noul": 0.9
},
"team": {
"type": "choice",
"choice": "technical",
"probabilities": {
"technical": 0.8,
"billing": 0.1,
"other": 0.1
},
"confidence": 0.41832813428211324
},
"severity": {
"type": "score",
"score": 1.5999999999999999,
"legend": {
"0": "No disruption",
"1": "Some disruption; work can continue",
"2": "A core activity is blocked"
},
"probabilities": {
"0": 0.1,
"1": 0.2,
"2": 0.7
},
"confidence": 0.27015330083790245
}
},
"usage": {
"input_tokens": 500,
"output_tokens": 24
}
}usage.input_tokens and usage.output_tokens sum actual backend work across physical reads. Input usage includes rendered instructions and scaffolding, image-expanded tokens, and repeated context in each independent or sampled physical read; it is not the raw user string length. Deduplicated reads count once. Output tokens are free when prepaid billing is enabled. They do not measure the size of the returned JSON. See Usage & credits for charging and holds. Optional diagnostics explain how those reads were organized.
Noul · Yes / no
Use Noul for a binary judgment.
A probability of yes, between 0 and 1.
Noul returns a number, not a boolean. Your application chooses a threshold for its own decision.
{
"type": "noul",
"instructions": "Does this describe an outage?",
"criteria": {
"true": "A service or feature is unavailable",
"false": "No service outage is described"
}
}{
"type": "noul",
"noul": 0.9
}criteria.true and criteria.false are optional descriptions of the two outcomes. A value of 0.9 is the model’s normalized probability for yes; it is not evidence of 90% real-world accuracy.
Choice
Choose among alternatives you define.
Provide named alternatives under criteria. The selected key is the highest-probability option, and probabilities contains every key you supplied.
{
"type": "choice",
"instructions": "Which team should handle this request?",
"criteria": {
"technical": "Broken features and outages",
"billing": "Charges and invoices",
"other": "Anything else"
}
}{
"type": "choice",
"choice": "technical",
"probabilities": {
"technical": 0.8,
"billing": 0.1,
"other": 0.1
},
"confidence": 0.41832813428211324
}Probabilities are normalized within your allowed alternatives and sum to one. They do not measure whether that set contains the right answer. On an exact tie, the first criterion wins.
Score
Map a judgment onto an ordered scale.
Order the descriptions from level zero upward. Score is the probability-weighted mean of those zero-based level indices; it may be fractional.
{
"type": "score",
"instructions": "How severely is the business affected?",
"criteria": [
"No disruption",
"Some disruption; work can continue",
"A core activity is blocked"
]
}{
"type": "score",
"score": 1.5999999999999999,
"legend": {
"0": "No disruption",
"1": "Some disruption; work can continue",
"2": "A core activity is blocked"
},
"probabilities": {
"0": 0.1,
"1": 0.2,
"2": 0.7
},
"confidence": 0.27015330083790245
}0 × 0.1 + 1 × 0.2 + 2 × 0.7 = 1.6Three levels produce a score on the 0–2 scale. legend preserves the original descriptions.
Choice and Score confidence measures how concentrated the returned distribution is. It is not a calibrated probability of correctness. All probabilities are experimental and uncalibrated; verify performance on your own examples.
Images
Attach a real image to the same request.
Keep state present and attach one base64 data URL under the top-level images field. Text and questions are evaluated with the image’s real visual embeddings. Remote URLs and animated files are not accepted.
GET /config reports features.images. Send image requests only when it is true. The same typed-answer response is used for text and images.
import base64
from pathlib import Path
# Reuse evaluate() from the Python quickstart above.
# Attach an image to the same request endpoint.
# Use an actual JPEG file.
encoded = base64.b64encode(Path("frame.jpg").read_bytes()).decode("ascii")
payload = {
"model": "djev",
"state": "Inspect the supplied image.",
"images": ["data:image/jpeg;base64," + encoded],
"questions": {
"clear": {"type": "noul", "instructions": "Is the scene well lit?"}
}
}
print(evaluate(payload))Self-contained JSON image example
This includes a synthetic 32 × 32 orange PNG, solely to make the example runnable. It is not a quality test.
{
"model": "djev",
"state": "",
"images": [
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAAAOklEQVR4nO3RQREAMAjEwKNq6rliqYTw4ZcVcMyE6nez6ayuxwMD/gCZCJkImQiZCJkImQiZCJko5APe/QICOfwHWAAAAABJRU5ErkJggg=="
],
"questions": {
"orange": {
"type": "noul",
"instructions": "Is the image mostly orange?"
}
}
}The file limit is 5 MiB decoded, with a maximum of 2048 pixels per side. JPEG, PNG and WebP must each contain one frame, and the MIME type must match the actual file. Base64 increases the request size. Photos uploaded in the playground are resized to a 512-pixel longest edge and encoded as JPEG at quality 0.8. Documentation examples keep their original image bytes.
Images in questions and answer options
In the Visual editor, choose Text, Image or Text + image for question instructions or an answer description. In JSON, use {"image": "data:image/png;base64,...", "text": "Optional text"}. Leave out text for an image alone. The same form works in Choice options, Score levels and Noul true/false criteria.
GET /config reports features.question_images. The feature must be enabled before submitting reference images. Up to six image attachments are allowed across the whole request, including the optional state image. Repeated attachments count again. Every image is processed separately by the vision encoder.
{
"model": "djev",
"state": "",
"questions": {
"orange": {
"type": "noul",
"instructions": {
"text": "Is this reference mostly orange?",
"image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAAAOklEQVR4nO3RQREAMAjEwKNq6rliqYTw4ZcVcMyE6nez6ayuxwMD/gCZCJkImQiZCJkImQiZCJko5APe/QICOfwHWAAAAABJRU5ErkJggg=="
}
}
}
}{
"model": "djev",
"state": "Pick the option containing an orange picture.",
"questions": {
"match": {
"type": "choice",
"instructions": "Which option is illustrated?",
"criteria": {
"illustrated": {
"image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAAAOklEQVR4nO3RQREAMAjEwKNq6rliqYTw4ZcVcMyE6nez6ayuxwMD/gCZCJkImQiZCJkImQiZCJko5APe/QICOfwHWAAAAABJRU5ErkJggg==",
"text": "An orange reference image"
},
"plain": "A text-only option with no picture"
}
}
}
}With isolation: independent, each question receives the shared state image and only its own reference images. Independent Score levels receive the question reference plus that level’s reference. Image bytes do not count toward text character limits; the 8 MiB request-body limit still applies. Score legends preserve their original image descriptions, so the response may include image data.
Webcam frames
Fresh images, one round trip at a time.
A webcam request is an ordinary image request with one fresh frame in images[0]. There is no separate webcam API or server-side video stream.
- Ask permission on a user action.
Start the camera from a button and show a local preview. Previewing does not have to send any image.
- Keep one accepted frame outstanding.
Await its terminal result before capturing the next frame; a 202 receipt is not a finished frame. Drop stale frames instead of accumulating jobs. The playground spaces dispatch starts by at least 250ms; this is not a promised frame rate.
- Stop cleanly.
Stop capture and polling on an error, hidden page, mode change or user action, and call
stop()on every camera track. Preserve the pending ID. Aborting local waiting does not cancel an already accepted frame; resolve that job before starting another loop.
async function evaluateFrame(video, signal) {
// Supply evaluate() through your own authenticated server endpoint.
// Keep the Node.js quickstart and its API key on that server, not in this page.
// Call only after camera permission, readiness and features.images === true.
if (!video.videoWidth || !video.videoHeight)
throw new Error("Wait for the camera preview");
const scale = Math.min(1, 512 / Math.max(video.videoWidth, video.videoHeight));
const canvas = document.createElement("canvas");
canvas.width = Math.max(1, Math.round(video.videoWidth * scale));
canvas.height = Math.max(1, Math.round(video.videoHeight * scale));
canvas.getContext("2d").drawImage(video, 0, 0, canvas.width, canvas.height);
return evaluate({
model: "djev", state: "Evaluate this camera frame.",
images: [canvas.toDataURL("image/jpeg", 0.8)],
questions: { present: { type: "noul", instructions: "Is a person visible?" } }
}, signal);
}Options
Choose your profile explicitly when comparing results.
| Option | Default | Behavior |
|---|---|---|
samples | 1 | Integers 1–4. Additional samples average model evidence and require more backend reads. |
steps | 1 | Djev uses one denoising step. Explicit incompatible values are rejected. |
isolation | joint | joint evaluates questions in a shared read. independent evaluates each question separately. These can produce different answers and amounts of backend work. |
score_mode | categorical | categorical uses one distribution across Score levels. independent_levels evaluates levels separately, then normalizes them; it requires isolation: "independent" and nonempty level descriptions. |
seed | 0 | Reuse the same initial diffusion canvas for repeated requests. Any explicit integer is preserved; null deliberately selects fresh random noise. Keep state, image bytes, question wording, option order and other options unchanged when comparing runs. A fixed seed does not promise bitwise-identical replies across batching or model releases. |
diagnostics | false | Include the chosen execution profile, physical read count and question-level evidence diagnostics. |
Playground library examples use seed: 0 and evaluate questions independently. Examples opened from this reference preserve their original options, including API defaults when options are omitted. Copy its request JSON to reproduce the submitted input and chosen Score mode. More samples cost more reads and do not guarantee better accuracy. Camera frames contain new pixels, so successive frames are different inputs even with a fixed seed. Independent-level evaluation is capped at 128 physical reads per request, including samples.
Limits
Validate before sending, then handle context and capacity errors.
| Input | Validated limit |
|---|---|
| State | 20,000 Unicode code points |
| Each question’s instructions | 2,000 Unicode code points |
| Each criterion description or Choice key | 500 Unicode code points |
| Questions | 1–32 per request |
| Choice alternatives | 1–255 |
| Score levels | 2–10 |
| State images | At most one static JPEG, PNG or WebP |
| All image attachments | Six across state, question instructions and answer descriptions |
| Decoded image file | 5 MiB; maximum 2048 × 2048 pixels |
| JSON request body | 8 MiB |
| Model context | 32,768 tokens, including the rendered prompt, expanded image tokens and answer canvas |
Structured values count compact JSON, including keys and punctuation. Unknown fields, duplicate JSON keys and non-finite numbers are rejected. Field limits do not guarantee that a combined request fits the token context or answer canvas; shorten the context or split the question set when needed.
Long-context accuracy is experimental. Capacity and latency depend on the request, number of physical reads and current load.
Errors & retries
Errors use an error object with a code and a human-readable message.
{
"error": {
"code": "capacity_paused",
"message": "Serving is paused by the administrator."
}
}| HTTP | Meaning | Next step |
|---|---|---|
400 | Malformed metadata or missing operation ID | Correct request metadata. When billing is enabled, supply a saved X-Djev-Operation-Id for direct delivery or Idempotency-Key for durable delivery; no hold or model call occurs without it. |
401 | Missing, invalid or expired credential | Check your API key. If using an older token integration, refresh an expired token. Resume with the same account, request ID and bytes. |
402 | Insufficient available credits | Do not automatically retry. Open Usage & credits, add credit, then explicitly resume with the same ID and bytes. |
409 | Idempotency or billing operation conflict | The key belongs to different exact bytes. Recover the original request, or create a new key only for a genuinely new job. |
413 | Body limit exceeded | Reduce the payload or image size. |
415 | Unsupported content type | Send application/json. |
422 | Invalid fields, model, image or context | Read error.message and optional error.issues; fix the input. |
429 / 529 | Submission rate limit or admission capacity | Respect Retry-After; retry the same key and bytes. Already accepted jobs stay pending through inference capacity limits. |
503 | Temporary capacity, storage or billing failure | For billing_pending or billing_unavailable, retain the same operation ID and bytes, respect Retry-After, and use bounded recovery. For capacity_paused, wait for the administrator to resume serving. |
502 / 504 | Service error or HTTP waiting timeout | Acceptance may be ambiguous. Look up your ID; preserve the same key and bytes for any resubmission. |
Inspect the body as well as HTTP status. A saved failure is retrieved with GET 200 and status: failed; response_status records its failure status. A failed POST without a receipt does not prove that persistence did not occur. The durable retry protocol applies only when features.durable_requests is true; do not automatically replay inference against older direct-only deployments.
Authenticate with your API key in the Authorization header. A validation error may contain field issues without echoing the submitted image or state.
Understand the timing
Separate model work from the full user experience.
Image preparation, queueing, request routing and browser transfer add time around model work. A model timing is not the time a user waits.
Warm regional responses below 100 ms are a target, not a latency guarantee. Direct delivery removes durable dispatch and result polling from the first attempt; upload size, model work, network distance and load still affect the total.
| Header, when supplied | What it measures |
|---|---|
x-djev-compile-ms | Request/compiler setup in the model API. |
x-djev-model-ms | Model orchestration, including read waits, image preflights and upstream transport; not isolated model computation. |
x-djev-server-ms | Elapsed time in the model API, excluding the public gateway and client network. |
x-djev-gateway-ms | Gateway entry through response-header creation, including route cleanup. |
x-djev-body-ms / x-djev-validation-ms | Gateway body receipt and request JSON/schema/image validation. |
x-djev-proxy-headers-ms / x-djev-proxy-ms | Gateway upstream dispatch through headers / response close. Includes routing, connection waits and upstream work. |
Server-Timing | The same available spans, with distinct gateway metric names. Missing spans were not observed. |
A saved result has timing.model_ms when reported, timing.queue_ms for acceptance to the first recorded worker start, and timing.execution_ms for elapsed time since that start. Execution includes retries, backoff and recovery gaps; it is not pure model time. Original allowlisted spans appear in timing.headers; the status GET’s duration is only a retrieval, not model latency.
For durable work, measure total user waiting from first submission through terminal retrieval. Polling delays, queueing and retries belong to that total; do not label the entire remainder as hosting or transfer. Subtracting a saved model span does not isolate network RTT. Image uploads and new webcam frames add work, and repeated-frame cache hits do not establish new-frame speed.