Pre-flight
Score a proposed transfer before origination.
The same features, scorer and bands as a landed transfer, answered inside the bank's decision window and recorded so it can be graded against the outcome once the transfer lands and is matched back (same program, counterparty and amount within 24 hours). degraded is true when a source the answer leans on is missing or stale, so a thin answer is never mistaken for a confident one.
Preflight
| Field | Type | Notes |
|---|---|---|
| preflightId | string (uuid) | |
| decision | "clear" | "review" | "hold" | hold = hold band, hard signal or hard limit hit; review = review band, unscored or any soft limit hit; clear otherwise. |
| skoor | integer | null | |
| band | "clear" | "review" | "hold" | "unscored" | |
| version | string | |
| n | integer | |
| confidence | number | null | |
| reasons | Reason[] | |
| limitsHit | LimitHit[] | |
| degraded | boolean | True when a feature source the answer leans on is missing or stale. |
| degradedReasons | string[] | Why it is degraded (empty on a replayed answer). |
| latencyMs | integer | |
| idempotent | boolean | True when the answer was replayed from the idempotency key. |
| programId | string (uuid) |
PreflightRequest
| Field | Type | Notes |
|---|---|---|
| programId (optional) | string (uuid) | One of programId or programSlug is required. |
| programSlug (optional) | string | |
| idempotencyKey (optional) | string | Same key on the same tenant returns the same answer (200 instead of 201). |
| transfer | object | |
| transfer.rail | "ach" | "wire" | "realtime" | "book" | "intl_wire" | "check" | "card" | |
| transfer.direction | "outgoing" | "incoming" | |
| transfer.type (optional) | "CREDIT" | "DEBIT" | null | |
| transfer.amountCents | string | Integer cents as a string. |
| transfer.currency (optional) | string | Default USD. |
| transfer.entityId (optional) | string | |
| transfer.counterpartyId (optional) | string | |
| transfer.counterparty (optional) | object | Describe a counterparty not on file; matched by routing + last4, else by name, else scored as a first-time stranger. |
| transfer.counterparty.name | string | |
| transfer.counterparty.routingNumber (optional) | string | |
| transfer.counterparty.accountLast4 (optional) | string | |
| transfer.counterparty.countryCode (optional) | string | |
| transfer.effectiveOn (optional) | string | |
| transfer.description (optional) | string |
PreflightDetail
| Field | Type | Notes |
|---|---|---|
| preflightId | string (uuid) | |
| decision | "clear" | "review" | "hold" | |
| skoor | integer | null | |
| band | "clear" | "review" | "hold" | "unscored" | |
| version | string | |
| n | integer | |
| confidence | number | null | |
| reasons | Reason[] | |
| limitsHit | LimitHit[] | |
| degraded | boolean | |
| degradedReasons | string[] | |
| latencyMs | integer | |
| idempotent | boolean | |
| programId | string (uuid) | |
| request | object | The request as submitted plus operator and resolved {entityId, counterpartyId, counterpartyCountry}. |
| matchedTransferId | string | null | Set when the transfer later landed and was matched back (same program, counterparty, amount, within 24h). |
| createdAt | string (date-time) |
PreflightStats
| Field | Type | Notes |
|---|---|---|
| windowDays | integer | |
| count | integer | |
| byDecision | object | |
| byDecision.clear | integer | |
| byDecision.review | integer | |
| byDecision.hold | integer | |
| latencyMs | object | |
| latencyMs.p50 | number | null | |
| latencyMs.p90 | number | null | |
| latencyMs.n | integer | |
| matched | object | Share of pre-flights matched to a landed transfer. |
| matched.value | number | null | null when nothing was measured |
| matched.n | integer | observations behind the value |
| degraded | object | Share of degraded answers. |
| degraded.value | number | null | null when nothing was measured |
| degraded.n | integer | observations behind the value |
| graded | object | Matched pre-flights whose transfer later received a bad/ok label. |
| graded.n | integer | |
| graded.holdAndBad | integer | |
| graded.holdAndOk | integer | |
| graded.notHoldAndBad | integer | |
| graded.notHoldAndOk | integer | |
| graded.holdPrecision | number | null | |
| graded.holdRecall | number | null |
LimitHit
| Field | Type | Notes |
|---|---|---|
| limit | string | <actor>.<kind>: entity|counterparty × per_transaction|daily|monthly. |
| actor | "counterparty" | "entity" | |
| actorId | string | |
| band | "clear" | "review" | "hold" | "unscored" | |
| cap | string | The cap at that band. Integer cents as a string (bigint-safe). |
| observed | string | Already moved in the window (0 for per_transaction). Integer cents as a string (bigint-safe). |
| proposed | string | This amount. Integer cents as a string (bigint-safe). |
| severity | "hard" | "soft" |
Endpoints
post/api/preflight
Score a proposed transfer
Answers clear / review / hold for a transfer that has not been originated yet, and records the answer. 201 on a new answer; 200 when idempotencyKey replays a stored one (then idempotent: true and degradedReasons is empty). Body limit 64 KB.
| Auth | Bearer + X-Operator |
|---|---|
| Operation | Pre-flight · POST /api/preflight |
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| tenant (optional) | query | string | Tenant slug. Optional: the bearer token already proves the tenant. Pass it to be explicit. |
| Idempotency-Key (optional) | header | string | 1–255 characters. A replay with the same body returns the stored status and body with Idempotent-Replayed: true; a different body under the same key is 409. |
Request body (application/json, required) · PreflightRequest
| Field | Type | Notes |
|---|---|---|
| programId (optional) | string (uuid) | One of programId or programSlug is required. |
| programSlug (optional) | string | |
| idempotencyKey (optional) | string | Same key on the same tenant returns the same answer (200 instead of 201). |
| transfer | object | |
| transfer.rail | "ach" | "wire" | "realtime" | "book" | "intl_wire" | "check" | "card" | |
| transfer.direction | "outgoing" | "incoming" | |
| transfer.type (optional) | "CREDIT" | "DEBIT" | null | |
| transfer.amountCents | string | Integer cents as a string. |
| transfer.currency (optional) | string | Default USD. |
| transfer.entityId (optional) | string | |
| transfer.counterpartyId (optional) | string | |
| transfer.counterparty (optional) | object | Describe a counterparty not on file; matched by routing + last4, else by name, else scored as a first-time stranger. |
| transfer.counterparty.name | string | |
| transfer.counterparty.routingNumber (optional) | string | |
| transfer.counterparty.accountLast4 (optional) | string | |
| transfer.counterparty.countryCode (optional) | string | |
| transfer.effectiveOn (optional) | string | |
| transfer.description (optional) | string |
Responses
200 — Replayed from the idempotency key. Preflight
| Field | Type | Notes |
|---|---|---|
| preflightId | string (uuid) | |
| decision | "clear" | "review" | "hold" | hold = hold band, hard signal or hard limit hit; review = review band, unscored or any soft limit hit; clear otherwise. |
| skoor | integer | null | |
| band | "clear" | "review" | "hold" | "unscored" | |
| version | string | |
| n | integer | |
| confidence | number | null | |
| reasons | Reason[] | |
| limitsHit | LimitHit[] | |
| degraded | boolean | True when a feature source the answer leans on is missing or stale. |
| degradedReasons | string[] | Why it is degraded (empty on a replayed answer). |
| latencyMs | integer | |
| idempotent | boolean | True when the answer was replayed from the idempotency key. |
| programId | string (uuid) |
201 — Scored and recorded. Preflight
| Field | Type | Notes |
|---|---|---|
| preflightId | string (uuid) | |
| decision | "clear" | "review" | "hold" | hold = hold band, hard signal or hard limit hit; review = review band, unscored or any soft limit hit; clear otherwise. |
| skoor | integer | null | |
| band | "clear" | "review" | "hold" | "unscored" | |
| version | string | |
| n | integer | |
| confidence | number | null | |
| reasons | Reason[] | |
| limitsHit | LimitHit[] | |
| degraded | boolean | True when a feature source the answer leans on is missing or stale. |
| degradedReasons | string[] | Why it is degraded (empty on a replayed answer). |
| latencyMs | integer | |
| idempotent | boolean | True when the answer was replayed from the idempotency key. |
| programId | string (uuid) |
400 — Validation failed; error lists the paths. Error
| Field | Type | Notes |
|---|---|---|
| error | string | What went wrong, in one sentence. |
| requestId (optional) | string | Present once the gateway is live: the X-Request-Id of the failed request. |
401 — Bearer token missing or unknown. Error
| Field | Type | Notes |
|---|---|---|
| error | string | What went wrong, in one sentence. |
| requestId (optional) | string | Present once the gateway is live: the X-Request-Id of the failed request. |
404 — Unknown program on this tenant. Error
| Field | Type | Notes |
|---|---|---|
| error | string | What went wrong, in one sentence. |
| requestId (optional) | string | Present once the gateway is live: the X-Request-Id of the failed request. |
Example
curl -s -X POST "https://skoor.ai/risk/api/preflight?tenant=demo" \
-H "Authorization: Bearer test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "X-Operator: you@bank.example" \
-H "Content-Type: application/json" \
-d '{"programSlug":"northwind-payroll","idempotencyKey":"pf-2026-09-17-0001","transfer":{"rail":"ach","direction":"outgoing","type":"CREDIT","amountCents":"185000","currency":"USD","entityId":"enti_sim_acme","counterparty":{"name":"Blue Ridge Landscaping","routingNumber":"021000021","accountLast4":"4421","countryCode":"US"},"description":"Payroll 2026-09-19"}}'
Response
{
"preflightId": "5f3c1c8e-2a44-4c0b-9d0f-2b0a2f6e7c11",
"decision": "clear",
"skoor": 12,
"band": "clear",
"version": "trs-v1",
"n": 210,
"confidence": 0.81,
"reasons": [
{
"id": "TRS-CP-01",
"signal": "counterparty.first_time",
"weight": 8,
"hard": false,
"sentence": "This is the first transfer with this counterparty."
}
],
"limitsHit": [],
"degraded": false,
"degradedReasons": [],
"latencyMs": 41,
"idempotent": false,
"programId": "a1e0c2d4-6b7f-4d8e-9a3b-0c1d2e3f4a55"
}
get/api/preflight/stats
Pre-flight statistics for a window
| Auth | Bearer + X-Operator |
|---|---|
| Operation | Pre-flight · GET /api/preflight/stats |
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| tenant (optional) | query | string | Tenant slug. Optional: the bearer token already proves the tenant. Pass it to be explicit. |
| days (optional) | query | integer | Window in days, 1–365 (default 30). |
Responses
200 — Counts by decision, latency percentiles, matched and degraded shares, and the graded confusion counts. PreflightStats
| Field | Type | Notes |
|---|---|---|
| windowDays | integer | |
| count | integer | |
| byDecision | object | |
| byDecision.clear | integer | |
| byDecision.review | integer | |
| byDecision.hold | integer | |
| latencyMs | object | |
| latencyMs.p50 | number | null | |
| latencyMs.p90 | number | null | |
| latencyMs.n | integer | |
| matched | object | Share of pre-flights matched to a landed transfer. |
| matched.value | number | null | null when nothing was measured |
| matched.n | integer | observations behind the value |
| degraded | object | Share of degraded answers. |
| degraded.value | number | null | null when nothing was measured |
| degraded.n | integer | observations behind the value |
| graded | object | Matched pre-flights whose transfer later received a bad/ok label. |
| graded.n | integer | |
| graded.holdAndBad | integer | |
| graded.holdAndOk | integer | |
| graded.notHoldAndBad | integer | |
| graded.notHoldAndOk | integer | |
| graded.holdPrecision | number | null | |
| graded.holdRecall | number | null |
401 — Bearer token missing or unknown. Error
| Field | Type | Notes |
|---|---|---|
| error | string | What went wrong, in one sentence. |
| requestId (optional) | string | Present once the gateway is live: the X-Request-Id of the failed request. |
Example
curl -s "https://skoor.ai/risk/api/preflight/stats?tenant=demo&days=30" \ -H "Authorization: Bearer test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \ -H "X-Operator: you@bank.example"
get/api/preflight/{id}
One pre-flight answer with its request
| Auth | Bearer + X-Operator |
|---|---|
| Operation | Pre-flight · GET /api/preflight/{id} |
Parameters
| Name | In | Type | Notes |
|---|---|---|---|
| id | path | string (uuid) | Pre-flight id. |
| tenant (optional) | query | string | Tenant slug. Optional: the bearer token already proves the tenant. Pass it to be explicit. |
Responses
200 — The stored answer, the request as submitted, and the matched transfer when one landed. PreflightDetail
| Field | Type | Notes |
|---|---|---|
| preflightId | string (uuid) | |
| decision | "clear" | "review" | "hold" | |
| skoor | integer | null | |
| band | "clear" | "review" | "hold" | "unscored" | |
| version | string | |
| n | integer | |
| confidence | number | null | |
| reasons | Reason[] | |
| limitsHit | LimitHit[] | |
| degraded | boolean | |
| degradedReasons | string[] | |
| latencyMs | integer | |
| idempotent | boolean | |
| programId | string (uuid) | |
| request | object | The request as submitted plus operator and resolved {entityId, counterpartyId, counterpartyCountry}. |
| matchedTransferId | string | null | Set when the transfer later landed and was matched back (same program, counterparty, amount, within 24h). |
| createdAt | string (date-time) |
401 — Bearer token missing or unknown. Error
| Field | Type | Notes |
|---|---|---|
| error | string | What went wrong, in one sentence. |
| requestId (optional) | string | Present once the gateway is live: the X-Request-Id of the failed request. |
404 — Unknown pre-flight on this tenant. Error
| Field | Type | Notes |
|---|---|---|
| error | string | What went wrong, in one sentence. |
| requestId (optional) | string | Present once the gateway is live: the X-Request-Id of the failed request. |
Example
curl -s "https://skoor.ai/risk/api/preflight/5f3c1c8e-2a44-4c0b-9d0f-2b0a2f6e7c11?tenant=demo" \ -H "Authorization: Bearer test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \ -H "X-Operator: you@bank.example"