Events

Tell us what happened. A call starting is scored on the spot; a call ending settles its record; the turns in between feed the live monitor. All on one endpoint, keyed on your call id.

One endpoint, six types

Every event is a JSON object with a type and a call_id. The call_id is yours (the carrier's SID, the platform's id, anything stable) and it is what ties the events of one call together. Post the same start twice and you get the first answer back with duplicate: true; nothing is counted twice.

typeWhenAnswer
call.startedThe moment the call arrives, before your agent speaks.A score, an action, reasons.
call.endedWhen it ends, from either side.The duration and the call's settled spam score.
turn.startedSomebody begins speaking.The live picture so far.
turn.endedThey stop. Carries how much they said.The live picture so far.
interruptionThe caller cut the agent off.The live picture so far.
transcriptFinal text for a turn, if it arrives separately.The live picture so far.
Order matters only loosely. A call.ended for a call we never saw start is a 404; turns for one are too. Everything else can arrive out of order and late, and is placed by its at.

A call starts

POST/v1/eventsfull key or ingest token

The call is recorded under your call_id, the protected number is created if this is the first time we have seen it, and the call is scored against everything we know. The answer is the same shape as a score, with the call now on record.

Request body

type"call.started"required
call_idstringrequired
Your id for the call. Stable, unique within your account.
tostringrequired
The number that was dialled, E.164. On an inbound call, your protected number.
fromstringrequired
The number that dialled, E.164. On an inbound call, the caller.
direction"inbound" | "outbound"default "inbound"
On an outbound call the roles flip: from is your number and to is who you dialled.
atstringdefault now
When it started, ISO 8601. Set it when you are reporting late.
registerbooleandefault true
Add to as a protected number if it is new. false refuses unknown numbers with 409 not_protected instead, which is right when the list of numbers is managed elsewhere.
quietbooleandefault false
History, not news: score and record the call, but raise no alerts and place no blocks. For importing what happened before you connected.
sourcestringdefault "api"
Where the call came from, for your own filtering. Free text.
costnumber | null
What the call cost in USD, if you know it now. Usually you know it at the end.
cost_breakdownobject | null
The cost by part: llm, stt, tts, carrier, platform, in USD.
attrsobject
What the carrier said about the caller at ring time. See attrs.
provider_call_idstring | null
The carrier's id beneath a platform call, when a platform and its carrier both report the same call. The two are folded into one record.

Response

call_idstring
As you sent it.
scorenumber
0 to 1.
action"allow" | "review" | "reject"
Where we would draw the line.
thresholdsobject
Where action changes: review and reject. Draw your own line on the score with these beside it.
reasonsstring[]
Why.
blockobject | null
The active block on this caller, if any.
checksobject
The counts behind the score. Same shape as on a score.
priorobject
What this number scored before, on your account and on the network: points, calls, good, and last, the score and age in days of its most recent call, or null.
duplicatetrue
Present when this call_id had already started. The score, action and reasons are the ones decided the first time.
cappedtrue
Present when your plan's daily scoring allowance is spent. The call is on record, allowed, and not scored; cap carries the allowance.

Errors

missing_call_id400
No call_id.
invalid_to / invalid_from400
A number that could not be parsed. On an outbound call the names follow the roles, so a bad caller is invalid_to.
invalid_direction400
Something other than inbound or outbound.
not_protected409
With register: false, a number you have not added.
number_limit402
A new number past your plan's cap. The cap is in the body.

A call ends

POST/v1/eventsfull key or ingest token

Closes the record, settles the call's spam score from its full length and everything the turns showed, and folds in whatever you learned at the end, usually the cost. A second end for the same call is a 200 with duplicate: true and no change.

Request body

type"call.ended"required
call_idstringrequired
The id the call started with. The carrier's id is accepted too, when one was given as provider_call_id.
atstringdefault now
When it ended, ISO 8601.
reasonstringdefault "ended"
Why, in your words or the carrier's: completed, busy, no-answer, hangup. The gate writes gate_fail or gate_timeout.
costnumber | null
What the call cost in USD. When two sources report a cost, the larger is kept: a platform's total includes its carrier's.
cost_breakdownobject | null
The cost by part. A platform-side breakdown replaces what was there so the parts add up to its total; a carrier's price only fills a gap.
attrsobject
Anything the carrier learned by the end: machine detection, ring time. Merged into what was already there.

Response

call_idstring
The id the call is stored under.
duration_sinteger
Start to end, in seconds.
spam_scoreinteger | null
0 to 100, the call's settled score. See the two scores. null when there was nothing to score.

Errors

missing_call_id400
No call_id.
unknown_call404
We never saw this call start.

Turns, while the call runs

POST/v1/eventsfull key or ingest token

The live monitor. Four event types describe the shape of a conversation as it happens (who is speaking, for how long, how much they said, whether they were cut off) and each answers with the call's live picture: the flags it has tripped and a score. We keep lengths, counts and a hash. We never keep the words.

Part of Guard. On Watch these events are accepted and dropped unread, with ignored: true.

Request body

type"turn.started" | "turn.ended" | "interruption" | "transcript"required
call_idstringrequired
The call this belongs to.
role"user" | "agent"required
Who spoke. Not needed on interruption, which is always the caller cutting the agent off.
atstringdefault now
When, ISO 8601.
turninteger
Your own index for the turn, if you keep one.
textstring
On turn.ended and transcript: what was said. It is measured (characters, words, a short hash for spotting repeats) and dropped before anything is written.
chars / wordsinteger
Instead of text: send the counts yourself and no words ever leave your system.
interruptedboolean
On turn.ended: this turn was cut off.

Response

flagsstring[]
What the call has tripped so far. See the table below.
scorenumber
0 to 1, the live score. It only rises; a new flag at alert level raises a call.suspicious alert once.
spam_scoreinteger | null
0 to 100, recomputed on every turn.
user_turns / agent_turnsinteger
Counts so far.
interruptionsinteger
Times the caller cut the agent off.
ignoredtrue
Present when the event was dropped: why is live_monitor_is_guard on Watch, or call_turn_cap once one call has written 3,000 turns.

Errors

invalid_role400
Something other than user or agent.
unknown_call404
We never saw this call start.

Live flags

FlagWhat tripped it
recording_likelyOne caller turn far longer than a person speaks without a pause.
monologueThe caller's turns, averaged, are far longer than conversation.
barge_stormThe caller keeps cutting the agent off, well past a lively human's rate.
rapid_fireMany caller turns in a short window.
repeated_turnsThe same caller turn, several times over.
silent_listenerA minute in and the caller has said nothing.
one_sidedThe caller has said a great deal and the agent almost nothing.

A flag also appears on the call's reasons as live_<flag>, so the log tells the whole story afterwards.

With an ingest token

The same endpoint, with the credential in the path instead of a header. For a pipeline that cannot hold a key, or a platform that will only post to a bare URL. Issue the token under Settings → Live events, Your own server; it can post events and do nothing else.

RequestShell
curl -X POST https://api.murusai.com/v1/events/lv_4Qx9pLm2aXr8sT \
  -H "Content-Type: application/json" \
  -d '{ "type": "call.ended", "call_id": "CA7f3e2a91c04b" }'

attrs

What the carrier knew about the other party when the call rang. All optional; what you send is kept on the call and some of it feeds the score. Keys we do not know are kept and ignored.

geoobject
city, region, zip, country: where the number is registered, per the carrier's prefix database. The zip is what lets us notice many fresh callers from one place.
attestation"A" | "B" | "C" | "failed" | "none"
STIR/SHAKEN as the terminating carrier reported it. A means the originating carrier vouches the caller may use that number; failed means a signature arrived and did not verify; none means nothing was signed.
caller_namestring
CNAM, when your number has it switched on.
answered_bystring
Machine detection, as the carrier names it: human, machine_start, unknown.
carrierstring
Which carrier delivered it.
forwarded_fromstring
The number that forwarded the call here, if one did.
perfobject
Your platform's timings, averages in ms (turn_ms, model_ms, voice_ms, stt_ms), and interrupted, its own count of the caller cutting in.
tokensobject
in, out, cached. The number that runs away when a caller keeps the agent talking.
If you are on Twilio, all of this is in the parameters of the voice webhook: FromCity, FromZip, StirVerstat, CallerName. The gate reads them for you.