Scores a phone call for scams while it's happening. Start a conversation when the call starts, send what each side says as it's said, and every response carries a live score from 0 to 100 for how likely it is that the other party is running a scam.
It's one endpoint for the whole call: what you put in the body decides what happens.
New to this? The setup guide walks through a whole call, start to end, with the requests in order. It also explains every field of the score.
POST https://api.spoofey.com/v1/fraudWhat to send
| You want to | Send | You get back |
|---|---|---|
| Start a conversation with what was just said | number and text (or utterances) |
201 and the conversation, already scored |
| Start one before anyone has spoken, to stream its audio | start: true and number |
201 and the conversation |
| Add what was said | id and text (or utterances) |
200 and the updated score |
| Read the score | id |
200 and the conversation, as it stands |
| Attach what you know about the number | id and classification, or id and classify: true |
200 and the updated score |
| End it | id and end: true, with any last lines |
200 and the final score with its history |
A body with only a number (no text, utterances, start or id) isn't a conversation: it's a number check, billed per check.
Body
| Field | Required | Type | Notes |
|---|---|---|---|
id |
To work on a conversation | string | The conversation's id, from the response that started it. conversation_id works too. |
text |
No | string | One line that was said, up to 2,000 characters. Counts as the remote party unless you set speaker. |
speaker |
No | string | Who said text: remote (the party being assessed, the default), user (the person you're protecting) or unknown. caller and callee are accepted too. |
utterances |
No | array | Several lines at once, in the order they were said: up to 50 { "speaker", "text" } objects. Use instead of text. Here speaker defaults to unknown, so set it. |
number |
No | string | When starting: the number of the party being assessed, with its country code, like +14155550199. remote_number and phone work too. Needed for classify: true. |
start |
No | boolean | true starts a conversation with nothing said yet. |
end |
No | boolean | true ends the conversation, after adding any text or utterances sent with it. |
classification |
No | string, object or null | What's known about the number: the id of one of your number checks, the whole number check response, or one of emergency, financial, carrier, business, ordinary. With id, it replaces the conversation's classification, and null removes it. |
classify |
No | boolean | true runs a number check on the number now and attaches the result, at the number check's price. The result also comes back as classify. |
include |
No | string | With id: transcript, history or transcript,history adds every line said and each change of score to the response. |
user_number |
No | string | When starting: the number of the person you're protecting. For your own records. |
language |
No | string | When starting: the call's language, for audio. Default en. |
metadata |
No | object | When starting: anything you want back on the conversation, such as your own call id. Up to 2,000 characters as JSON. |
Start a conversation
Start it as soon as you have the first thing the caller said. Keep the id from the response: everything else uses it.
curl https://api.spoofey.com/v1/fraud \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{"number": "+14155550199", "text": "Hi, this is the fraud department at Chase. We saw suspicious activity on your account.", "metadata": {"call_id": "abc-123"}}'201 and the conversation, already scored:
{
"id": "5fa8406c-0601-4eba-aa70-797917434f29",
"object": "conversation",
"status": "active",
"end_reason": null,
"risk_score": 45,
"trust_score": 55,
"peak_score": 45,
"band": "elevated",
"recommended_action": "caution",
"scam_type": null,
"claimed_identity": null,
"summary": null,
"advice": null,
"cues": [
{ "cue": "authority_impersonation", "severe": false, "count": 1, "speakers": ["remote"], "evidence": "Hi, this is the fraud department at Chase. We saw suspicious activity on your account.", "source": "detector" },
{ "cue": "account_compromise_claim", "severe": false, "count": 1, "speakers": ["remote"], "evidence": "Hi, this is the fraud department at Chase. We saw suspicious activity on your account.", "source": "detector" }
],
"components": { "model_score": null, "cue_score": 45, "classification_adjustment": 0, "adjustments": [] },
"analysis": "full",
"remote_number": "+14155550199",
"user_number": null,
"classification": null,
"mode": "text",
"words": 15,
"utterances": 1,
"billed_minutes": 1,
"credits_charged": 0.06,
"metadata": { "call_id": "abc-123" },
"created_at": 1790812800000,
"updated_at": 1790812800000,
"ended_at": null
}model_score is null until our AI has read the call. That read lands a few seconds later: get it from Follow the score live, from the next response, or by reading the score. What each field means is in the guide, under Reading the score.
If you're going to stream the call's audio instead of sending text, start with nothing said:
curl https://api.spoofey.com/v1/fraud \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{"start": true, "number": "+14155550199"}'Add what was said
Send each line as soon as you have it, with the conversation's id. The sooner it arrives, the sooner a scam is caught. Batch lines with utterances if you have several.
curl https://api.spoofey.com/v1/fraud \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{"id": "5fa8406c-0601-4eba-aa70-797917434f29", "utterances": [{"speaker": "user", "text": "Oh no, what do I do?"}, {"speaker": "remote", "text": "Read me the verification code we just sent, right now, and do not tell anyone at the branch."}]}'200 and the updated score, shortened here:
{
"id": "5fa8406c-0601-4eba-aa70-797917434f29",
"status": "active",
"risk_score": 91,
"trust_score": 9,
"peak_score": 91,
"band": "critical",
"recommended_action": "end_call",
"cues": [
{ "cue": "authority_impersonation", "severe": false, "count": 1, "speakers": ["remote"], "evidence": "Hi, this is the fraud department at Chase…", "source": "detector" },
{ "cue": "account_compromise_claim", "severe": false, "count": 1, "speakers": ["remote"], "evidence": "Hi, this is the fraud department at Chase…", "source": "detector" },
{ "cue": "requests_otp", "severe": true, "count": 1, "speakers": ["remote"], "evidence": "Read me the verification code we just sent…", "source": "detector" },
{ "cue": "urgency_pressure", "severe": false, "count": 1, "speakers": ["remote"], "evidence": "Read me the verification code we just sent…", "source": "detector" },
{ "cue": "secrecy_request", "severe": false, "count": 1, "speakers": ["remote"], "evidence": "Read me the verification code we just sent…", "source": "detector" }
],
"components": { "model_score": null, "cue_score": 91, "classification_adjustment": 0, "adjustments": [] },
"words": 39,
"utterances": 3,
"billed_minutes": 1
}The score in this response comes from the instant check for clear scam signs. Act on recommended_action: at warn_user or end_call, show the person on the line the advice.
Read the score
Send just the id, or use GET /v1/fraud/:id. Add "include": "history" for each change of score, or "transcript" for everything said.
curl https://api.spoofey.com/v1/fraud \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{"id": "5fa8406c-0601-4eba-aa70-797917434f29", "include": "history"}'Attach what you know about the number
Telling us what the caller's number is makes the score sharper: for example, a caller who claims to be a bank from a number that isn't tied to anyone. Do it when you start, or at any time during the call. The score updates at once.
curl https://api.spoofey.com/v1/fraud \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{"id": "5fa8406c-0601-4eba-aa70-797917434f29", "classify": true}'classify: true checks the conversation's number now, at the number check price. If you already ran a number check, pass its id as classification instead and pay nothing more. How a classification moves the score is in the guide, under Using number classification.
End it
When the call ends, end the conversation. It stops billing, runs one last read of anything not yet assessed, and returns the final score with its score_history. Send the last lines with it if you have them. Ending a conversation that has already ended just returns it.
curl https://api.spoofey.com/v1/fraud \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{"id": "5fa8406c-0601-4eba-aa70-797917434f29", "end": true}'200, shortened here:
{
"id": "5fa8406c-0601-4eba-aa70-797917434f29",
"status": "ended",
"end_reason": "ended_by_client",
"risk_score": 95,
"peak_score": 100,
"band": "critical",
"scam_type": "bank_impersonation",
"summary": "Caller claiming to be Chase's fraud team asked for the code just texted and said not to tell the branch.",
"billed_minutes": 3,
"score_history": [
{ "at": 1790812800000, "score": 45, "ai_score": null, "cue_score": 45, "adjustment": 0, "trigger": "cues" },
{ "at": 1790812805000, "score": 91, "ai_score": null, "cue_score": 91, "adjustment": 0, "trigger": "cues" },
{ "at": 1790812809000, "score": 100, "ai_score": 92, "cue_score": 91, "adjustment": 20, "trigger": "ai" },
{ "at": 1790812950000, "score": 95, "ai_score": 92, "cue_score": 91, "adjustment": 0, "trigger": "final" }
],
"ended_at": 1790812950000
}A conversation nobody sends anything to for 15 minutes ends on its own (end_reason: "idle"), and so does one that reaches 4 hours (max_duration).
Billing
Conversations are billed per minute from your credit balance, one minute ahead, at the text price shown at the top of this page. Starting one is free; billing starts with the first line. Audio streamed into a conversation is billed at the audio price. classify: true is charged at the number check price. The guide's Billing section explains how a minute is counted.
If your balance runs out mid-call, scoring pauses and requests return 402 insufficient_credits. Top up and send again, and it carries on where it stopped.
Errors
Errors come back as {"error":{"message":"...","code":"..."}}. See API errors and rate limits.
| Status | Code | When |
|---|---|---|
400 |
missing_input |
There's nothing to do: no number, text, utterances, start or id. |
400 |
missing_id |
end: true without the conversation's id. |
400 |
invalid_utterance |
A line has no text, or utterances isn't a list. |
400 |
missing_utterances |
utterances is an empty list. |
400 |
too_many_utterances |
More than 50 lines in one request. |
400 |
invalid_speaker |
speaker isn't remote, user or unknown. |
400 |
invalid_number |
A phone number isn't digits with a country code. |
400 |
invalid_classification |
classification isn't one of the accepted forms, or is an id that isn't yours. |
400 |
missing_remote_number |
classify: true, and there's no number to check. |
400 |
invalid_metadata |
metadata isn't an object, or is too long. |
402 |
insufficient_credits |
Your balance doesn't cover the next minute (or the number check). Scoring pauses; top up and send again. |
402 |
max_duration |
The conversation reached 4 hours and has ended. |
404 |
not_found |
No conversation of yours has that id. |
409 |
conversation_ended |
The conversation has ended. Start a new one. |
429 |
too_many_conversations |
You already have 50 live conversations. End one first. |
429 |
rate_limited |
Over 1,200 requests a minute. Batch lines with utterances, or stream the audio. |
503 |
lookup_unavailable |
classify: true and the number check couldn't run. You're not charged for it. |
If the conversation was started but its first lines were refused (for example by 402), the error's message ends with the conversation's id, so you can send them again with that id.