KYTRIX Developer documentation Financial Crime Control Plane

Generated from the contract

API reference

Every endpoint on the KYTRIX public API host, with request and response shapes read directly out of the zod schemas the server validates against.

Generated from @kytrix/contracts · payload schema_version 2026-09-01 · 31 operations · 19 schemas

#How this page is generated

Every field, type and constraint below is read out of the zod schemas in @kytrix/contracts at build time — the same schema objects the edge calls safeParse with on your request. Nothing on this page is transcribed by hand, so it cannot drift from what the server enforces.

Endpoint paths come from a table that names the source file registering each route; the build asserts the path literal is still in that file and fails otherwise. Four response shapes are mirrored from their route handler rather than generated, because @kytrix/contracts does not define them yet; each one says so where it appears.

The same document is published as OpenAPI 3.1 for client generation.

#Conventions

RuleDetail
JSON field namesAlways snake_case, request and response alike.
Schema versionEvery event envelope carries schema_version: "2026-09-01". Any other value is kytrix:validation/unsupported_schema_version.
Unknown membersEnvelopes are strict. An unrecognised key is a rejection, never a silently dropped field.
TimestampsRFC 3339 with an explicit offset or Z, colon required in the offset (-04:00, not -0400). Lowercase t/z are refused.
MoneyAlways { value, asset, scale } where value is a string of signed integer minor units. A JSON number is rejected.
ErrorsRFC 9457 application/problem+json with a code from the governed catalog. Branch on code, never on detail.
PaginationOpaque cursors. Pass next_cursor back verbatim; never construct or parse one.
IdsKYTRIX-issued ids are prefixed (evt_, alrt_, epsd_, whk_, imp_). Your own ids are external_id and stay yours.

#Ingestion

The six event families, the batch endpoint and the dry-run. Every write here is behind the single-transaction acknowledgment of invariant I6.

POST/v1/partiesAPI key, scope ingest
A customer, business, agent or institution you have onboarded. The body is one envelope validated by EVENT_SCHEMAS.party; unknown members are rejected rather than dropped. The 202 is returned only after one Postgres transaction has committed both the raw event and its processing obligation (invariant I6) — there is no acknowledged-but-lost state.

Defined in 05 G.1 · 05 G.3 · 06 H.1

Request headers

HeaderRequiredMeaning
Kytrix-Key-IdyesYour API key id (kx_test_…/kx_live_…).
Kytrix-TimestampyesUnix seconds, decimal. ±300 s of KYTRIX time.
Kytrix-Nonceyes1–64 chars of A-Za-z0-9._~-, single-use for 600 s per key.
Kytrix-Signatureyesv1=<lower-hex HMAC-SHA256> over the canonical string.
Idempotency-Keyno1–255 characters, scoped to (tenant, endpoint, key) and retained ≥ 24 h. Send one per logical event so a retry replays instead of double-posting.
Kytrix-Correlation-IdnoYour trace id; recorded on the inbox row and every KYTRIX log line for it.

Request body

Schema: PartyEvent — the full expansion is below.

  • external_id string required 1–256 chars
  • schema_version "2026-09-01" required
  • occurred_at string (date-time) required
  • recorded_at string (date-time) optional
  • sequence integer optional ≥ 0
  • data object required
    • type "person" | "business" | "merchant" | "agent" | "financial_institution" | "internal" | "government" | "ngo" | "trust" | "other" required
    • status string required 1–64 chars
    • created_at string (date-time) required
    • legal_names string[] optional max 20 items
    • birth_or_inc_date string (date) optional
    • identifiers object[] optional max 50 items
      • array of
        • type "national_id" | "tax_id" | "passport" | "phone" | "email" | "other" required
        • value string required 1–256 chars
        • verification_status "verified" | "unverified" | "pending" | "failed" optional
        • No other members are accepted — an unrecognised key is a reject, not a dropped field.
    • jurisdictions object optional
      • nationality string optional pattern ^[A-Z]{2}$
      • residency string optional pattern ^[A-Z]{2}$
      • registration string optional pattern ^[A-Z]{2}$
      • No other members are accepted — an unrecognised key is a reject, not a dropped field.
    • addresses object[] optional max 20 items
      • array of
        • line1 string optional max 256 chars
        • line2 string optional max 256 chars
        • city string optional max 128 chars
        • region string optional max 128 chars
        • postal_code string optional max 32 chars
        • country string optional pattern ^[A-Z]{2}$
        • No other members are accepted — an unrecognised key is a reject, not a dropped field.
    • occupation string optional max 256 chars
    • industry_code string optional max 32 chars
    • expected_profile object optional
      • declared_monthly_volume Money optional
      • corridors string[] optional max 50 items
      • cash_intensity "low" | "medium" | "high" optional
      • No other members are accepted — an unrecognised key is a reject, not a dropped field.
    • kyc object optional
      • tier string optional 1–64 chars
      • updated_at string (date-time) optional
      • No other members are accepted — an unrecognised key is a reject, not a dropped field.
    • screening_status_reported object optional
      • status "clear" | "potential_match" | "confirmed_match" | "pending" | "unknown" required
      • pep boolean optional
      • as_of string (date-time) optional
      • provider string optional max 128 chars
      • No other members are accepted — an unrecognised key is a reject, not a dropped field.
    • segment "consumer" | "merchant" | "remittance_agent" | "cash_agent" | "payroll" | "marketplace" | "financial_institution" | "other" optional
    • external_ref string optional 1–256 chars
    • correction_reason string optional max 256 chars
    • No other members are accepted — an unrecognised key is a reject, not a dropped field.
  • No other members are accepted — an unrecognised key is a reject, not a dropped field.
json — request
{
  "external_id": "party-001",
  "schema_version": "2026-09-01",
  "occurred_at": "2026-08-01T09:30:00-04:00",
  "sequence": 1,
  "data": {
    "type": "person",
    "status": "active",
    "created_at": "2026-08-01T09:30:00-04:00",
    "legal_names": [
      "Ana Pérez"
    ],
    "identifiers": [
      {
        "type": "national_id",
        "value": "001-1234567-8",
        "verification_status": "verified"
      }
    ],
    "jurisdictions": {
      "nationality": "DO",
      "residency": "DO"
    },
    "expected_profile": {
      "declared_monthly_volume": {
        "value": "5000000",
        "asset": "DOP",
        "scale": 2
      },
      "corridors": [
        "US"
      ],
      "cash_intensity": "medium"
    },
    "kyc": {
      "tier": "tier2",
      "updated_at": "2026-08-01T09:30:00-04:00"
    },
    "segment": "consumer"
  }
}

Responses

StatusBodyWhen
202IngestReceiptAccepted and committed.
400ProblemDetailsThe payload does not match the schema — kytrix:validation/* with field paths.
401ProblemDetailsSignature missing, wrong, stale or replayed — kytrix:auth/*.
403ProblemDetailsThe credential lacks the ingest scope — kytrix:auth/insufficient_scope.
409ProblemDetailsIdempotency-Key reuse with a different body, or a sequence conflict.
413ProblemDetailsBody above the payload cap — kytrix:request/payload_too_large.
429ProblemDetailsPer-tenant rate limit — kytrix:rate_limit/exceeded, with Retry-After.
json — 202 response
{
  "event_id": "evt_01k4f2b9m7q0z8t3d5r6y7w8xc",
  "object": "party",
  "external_id": "party-001",
  "received_at": "2026-09-05T14:22:11.412Z",
  "idempotent_replay": false
}

Response headers

HeaderMeaning
Kytrix-RateLimit-LimitBurst size in events (the token-bucket capacity).
Kytrix-RateLimit-RemainingTokens left in this tenant’s bucket.
Kytrix-RateLimit-ResetSeconds until the bucket is full again.
Idempotent-Replayedtrue when the body is the stored result of an earlier identical request.

Notes

  • Entities are versioned upserts: the dedup key is (tenant, object, external_id, sequence). Send a strictly higher sequence for each new version; the same sequence with different content is 409 kytrix:sequence/conflict, never a silent overwrite.
  • An exact resubmission (same identity, same canonical body) is acknowledged as a duplicate and coalesced onto the ORIGINAL event_id, with idempotent_replay: true.

Registered in packages/ingest/src/routes.ts — the build fails if this path is no longer there.

POST/v1/accountsAPI key, scope ingest
A wallet or ledger account held by a party. The body is one envelope validated by EVENT_SCHEMAS.account; unknown members are rejected rather than dropped. The 202 is returned only after one Postgres transaction has committed both the raw event and its processing obligation (invariant I6) — there is no acknowledged-but-lost state.

Defined in 05 G.1 · 05 G.3 · 06 H.1

Request headers

HeaderRequiredMeaning
Kytrix-Key-IdyesYour API key id (kx_test_…/kx_live_…).
Kytrix-TimestampyesUnix seconds, decimal. ±300 s of KYTRIX time.
Kytrix-Nonceyes1–64 chars of A-Za-z0-9._~-, single-use for 600 s per key.
Kytrix-Signatureyesv1=<lower-hex HMAC-SHA256> over the canonical string.
Idempotency-Keyno1–255 characters, scoped to (tenant, endpoint, key) and retained ≥ 24 h. Send one per logical event so a retry replays instead of double-posting.
Kytrix-Correlation-IdnoYour trace id; recorded on the inbox row and every KYTRIX log line for it.

Request body

Schema: AccountEvent — the full expansion is below.

  • external_id string required 1–256 chars
  • schema_version "2026-09-01" required
  • occurred_at string (date-time) required
  • recorded_at string (date-time) optional
  • sequence integer optional ≥ 0
  • data object required
    • type "wallet" | "checking" | "savings" | "merchant" | "settlement" | "stored_value" | "loan" | "remittance" | "other" required
    • holder_party_id string required 1–256 chars
    • asset string required pattern ^[A-Z]{3}$|^X-[A-Z0-9]{2,10}$
    • status "active" | "dormant" | "closed" | "restricted" | "frozen" | "unrestricted" | "withdrawals_restricted" | "account_restricted" | "transfers_suspended" required
    • opened_at string (date-time) required
    • product_code string optional 1–64 chars
    • purpose string optional max 256 chars
    • expected_use string optional max 512 chars
    • agent_of_opening_party_id string optional 1–256 chars
    • jurisdiction string optional pattern ^[A-Z]{2}$
    • closed_at string (date-time) optional
    • closure_reason string optional max 256 chars
    • external_ref string optional 1–256 chars
    • correction_reason string optional max 256 chars
    • No other members are accepted — an unrecognised key is a reject, not a dropped field.
  • No other members are accepted — an unrecognised key is a reject, not a dropped field.
json — request
{
  "external_id": "acct-001",
  "schema_version": "2026-09-01",
  "occurred_at": "2026-08-01T09:30:00-04:00",
  "sequence": 1,
  "data": {
    "type": "wallet",
    "holder_party_id": "party-001",
    "asset": "DOP",
    "status": "active",
    "opened_at": "2026-08-01T09:30:00-04:00",
    "product_code": "wallet-basic"
  }
}

Responses

StatusBodyWhen
202IngestReceiptAccepted and committed.
400ProblemDetailsThe payload does not match the schema — kytrix:validation/* with field paths.
401ProblemDetailsSignature missing, wrong, stale or replayed — kytrix:auth/*.
403ProblemDetailsThe credential lacks the ingest scope — kytrix:auth/insufficient_scope.
409ProblemDetailsIdempotency-Key reuse with a different body, or a sequence conflict.
413ProblemDetailsBody above the payload cap — kytrix:request/payload_too_large.
429ProblemDetailsPer-tenant rate limit — kytrix:rate_limit/exceeded, with Retry-After.
json — 202 response
{
  "event_id": "evt_01k4f2b9m7q0z8t3d5r6y7w8xc",
  "object": "account",
  "external_id": "acct-001",
  "received_at": "2026-09-05T14:22:11.412Z",
  "idempotent_replay": false
}

Response headers

HeaderMeaning
Kytrix-RateLimit-LimitBurst size in events (the token-bucket capacity).
Kytrix-RateLimit-RemainingTokens left in this tenant’s bucket.
Kytrix-RateLimit-ResetSeconds until the bucket is full again.
Idempotent-Replayedtrue when the body is the stored result of an earlier identical request.

Notes

  • Entities are versioned upserts: the dedup key is (tenant, object, external_id, sequence). Send a strictly higher sequence for each new version; the same sequence with different content is 409 kytrix:sequence/conflict, never a silent overwrite.
  • An exact resubmission (same identity, same canonical body) is acknowledged as a duplicate and coalesced onto the ORIGINAL event_id, with idempotent_replay: true.

Registered in packages/ingest/src/routes.ts — the build fails if this path is no longer there.

POST/v1/instrumentsAPI key, scope ingest
A card token, device, phone number or other payment instrument. The body is one envelope validated by EVENT_SCHEMAS.instrument; unknown members are rejected rather than dropped. The 202 is returned only after one Postgres transaction has committed both the raw event and its processing obligation (invariant I6) — there is no acknowledged-but-lost state.

Defined in 05 G.1 · 05 G.3 · 06 H.1

Request headers

HeaderRequiredMeaning
Kytrix-Key-IdyesYour API key id (kx_test_…/kx_live_…).
Kytrix-TimestampyesUnix seconds, decimal. ±300 s of KYTRIX time.
Kytrix-Nonceyes1–64 chars of A-Za-z0-9._~-, single-use for 600 s per key.
Kytrix-Signatureyesv1=<lower-hex HMAC-SHA256> over the canonical string.
Idempotency-Keyno1–255 characters, scoped to (tenant, endpoint, key) and retained ≥ 24 h. Send one per logical event so a retry replays instead of double-posting.
Kytrix-Correlation-IdnoYour trace id; recorded on the inbox row and every KYTRIX log line for it.

Request body

Schema: InstrumentEvent — the full expansion is below.

  • external_id string required 1–256 chars
  • schema_version "2026-09-01" required
  • occurred_at string (date-time) required
  • recorded_at string (date-time) optional
  • sequence integer optional ≥ 0
  • data object required
    • type "external_bank_account" | "card_token" | "mobile_wallet" | "payment_credential" | "virtual_account" | "other" required
    • status string required 1–64 chars
    • first_seen_at string (date-time) required
    • issuer_ref string optional 1–128 chars
    • masked_identifier string optional 1–64 chars
    • country string optional pattern ^[A-Z]{2}$
    • holder_name_as_entered string optional max 256 chars
    • verification_status "verified" | "unverified" | "pending" | "failed" optional
    • fingerprint string optional 1–128 chars
    • external_ref string optional 1–256 chars
    • No other members are accepted — an unrecognised key is a reject, not a dropped field.
  • No other members are accepted — an unrecognised key is a reject, not a dropped field.
json — request
{
  "external_id": "inst-001",
  "schema_version": "2026-09-01",
  "occurred_at": "2026-08-01T09:30:00-04:00",
  "sequence": 1,
  "data": {
    "type": "card_token",
    "status": "active",
    "first_seen_at": "2026-08-01T09:30:00-04:00",
    "issuer_ref": "BPD",
    "masked_identifier": "411111******1111",
    "country": "DO",
    "verification_status": "verified",
    "fingerprint": "fp_9c1e2d"
  }
}

Responses

StatusBodyWhen
202IngestReceiptAccepted and committed.
400ProblemDetailsThe payload does not match the schema — kytrix:validation/* with field paths.
401ProblemDetailsSignature missing, wrong, stale or replayed — kytrix:auth/*.
403ProblemDetailsThe credential lacks the ingest scope — kytrix:auth/insufficient_scope.
409ProblemDetailsIdempotency-Key reuse with a different body, or a sequence conflict.
413ProblemDetailsBody above the payload cap — kytrix:request/payload_too_large.
429ProblemDetailsPer-tenant rate limit — kytrix:rate_limit/exceeded, with Retry-After.
json — 202 response
{
  "event_id": "evt_01k4f2b9m7q0z8t3d5r6y7w8xc",
  "object": "instrument",
  "external_id": "inst-001",
  "received_at": "2026-09-05T14:22:11.412Z",
  "idempotent_replay": false
}

Response headers

HeaderMeaning
Kytrix-RateLimit-LimitBurst size in events (the token-bucket capacity).
Kytrix-RateLimit-RemainingTokens left in this tenant’s bucket.
Kytrix-RateLimit-ResetSeconds until the bucket is full again.
Idempotent-Replayedtrue when the body is the stored result of an earlier identical request.

Notes

  • Entities are versioned upserts: the dedup key is (tenant, object, external_id, sequence). Send a strictly higher sequence for each new version; the same sequence with different content is 409 kytrix:sequence/conflict, never a silent overwrite.
  • An exact resubmission (same identity, same canonical body) is acknowledged as a duplicate and coalesced onto the ORIGINAL event_id, with idempotent_replay: true.

Registered in packages/ingest/src/routes.ts — the build fails if this path is no longer there.

POST/v1/movementsAPI key, scope ingest
A value transfer — including failed, pending and reversed ones. The body is one envelope validated by EVENT_SCHEMAS.movement; unknown members are rejected rather than dropped. The 202 is returned only after one Postgres transaction has committed both the raw event and its processing obligation (invariant I6) — there is no acknowledged-but-lost state.

Defined in 05 G.1 · 05 G.3 · 06 H.1

Request headers

HeaderRequiredMeaning
Kytrix-Key-IdyesYour API key id (kx_test_…/kx_live_…).
Kytrix-TimestampyesUnix seconds, decimal. ±300 s of KYTRIX time.
Kytrix-Nonceyes1–64 chars of A-Za-z0-9._~-, single-use for 600 s per key.
Kytrix-Signatureyesv1=<lower-hex HMAC-SHA256> over the canonical string.
Idempotency-Keyno1–255 characters, scoped to (tenant, endpoint, key) and retained ≥ 24 h. Send one per logical event so a retry replays instead of double-posting.
Kytrix-Correlation-IdnoYour trace id; recorded on the inbox row and every KYTRIX log line for it.

Request body

Schema: MovementEvent — the full expansion is below.

  • external_id string required 1–256 chars
  • schema_version "2026-09-01" required
  • occurred_at string (date-time) required
  • recorded_at string (date-time) optional
  • sequence integer optional ≥ 0
  • data object required
    • type "p2p" | "merchant_payment" | "deposit" | "withdrawal" | "cash_in" | "cash_out" | "remittance" | "ach" | "wire" | "refund" | "reversal" | "settlement" | "internal_transfer" | "other" required
    • status "completed" | "pending" | "failed" | "reversed" required
    • amount Money required
    • debit object required
      • account_id string optional 1–256 chars
      • party_id string optional 1–256 chars
      • counterparty object optional
        • institution string optional 1–128 chars
        • country string optional pattern ^[A-Z]{2}$
        • name_as_given string optional max 256 chars
        • instrument_ref string optional 1–256 chars
        • No other members are accepted — an unrecognised key is a reject, not a dropped field.
      • No other members are accepted — an unrecognised key is a reject, not a dropped field.
    • credit object required
      • account_id string optional 1–256 chars
      • party_id string optional 1–256 chars
      • counterparty object optional
        • institution string optional 1–128 chars
        • country string optional pattern ^[A-Z]{2}$
        • name_as_given string optional max 256 chars
        • instrument_ref string optional 1–256 chars
        • No other members are accepted — an unrecognised key is a reject, not a dropped field.
      • No other members are accepted — an unrecognised key is a reject, not a dropped field.
    • links object optional
      • reverses string optional 1–256 chars
      • refunds string optional 1–256 chars
      • settles string optional 1–256 chars
      • part_of_batch string optional 1–256 chars
      • correction_of string optional 1–256 chars
      • No other members are accepted — an unrecognised key is a reject, not a dropped field.
    • cash boolean optional
    • memo string optional max 1000 chars
    • channel string optional 1–64 chars
    • rail string optional 1–64 chars
    • product string optional 1–64 chars
    • agent_party_id string optional 1–256 chars
    • instrument_ref string optional 1–256 chars
    • device_ref string optional 1–256 chars
    • ip string (ip) optional
    • geo object optional
      • country string optional pattern ^[A-Z]{2}$
      • region string optional max 128 chars
      • lat number optional ≥ -90 · ≤ 90
      • lon number optional ≥ -180 · ≤ 180
      • No other members are accepted — an unrecognised key is a reject, not a dropped field.
    • fees Money[] optional max 20 items
    • external_ref string optional 1–256 chars
    • correction_reason string optional max 256 chars
    • No other members are accepted — an unrecognised key is a reject, not a dropped field.
  • No other members are accepted — an unrecognised key is a reject, not a dropped field.
json — request
{
  "external_id": "mov-001",
  "schema_version": "2026-09-01",
  "occurred_at": "2026-08-30T12:00:00-04:00",
  "recorded_at": "2026-08-30T12:00:00-04:00",
  "data": {
    "type": "p2p",
    "status": "completed",
    "amount": {
      "value": "18000000",
      "asset": "DOP",
      "scale": 2
    },
    "debit": {
      "account_id": "acct-001",
      "party_id": "party-001"
    },
    "credit": {
      "counterparty": {
        "institution": "BPD",
        "country": "DO",
        "name_as_given": "J. Gómez"
      }
    },
    "cash": false,
    "memo": "rent august",
    "channel": "app",
    "rail": "internal",
    "ip": "190.166.1.10",
    "geo": {
      "country": "DO",
      "lat": 18.47,
      "lon": -69.9
    }
  }
}

Responses

StatusBodyWhen
202IngestReceiptAccepted and committed.
400ProblemDetailsThe payload does not match the schema — kytrix:validation/* with field paths.
401ProblemDetailsSignature missing, wrong, stale or replayed — kytrix:auth/*.
403ProblemDetailsThe credential lacks the ingest scope — kytrix:auth/insufficient_scope.
409ProblemDetailsIdempotency-Key reuse with a different body, or a sequence conflict.
413ProblemDetailsBody above the payload cap — kytrix:request/payload_too_large.
429ProblemDetailsPer-tenant rate limit — kytrix:rate_limit/exceeded, with Retry-After.
json — 202 response
{
  "event_id": "evt_01k4f2b9m7q0z8t3d5r6y7w8xc",
  "object": "movement",
  "external_id": "mov-001",
  "received_at": "2026-09-05T14:22:11.412Z",
  "idempotent_replay": false
}

Response headers

HeaderMeaning
Kytrix-RateLimit-LimitBurst size in events (the token-bucket capacity).
Kytrix-RateLimit-RemainingTokens left in this tenant’s bucket.
Kytrix-RateLimit-ResetSeconds until the bucket is full again.
Idempotent-Replayedtrue when the body is the stored result of an earlier identical request.

Notes

  • Facts are immutable: the dedup key is (tenant, object, external_id) with no sequence. Any differing resubmission of the same external_id is 409 kytrix:sequence/conflict — corrections are NEW events carrying links.correction_of and a correction_reason.
  • An exact resubmission is acknowledged as a duplicate and coalesced onto the ORIGINAL event_id, with idempotent_replay: true.

Registered in packages/ingest/src/routes.ts — the build fails if this path is no longer there.

POST/v1/relationshipsAPI key, scope ingest
A declared link between two entities (ownership, signatory, device sharing). The body is one envelope validated by EVENT_SCHEMAS.relationship; unknown members are rejected rather than dropped. The 202 is returned only after one Postgres transaction has committed both the raw event and its processing obligation (invariant I6) — there is no acknowledged-but-lost state.

Defined in 05 G.1 · 05 G.3 · 06 H.1

Request headers

HeaderRequiredMeaning
Kytrix-Key-IdyesYour API key id (kx_test_…/kx_live_…).
Kytrix-TimestampyesUnix seconds, decimal. ±300 s of KYTRIX time.
Kytrix-Nonceyes1–64 chars of A-Za-z0-9._~-, single-use for 600 s per key.
Kytrix-Signatureyesv1=<lower-hex HMAC-SHA256> over the canonical string.
Idempotency-Keyno1–255 characters, scoped to (tenant, endpoint, key) and retained ≥ 24 h. Send one per logical event so a retry replays instead of double-posting.
Kytrix-Correlation-IdnoYour trace id; recorded on the inbox row and every KYTRIX log line for it.

Request body

Schema: RelationshipEvent — the full expansion is below.

  • external_id string required 1–256 chars
  • schema_version "2026-09-01" required
  • occurred_at string (date-time) required
  • recorded_at string (date-time) optional
  • sequence integer optional ≥ 0
  • data object required
    • type "beneficial_owner" | "authorized_signer" | "joint_holder" | "employer" | "household_member" | "parent_org" | "merchant_owner" | "agent" | "referral" | "onboarding" | "owns_instrument" | "funds_account" | "same_as" | "other" required
    • from EntityRef required
    • to EntityRef required
    • valid_from string (date-time) required
    • valid_to string (date-time) optional
    • ownership_pct number optional ≥ 0 · ≤ 100
    • source "kyc" | "declared" | "derived_by_customer" optional
    • confidence number optional ≥ 0 · ≤ 1
    • external_ref string optional 1–256 chars
    • correction_reason string optional max 256 chars
    • No other members are accepted — an unrecognised key is a reject, not a dropped field.
  • No other members are accepted — an unrecognised key is a reject, not a dropped field.
json — request
{
  "external_id": "rel-001",
  "schema_version": "2026-09-01",
  "occurred_at": "2026-08-01T09:30:00-04:00",
  "sequence": 1,
  "data": {
    "type": "owns_instrument",
    "from": {
      "kind": "party",
      "external_id": "party-001"
    },
    "to": {
      "kind": "instrument",
      "external_id": "inst-001"
    },
    "valid_from": "2026-08-01T09:30:00-04:00",
    "source": "kyc",
    "confidence": 1
  }
}

Responses

StatusBodyWhen
202IngestReceiptAccepted and committed.
400ProblemDetailsThe payload does not match the schema — kytrix:validation/* with field paths.
401ProblemDetailsSignature missing, wrong, stale or replayed — kytrix:auth/*.
403ProblemDetailsThe credential lacks the ingest scope — kytrix:auth/insufficient_scope.
409ProblemDetailsIdempotency-Key reuse with a different body, or a sequence conflict.
413ProblemDetailsBody above the payload cap — kytrix:request/payload_too_large.
429ProblemDetailsPer-tenant rate limit — kytrix:rate_limit/exceeded, with Retry-After.
json — 202 response
{
  "event_id": "evt_01k4f2b9m7q0z8t3d5r6y7w8xc",
  "object": "relationship",
  "external_id": "rel-001",
  "received_at": "2026-09-05T14:22:11.412Z",
  "idempotent_replay": false
}

Response headers

HeaderMeaning
Kytrix-RateLimit-LimitBurst size in events (the token-bucket capacity).
Kytrix-RateLimit-RemainingTokens left in this tenant’s bucket.
Kytrix-RateLimit-ResetSeconds until the bucket is full again.
Idempotent-Replayedtrue when the body is the stored result of an earlier identical request.

Notes

  • Entities are versioned upserts: the dedup key is (tenant, object, external_id, sequence). Send a strictly higher sequence for each new version; the same sequence with different content is 409 kytrix:sequence/conflict, never a silent overwrite.
  • An exact resubmission (same identity, same canonical body) is acknowledged as a duplicate and coalesced onto the ORIGINAL event_id, with idempotent_replay: true.

Registered in packages/ingest/src/routes.ts — the build fails if this path is no longer there.

POST/v1/control-eventsAPI key, scope ingest
Something your own controls did: a blocked withdrawal, a limit hit, a manual review. The body is one envelope validated by EVENT_SCHEMAS.control_event; unknown members are rejected rather than dropped. The 202 is returned only after one Postgres transaction has committed both the raw event and its processing obligation (invariant I6) — there is no acknowledged-but-lost state.

Defined in 05 G.1 · 05 G.3 · 06 H.1

Request headers

HeaderRequiredMeaning
Kytrix-Key-IdyesYour API key id (kx_test_…/kx_live_…).
Kytrix-TimestampyesUnix seconds, decimal. ±300 s of KYTRIX time.
Kytrix-Nonceyes1–64 chars of A-Za-z0-9._~-, single-use for 600 s per key.
Kytrix-Signatureyesv1=<lower-hex HMAC-SHA256> over the canonical string.
Idempotency-Keyno1–255 characters, scoped to (tenant, endpoint, key) and retained ≥ 24 h. Send one per logical event so a retry replays instead of double-posting.
Kytrix-Correlation-IdnoYour trace id; recorded on the inbox row and every KYTRIX log line for it.

Request body

Schema: ControlEventEvent — the full expansion is below.

  • external_id string required 1–256 chars
  • schema_version "2026-09-01" required
  • occurred_at string (date-time) required
  • recorded_at string (date-time) optional
  • sequence integer optional ≥ 0
  • data object required
    • type "transfer.failed" | "withdrawal.blocked" | "velocity_check.failed" | "sanctions.hit_reported" | "screening.status_changed" | "kyc.upgraded" | "kyc.downgraded" | "kyc.verification_failed" | "auth.failed" | "auth.suspicious_login" | "device.changed" | "password.reset" | "beneficiary.added" | "account.restricted_by_customer" | "transfer.retried_repeatedly" | "limit.changed" required
    • subject EntityRef required
    • outcome string optional 1–64 chars
    • reason_code string optional 1–128 chars
    • device_ref string optional 1–256 chars
    • ip string (ip) optional
    • actor_kind "customer_system" | "end_user" optional
    • customer_severity string optional 1–32 chars
    • linked_movement_id string optional 1–256 chars
    • external_ref string optional 1–256 chars
    • No other members are accepted — an unrecognised key is a reject, not a dropped field.
  • No other members are accepted — an unrecognised key is a reject, not a dropped field.
json — request
{
  "external_id": "ctl-001",
  "schema_version": "2026-09-01",
  "occurred_at": "2026-08-30T12:00:00-04:00",
  "data": {
    "type": "withdrawal.blocked",
    "subject": {
      "kind": "account",
      "external_id": "acct-001"
    },
    "outcome": "blocked",
    "reason_code": "daily_limit",
    "actor_kind": "customer_system",
    "customer_severity": "medium",
    "linked_movement_id": "mov-001"
  }
}

Responses

StatusBodyWhen
202IngestReceiptAccepted and committed.
400ProblemDetailsThe payload does not match the schema — kytrix:validation/* with field paths.
401ProblemDetailsSignature missing, wrong, stale or replayed — kytrix:auth/*.
403ProblemDetailsThe credential lacks the ingest scope — kytrix:auth/insufficient_scope.
409ProblemDetailsIdempotency-Key reuse with a different body, or a sequence conflict.
413ProblemDetailsBody above the payload cap — kytrix:request/payload_too_large.
429ProblemDetailsPer-tenant rate limit — kytrix:rate_limit/exceeded, with Retry-After.
json — 202 response
{
  "event_id": "evt_01k4f2b9m7q0z8t3d5r6y7w8xc",
  "object": "control_event",
  "external_id": "ctl-001",
  "received_at": "2026-09-05T14:22:11.412Z",
  "idempotent_replay": false
}

Response headers

HeaderMeaning
Kytrix-RateLimit-LimitBurst size in events (the token-bucket capacity).
Kytrix-RateLimit-RemainingTokens left in this tenant’s bucket.
Kytrix-RateLimit-ResetSeconds until the bucket is full again.
Idempotent-Replayedtrue when the body is the stored result of an earlier identical request.

Notes

  • Facts are immutable: the dedup key is (tenant, object, external_id) with no sequence. Any differing resubmission of the same external_id is 409 kytrix:sequence/conflict — corrections are NEW events carrying links.correction_of and a correction_reason.
  • An exact resubmission is acknowledged as a duplicate and coalesced onto the ORIGINAL event_id, with idempotent_replay: true.

Registered in packages/ingest/src/routes.ts — the build fails if this path is no longer there.

POST/v1/batchesAPI key, scope ingest
Acceptance is atomic, validation is per item: one malformed event becomes its own rejected result instead of failing the request, so a bad row never loses the other 999. Duplicates count as accepted and carry idempotent_replay: true. The rate-limit cost is the number of submitted events, so one 1,000-event batch and 1,000 single submits are throttled identically.

Defined in 06 H.1

Request headers

HeaderRequiredMeaning
Kytrix-Key-IdyesYour API key id (kx_test_…/kx_live_…).
Kytrix-TimestampyesUnix seconds, decimal. ±300 s of KYTRIX time.
Kytrix-Nonceyes1–64 chars of A-Za-z0-9._~-, single-use for 600 s per key.
Kytrix-Signatureyesv1=<lower-hex HMAC-SHA256> over the canonical string.
Idempotency-Keyno1–255 characters, scoped to (tenant, endpoint, key) and retained ≥ 24 h. Send one per logical event so a retry replays instead of double-posting.
Kytrix-Correlation-IdnoYour trace id; recorded on the inbox row and every KYTRIX log line for it.

Request body

Schema: BatchRequest — the full expansion is below.

  • events one of 6[] required min 1 items · max 1000 items
  • No other members are accepted — an unrecognised key is a reject, not a dropped field.
json — request
{
  "events": [
    {
      "object": "party",
      "event": {
        "external_id": "party-001",
        "schema_version": "2026-09-01",
        "occurred_at": "2026-08-01T09:30:00-04:00",
        "sequence": 1,
        "data": {
          "type": "person",
          "status": "active",
          "created_at": "2026-08-01T09:30:00-04:00",
          "legal_names": [
            "Ana Pérez"
          ],
          "identifiers": [
            {
              "type": "national_id",
              "value": "001-1234567-8",
              "verification_status": "verified"
            }
          ],
          "jurisdictions": {
            "nationality": "DO",
            "residency": "DO"
          },
          "expected_profile": {
            "declared_monthly_volume": {
              "value": "5000000",
              "asset": "DOP",
              "scale": 2
            },
            "corridors": [
              "US"
            ],
            "cash_intensity": "medium"
          },
          "kyc": {
            "tier": "tier2",
            "updated_at": "2026-08-01T09:30:00-04:00"
          },
          "segment": "consumer"
        }
      }
    },
    {
      "object": "movement",
      "event": {
        "external_id": "mov-001",
        "schema_version": "2026-09-01",
        "occurred_at": "2026-08-30T12:00:00-04:00",
        "recorded_at": "2026-08-30T12:00:00-04:00",
        "data": {
          "type": "p2p",
          "status": "completed",
          "amount": {
            "value": "18000000",
            "asset": "DOP",
            "scale": 2
          },
          "debit": {
            "account_id": "acct-001",
            "party_id": "party-001"
          },
          "credit": {
            "counterparty": {
              "institution": "BPD",
              "country": "DO",
              "name_as_given": "J. Gómez"
            }
          },
          "cash": false,
          "memo": "rent august",
          "channel": "app",
          "rail": "internal",
          "ip": "190.166.1.10",
          "geo": {
            "country": "DO",
            "lat": 18.47,
            "lon": -69.9
          }
        }
      }
    }
  ]
}

Responses

StatusBodyWhen
202BatchResponseAt least one item was accepted.
422BatchResponseEvery item was rejected — the same BatchResponse body with accepted: 0.
400ProblemDetailsThe batch envelope itself is malformed (events missing or empty).
401ProblemDetailsSignature missing, wrong, stale or replayed.
403ProblemDetailsThe credential lacks the ingest scope.
409ProblemDetailsIdempotency-Key reuse with a different body.
413ProblemDetailsMore than 1,000 events (kytrix:batch/too_large) or an oversized body.
429ProblemDetailsPer-tenant rate limit.
json — 202 response
{
  "accepted": 1,
  "rejected": 1,
  "results": [
    {
      "index": 0,
      "outcome": "accepted",
      "receipt": {
        "event_id": "evt_01k4f2b9m7q0z8t3d5r6y7w8xc",
        "object": "party",
        "external_id": "party-001",
        "received_at": "2026-09-05T14:22:11.412Z",
        "idempotent_replay": false
      }
    },
    {
      "index": 1,
      "outcome": "rejected",
      "problem": {
        "type": "kytrix:validation/schema",
        "title": "Request does not match the schema",
        "status": 400,
        "code": "kytrix:validation/schema",
        "detail": "event.data.amount: Required",
        "errors": [
          {
            "path": "event.data.amount",
            "code": "kytrix:validation/schema",
            "message": "Required"
          }
        ]
      }
    }
  ]
}

Response headers

HeaderMeaning
Kytrix-RateLimit-LimitBurst size in events (the token-bucket capacity).
Kytrix-RateLimit-RemainingTokens left in this tenant’s bucket.
Kytrix-RateLimit-ResetSeconds until the bucket is full again.
Idempotent-Replayedtrue when the body is the stored result of an earlier identical request.

Notes

  • Check results[i].outcome for every item. A 202 does NOT mean every event landed.
  • An Idempotency-Key on a batch covers the batch acceptance; per-event dedup still applies through the external ids.

Registered in packages/ingest/src/routes.ts — the build fails if this path is no longer there.

POST/v1/validateAPI key, scope ingest or read
Takes either a batch body ({ events: [...] }) or a single { object, event } item and runs exactly the validation the live endpoints run. No transaction is opened on this path at all, so nothing is written: no inbox row, no obligation, no idempotency record. This is the mapping-iteration loop — run it until valid is true before you send anything real.

Defined in 06 H.1 · 05 G.2

Request headers

HeaderRequiredMeaning
Kytrix-Key-IdyesYour API key id (kx_test_…/kx_live_…).
Kytrix-TimestampyesUnix seconds, decimal. ±300 s of KYTRIX time.
Kytrix-Nonceyes1–64 chars of A-Za-z0-9._~-, single-use for 600 s per key.
Kytrix-Signatureyesv1=<lower-hex HMAC-SHA256> over the canonical string.

Request body

A BatchRequest, or one { object, event } item.

json — request
{
  "object": "movement",
  "event": {
    "external_id": "mov-001",
    "schema_version": "2026-09-01",
    "occurred_at": "2026-08-30T12:00:00-04:00",
    "recorded_at": "2026-08-30T12:00:00-04:00",
    "data": {
      "type": "p2p",
      "status": "completed",
      "amount": {
        "value": "18000000",
        "asset": "DOP",
        "scale": 2
      },
      "debit": {
        "account_id": "acct-001",
        "party_id": "party-001"
      },
      "credit": {
        "counterparty": {
          "institution": "BPD",
          "country": "DO",
          "name_as_given": "J. Gómez"
        }
      },
      "cash": false,
      "memo": "rent august",
      "channel": "app",
      "rail": "internal",
      "ip": "190.166.1.10",
      "geo": {
        "country": "DO",
        "lat": 18.47,
        "lon": -69.9
      }
    }
  }
}

Responses

StatusBodyWhen
200inline schema (below)Every item validated.
422inline schema (below)At least one item is invalid — same body, valid: false.
401ProblemDetailsSignature missing, wrong, stale or replayed.
403ProblemDetailsThe credential has neither the ingest nor the read scope.
413ProblemDetailsMore than 1,000 items, or an oversized body.
429ProblemDetailsPer-tenant rate limit.

200 body

  • valid boolean required true only when every item validated.
  • checked integer required
  • accepted integer required Items that would be accepted. Nothing is stored.
  • rejected integer required
  • results object[] required
    • array of
      • index integer required
      • outcome "valid" | "rejected" required
      • object "party" | "account" | "instrument" | "movement" | "relationship" | "control_event" optional
      • external_id string optional
      • problem ProblemDetails optional

422 body

  • valid boolean required true only when every item validated.
  • checked integer required
  • accepted integer required Items that would be accepted. Nothing is stored.
  • rejected integer required
  • results object[] required
    • array of
      • index integer required
      • outcome "valid" | "rejected" required
      • object "party" | "account" | "instrument" | "movement" | "relationship" | "control_event" optional
      • external_id string optional
      • problem ProblemDetails optional
json — 200 response
{
  "valid": true,
  "checked": 1,
  "accepted": 1,
  "rejected": 0,
  "results": [
    {
      "index": 0,
      "outcome": "valid",
      "object": "movement",
      "external_id": "mov-001"
    }
  ]
}

Response headers

HeaderMeaning
Kytrix-RateLimit-LimitBurst size in events (the token-bucket capacity).
Kytrix-RateLimit-RemainingTokens left in this tenant’s bucket.
Kytrix-RateLimit-ResetSeconds until the bucket is full again.

Notes

  • A dry-run is not a reservation. It proves the shape is right; it does not hold an external_id or promise the live submit will not hit a sequence conflict.

Registered in packages/ingest/src/routes.ts — the build fails if this path is no longer there.

#Event status

Where is event X — answerable from the moment of the 202.

GET/v1/events/{event_id}API key, scope ingest
Answers from the moment of the 202: the ingest transaction writes a received stage ledger row, so there is never a window where a receipt exists but the event is "not found". stage_history is the processing trail; rejection is the structured reason to resubmit on.

Defined in 05 G.2

Parameters

NameInTypeDescription
event_idpath · requiredstringThe evt_… id from the receipt. A bare uuid and evt_<uuid> are also accepted.

Request headers

HeaderRequiredMeaning
Kytrix-Key-IdyesYour API key id (kx_test_…/kx_live_…).
Kytrix-TimestampyesUnix seconds, decimal. ±300 s of KYTRIX time.
Kytrix-Nonceyes1–64 chars of A-Za-z0-9._~-, single-use for 600 s per key.
Kytrix-Signatureyesv1=<lower-hex HMAC-SHA256> over the canonical string.

Responses

StatusBodyWhen
200inline schema (below)The inbox record and its stage history.
401ProblemDetailsSignature missing, wrong, stale or replayed.
403ProblemDetailsThe credential lacks the ingest scope.
404ProblemDetailsNo such event in this tenant, or the id is not a KYTRIX event id.

200 body

  • event_id string required pattern ^evt_[0-9A-Za-z-]{8,64}$
  • object "party" | "account" | "instrument" | "movement" | "relationship" | "control_event" required
  • external_id string required
  • sequence integer required nullablenull for movements and control events (immutable facts carry no sequence).
  • status string required ingest.inbox_events.status, e.g. pending, rejected.
  • rejection object required nullableThe structured reject reason; null unless status is rejected.
  • status_reason object required nullable
  • attempts integer required
  • received_at string (date-time) required
  • occurred_at string (date-time) required nullable
  • terminal_at string (date-time) required nullable
  • historical boolean required
  • import_id string required nullable
  • content_hash string required pattern ^sha256:[0-9a-f]{64}$
  • stage_history object[] required
    • array of
      • stage string required
      • status string required
      • at string (date-time) required
      • worker string required nullable
      • duration_ms integer required nullable
      • detail object required nullable
json — 200 response
{
  "event_id": "evt_01k4f2b9m7q0z8t3d5r6y7w8xc",
  "object": "movement",
  "external_id": "mov-001",
  "sequence": null,
  "status": "pending",
  "rejection": null,
  "status_reason": null,
  "attempts": 0,
  "received_at": "2026-09-05T14:22:11.412Z",
  "occurred_at": "2026-08-30T12:00:00-04:00",
  "terminal_at": null,
  "historical": false,
  "import_id": null,
  "content_hash": "sha256:0f5c…",
  "stage_history": [
    {
      "stage": "received",
      "status": "ok",
      "at": "2026-09-05T14:22:11.412Z",
      "worker": null,
      "duration_ms": null,
      "detail": null
    }
  ]
}

Response headers

HeaderMeaning
Kytrix-RateLimit-LimitBurst size in events (the token-bucket capacity).
Kytrix-RateLimit-RemainingTokens left in this tenant’s bucket.
Kytrix-RateLimit-ResetSeconds until the bucket is full again.

Notes

  • sequence is null for movements and control events: immutable facts have no version.

Registered in packages/ingest/src/routes.ts — the build fails if this path is no longer there.

#Canonical reads and posture

What KYTRIX made of what you sent, and the band-only posture read. Eventually consistent: the 202 acknowledges the inbox commit, canonicalization happens after it.

GET/v1/parties/{external_id}API key, scope read
What KYTRIX made of the party events you sent, at the current version. The response is the PartyView TypeScript interface in packages/contracts/src/views.ts — a type, not a zod schema — so it is not in the generated schema section below and the SDK does not validate it either. Identifiers come back as keyed hashes, never as the raw values you sent.

Defined in 06 H.1 · 03 §3.7

Parameters

NameInTypeDescription
external_idpath · requiredstringYour id for the party, exactly as submitted.

Request headers

HeaderRequiredMeaning
Kytrix-Key-IdyesYour API key id (kx_test_…/kx_live_…).
Kytrix-TimestampyesUnix seconds, decimal. ±300 s of KYTRIX time.
Kytrix-Nonceyes1–64 chars of A-Za-z0-9._~-, single-use for 600 s per key.
Kytrix-Signatureyesv1=<lower-hex HMAC-SHA256> over the canonical string.

Responses

StatusBodyWhen
200PartyView (see packages/contracts/src/views.ts).
401ProblemDetailsSignature missing, wrong, stale or replayed.
403ProblemDetailsThe credential lacks the read scope.
404ProblemDetailsUnknown in this tenant — a governed 404 problem, never an empty 200.

Notes

  • Eventually consistent. The 202 acknowledges the inbox commit; canonicalization runs after it. Do not poll this in a loop straight after a submit — use GET /v1/events/{event_id} to follow processing.

Registered in packages/canonical/src/routes.ts — the build fails if this path is no longer there.

GET/v1/accounts/{external_id}API key, scope read
Response body is the AccountView interface in packages/contracts/src/views.ts.

Defined in 06 H.1

Parameters

NameInTypeDescription
external_idpath · requiredstringYour account id.

Request headers

HeaderRequiredMeaning
Kytrix-Key-IdyesYour API key id (kx_test_…/kx_live_…).
Kytrix-TimestampyesUnix seconds, decimal. ±300 s of KYTRIX time.
Kytrix-Nonceyes1–64 chars of A-Za-z0-9._~-, single-use for 600 s per key.
Kytrix-Signatureyesv1=<lower-hex HMAC-SHA256> over the canonical string.

Responses

StatusBodyWhen
200AccountView.
401ProblemDetailsSignature missing, wrong, stale or replayed.
403ProblemDetailsThe credential lacks the read scope.
404ProblemDetailsUnknown in this tenant.

Registered in packages/canonical/src/routes.ts — the build fails if this path is no longer there.

GET/v1/instruments/{external_id}API key, scope read
Response body is the InstrumentView interface in packages/contracts/src/views.ts.

Defined in 06 H.1

Parameters

NameInTypeDescription
external_idpath · requiredstringYour instrument id.

Request headers

HeaderRequiredMeaning
Kytrix-Key-IdyesYour API key id (kx_test_…/kx_live_…).
Kytrix-TimestampyesUnix seconds, decimal. ±300 s of KYTRIX time.
Kytrix-Nonceyes1–64 chars of A-Za-z0-9._~-, single-use for 600 s per key.
Kytrix-Signatureyesv1=<lower-hex HMAC-SHA256> over the canonical string.

Responses

StatusBodyWhen
200InstrumentView.
401ProblemDetailsSignature missing, wrong, stale or replayed.
403ProblemDetailsThe credential lacks the read scope.
404ProblemDetailsUnknown in this tenant.

Registered in packages/canonical/src/routes.ts — the build fails if this path is no longer there.

GET/v1/movements/{external_id}API key, scope read
Response body is the MovementView interface in packages/contracts/src/views.ts. It carries your amount plus, when the tenant base asset differs, a normalized amount with its complete FX provenance record.

Defined in 06 H.1 · 03 §3.2

Parameters

NameInTypeDescription
external_idpath · requiredstringYour movement id.

Request headers

HeaderRequiredMeaning
Kytrix-Key-IdyesYour API key id (kx_test_…/kx_live_…).
Kytrix-TimestampyesUnix seconds, decimal. ±300 s of KYTRIX time.
Kytrix-Nonceyes1–64 chars of A-Za-z0-9._~-, single-use for 600 s per key.
Kytrix-Signatureyesv1=<lower-hex HMAC-SHA256> over the canonical string.

Responses

StatusBodyWhen
200MovementView.
401ProblemDetailsSignature missing, wrong, stale or replayed.
403ProblemDetailsThe credential lacks the read scope.
404ProblemDetailsUnknown in this tenant.

Registered in packages/canonical/src/routes.ts — the build fails if this path is no longer there.

GET/v1/parties/{external_id}/movementsAPI key, scope read
Cursor-paginated page of MovementView. Cursors are opaque — pass next_cursor back verbatim and never construct one.

Defined in 06 H.2

Parameters

NameInTypeDescription
external_idpath · requiredstringYour party id.
cursorquerystringOpaque cursor from a previous page.
limitqueryinteger (1–200, default 50)Page size.
directionqueryin \| out \| bothin = the party is on the credit side (value arriving), out = the debit side (value leaving), both = either.
fromqueryRFC 3339Lower bound on the movement time.
toqueryRFC 3339Upper bound on the movement time.

Request headers

HeaderRequiredMeaning
Kytrix-Key-IdyesYour API key id (kx_test_…/kx_live_…).
Kytrix-TimestampyesUnix seconds, decimal. ±300 s of KYTRIX time.
Kytrix-Nonceyes1–64 chars of A-Za-z0-9._~-, single-use for 600 s per key.
Kytrix-Signatureyesv1=<lower-hex HMAC-SHA256> over the canonical string.

Responses

StatusBodyWhen
200{ data: MovementView[], has_more: boolean, next_cursor?: string }.
400ProblemDetailsMalformed cursor, limit, direction or time range.
401ProblemDetailsSignature missing, wrong, stale or replayed.
403ProblemDetailsThe credential lacks the read scope.
404ProblemDetailsUnknown party in this tenant.

Registered in packages/canonical/src/routes.ts — the build fails if this path is no longer there.

GET/v1/posture/parties/{external_id}API key, scope read
The one risk read an operational system may have: a band, when it was computed, how stale it is, and the coverage state behind it. There are deliberately no reasons, typologies, detector names or narratives here — putting those in an operational system is a tipping-off risk (14 P.5).

Defined in 06 H.1 · 14 P.5 · 08 J.1

Parameters

NameInTypeDescription
external_idpath · requiredstringYour party id.

Request headers

HeaderRequiredMeaning
Kytrix-Key-IdyesYour API key id (kx_test_…/kx_live_…).
Kytrix-TimestampyesUnix seconds, decimal. ±300 s of KYTRIX time.
Kytrix-Nonceyes1–64 chars of A-Za-z0-9._~-, single-use for 600 s per key.
Kytrix-Signatureyesv1=<lower-hex HMAC-SHA256> over the canonical string.

Responses

StatusBodyWhen
200PostureResponseThe posture.
401ProblemDetailsSignature missing, wrong, stale or replayed.
403ProblemDetailsThe credential lacks the read scope.
404ProblemDetailsUnknown party in this tenant.
json — 200 response
{
  "subject": {
    "kind": "party",
    "external_id": "party-001"
  },
  "risk_state": "watch",
  "as_of": "2026-09-05T14:20:00Z",
  "freshness_seconds": 131,
  "coverage_state": "complete"
}

Notes

  • unknown and incomplete are real answers, not errors — they mean "no basis" and "gaps in the data behind this band". Treat them as safely ignorable, never as "blocked" (invariant I7).
  • KYTRIX advises; you enforce. Nothing in this API writes your account status.

Registered in packages/alerts/src/routes.ts — the build fails if this path is no longer there.

GET/v1/posture/accounts/{external_id}API key, scope read
As above, for an account.

Defined in 06 H.1 · 14 P.5

Parameters

NameInTypeDescription
external_idpath · requiredstringYour account id.

Request headers

HeaderRequiredMeaning
Kytrix-Key-IdyesYour API key id (kx_test_…/kx_live_…).
Kytrix-TimestampyesUnix seconds, decimal. ±300 s of KYTRIX time.
Kytrix-Nonceyes1–64 chars of A-Za-z0-9._~-, single-use for 600 s per key.
Kytrix-Signatureyesv1=<lower-hex HMAC-SHA256> over the canonical string.

Responses

StatusBodyWhen
200PostureResponseThe posture.
401ProblemDetailsSignature missing, wrong, stale or replayed.
403ProblemDetailsThe credential lacks the read scope.
404ProblemDetailsUnknown account in this tenant.

Registered in packages/alerts/src/routes.ts — the build fails if this path is no longer there.

GET/v1/alertsAPI key, scope read
Restricted AML content: every alert is classification: "restricted_aml". Keep it out of operational systems, support tooling and status pages.

Defined in 06 H.1 · 10 L.5 · 14 P.5

Parameters

NameInTypeDescription
statusquerystring (≤64)Alert status filter.
sincequeryRFC 3339Only alerts opened at or after this instant.
cursorquerystring (≤4096)Opaque cursor.
limitqueryinteger (1–200, default 50)Page size.

Request headers

HeaderRequiredMeaning
Kytrix-Key-IdyesYour API key id (kx_test_…/kx_live_…).
Kytrix-TimestampyesUnix seconds, decimal. ±300 s of KYTRIX time.
Kytrix-Nonceyes1–64 chars of A-Za-z0-9._~-, single-use for 600 s per key.
Kytrix-Signatureyesv1=<lower-hex HMAC-SHA256> over the canonical string.

Responses

StatusBodyWhen
200Alert{ data: Alert[], has_more: boolean, next_cursor?: string }.
400ProblemDetailsBad query parameter.
401ProblemDetailsSignature missing, wrong, stale or replayed.
403ProblemDetailsThe credential lacks the read scope.

Notes

  • The served resource widens the contracts Alert in one place: ruleset_version_id may be null for firings produced before ruleset provenance was stamped. Inventing one would be a fabricated fact.

Registered in packages/alerts/src/routes.ts — the build fails if this path is no longer there.

GET/v1/alerts/{alert_id}API key, scope read
The full alert resource, including observed values, thresholds and the coverage stamp.

Defined in 06 H.1

Parameters

NameInTypeDescription
alert_idpath · requiredstring (alrt_…)The alert id.

Request headers

HeaderRequiredMeaning
Kytrix-Key-IdyesYour API key id (kx_test_…/kx_live_…).
Kytrix-TimestampyesUnix seconds, decimal. ±300 s of KYTRIX time.
Kytrix-Nonceyes1–64 chars of A-Za-z0-9._~-, single-use for 600 s per key.
Kytrix-Signatureyesv1=<lower-hex HMAC-SHA256> over the canonical string.

Responses

StatusBodyWhen
200AlertThe alert.
401ProblemDetailsSignature missing, wrong, stale or replayed.
403ProblemDetailsThe credential lacks the read scope.
404ProblemDetailsNo such alert in this tenant.

Registered in packages/alerts/src/routes.ts — the build fails if this path is no longer there.

GET/v1/episodesAPI key, scope read
An episode is the investigation unit: related alerts about one subject, grouped so an analyst sees one thing to work rather than forty.

Defined in 06 H.1 · 10 L.5

Parameters

NameInTypeDescription
statequeryEpisodeStateEpisode state filter.
sincequeryRFC 3339Lower bound on time.
cursorquerystring (≤4096)Opaque cursor.
limitqueryinteger (1–200, default 50)Page size.

Request headers

HeaderRequiredMeaning
Kytrix-Key-IdyesYour API key id (kx_test_…/kx_live_…).
Kytrix-TimestampyesUnix seconds, decimal. ±300 s of KYTRIX time.
Kytrix-Nonceyes1–64 chars of A-Za-z0-9._~-, single-use for 600 s per key.
Kytrix-Signatureyesv1=<lower-hex HMAC-SHA256> over the canonical string.

Responses

StatusBodyWhen
200Episode{ data: Episode[], has_more: boolean, next_cursor?: string }.
400ProblemDetailsBad query parameter.
401ProblemDetailsSignature missing, wrong, stale or replayed.
403ProblemDetailsThe credential lacks the read scope.

Registered in packages/alerts/src/routes.ts — the build fails if this path is no longer there.

GET/v1/episodes/{episode_id}API key, scope read
The episode with its state and escalation history.

Defined in 06 H.1

Parameters

NameInTypeDescription
episode_idpath · requiredstring (epsd_…)The episode id.

Request headers

HeaderRequiredMeaning
Kytrix-Key-IdyesYour API key id (kx_test_…/kx_live_…).
Kytrix-TimestampyesUnix seconds, decimal. ±300 s of KYTRIX time.
Kytrix-Nonceyes1–64 chars of A-Za-z0-9._~-, single-use for 600 s per key.
Kytrix-Signatureyesv1=<lower-hex HMAC-SHA256> over the canonical string.

Responses

StatusBodyWhen
200EpisodeThe episode.
401ProblemDetailsSignature missing, wrong, stale or replayed.
403ProblemDetailsThe credential lacks the read scope.
404ProblemDetailsNo such episode in this tenant.

Registered in packages/alerts/src/routes.ts — the build fails if this path is no longer there.

#Webhook endpoint management

Register, list, disable, test and rotate the endpoints KYTRIX delivers to.

POST/v1/webhook-endpointsAPI key, scope webhook_mgmt
Registers a delivery target and returns its signing secret once. Endpoint management is its own scope so an ingest-only key cannot repoint your egress. The channel is a compliance control, not a preference: compliance endpoints receive restricted AML content, operational endpoints receive only sanitized events.

Defined in 06 H.1 · 06 H.6 · 14 P.5

Request headers

HeaderRequiredMeaning
Kytrix-Key-IdyesYour API key id (kx_test_…/kx_live_…).
Kytrix-TimestampyesUnix seconds, decimal. ±300 s of KYTRIX time.
Kytrix-Nonceyes1–64 chars of A-Za-z0-9._~-, single-use for 600 s per key.
Kytrix-Signatureyesv1=<lower-hex HMAC-SHA256> over the canonical string.

Request body

  • url string required 1–2000 charsHTTPS only. Private, link-local and cloud-metadata ranges are refused with kytrix:webhook/invalid_endpoint (06 H.6 SSRF posture, AT-23).
  • channel "compliance" | "operational" required
  • description string optional max 1000 chars
  • event_types string[] optional max 64 itemsSubset of the catalog deliverable on this channel; omit or leave empty for all deliverable events. Subscribing a restricted_aml event on an operational endpoint is refused.
  • No other members are accepted — an unrecognised key is a reject, not a dropped field.
json — request
{
  "url": "https://paydoly.example/kytrix/webhooks",
  "channel": "compliance",
  "event_types": [
    "alert.opened",
    "episode.escalated"
  ]
}

Responses

StatusBodyWhen
201inline schema (below)Registered. secret is present in this response and never again.
400ProblemDetailsNot HTTPS, a private/link-local/metadata address, or a restricted event type on an operational channel — kytrix:webhook/invalid_endpoint.
401ProblemDetailsSignature missing, wrong, stale or replayed.
403ProblemDetailsThe credential lacks the webhook_mgmt scope.

201 body

  • endpoint_id string required pattern ^whk_[0-9A-Za-z-]{8,64}$
  • tenant_id string (uuid) required
  • url string (uri) required
  • channel "compliance" | "operational" required
  • event_types string[] required
  • status "active" | "disabled" required
  • created_at string (date-time) required
  • secret string required base64url, 32 bytes. Store it in your secret manager immediately.

Notes

  • This endpoint does not honour Idempotency-Key — only the ingest endpoints implement the idempotency matrix today, so the SDK sends no automatic key here and never retries it.

Registered in packages/webhooks/src/routes.ts — the build fails if this path is no longer there.

GET/v1/webhook-endpointsAPI key, scope webhook_mgmt
Every endpoint of the tenant. Secrets are never returned.

Defined in 06 H.1

Request headers

HeaderRequiredMeaning
Kytrix-Key-IdyesYour API key id (kx_test_…/kx_live_…).
Kytrix-TimestampyesUnix seconds, decimal. ±300 s of KYTRIX time.
Kytrix-Nonceyes1–64 chars of A-Za-z0-9._~-, single-use for 600 s per key.
Kytrix-Signatureyesv1=<lower-hex HMAC-SHA256> over the canonical string.

Responses

StatusBodyWhen
200WebhookEndpoint{ endpoints: WebhookEndpoint[] }.
401ProblemDetailsSignature missing, wrong, stale or replayed.
403ProblemDetailsThe credential lacks the webhook_mgmt scope.

Registered in packages/webhooks/src/routes.ts — the build fails if this path is no longer there.

GET/v1/webhook-endpoints/{id}API key, scope webhook_mgmt
One endpoint, without its secret.

Defined in 06 H.1

Parameters

NameInTypeDescription
idpath · requiredstring (whk_…)The endpoint id.

Request headers

HeaderRequiredMeaning
Kytrix-Key-IdyesYour API key id (kx_test_…/kx_live_…).
Kytrix-TimestampyesUnix seconds, decimal. ±300 s of KYTRIX time.
Kytrix-Nonceyes1–64 chars of A-Za-z0-9._~-, single-use for 600 s per key.
Kytrix-Signatureyesv1=<lower-hex HMAC-SHA256> over the canonical string.

Responses

StatusBodyWhen
200WebhookEndpointThe endpoint.
401ProblemDetailsSignature missing, wrong, stale or replayed.
403ProblemDetailsThe credential lacks the webhook_mgmt scope.
404ProblemDetailsNo such endpoint in this tenant.

Registered in packages/webhooks/src/routes.ts — the build fails if this path is no longer there.

DELETE/v1/webhook-endpoints/{id}API key, scope webhook_mgmt
Endpoints are disabled, never deleted — their delivery history has to stay auditable. The response is the endpoint with status: "disabled".

Defined in 06 H.6

Parameters

NameInTypeDescription
idpath · requiredstring (whk_…)The endpoint id.

Request headers

HeaderRequiredMeaning
Kytrix-Key-IdyesYour API key id (kx_test_…/kx_live_…).
Kytrix-TimestampyesUnix seconds, decimal. ±300 s of KYTRIX time.
Kytrix-Nonceyes1–64 chars of A-Za-z0-9._~-, single-use for 600 s per key.
Kytrix-Signatureyesv1=<lower-hex HMAC-SHA256> over the canonical string.

Responses

StatusBodyWhen
200WebhookEndpointThe disabled endpoint.
401ProblemDetailsSignature missing, wrong, stale or replayed.
403ProblemDetailsThe credential lacks the webhook_mgmt scope.
404ProblemDetailsNo such endpoint in this tenant.

Registered in packages/webhooks/src/routes.ts — the build fails if this path is no longer there.

POST/v1/webhook-endpoints/{id}/rotate-secretAPI key, scope webhook_mgmt
Issues a new secret and keeps the previous one valid until previous_secret_valid_until. During the overlap KYTRIX signs every delivery with both secrets (two v1= entries), so a receiver that verifies against [new, previous] never drops a message.

Defined in 06 H.6

Parameters

NameInTypeDescription
idpath · requiredstring (whk_…)The endpoint id.

Request headers

HeaderRequiredMeaning
Kytrix-Key-IdyesYour API key id (kx_test_…/kx_live_…).
Kytrix-TimestampyesUnix seconds, decimal. ±300 s of KYTRIX time.
Kytrix-Nonceyes1–64 chars of A-Za-z0-9._~-, single-use for 600 s per key.
Kytrix-Signatureyesv1=<lower-hex HMAC-SHA256> over the canonical string.

Responses

StatusBodyWhen
200inline schema (below)Rotated. The new secret is present here and never again.
401ProblemDetailsSignature missing, wrong, stale or replayed.
403ProblemDetailsThe credential lacks the webhook_mgmt scope.
404ProblemDetailsNo such endpoint in this tenant.

200 body

  • endpoint_id string required pattern ^whk_[0-9A-Za-z-]{8,64}$
  • tenant_id string (uuid) required
  • url string (uri) required
  • channel "compliance" | "operational" required
  • event_types string[] required
  • status "active" | "disabled" required
  • created_at string (date-time) required
  • secret string required base64url, 32 bytes. Store it in your secret manager immediately.
  • previous_secret_valid_until string (date-time) required Until this instant BOTH secrets sign every delivery, so verify with both. The overlap is the webhooks package `secret_rotation_overlap_hours` config surface.

Registered in packages/webhooks/src/routes.ts — the build fails if this path is no longer there.

POST/v1/webhook-endpoints/{id}/testAPI key, scope webhook_mgmt
Delivers a real, signed webhook.test envelope through the ordinary delivery path — same envelope builder, same signature, same SSRF checks, same delivery row. A test that took a shortcut would verify nothing. webhook.test is classified operational and is deliverable on both channels, so it can verify a compliance endpoint without carrying AML content.

Defined in 06 H.6 · A10

Parameters

NameInTypeDescription
idpath · requiredstring (whk_…)The endpoint id.

Request headers

HeaderRequiredMeaning
Kytrix-Key-IdyesYour API key id (kx_test_…/kx_live_…).
Kytrix-TimestampyesUnix seconds, decimal. ±300 s of KYTRIX time.
Kytrix-Nonceyes1–64 chars of A-Za-z0-9._~-, single-use for 600 s per key.
Kytrix-Signatureyesv1=<lower-hex HMAC-SHA256> over the canonical string.

Responses

StatusBodyWhen
200inline schema (below)Your endpoint answered 2xx (verified: true).
502inline schema (below)The delivery attempt did not succeed. The body is the same TestEventResult, with the status code or transport reason — the failure is reported, never hidden.
401ProblemDetailsSignature missing, wrong, stale or replayed.
403ProblemDetailsThe credential lacks the webhook_mgmt scope.
404ProblemDetailsNo such endpoint in this tenant.

200 body

  • endpoint_id string required
  • delivery_id string required nullable
  • event_id string required
  • disposition "delivered" | "retry_scheduled" | "dead" | "skipped_duplicate" | "not_deliverable" required
  • response_status integer required nullable
  • error string required nullable
  • verified boolean required true when your endpoint answered 2xx.

502 body

  • endpoint_id string required
  • delivery_id string required nullable
  • event_id string required
  • disposition "delivered" | "retry_scheduled" | "dead" | "skipped_duplicate" | "not_deliverable" required
  • response_status integer required nullable
  • error string required nullable
  • verified boolean required true when your endpoint answered 2xx.

Notes

  • KNOWN DEPLOYMENT LIMITATION: this is the only route on the API host that makes an outbound request, and in the current VPS deployment the edge container is attached to the internal network only. Until egress is granted it fails visibly with a transport error (502). Documented in apps/edge/src/app.ts; verify signatures against the published webhook test vectors in the meantime.

Registered in packages/webhooks/src/routes.ts — the build fails if this path is no longer there.

GET/v1/webhook-deliveriesAPI key, scope webhook_mgmt
Every attempt, newest first, with response status, latency and the next scheduled retry. Keyset-paginated over (created_at, delivery_id), so the page is stable under concurrent writes.

Defined in 06 H.6

Parameters

NameInTypeDescription
endpointquerystring (whk_…)Filter to one endpoint.
statusquerypending \| in_flight \| delivered \| failed \| deadDelivery status filter.
event_typequeryWebhookEventTypeFilter to one event type.
cursorquerystring (≤512)Opaque cursor from next_cursor.
limitqueryinteger (1–200, default 50)Page size.

Request headers

HeaderRequiredMeaning
Kytrix-Key-IdyesYour API key id (kx_test_…/kx_live_…).
Kytrix-TimestampyesUnix seconds, decimal. ±300 s of KYTRIX time.
Kytrix-Nonceyes1–64 chars of A-Za-z0-9._~-, single-use for 600 s per key.
Kytrix-Signatureyesv1=<lower-hex HMAC-SHA256> over the canonical string.

Responses

StatusBodyWhen
200inline schema (below){ deliveries: WebhookDelivery[], next_cursor: string | null }.
400ProblemDetailsMalformed cursor or query parameter.
401ProblemDetailsSignature missing, wrong, stale or replayed.
403ProblemDetailsThe credential lacks the webhook_mgmt scope.

200 body

  • delivery_id string required
  • endpoint_id string required pattern ^whk_[0-9A-Za-z-]{8,64}$
  • event_id string required
  • event_type string required
  • classification "restricted_aml" | "advisory" | "operational" required
  • attempt integer required
  • status "pending" | "in_flight" | "delivered" | "failed" | "dead" required
  • response_status integer required nullable
  • latency_ms integer required nullable
  • error string required nullable
  • next_attempt_at string (date-time) required nullable
  • created_at string (date-time) required
  • delivered_at string (date-time) required nullable

Registered in packages/webhooks/src/routes.ts — the build fails if this path is no longer there.

POST/v1/webhook-deliveries/{id}/replayAPI key, scope webhook_mgmt
Rebuilds the same envelope id from the retained source and delivers it again. Your consumer will see a duplicate id — dedupe on it, do not treat it as new.

Defined in 06 H.6

Parameters

NameInTypeDescription
idpath · requiredstringThe delivery id to replay.

Request headers

HeaderRequiredMeaning
Kytrix-Key-IdyesYour API key id (kx_test_…/kx_live_…).
Kytrix-TimestampyesUnix seconds, decimal. ±300 s of KYTRIX time.
Kytrix-Nonceyes1–64 chars of A-Za-z0-9._~-, single-use for 600 s per key.
Kytrix-Signatureyesv1=<lower-hex HMAC-SHA256> over the canonical string.

Responses

StatusBodyWhen
202{ replay_of, delivery_id, event_id, disposition, response_status, error } (mirrored from ReplayResult in packages/webhooks/src/replay.ts).
401ProblemDetailsSignature missing, wrong, stale or replayed.
403ProblemDetailsThe credential lacks the webhook_mgmt scope.
404ProblemDetailsUnknown delivery, an endpoint that no longer exists, or a source payload that is no longer retained.

Registered in packages/webhooks/src/routes.ts — the build fails if this path is no longer there.

#Reconciliation

Customer-declared control totals, compared against what KYTRIX observed.

POST/v1/reconciliationsAPI key, scope ingest
Declare what you believe you sent for a feed and a window — record count and/or monetary total. KYTRIX compares it against what it observed in the next coverage sweep; a delta opens a coverage finding rather than silently disappearing. This is how a silently broken feed gets noticed.

Defined in 17 S.2 · 22 X.2

Request headers

HeaderRequiredMeaning
Kytrix-Key-IdyesYour API key id (kx_test_…/kx_live_…).
Kytrix-TimestampyesUnix seconds, decimal. ±300 s of KYTRIX time.
Kytrix-Nonceyes1–64 chars of A-Za-z0-9._~-, single-use for 600 s per key.
Kytrix-Signatureyesv1=<lower-hex HMAC-SHA256> over the canonical string.

Request body

  • feed string required 1–200 chars
  • object_type "party" | "account" | "instrument" | "movement" | "relationship" | "control_event" optional
  • window_from string (date-time) required
  • window_to string (date-time) required
  • record_count integer optional ≥ 0
  • monetary_total Money optional
  • No other members are accepted — an unrecognised key is a reject, not a dropped field.
json — request
{
  "feed": "core-banking-movements",
  "object_type": "movement",
  "window_from": "2026-09-04T00:00:00-04:00",
  "window_to": "2026-09-05T00:00:00-04:00",
  "record_count": 184233,
  "monetary_total": {
    "value": "918442300",
    "asset": "DOP",
    "scale": 2
  }
}

Responses

StatusBodyWhen
202inline schema (below)Recorded; reconciliation happens in the next coverage sweep.
400ProblemDetailsBody does not match the schema.
401ProblemDetailsSignature missing, wrong, stale or replayed.
403ProblemDetailsThe credential lacks the ingest scope.

202 body

  • submission_id string required
  • feed string required
  • window_from string (date-time) required
  • window_to string (date-time) required
  • status string required
  • detail string required

Notes

  • monetary_total is Money — an integer string of minor units, like every other amount.

Registered in packages/coverage/src/routes.ts — the build fails if this path is no longer there.

#Historical import (backfill)

The Backfill stage, not the Connect stage. Rows are committed by the same acceptEvents transaction the live edge uses.

POST/v1/importsAPI key, scope ingest
Opens an import for a mapping and a family. The rest of the flow is chunk upload (PUT /v1/imports/{id}/chunks/{n}), dry-run, submit, and the reject report. Every imported row is committed by the same acceptEvents transaction the live edge uses — there is no second ingestion path. See the field-mapping worksheet for the mapping file.

Defined in 05 G.7 · 22 X.2

Request headers

HeaderRequiredMeaning
Kytrix-Key-IdyesYour API key id (kx_test_…/kx_live_…).
Kytrix-TimestampyesUnix seconds, decimal. ±300 s of KYTRIX time.
Kytrix-Nonceyes1–64 chars of A-Za-z0-9._~-, single-use for 600 s per key.
Kytrix-Signatureyesv1=<lower-hex HMAC-SHA256> over the canonical string.

Request body

{ mapping, mode?, dry_run?, declared_control_totals?, go_live_watermark_at? } — see packages/imports/README.md for every field and the full endpoint list.

Responses

StatusBodyWhen
201The import view (status open).
400ProblemDetailsThe mapping is invalid — unknown target paths are named, with the nearby real fields.
401ProblemDetailsSignature missing, wrong, stale or replayed.
403ProblemDetailsThe credential lacks the ingest scope.

Notes

  • The import surface is documented here as a pointer, not in full: it is the Backfill stage, not the Connect gate, the SDK has no typed methods for it yet, and the conformance CLI does not certify it. packages/imports/README.md is the reference.

Registered in packages/imports/src/routes.ts — the build fails if this path is no longer there.

#Health

Unauthenticated liveness of the API host.

GET/healthzNone — unauthenticated
Answers 2xx once the database and Redis are reachable. Do not sign this request.

Defined in 20 V

Responses

StatusBodyWhen
200HealthzResponseHealthy.
503A dependency is unreachable.
json — 200 response
{
  "status": "ok",
  "checks": {
    "db": true,
    "redis": true
  }
}

Registered in apps/edge/src/health.ts — the build fails if this path is no longer there.

#Schemas

Generated from @kytrix/contracts. required marks a member the schema demands; everything else is optional. Constraints are the zod checks themselves — a pattern here is the exact regular expression the server applies.

#AccountEvent

Generated from packages/contracts/src/entities/account.ts

Type: object

  • external_id string required 1–256 chars
  • schema_version "2026-09-01" required
  • occurred_at string (date-time) required
  • recorded_at string (date-time) optional
  • sequence integer optional ≥ 0
  • data object required
    • type "wallet" | "checking" | "savings" | "merchant" | "settlement" | "stored_value" | "loan" | "remittance" | "other" required
    • holder_party_id string required 1–256 chars
    • asset string required pattern ^[A-Z]{3}$|^X-[A-Z0-9]{2,10}$
    • status "active" | "dormant" | "closed" | "restricted" | "frozen" | "unrestricted" | "withdrawals_restricted" | "account_restricted" | "transfers_suspended" required
    • opened_at string (date-time) required
    • product_code string optional 1–64 chars
    • purpose string optional max 256 chars
    • expected_use string optional max 512 chars
    • agent_of_opening_party_id string optional 1–256 chars
    • jurisdiction string optional pattern ^[A-Z]{2}$
    • closed_at string (date-time) optional
    • closure_reason string optional max 256 chars
    • external_ref string optional 1–256 chars
    • correction_reason string optional max 256 chars
    • No other members are accepted — an unrecognised key is a reject, not a dropped field.
  • No other members are accepted — an unrecognised key is a reject, not a dropped field.

#Alert

Generated from packages/contracts/src/alerts.ts

Type: object

  • alert_id string required pattern ^alrt_[0-9A-Za-z-]{8,64}$
  • tenant_id string (uuid) required
  • subject EntityRef required
  • entity_refs EntityRef[] required
  • detectors object[] required min 1 items
    • array of
      • id string required pattern ^[A-Z]{3}-\d{3}$
      • version string required pattern ^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?$
  • signal_ids string[] required min 1 items
  • episode_id string optional pattern ^epsd_[0-9A-Za-z-]{8,64}$
  • family "AMT" | "VEL" | "STR" | "FAN" | "FLW" | "NID" | "BCH" | "CTL" required
  • typology string required 1–128 chars
  • severity "low" | "medium" | "high" | "critical" required
  • confidence object required
    • band "low" | "medium" | "high" required
    • factors object[] required
      • array of
        • factor string required 1–128 chars
        • weight number required ≥ 0 · ≤ 1
        • note string optional max 500 chars
  • evidence_bundle_hash string required pattern ^sha256:[0-9a-f]{64}$
  • movement_refs string[] required
  • control_event_refs string[] required
  • graph_paths object[] optional
    • array of
      • nodes object[] required min 1 items
        • array of
          • kind "party" | "account" | "instrument" | "device" | "external_entity" required
          • ref string required min 1 chars
      • edges object[] required
        • array of
          • type "movement" | "ownership" | "authorized_control" | "beneficial_ownership" | "shared_instrument" | "shared_device" | "shared_identifier" | "referral" | "onboarding" | "counterparty" | "same_as" required
          • from_idx integer required ≥ 0
          • to_idx integer required ≥ 0
          • occurred_at string (date-time) optional
          • amount Money optional
          • confidence number optional ≥ 0 · ≤ 1
          • reliability_class "high" | "medium" | "low" optional
          • supporting_refs string[] optional
      • span_seconds integer optional ≥ 0
      • total_amount Money optional
  • observed_values object required
  • thresholds object required
  • ruleset_version_id string required pattern ^rsv_[0-9A-Za-z-]{8,64}$
  • coverage_stamp object required
    • state "complete" | "degraded" | "unknown" required
    • detail string optional max 1000 chars
  • occurred_at string (date-time) required
  • evaluated_at string (date-time) required
  • opened_at string (date-time) required
  • recommended_action object optional
    • code "review_withdrawal_capability" | "review_account" | "enhanced_due_diligence" | "monitor" required
    • severity "low" | "medium" | "high" | "critical" required
  • classification "restricted_aml" required

#BatchRequest

Generated from packages/contracts/src/events.ts

Type: object

  • events one of 6[] required min 1 items · max 1000 items
  • No other members are accepted — an unrecognised key is a reject, not a dropped field.

#BatchResponse

Generated from packages/contracts/src/events.ts

Type: object

  • accepted integer required ≥ 0
  • rejected integer required ≥ 0
  • results one of 2[] required

#ControlEventEvent

Generated from packages/contracts/src/entities/control-event.ts

Type: object

  • external_id string required 1–256 chars
  • schema_version "2026-09-01" required
  • occurred_at string (date-time) required
  • recorded_at string (date-time) optional
  • sequence integer optional ≥ 0
  • data object required
    • type "transfer.failed" | "withdrawal.blocked" | "velocity_check.failed" | "sanctions.hit_reported" | "screening.status_changed" | "kyc.upgraded" | "kyc.downgraded" | "kyc.verification_failed" | "auth.failed" | "auth.suspicious_login" | "device.changed" | "password.reset" | "beneficiary.added" | "account.restricted_by_customer" | "transfer.retried_repeatedly" | "limit.changed" required
    • subject EntityRef required
    • outcome string optional 1–64 chars
    • reason_code string optional 1–128 chars
    • device_ref string optional 1–256 chars
    • ip string (ip) optional
    • actor_kind "customer_system" | "end_user" optional
    • customer_severity string optional 1–32 chars
    • linked_movement_id string optional 1–256 chars
    • external_ref string optional 1–256 chars
    • No other members are accepted — an unrecognised key is a reject, not a dropped field.
  • No other members are accepted — an unrecognised key is a reject, not a dropped field.

#EntityRef

Generated from packages/contracts/src/envelope.ts

Type: object

  • kind "party" | "account" | "instrument" required
  • external_id string required 1–256 chars
  • No other members are accepted — an unrecognised key is a reject, not a dropped field.

#Episode

Generated from packages/contracts/src/episodes.ts

Type: object

  • episode_id string required pattern ^epsd_[0-9A-Za-z-]{8,64}$
  • tenant_id string (uuid) required
  • key object required
    • subject EntityRef required
    • typology_family "AMT" | "VEL" | "STR" | "FAN" | "FLW" | "NID" | "BCH" | "CTL" required
    • correlation_group_id string required min 1 chars
  • state "opened" | "updated" | "materially_escalated" | "linked_to_case" | "resolved" | "reopened" | "continuing_activity" required
  • severity "low" | "medium" | "high" | "critical" required
  • member_signal_ids string[] required
  • alert_ids string[] required
  • escalation_events object[] required
    • array of
      • trigger "severity_increase" | "participant_threshold" | "new_signal_family" | "cash_exit" | "amount_band_jump" required
      • at string (date-time) required
      • delta object required
  • case_id string optional pattern ^case_[0-9A-Za-z-]{8,64}$
  • continuation_window one of 3 required
    • One of the following, selected by kind.
    • kind: "rolling"
      • kind "rolling" required
      • duration string required pattern ^P(?!$)(?:\d+Y)?(?:\d+M)?(?:\d+W)?(?:\d+D)?(?:T(?=\d)(?:\d+H)?(?:\d+M)?(?:\d+(?:\.\d+)?S)?)?$
      • No other members are accepted — an unrecognised key is a reject, not a dropped field.
    • kind: "calendar"
      • kind "calendar" required
      • unit "day" | "month" required
      • tz string required min 1 chars
      • No other members are accepted — an unrecognised key is a reject, not a dropped field.
    • kind: "business"
      • kind "business" required
      • calendar_id string required 1–128 chars
      • No other members are accepted — an unrecognised key is a reject, not a dropped field.
  • opened_at string (date-time) required
  • last_activity_at string (date-time) required
  • resolved_at string (date-time) optional
  • reopened_from object optional
    • resolved_at string (date-time) required
    • case_id string optional pattern ^case_[0-9A-Za-z-]{8,64}$
  • classification "restricted_aml" required

#FieldError

Generated from packages/contracts/src/errors.ts

Type: object

  • path string required
  • code "kytrix:validation/schema" | "kytrix:validation/pan_detected" | "kytrix:validation/money_scale_mismatch" | "kytrix:validation/timestamp_out_of_range" | "kytrix:validation/unsupported_schema_version" | "kytrix:idempotency/key_conflict" | "kytrix:idempotency/in_flight" | "kytrix:auth/invalid_signature" | "kytrix:auth/timestamp_skew" | "kytrix:auth/nonce_replayed" | "kytrix:auth/unknown_key" | "kytrix:auth/key_revoked" | "kytrix:auth/key_expired" | "kytrix:auth/insufficient_scope" | "kytrix:auth/unauthenticated" | "kytrix:authz/forbidden" | "kytrix:reference/unknown_party" | "kytrix:reference/unknown_account" | "kytrix:reference/unknown_instrument" | "kytrix:reference/unknown_movement" | "kytrix:sequence/conflict" | "kytrix:sequence/stale" | "kytrix:rate_limit/exceeded" | "kytrix:batch/too_large" | "kytrix:import/invalid_chunk" | "kytrix:import/not_found" | "kytrix:governance/approval_required" | "kytrix:governance/maker_checker_violation" | "kytrix:governance/immutable" | "kytrix:governance/invalid_transition" | "kytrix:tenant/unknown" | "kytrix:tenant/suspended" | "kytrix:webhook/invalid_endpoint" | "kytrix:request/payload_too_large" | "kytrix:conflict/resource" | "kytrix:not_found/resource" | "kytrix:service/unavailable" | "kytrix:internal/error" required
  • message string required

#HealthzResponse

Generated from packages/contracts/src/api.ts

Type: object

  • status "ok" | "degraded" required
  • checks object required

#IngestReceipt

Generated from packages/contracts/src/events.ts

Type: object

  • event_id string required pattern ^evt_[0-9A-Za-z-]{8,64}$
  • object "party" | "account" | "instrument" | "movement" | "relationship" | "control_event" required
  • external_id string required 1–256 chars
  • received_at string (date-time) required
  • idempotent_replay boolean required
  • import_id string optional pattern ^imp_[0-9A-Za-z-]{8,64}$

#InstrumentEvent

Generated from packages/contracts/src/entities/instrument.ts

Type: object

  • external_id string required 1–256 chars
  • schema_version "2026-09-01" required
  • occurred_at string (date-time) required
  • recorded_at string (date-time) optional
  • sequence integer optional ≥ 0
  • data object required
    • type "external_bank_account" | "card_token" | "mobile_wallet" | "payment_credential" | "virtual_account" | "other" required
    • status string required 1–64 chars
    • first_seen_at string (date-time) required
    • issuer_ref string optional 1–128 chars
    • masked_identifier string optional 1–64 chars
    • country string optional pattern ^[A-Z]{2}$
    • holder_name_as_entered string optional max 256 chars
    • verification_status "verified" | "unverified" | "pending" | "failed" optional
    • fingerprint string optional 1–128 chars
    • external_ref string optional 1–256 chars
    • No other members are accepted — an unrecognised key is a reject, not a dropped field.
  • No other members are accepted — an unrecognised key is a reject, not a dropped field.

#Money

Generated from packages/contracts/src/money.ts

Type: object

  • value string required pattern ^-?\d+$
  • asset string required pattern ^[A-Z]{3}$|^X-[A-Z0-9]{2,10}$
  • scale integer required ≥ 0 · ≤ 18

#MovementEvent

Generated from packages/contracts/src/entities/movement.ts

Type: object

  • external_id string required 1–256 chars
  • schema_version "2026-09-01" required
  • occurred_at string (date-time) required
  • recorded_at string (date-time) optional
  • sequence integer optional ≥ 0
  • data object required
    • type "p2p" | "merchant_payment" | "deposit" | "withdrawal" | "cash_in" | "cash_out" | "remittance" | "ach" | "wire" | "refund" | "reversal" | "settlement" | "internal_transfer" | "other" required
    • status "completed" | "pending" | "failed" | "reversed" required
    • amount Money required
    • debit object required
      • account_id string optional 1–256 chars
      • party_id string optional 1–256 chars
      • counterparty object optional
        • institution string optional 1–128 chars
        • country string optional pattern ^[A-Z]{2}$
        • name_as_given string optional max 256 chars
        • instrument_ref string optional 1–256 chars
        • No other members are accepted — an unrecognised key is a reject, not a dropped field.
      • No other members are accepted — an unrecognised key is a reject, not a dropped field.
    • credit object required
      • account_id string optional 1–256 chars
      • party_id string optional 1–256 chars
      • counterparty object optional
        • institution string optional 1–128 chars
        • country string optional pattern ^[A-Z]{2}$
        • name_as_given string optional max 256 chars
        • instrument_ref string optional 1–256 chars
        • No other members are accepted — an unrecognised key is a reject, not a dropped field.
      • No other members are accepted — an unrecognised key is a reject, not a dropped field.
    • links object optional
      • reverses string optional 1–256 chars
      • refunds string optional 1–256 chars
      • settles string optional 1–256 chars
      • part_of_batch string optional 1–256 chars
      • correction_of string optional 1–256 chars
      • No other members are accepted — an unrecognised key is a reject, not a dropped field.
    • cash boolean optional
    • memo string optional max 1000 chars
    • channel string optional 1–64 chars
    • rail string optional 1–64 chars
    • product string optional 1–64 chars
    • agent_party_id string optional 1–256 chars
    • instrument_ref string optional 1–256 chars
    • device_ref string optional 1–256 chars
    • ip string (ip) optional
    • geo object optional
      • country string optional pattern ^[A-Z]{2}$
      • region string optional max 128 chars
      • lat number optional ≥ -90 · ≤ 90
      • lon number optional ≥ -180 · ≤ 180
      • No other members are accepted — an unrecognised key is a reject, not a dropped field.
    • fees Money[] optional max 20 items
    • external_ref string optional 1–256 chars
    • correction_reason string optional max 256 chars
    • No other members are accepted — an unrecognised key is a reject, not a dropped field.
  • No other members are accepted — an unrecognised key is a reject, not a dropped field.

#PartyEvent

Generated from packages/contracts/src/entities/party.ts

Type: object

  • external_id string required 1–256 chars
  • schema_version "2026-09-01" required
  • occurred_at string (date-time) required
  • recorded_at string (date-time) optional
  • sequence integer optional ≥ 0
  • data object required
    • type "person" | "business" | "merchant" | "agent" | "financial_institution" | "internal" | "government" | "ngo" | "trust" | "other" required
    • status string required 1–64 chars
    • created_at string (date-time) required
    • legal_names string[] optional max 20 items
    • birth_or_inc_date string (date) optional
    • identifiers object[] optional max 50 items
      • array of
        • type "national_id" | "tax_id" | "passport" | "phone" | "email" | "other" required
        • value string required 1–256 chars
        • verification_status "verified" | "unverified" | "pending" | "failed" optional
        • No other members are accepted — an unrecognised key is a reject, not a dropped field.
    • jurisdictions object optional
      • nationality string optional pattern ^[A-Z]{2}$
      • residency string optional pattern ^[A-Z]{2}$
      • registration string optional pattern ^[A-Z]{2}$
      • No other members are accepted — an unrecognised key is a reject, not a dropped field.
    • addresses object[] optional max 20 items
      • array of
        • line1 string optional max 256 chars
        • line2 string optional max 256 chars
        • city string optional max 128 chars
        • region string optional max 128 chars
        • postal_code string optional max 32 chars
        • country string optional pattern ^[A-Z]{2}$
        • No other members are accepted — an unrecognised key is a reject, not a dropped field.
    • occupation string optional max 256 chars
    • industry_code string optional max 32 chars
    • expected_profile object optional
      • declared_monthly_volume Money optional
      • corridors string[] optional max 50 items
      • cash_intensity "low" | "medium" | "high" optional
      • No other members are accepted — an unrecognised key is a reject, not a dropped field.
    • kyc object optional
      • tier string optional 1–64 chars
      • updated_at string (date-time) optional
      • No other members are accepted — an unrecognised key is a reject, not a dropped field.
    • screening_status_reported object optional
      • status "clear" | "potential_match" | "confirmed_match" | "pending" | "unknown" required
      • pep boolean optional
      • as_of string (date-time) optional
      • provider string optional max 128 chars
      • No other members are accepted — an unrecognised key is a reject, not a dropped field.
    • segment "consumer" | "merchant" | "remittance_agent" | "cash_agent" | "payroll" | "marketplace" | "financial_institution" | "other" optional
    • external_ref string optional 1–256 chars
    • correction_reason string optional max 256 chars
    • No other members are accepted — an unrecognised key is a reject, not a dropped field.
  • No other members are accepted — an unrecognised key is a reject, not a dropped field.

#PostureResponse

Generated from packages/contracts/src/risk.ts

Type: object

  • subject EntityRef required
  • risk_state "clear" | "watch" | "elevated" | "critical" | "unknown" | "incomplete" required
  • as_of string (date-time) required
  • freshness_seconds integer required ≥ 0
  • coverage_state "complete" | "degraded" | "unknown" required
  • No other members are accepted — an unrecognised key is a reject, not a dropped field.

#ProblemDetails

Generated from packages/contracts/src/errors.ts

Type: object

  • type string required
  • title string required
  • status integer required ≥ 100 · ≤ 599
  • detail string optional
  • instance string optional
  • code "kytrix:validation/schema" | "kytrix:validation/pan_detected" | "kytrix:validation/money_scale_mismatch" | "kytrix:validation/timestamp_out_of_range" | "kytrix:validation/unsupported_schema_version" | "kytrix:idempotency/key_conflict" | "kytrix:idempotency/in_flight" | "kytrix:auth/invalid_signature" | "kytrix:auth/timestamp_skew" | "kytrix:auth/nonce_replayed" | "kytrix:auth/unknown_key" | "kytrix:auth/key_revoked" | "kytrix:auth/key_expired" | "kytrix:auth/insufficient_scope" | "kytrix:auth/unauthenticated" | "kytrix:authz/forbidden" | "kytrix:reference/unknown_party" | "kytrix:reference/unknown_account" | "kytrix:reference/unknown_instrument" | "kytrix:reference/unknown_movement" | "kytrix:sequence/conflict" | "kytrix:sequence/stale" | "kytrix:rate_limit/exceeded" | "kytrix:batch/too_large" | "kytrix:import/invalid_chunk" | "kytrix:import/not_found" | "kytrix:governance/approval_required" | "kytrix:governance/maker_checker_violation" | "kytrix:governance/immutable" | "kytrix:governance/invalid_transition" | "kytrix:tenant/unknown" | "kytrix:tenant/suspended" | "kytrix:webhook/invalid_endpoint" | "kytrix:request/payload_too_large" | "kytrix:conflict/resource" | "kytrix:not_found/resource" | "kytrix:service/unavailable" | "kytrix:internal/error" required
  • errors FieldError[] optional
  • correlation_id string optional

#RelationshipEvent

Generated from packages/contracts/src/entities/relationship.ts

Type: object

  • external_id string required 1–256 chars
  • schema_version "2026-09-01" required
  • occurred_at string (date-time) required
  • recorded_at string (date-time) optional
  • sequence integer optional ≥ 0
  • data object required
    • type "beneficial_owner" | "authorized_signer" | "joint_holder" | "employer" | "household_member" | "parent_org" | "merchant_owner" | "agent" | "referral" | "onboarding" | "owns_instrument" | "funds_account" | "same_as" | "other" required
    • from EntityRef required
    • to EntityRef required
    • valid_from string (date-time) required
    • valid_to string (date-time) optional
    • ownership_pct number optional ≥ 0 · ≤ 100
    • source "kyc" | "declared" | "derived_by_customer" optional
    • confidence number optional ≥ 0 · ≤ 1
    • external_ref string optional 1–256 chars
    • correction_reason string optional max 256 chars
    • No other members are accepted — an unrecognised key is a reject, not a dropped field.
  • No other members are accepted — an unrecognised key is a reject, not a dropped field.

#WebhookEndpoint

Generated from packages/contracts/src/webhooks.ts

Type: object

  • endpoint_id string required pattern ^whk_[0-9A-Za-z-]{8,64}$
  • tenant_id string (uuid) required
  • url string (uri) required pattern ^https://
  • channel "compliance" | "operational" required
  • event_types "alert.opened" | "episode.updated" | "episode.escalated" | "episode.resolved" | "episode.reopened" | "case.status_changed" | "case.disposed" | "risk_state.changed" | "recommendation.issued" | "coverage.degraded" | "coverage.recovered" | "import.progress" | "import.completed" | "import.failed" | "ruleset.candidate_ready" | "ruleset.activated" | "ruleset.rolled_back" | "conformance.drift_detected" | "webhook.test"[] required
  • status "active" | "disabled" required
  • created_at string (date-time) required

#WebhookEnvelope

Generated from packages/contracts/src/webhooks.ts

Type: object

  • id string required pattern ^whe_[0-9A-Za-z-]{8,64}$
  • type "alert.opened" | "episode.updated" | "episode.escalated" | "episode.resolved" | "episode.reopened" | "case.status_changed" | "case.disposed" | "risk_state.changed" | "recommendation.issued" | "coverage.degraded" | "coverage.recovered" | "import.progress" | "import.completed" | "import.failed" | "ruleset.candidate_ready" | "ruleset.activated" | "ruleset.rolled_back" | "conformance.drift_detected" | "webhook.test" required
  • classification "restricted_aml" | "advisory" | "operational" required
  • created_at string (date-time) required
  • tenant_id string (uuid) required
  • data object required
  • delivery object optional
    • attempt integer required ≥ 1
    • delivery_id string required pattern ^whd_[0-9A-Za-z-]{8,64}$

#OpenAPI document

The machine-readable form of everything above lives at /openapi.json. It is regenerated by the same build, from the same schemas.

bash
# generate a typed client
npx openapi-typescript https://docs.<your-domain>/openapi.json -o kytrix-api.d.ts

# or import it into Postman / Insomnia / Bruno directly