API reference
All API requests go to https://YOUR-REFLEX-HOST/v1/... and authenticate with a secret key:
authorization: Bearer rl_live_...
Requests and responses are JSON. Bodies can be up to 64 KB.
POST /v1/check
Checks one agent turn. Each new turn counts as one check against your plan. Follow-up checks of the same reply sent with a continuationToken (see Sentence-by-sentence checks) are free for the first 12 follow-ups of a reply; each one after that counts as one check.
Request
| Field | Type | Required | Notes |
|---|---|---|---|
agentId |
string | yes | The agent whose rule pack to use (agt_...). |
callId |
string | yes | Your call or session id, up to 128 characters. Turns with the same callId are grouped in the call log. |
turnIndex |
integer | no | Position of the turn in the call, starting at 0. |
callerText |
string | one of the two | What the caller just said, up to 4,000 characters. |
draftReply |
string | one of the two | What the agent is about to say, up to 4,000 characters. Without it, only caller signals are returned. |
history |
array | no | Up to 50 earlier turns, each { "role": "caller" or "agent", "text": "..." }. The last few are used. |
context |
string or object | no | Facts from tools for this call, such as an account balance or a booked slot, up to 8,000 characters as JSON. Dollar amounts here are allowed in the reply. |
options |
object | no | See below. |
options:
| Field | Default | Notes |
|---|---|---|
timeoutMs |
400 |
Overall budget for the model call, 50 to 2,000. |
hedgeAfterMs |
140 |
If the first model request hasn't answered by then, a second identical one is sent and the first answer back wins. null turns this off. |
onJevError |
"deterministic" |
What to do if the model times out or errors: "deterministic", "block" or "pass". See below. |
signals |
true |
Set to false to skip intent, handoff, mood and end-of-turn. This makes the request a little smaller. |
Response
| Field | Notes |
|---|---|
turnId |
Id of the logged turn. |
decision |
"pass", "review" or "block". |
say |
The text to speak: the draft on pass or review (when reviewAction is pass), otherwise the fallback line. null if no draft was sent. |
fallbackUsed |
true when say is a fallback line. |
fired |
Rules that fired or landed in the review band: ruleId, title, severity, band ("fire" or "review"), probability, source ("jev", "pattern", "price_list" or "hours"), and an optional detail. |
signals.intent |
{ id, confidence, probabilities } over the pack's intents. |
signals.handoff |
{ probability, recommended }. recommended uses the pack's handoff threshold. |
signals.sentiment |
{ score, label, probabilities }. Score runs from 0 (calm) to 3 (furious). Labels are calm, mildly annoyed, upset and furious. |
signals.endOfTurn |
{ probability, complete }: whether the caller seems to have finished. |
signals.callerManipulation |
{ probability, detected }: the caller is trying to make the agent break its rules. |
signals.backchannel |
{ probability, detected }: the caller only said "mm-hm" or "okay" and isn't taking the turn. |
latencyMs |
{ total, jev } in milliseconds, measured inside Reflex Layer. A server-timing header carries the same values. |
usage |
{ inputTokens, jevCalls }. |
degraded |
null, or { reason, policy } when the model didn't answer in time. |
packId, packVersion |
Which rules were applied. |
How the decision is made
Each rule produces a probability that it's broken. For the rule's thresholds (or the pack's defaultThresholds, by default block 0.65 and review 0.35):
- At or above block, the rule fires at its severity. A
blockrule blocks the reply, areviewrule queues it, and alogrule only records it. - Between review and block, the turn goes to review.
- Below review, nothing happens.
The strictest outcome wins. Pattern rules return 1 or 0. Price and hours rules are code, not the model, and score like this:
- A dollar amount in the draft that isn't on your price list (or in the
contextyou sent, when the rule allows it) scores 1. The check compares against the whole list, not per service, so quoting the right number for the wrong service is left to the judge rules. - A number that looks like a price but has no dollar sign or "dollars" ("it's 299") scores 0.5, so it goes to review, not block. So does an off-list amount the caller said first.
- An offered time outside your hours scores 1. Times without am or pm get a business-day reading: 7 to 11 means morning, 12 to 6 means afternoon or evening. A misread is possible, and judge rules still run alongside.
Timeouts and failures
If the model hasn't answered within timeoutMs, the response comes back with degraded set:
"deterministic"(default): pattern, price and hours rules still run. If none fire, the decision isreviewand the draft is spoken."block": the decision isblockand the fallback line is spoken. Use this when a wrong sentence costs more than an awkward one."pass": the decision ispass.
The same policy applies if the account's monthly model budget is reached (the reason then mentions "budget"). Each paid plan includes model use well above its check count, so on paid plans this only happens with unusually long turns. On the Free plan it can also happen late in a busy month, because free accounts share a monthly model budget.
Your own code should also set a network timeout (we suggest 600 ms) and speak the draft or a fallback if Reflex Layer can't be reached at all. The official integrations do this.
Sentence-by-sentence checks
When you check a streamed reply one sentence at a time, send the first check as usual, with turnIndex set. The response includes continuationToken (it is only issued when turnIndex is present). For each later sentence, send the whole reply so far as draftReply and add:
| Option | Type | Meaning |
|---|---|---|
options.continuationToken |
string | The token from the first check of this reply. |
options.alreadySpoken |
number | How many characters of draftReply were already spoken. |
options.signals |
boolean | Set false to skip intent and handoff signals on later sentences. |
The token is signed for your account, agent, call, turn index and the first sentence's text, and expires 2 minutes after the first check (follow-ups return the same token, they don't extend it). Each follow-up's draftReply must start with the text of that first check. Checks with a valid token update the same turn in the call log (the strictest decision wins) and are not billed as new checks, up to 12 follow-ups per reply; each follow-up after the 12th is billed as one check. A missing, expired, altered or mismatched token starts a new, billed turn. The Python and Node clients handle this for you.
POST /v1/endpoint
A light check on a partial transcript, to help decide whether the caller has finished speaking. Send agentId, callId, callerText and optionally history. Returns endOfTurn, backchannel, latencyMs, usage and model. Counts as one check.
POST /v1/vapi/{agentId}/chat/completions
An OpenAI-compatible endpoint for Vapi's Custom LLM. See Integrations.
GET /v1/health
Returns { "ok": true }. No key needed.
Errors
Errors look like { "error": { "code": "...", "message": "...", "details": { ... } } }.
| Status | Code | Meaning |
|---|---|---|
| 400 | bad_json |
The body isn't valid JSON. |
| 401 | unauthenticated |
Missing, invalid or revoked key. |
| 402 | quota_exceeded |
This month's plan checks are used up. Checks resume on the 1st (UTC) or after an upgrade. There is no overage billing. Treat it like a timeout: let the agent speak. |
| 403 | email_unverified |
The account email is not confirmed yet. Open the link in the confirmation email or resend it from the dashboard. |
| 404 | agent_not_found, not_found |
Unknown agent or route. |
| 409 | no_upstream |
Vapi proxy used before an upstream model was set. |
| 413 | too_large |
Body over 64 KB. |
| 422 | invalid |
Invalid request. details names each bad field. |
| 502 | upstream_error |
Your upstream model (Vapi proxy) returned an error. |
| 503 | not_configured |
The service is missing configuration. Contact support. |
Webhooks
Set one webhook URL under Connect. It must be an https URL on a public host name. Events:
turn.blocked: a reply was blocked.turn.review: a turn went to review.call.handoff: a handoff to a person was recommended.
Each delivery is a POST with a JSON body { id, type, created, data } and a header:
x-reflex-signature: t=1731300000,v1=5f2c...
v1 is the hex HMAC-SHA256 of t + "." + rawBody, using your webhook secret. Reject deliveries where the signature doesn't match or t is more than 5 minutes old. See the verifier in Integrations.
Deliveries time out after 5 seconds and aren't retried yet. Use them for alerts and follow-ups, not as your only record. The call log keeps everything.