Synchronous scoring (one event, answered now)
POST /slots/{slot}/scores:compute scores one event against the slot's
active model and answers in the same request. Nothing is stored: the event
is not ingested, no entity state is written, and the ledger is untouched.
Send the event through ingest as well when it should count toward the
entity's history.
The slot's active bundle decides how the event is scored, and the answer's
model_family says which:
- Event models (WP-M25) score the event's own fields, with explanations.
- Sequence models score the entity's recent history with the event included, exactly as the realtime scorer will once the event is ingested (see Sequence models). The slot must be scored in realtime.
A raw source record can be sent instead of an event (see below).
{
"event": {
"event_id": "pay-2026-10-01-000017",
"entity": {"type": "account", "id": "acct-sample-1"},
"event_type": "payment.authorized",
"timestamp": "2026-10-01T03:12:45.000017Z",
"attributes": {"amount": 1249, "channel": "card_not_present", "currency": "EUR"}
},
"explanation": {"top_k": 3}
}
The event is validated exactly as ingest validates it (at most 64 KiB, and
event_id is required here). The slot is addressed by its name, never by
its id, and the request runs with your API key's project and environment.
Scoring a raw source record
If you send events through a raw source bound to a raw-source preset (for
example mcc-canonical-event), send the record in your own shape instead of
an event:
{
"raw_source_event": {
"source_id": "src_01j9zr2b4d6f8h0k2m4p6r8t0v",
"record": {
"schema_version": "1.0",
"external_event_id": "mcc-0192d1a0-7c0e-7aaa-8bbb-999999999999",
"event_type": "payment.authorized",
"occurred_at": "2026-10-01T03:12:45.000Z",
"business_transaction_id": "SAMPLE-TXN-0099",
"amount": {"currency": "EUR", "minor_units": "1249"},
"entities": [{"entity_type": "account", "token": "acct-sample-1", "role": "primary"}],
"attributes": {}
}
}
}
The event is derived with the preset release the source is bound to now,
exactly as raw-source intake derives it. Intake maps a record when it
processes it, with the binding current at that time, so the event matches
the one intake writes when the binding has not changed in between: the same
event_id and the same content. The record is held to intake's own limits
(size, depth, number of fields, field-name collisions, finite numbers), and
the event's type must be included in the approved contract, as intake
requires. Then it is scored like any other event. inclusion.raw_source names the source, the
preset release (preset, preset_version, preset_fingerprint) and the
record's own id (external_event_id), and the digest covers them, so a new
binding gives a different digest.
What you get back
scoresandbands: one entry per label of the model, with the slot's band bindings applied.explanations: per label, thetop_klargest attributions (TreeSHAP). Each cited value is the event's own field value, so a reason always names a real field of the event you sent.top_kdefaults to the model's own default and is at most 20.inclusion: which event was scored (event_id,event_timestamp,event_cursor), by which bundle and model (bundle_id,model_id,model_fingerprint), when (computed_at), and adigestthat binds the scores and explanations to that proof.
Checking the digest
The response body is canonical JSON (sorted keys, no insignificant
whitespace). inclusion.digest is the lowercase hex SHA-256 of those exact
bytes with the one "digest":"<hex>", member removed. To check a response
you saved as response.json:
digest=$(jq -r .inclusion.digest response.json)
sed "s/\"digest\":\"$digest\",//" response.json | tr -d '\n' | sha256sum
The printed hash equals $digest. The digest is an integrity binding for
your own records, not a signature: it shows the scores were not edited after
the fact, not who produced them.
Sequence models
For a sequence slot, the slot's realtime scorer computes the answer over the
entity's newest window of ingested events plus this event, with the same
model, window and ordering the realtime scorer uses, and stores nothing. The
answer carries inclusion.sequence:
event_in_ledger: the event was already ingested;served_state_cursor: the newest event of the stored score when you asked (nullfor a new entity);scored_cursor: the newest event the answer includes, which is this event, or a newer event that is ingested but not yet scored;window_eventsandactivation_generation.
outcome is already_incorporated when the event is already ingested and the
stored score covers it: the answer is the stored score itself, nothing is
computed, and it meters one state read instead of a score. Explanations are
always empty for sequence models.
An event at or before the stored score's newest event is refused
(event_not_after_state_cursor, with served_state_cursor and a cause):
the realtime scorer would never store a score that includes it, so no
synchronous answer is given for it. Read the stored score instead.
Refusals
| Status | Reason | When |
|---|---|---|
404 not_found | the slot name is not in your project and environment | |
404 not_found | entity_unavailable | the entity was erased, or a suppression set applying to the slot's purpose names it; both answer identically |
403 permission_denied | purpose_not_allowed | your key's purposes exclude the slot's declared purpose |
404 not_found | raw_source_event.source_id is not a source of your project and environment | |
400 failed_precondition | source_not_active | the source is paused or disabled |
400 failed_precondition | source_not_preset_bound | the source is not bound to a raw-source preset (including manual sources and sources with a mapping) |
400 failed_precondition | preset_binding_unverified | the source's binding no longer verifies against its preset release; bind the source again |
400 invalid_argument | the preset's reason (for example unsupported_source_shape) | the source's preset does not map the record |
400 invalid_argument | record_too_large, record_too_deep, record_too_many_paths, attribute_key_collision, non_finite_number | the record exceeds intake's limits |
400 invalid_argument | empty_record | the record is not a nonempty JSON object |
400 invalid_argument | record_not_flattenable | a member's path cannot form an attribute name, so intake could not flatten the record |
503 unavailable | preset_release_unavailable | the source's preset release is not available on this server yet (during a deploy); retry |
400 failed_precondition | event_type_not_scored | the model was not trained on the event type, or the approved contract excludes it |
400 failed_precondition | project_ingest_paused | the project's ingest is paused, so the event would not be ingested |
429 resource_exhausted | sync_scoring_saturated | this replica's in-flight bound is reached; retry after Retry-After |
503 unavailable | scorer_timeout | the request deadline passed; nothing was metered |
503 unavailable | scorer_unavailable | event models: the model is loading or does not fit this replica's scoring memory yet; sequence models: no ready scorer replica answered; retry |
503 unavailable | erasure_unavailable | the erasure state could not be read, so nothing is answered; retry |
503 unavailable | bands_unavailable | the slot's band bindings could not be read; retry |
400 failed_precondition | banding_unavailable | the slot's band bindings cannot be served yet |
400 failed_precondition | model_family_not_supported | the slot serves an entity-baseline model, which synchronous scoring does not support |
503 unavailable | bundle_unverified | a pinned model artifact failed its fingerprint check; the slot answers nothing until it is fixed |
400 failed_precondition | slot_not_realtime | a sequence slot that is not scored in realtime |
400 failed_precondition | event_not_after_state_cursor | sequence models: the event is at or before the stored score's newest event (cause: before_cursor, or same_timestamp_order when it shares that timestamp and does not sort last) |
400 failed_precondition | event_conflicts_with_ledger | sequence models: a different event is already ingested under this event's key |
400 failed_precondition | event_outside_window | sequence models: newer events push this event out of the model's window |
400 failed_precondition | semantic_slot_unsupported | sequence models: the slot's model uses semantic inputs, which synchronous scoring does not serve yet |
503 unavailable | scorer_activation_pending | sequence models: the slot's scorer is still rolling out a new activation; retry |
An erased or suppressed entity, a refusal and a timeout are never scored and never metered.
Capacity
For sequence models, the realtime scorer runs the model on each entity's window on its own, while it still collects scoring requests in micro-batches. That keeps every realtime score independent of the other entities scored at the same time, so a synchronous score of a sequence model equals the stored realtime score bit for bit for the same scorer configuration (the same device and precision). It costs scorer CPU on short windows. Measured on a CPU tier S model with 2 threads per replica, with windows between W/2 and W events, against running a micro-batch of 16 windows together:
| Window (events) | fp32 | int8 |
|---|---|---|
| 64 | 1.38x the CPU per entity | 1.64x the CPU per entity |
| 256 | 1.06x | 1.09x |
| 1024 | 0.89x | 0.77x |
A slot with short windows can therefore need more scorer replicas for the same load: at 64-event windows on int8, a replica keeps only about 15% headroom against the realtime latency objective at its rated load. Windows shorter than 64 events and GPU (L4) tiers were not measured. A refresh of a realtime slot runs the model the same way.
When the window's newest event is a newer event that is ingested but not yet scored, the synchronous answer includes it and names it as the scored cursor. The stored realtime state matches that answer if the realtime scorer processes the newer event after this event is ingested. If it processes the newer event first, the stored state never includes this event.
Synchronous scoring latency, measured on 22 Oct 2026
These figures come from one run of the latency harness
(tools/harness/measure_sync_scoring.py, --profile spec). They are not a
service-level objective.
Setup
- Environment: our hosted deployment, Test environment, release
1869e21bff. - Model: an event-family model with 5 features, on a dedicated Test slot.
- serving-api: 6 to 8 replicas on CPU.
- Client: one API key, calling from the same cloud region.
What is measured
- Latency is client side, from sending the call to receiving the full answer body.
- Percentiles are nearest rank over the calls answered 200.
- Calls refused by the key's rate limit are counted apart and are not latency.
Event family
| Phase | Answered 200 | p50 | p95 | p99 | max |
|---|---|---|---|---|---|
| 300 calls, one client | 300 of 300 | 32.3 ms | 38.2 ms | 45.9 ms | 51.7 ms |
| 4 concurrent clients, 60 s | 3,392 | 36.6 ms | 49.7 ms | 67.5 ms | 1,526 ms |
| 8 concurrent clients, 60 s | 2,912 | 40.9 ms | 75.9 ms | 142.9 ms | 1,922 ms |
top_k 0, 5 and 20, 100 calls each | 141 | about 32 ms each | 38 to 49 ms | 54 to 92 ms | 92 ms |
Server-side time (serving-api's sync_score_duration_seconds, the same window):
- total: p50 18.5 ms, p95 40.0 ms;
- the model predict alone: p50 1.8 ms, p95 4.1 ms.
Per-key rate limit. The concurrent and top_k phases were capped by the key's plan rate limit, not by scoring:
- The limit answered 429
resource_exhaustedwithdetails[0].type: rate_limitandretry_after_seconds. - It admitted about 50 scored calls per second for one key: about 56 per second with 4 clients and about 49 per second with 8.
- To score faster than your plan's read rate, spread the load over keys or raise the plan limit. Honour
retry_after_secondson a 429. - The few answers above 1.5 s, and three 503
scorer_timeoutanswers out of about 29,900 calls, came while serving-api scaled out: a new replica loads the model on its first call.
Sequence family
Setup:
- Model: a CPU sequence model on a dedicated realtime Test slot with one scorer replica.
- Entities: each call scores a new entity, so the window holds only the event itself.
- Method: the same as for the event family.
| Phase | Answered 200 | p50 | p95 | p99 | max |
|---|---|---|---|---|---|
| 300 calls, one client | 300 of 300 | 75.0 ms | 88.9 ms | 133.9 ms | 414 ms |
| 4 concurrent clients, 60 s | 2,127 | 106.7 ms | 135.1 ms | 196.2 ms | 1,127 ms |
| 8 concurrent clients, 60 s | 2,153 | 108.3 ms | 151.5 ms | 248.8 ms | 1,083 ms |
| 50 new entities, one client | 50 of 50 | 74.0 ms | 89.2 ms | 98.1 ms | 98.1 ms |
Scorer-side time (scorer_sync_score_duration_seconds): p50 72.9 ms, p95 98.7 ms. Almost all of it is the model forward.
Capacity:
- One scorer replica scored about 35 calls per second, because the scorer runs one forward at a time.
- With 8 clients, 219 calls (9 %) answered 429
resource_exhaustedwithdetails[0].reason: sync_scoring_saturatedandretry_after_seconds: 1. That is the scorer's in-flight bound, not the key's rate limit. Add scorer replicas for more throughput. - No call hit the key's rate limit.
Not yet measured: entities with a full W-event window.
Metering
Each scored answer meters one scores_served (mode: sync). Retrying the
same slot, entity, event_id and bundle does not meter twice. For a sequence
model, a retry over the same stored score, window and activation meters once,
and an already_incorporated answer meters one state read.