Skip to main content

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​

  • scores and bands: one entry per label of the model, with the slot's band bindings applied.
  • explanations: per label, the top_k largest 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_k defaults 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 a digest that 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 (null for 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_events and activation_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​

StatusReasonWhen
404 not_foundthe slot name is not in your project and environment
404 not_foundentity_unavailablethe entity was erased, or a suppression set applying to the slot's purpose names it; both answer identically
403 permission_deniedpurpose_not_allowedyour key's purposes exclude the slot's declared purpose
404 not_foundraw_source_event.source_id is not a source of your project and environment
400 failed_preconditionsource_not_activethe source is paused or disabled
400 failed_preconditionsource_not_preset_boundthe source is not bound to a raw-source preset (including manual sources and sources with a mapping)
400 failed_preconditionpreset_binding_unverifiedthe source's binding no longer verifies against its preset release; bind the source again
400 invalid_argumentthe preset's reason (for example unsupported_source_shape)the source's preset does not map the record
400 invalid_argumentrecord_too_large, record_too_deep, record_too_many_paths, attribute_key_collision, non_finite_numberthe record exceeds intake's limits
400 invalid_argumentempty_recordthe record is not a nonempty JSON object
400 invalid_argumentrecord_not_flattenablea member's path cannot form an attribute name, so intake could not flatten the record
503 unavailablepreset_release_unavailablethe source's preset release is not available on this server yet (during a deploy); retry
400 failed_preconditionevent_type_not_scoredthe model was not trained on the event type, or the approved contract excludes it
400 failed_preconditionproject_ingest_pausedthe project's ingest is paused, so the event would not be ingested
429 resource_exhaustedsync_scoring_saturatedthis replica's in-flight bound is reached; retry after Retry-After
503 unavailablescorer_timeoutthe request deadline passed; nothing was metered
503 unavailablescorer_unavailableevent models: the model is loading or does not fit this replica's scoring memory yet; sequence models: no ready scorer replica answered; retry
503 unavailableerasure_unavailablethe erasure state could not be read, so nothing is answered; retry
503 unavailablebands_unavailablethe slot's band bindings could not be read; retry
400 failed_preconditionbanding_unavailablethe slot's band bindings cannot be served yet
400 failed_preconditionmodel_family_not_supportedthe slot serves an entity-baseline model, which synchronous scoring does not support
503 unavailablebundle_unverifieda pinned model artifact failed its fingerprint check; the slot answers nothing until it is fixed
400 failed_preconditionslot_not_realtimea sequence slot that is not scored in realtime
400 failed_preconditionevent_not_after_state_cursorsequence 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_preconditionevent_conflicts_with_ledgersequence models: a different event is already ingested under this event's key
400 failed_preconditionevent_outside_windowsequence models: newer events push this event out of the model's window
400 failed_preconditionsemantic_slot_unsupportedsequence models: the slot's model uses semantic inputs, which synchronous scoring does not serve yet
503 unavailablescorer_activation_pendingsequence 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)fp32int8
641.38x the CPU per entity1.64x the CPU per entity
2561.06x1.09x
10240.89x0.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

PhaseAnswered 200p50p95p99max
300 calls, one client300 of 30032.3 ms38.2 ms45.9 ms51.7 ms
4 concurrent clients, 60 s3,39236.6 ms49.7 ms67.5 ms1,526 ms
8 concurrent clients, 60 s2,91240.9 ms75.9 ms142.9 ms1,922 ms
top_k 0, 5 and 20, 100 calls each141about 32 ms each38 to 49 ms54 to 92 ms92 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_exhausted with details[0].type: rate_limit and retry_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_seconds on a 429.
  • The few answers above 1.5 s, and three 503 scorer_timeout answers 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.
PhaseAnswered 200p50p95p99max
300 calls, one client300 of 30075.0 ms88.9 ms133.9 ms414 ms
4 concurrent clients, 60 s2,127106.7 ms135.1 ms196.2 ms1,127 ms
8 concurrent clients, 60 s2,153108.3 ms151.5 ms248.8 ms1,083 ms
50 new entities, one client50 of 5074.0 ms89.2 ms98.1 ms98.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_exhausted with details[0].reason: sync_scoring_saturated and retry_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.