Fusion API
Face plus voice in one call. Both modalities required for the model path.
- Endpoint
- POST /fusion
- Auth
- Bearer api_key
- Price
- $0.08 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/fusion/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 frame. Capped at 15,000,000 characters (413 above it). |
| audio_b64 | string | one of these | Base64 clip. Capped at 40,000,000 characters (413 above it). |
| landmarks | number[][] | one of these | Cap here is 1000 points, NOT the 478 of /emotions — the two endpoints do not share a limit. Elements must be arrays. |
| mfcc_features | number[][] | one of these | Cap here is 2000 frames, NOT the 200 of /voice. Elements must be arrays. |
Smallest body that clears validation. The shape is real; the values in angle brackets are placeholders you replace.
curl https://krrrxshxncvcsumugxbi.functions.supabase.co/fusion \
-X POST \
-H "Authorization: Bearer $VEXKIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"session_id": "demo-0001",
"image_b64": "<base64 of one frame>",
"audio_b64": "<base64 of the matching clip>"
}'Headers
| Header | Required | Description |
|---|---|---|
| Authorization | yes | Bearer <api_key>. The key must carry the fusion 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 |
|---|---|---|---|
| state | string | required | One of the five VEXKIO states. |
| 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 | required | One entry per VEXKIO state. |
| inference | string | required | "real" or "heuristic". |
| session_id | string | required | Echoed back, or the generated UUID. |
| latency_ms | number | required | Wall time measured inside the function. |
| model_version | string | required | Defaults to "ia-09-v0.1.0"; replaced by the origin's tag when a model ran. |
| 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.
{
"state": "friccion",
"confidence": 0.28,
"probabilities": {
"neutral": 0.20,
"apertura": 0.20,
"friccion": 0.28,
"tension": 0.16,
"desconexion": 0.16
},
"inference": "heuristic",
"session_id": "demo-0001",
"latency_ms": 41,
"model_version": "ia-09-v0.1.0",
"timestamp": "2026-01-01T00:00:00.000Z",
"llm_enrichment": null
}Before you trust a number
- The multimodal model only runs when image_b64 AND audio_b64 are BOTH present and the deployment has INFERENCE_URL set. One modality alone always lands on the heuristic.
- With sparse or empty inputs the heuristic returns a near-uniform map, so confidence lands around 0.2. That is the classifier declining to choose, not a weak reading.
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.