Score
A number from 0 to 1 for a call, before you answer it, and where we would draw the line on it. The line is yours to move.
Score a call
A dry run. It runs the same checks a real call runs, counts the call you are asking about as if it had happened, and writes nothing down. Asking does not change the answer you get next time. How scoring works says what the number means; the short version is that 0.5 is doubt and 0.8 is refusal, and both come back with every answer.
Query parameters
- callerstringrequired
- Who is calling, E.164. On an outbound call, who you are about to dial.
- numberstringrequired
- Your own number they are calling. One of your protected numbers.
- direction"inbound" | "outbound"default "inbound"
- Pass
outboundto ask about a number you are about to dial, the pre-dial check for a web form.
Response
- scorenumber
- 0 to 1. How bad this looks. The answer.
- action"allow" | "review" | "reject"
- Where we would draw the line: what we would do at
thresholds. Take it as it is, or draw your own. - thresholdsobject
- Where
actionchanges:reviewat 0.5,rejectat 0.8. Fixed and public, so a line you draw on the score keeps its meaning. - reasonsstring[]
- Short stable labels for what drove the score. Display them; don't branch on them.
- blockobject | null
- The active block that applies, if there is one:
pattern,type,until,remaining_s,violations. - checksobject | null
- The counts behind the score, one entry per check: what was counted in the window, the threshold in force for your protection level, and whether it passed.
nullwhen the caller is on your trusted list. - levelstring
- The protection level in force for this number, as you configured it.
- dry_runtrue
- Always true here. Nothing was recorded.
Errors
- invalid_caller400
- The caller number could not be parsed as E.164.
- invalid_number400
- Your own number could not be parsed as E.164.
- invalid_direction400
- Something other than
inboundoroutbound. - unauthorized401
- Missing or unknown key.
Drawing your own line
The whole integration, in the place where you decide whether to answer. Two numbers are yours: where you refuse, and where you would rather ask a question first.
// Where we would draw them. Move either; a sales line
// can afford to be stricter than a support line.
const REFUSE_AT = 0.8;
const VERIFY_AT = 0.5;
const q = new URLSearchParams({ caller, number: yours });
const r = await fetch(`https://api.murusai.com/v1/score?${q}`, {
headers: { Authorization: `Bearer ${process.env.MURUSAI_KEY}` },
signal: AbortSignal.timeout(800),
});
// Fail open. If we are slow or down, answer the call: a missed
// customer costs more than a spam call, and the alert path is
// still behind you.
if (!r.ok) return connect();
const { score } = await r.json();
if (score >= REFUSE_AT) return hangUp();
if (score >= VERIFY_AT) return connect({ verifyFirst: true });
return connect();If you would rather not think about it, action is the same decision at the defaults: if (action === "reject") return hangUp(). The two agree exactly when your numbers are ours.
Before you dial
With direction=outbound the question turns around: you are asking about a number somebody typed into your web form, before you spend a minute dialling it. The answer has the same shape.caller is the number you are about to dial; number is the one you will dial from.
GET /v1/score?caller=%2B15125550142&number=%2B14155550100&direction=outboundThen tell us what happened
A score on its own teaches us nothing. The calls you actually connect should be reported to events: that is what builds the history behind bad_history, and what puts a caller on the network for everybody else. A call.started answers with this same shape, so if you report every call you need not ask separately at all.
Its first name
This endpoint began life as GET /v1/verdict, and still answers there, identically. Nothing built against that name needs to change; new code should use /v1/score.