Skip to content

API docs

A draft. The API opens later, and this reference is settled when it does.

One resource, the session: one video of one person, up to an hour. You create it, we measure it, and you read its outputs. Paths below start at the API’s own address, given when it opens.

Quickstart

1 · Get a key. Keys come with Pro: create one in your Pro account. It is shown once, with its webhook signing secret. Send it as Authorization: Bearer pk_live_….

2 · Create a session.

Create a session
POST /v1/sessions
Authorization: Bearer pk_live_…
Content-Type: application/json

{
  "source": { "url": "https://example.com/videos/talk.mp4" },
  "outputs": { "signals": true, "reports": ["99"] },
  "release": "first@1",
  "consent": { "token": "consent-record-8812" },
  "webhook": { "url": "https://example.com/hooks/99traits" }
}

3 · Wait for the webhook. Measuring takes from a few minutes to about an hour. We post session.ready to your webhook, or you poll, following Retry-After.

4 · Read the outputs.

Read a session
GET /v1/sessions/{id}
Authorization: Bearer pk_live_…

See an example response

Videos we accept

We check every video before we measure it. One that breaks a rule comes back rejected, and is not billed. Its reject.codes name the rules it broke, and reject.detail says what to send instead.

Ten minutes to an hour long.
More minutes measure better, up to the hour. Code: video_length
One person.
Alone in picture and sound from start to end: no interviewer, co-host or split screen. Code: one_person
Facing the camera.
Their face is in view nearly all the time, and they talk to the camera. Code: facing_camera
One take.
From one camera angle, with few cuts and long stretches between them. Code: one_take
Real video.
Moving video at its full frame rate: not a photo, a slideshow or a still face over sound. Code: real_video
Speech, with no music.
They talk most of the time, with clear sound and no music under it. Code: speech
In English.
The API opens for English only. Code: language
A file we can open.
A video file with its sound track, at 4K or less. Code: file

Endpoints

POST /v1/sessions
Create a session: the video by link or by chunks, the outputs, the consent reference. Returns 202 and the session. To retry safely, send an Idempotency-Key header: the same key and body within a day returns that session.
GET /v1/sessions/{id}
The session’s status, and its outputs once ready. A Retry-After header comes with every status that is not final.
DELETE /v1/sessions/{id}
Remove the outputs now, and the video if we still hold it. Returns 204.
PUT /v1/sessions/{id}/chunks/{seq}
One chunk of the file (for a session created with source.upload), numbered from 0, with X-Chunk-SHA256. Up to 16 MiB.
POST /v1/sessions/{id}/finalize
Close the upload with the number of chunks sent. A 409 names any chunk still missing.

The request body of POST /v1/sessions

source
Required. { "url": "…" }, a direct https link to the video file, which we fetch; or { "upload": true }, to send it in chunks. Write the link in plain ASCII, percent-encoding the rest. The file can be up to 4 GiB. A fetch can take up to an hour, and the file must keep coming at 256 KiB a second or more. A link we cannot reach, a fetch slower than that, or a file that is not a video comes back rejected.
consent
Required. { "token": "…" }, a reference to your own record of this person’s consent.
outputs.signals
Places and bands. On unless set to false.
outputs.reports
The reports to return as block trees, by ID: 99 (the person’s profile with its chapters closed) and chapters (with every chapter open). None unless you ask.
release
The release to pin. There is one for now, first@1, and it is the default. Any other name is refused.
webhook.url
An https address where we send the session’s events.

Sending the file in chunks

Upload in chunks
# 1. create the session with "source": { "upload": true }
#    (its upload block gives chunk_url and chunk_bytes)
# 2. send the file in order, a chunk at a time, with its SHA-256
PUT /v1/sessions/{id}/chunks/0
Authorization: Bearer pk_live_…
X-Chunk-SHA256: <the chunk's SHA-256, in hex>
Content-Type: application/octet-stream

<the chunk's bytes>

# 3. finalize with the number of chunks sent
POST /v1/sessions/{id}/finalize
Authorization: Bearer pk_live_…
Content-Type: application/json

{ "chunks": 12 }

A chunk that arrives intact is answered with its sequence number, size and hash; send it again on any error. A session is billed as one Pro session once its video passes the checks. If it then fails on our side, it is not.

Statuses

awaiting_media
Created with source.upload; waiting for the chunks and finalize.
gating
Checking that the video can be measured.
rejected
It cannot: reject.codes and reject.detail say why and what to send instead. Not billed.
processing
Being measured.
ready
The outputs are in the response, until expires_at.
failed
Measuring failed on our side, or the account’s Pro plan ended before the video arrived. Not billed.
expired
The outputs are no longer served.

Outputs

When the status is ready, the session carries outputs and its consent block (mode, your token as given, and when it was represented).

signals
An object keyed by each trait’s public key (made from its plain name), each { "place", "band" }. A trait in the middle, or one the video does not make sure, is not in it.
reports
An object keyed by report ID, each a block tree: its language and blocks in order, as our site renders them. Every trait in a tree carries its own two ends, low and high.
manifest
The release that served the outputs and one version of everything that made them.

Places and bands

A place runs from 0 to 99 among people: 91 is further toward the high end than about nine in ten people. A band names the outer tenths:

hi1
In the top tenth: further toward the high end than 9 in 10 people.
hi2
In the second tenth from the top: further than 8 in 10 people.
lo1
In the bottom tenth: further toward the low end than 9 in 10 people.
lo2
In the second tenth from the bottom.

Show a place as a position, not as a score: neither end of a trait is better. Outputs never carry raw values, timestamps or excerpts of the video.

Versions

The manifest names the release that made the outputs, by its public name, and one version of everything that made them. Outputs are made once: they do not change while they are served.

How long outputs stay

The video is deleted once the outputs are ready, and when a session is rejected or fails. The outputs are served until expires_at, 30 days later, or until you delete the session. After that the session reads expired.

Signals

Each signal runs between two named ends: a place near 0 sits toward its low end, near 99 toward its high end. Read a key by its ends, not by its name alone.

The lists below hold the signals of our example, a made-up person, with their two ends. The full list of keys comes with the API.

Timing and rhythm (1)
answer-speed
Low: takes time to answer
High: answers fast
Words (1)
answer-first
Low: gives the reason before the answer
High: gives the answer before the reason
Body and energy (1)
hand-talk
Low: keeps the hands still
High: talks with the hands

Webhooks

We post to the session’s webhook URL on session.ready, session.rejected and session.failed. The body is { "id", "event", "at" }: fetch the session for its outputs. Answer with any 2xx; we retry with backoff for 24 hours. Your webhook URL must be https, and we do not follow its redirects.

Each post is signed with your key’s signing secret: X-99traits-Signature: t=<unix seconds>,v1=<hex>, where v1 is the HMAC-SHA256 of the timestamp, a dot and the raw body.

Verify a signature (Python)
import hmac, hashlib, time

def verify(body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    signed = f"{parts['t']}.".encode() + body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    fresh = abs(time.time() - int(parts["t"])) < 300
    return fresh and hmac.compare_digest(expected, parts["v1"])

Errors

Errors are application/problem+json with a stable code and a plain detail. 401: the key is missing or revoked. 402: the account has no active Pro plan. 404: no such session. 409: the upload is incomplete or closed, an Idempotency-Key came with another body, or the account has not accepted the current terms, which you can do on your Pro page. 413: the request body is too large. 422: the request is not valid. 429: too many sessions today. 503: we have reached today’s limit. 507: we cannot take new files right now.

Start Pro and get a key