Signs API
Hand-shape REST endpoint. Base64 image or 21+ hand landmarks.
- Endpoint
- POST /signs
- Auth
- Bearer api_key
- Price
- $0.07 per session
Request
POST a JSON object. The fields below are the ones the handler reads; anything else in the body is ignored. Every rule in the table was transcribed from supabase/functions/signs/index.ts.
| Field | Type | Presence | Behaviour in the handler |
|---|---|---|---|
| session_id | string | optional | Opaque string. Omit and a UUID is generated for you. |
| image_b64 | string | one of these | Base64 of a hand image, non-empty. Only path that can reach the IA-03 backend, and only where INFERENCE_URL is configured. |
| hand_landmarks | number[][] | one of these | At least 21 points, and the first 21 must each be an [x,y,z] of three finite numbers. Anything shorter or malformed is a 400. |
Smallest body that clears validation. The shape is real; the values in angle brackets are placeholders you replace.
curl https://krrrxshxncvcsumugxbi.functions.supabase.co/signs \
-X POST \
-H "Authorization: Bearer $VEXKIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"session_id": "demo-0001",
"image_b64": "<base64 of one hand image>"
}'Headers
| Header | Required | Description |
|---|---|---|
| Authorization | yes | Bearer <api_key>. The key must carry the signs scope; a key without it is rejected as 401, not 403. |
| Content-Type | yes | application/json |
No other request header changes behaviour. This page used to document an X-Idempotency-Key; no endpoint reads it, so it has been removed rather than left as a no-op promise.
Response
200 returns the object below. There is no envelope and no wrapper: the fields are top-level.
| Field | Type | Presence | Behaviour in the handler |
|---|---|---|---|
| sign | string | required | This endpoint returns `sign`, NOT `state`. Its response is not the Core-5 envelope. |
| confidence | number | required | Probability the classifier assigned to `state` IN THIS REQUEST, rounded to 3 decimals. It is not model accuracy, not a benchmark, and not a quality guarantee. Read `inference` before you trust it. |
| probabilities | object | optional | Present only when the real backend returned one. Absent on the heuristic path. |
| inference | string | required | real = IA-03 answered. real_failed_fallback = IA-03 was configured but failed, so the legacy heuristic answered instead. heuristic = no model was configured at all. |
| models_used | string[] | required | Empty array on every non-real path — nothing ran, nothing is claimed. |
| cost | number | required | 0 on every non-real path. |
| session_id | string | required | Echoed back, or the generated UUID. |
| latency_ms | number | required | Wall time measured inside the function. |
| model_version | string | null | required | null on the heuristic path — no model version is invented. |
| timestamp | string | required | ISO-8601, generated at response time. |
| llm_enrichment | object | null | required | null unless VEXKIO_LLM_ENRICH=true on the deployment. |
Example values, not a measurement
The field names, types and nesting below are transcribed from the deployed handler. The numbers and strings are placeholders chosen to illustrate the shape. They are not a recorded call, not a benchmark, and not a claim about how any VEXKIO model performs.
In particular confidence is the probability the classifier assigned within a single request. It is not accuracy. VEXKIO publishes no accuracy figure on this site.
{
"sign": "hola",
"confidence": 0.5,
"inference": "heuristic",
"models_used": [],
"cost": 0,
"session_id": "demo-0001",
"latency_ms": 12,
"model_version": null,
"timestamp": "2026-01-01T00:00:00.000Z",
"llm_enrichment": null
}Before you trust a number
- The heuristic path is a modulo hash over five finger-extension distances, indexed into a fixed 77-word list. It is not a recognition model. Its `confidence` is a hardcoded formula (0.5 when fewer than 21 points arrive; otherwise 0.7 plus fixed bonuses, capped at 0.97) — a constant, not a probability. When `inference` is "heuristic" or "real_failed_fallback", `sign` and `confidence` carry no accuracy claim whatsoever.
- VEXKIO publishes no accuracy figure for sign-language recognition on this site. The measured numbers that exist in the repo are for a hand-gesture task, not Colombian Sign Language, so quoting them here would be a category error.
Errors
Almost every error body is a JSON object with a single human-readable error string. There is no stable machine-readable code field on these endpoints - branch on the HTTP status, not on the string.
{ "error": "Provide image_b64 (string) or landmarks (array)" }The second tab is the one body on these endpoints that does NOT carry an error key. A client that reads only body.error will log undefined for a cost-cap 429, so read the status first. Note the field is retry_after_seconds - seconds, not milliseconds - it appears on this body only, and its value is however many seconds remain until 00:00 UTC, so the number above is just an illustration.
| HTTP | When |
|---|---|
| 400 | Body is not a JSON object, or a field failed validation. The error string names the field. |
| 401 | Missing Bearer header, unknown key, revoked key, or a key that lacks this endpoint's scope. All four collapse into the same 401 - a missing scope is NOT a 403 here. |
| 403 | Organization is suspended. Some non-core endpoints also use 403 for missing consent or attestation flags. |
| 413 | Body over 12,000,000 bytes, or a base64 field over its own cap (15,000,000 chars for image_b64, 40,000,000 for audio_b64). |
| 429 | Per-IP throttle, per-org daily limit, monthly session quota exhausted, or a cost cap. See Rate limits below. |
Codes this page used to list - invalid_payload, scope_missing, session_not_found, model_unavailable - are not emitted by any handler and have been removed.
Rate limits
Three independent limits can produce a 429. All of them are read from supabase/functions/_shared/rate_limit.ts and the handler itself.
| Limit | Value | Scope |
|---|---|---|
| Pre-auth throttle | 60 / minute | Per client IP, in-memory, checked before the key is even validated. |
| Daily request limit | 10,000 / day (default) | Per organization. Resolved from your daily_quota, else your sessions_limit, else the deployment default. |
| Session quota | per plan | Cumulative sessions used against your plan ceiling. Exhausting it returns 429 until the plan is upgraded. |
Two corrections to what this page used to claim. There is no Starter / Pro / Business / Enterprise rps table in force - the tiered limiter exists in the repo but is not imported by any deployed function. And no endpoint emits X-RateLimit-Limit, X-RateLimit-Remaining or X-RateLimit-Reset headers, so do not build a budget on reading them. Retry with your own bounded exponential backoff.