Skip to content

Event schema (for engineers)

Every accepted transaction becomes one TxnIngested message on a Kafka (or Redpanda) topic, and every account event one AccountEvent message. SpeculaPro’s own components read them: the persister writes them to the database, and the stream evaluator runs your rules. This page is for engineers who operate SpeculaPro, inspect the queue while troubleshooting, or build an approved consumer of their own.

You don’t need this page to send data.

Topic <prefix>txn.ingested, which is sp.txn.ingested with the default prefix (SP_KAFKA_TOPIC_PREFIX)
Dead-letter topic <prefix>txn.ingested.dlq: messages the persister couldn’t read are moved here instead of blocking the topic
Key <tenant_id>:<account_external_id>, or <tenant_id>:c:<customer_external_id> when the transaction has no account
Format JSON, UTF-8
Delivery Written with acknowledgement from all in-sync replicas before ingest answers accepted

Because the key is the account, all transactions for one account land on the same partition, in the order they were accepted. Consumers can therefore keep per-account state without locks.

Field Type Always present Meaning
v integer Yes Schema version: 1
id UUID (v7) Yes SpeculaPro’s id for the transaction. Stable across retries
tenant_id UUID Yes The workspace
external_id string Yes The sender’s reference
occurred_at date-time, UTC Yes When it happened at the institution
received_at date-time, UTC Yes When SpeculaPro accepted it
customer_external_id string Yes
customer_type string No individual · business, if sent
account_external_id string Yes (may be empty)
account_type string No If sent
direction string Yes in · out · internal
type string Yes As sent, e.g. cash_in
channel string Yes As sent, e.g. agent
status string Yes Defaults to completed
amount_minor integer Yes Original amount in minor units
currency string Yes Original currency
amount_base_minor integer Yes Amount in the workspace base currency, minor units
base_currency string Yes
fx_rate string Yes Rate used (base per 1 unit of currency); "1" when no conversion
country, counterparty_country string No 2-letter codes
mcc string No
ip string No
geo_lat, geo_lon number No
reference, description string No As sent
device_id string No Keyed hash (hex) of the device_fingerprint you sent, unique to the workspace. The fingerprint itself never reaches the queue
session_id string No As sent
counterparty object No See below. Present only if a name or identifier was sent
attributes object No The sender’s custom fields, as sent
decision object No Present only when the transaction came through the real-time decision API: the decision’s id, time, outcome (allow, review, block or step_up), score, whether it was degraded, the live and shadow rules that fired, and the feature values they read
payload_hash bytes (base64) No On decided transactions only: a fingerprint of the request, so a later resend of the same transaction through the ingest API is recognised
Field Type Meaning
fingerprint bytes (base64) Keyed fingerprint of the normalised name, identifier and provider. Identifies the counterparty within the workspace
name string As sent, trimmed
identifier_mask string Display form, e.g. +233•••••••67
identifier_hash bytes (base64) Keyed fingerprint of the identifier, comparable with the workspace’s account identifiers
identifier_enc bytes (base64) The identifier, encrypted with the workspace’s key
provider string Lower-cased
country, type string As sent
{
"v": 1,
"id": "0199c8f2-41a7-7d02-b3e5-20c7d8e9fa13",
"tenant_id": "0199a1c0-5e2d-7b44-9a10-6f3c2e1d0b9a",
"external_id": "MOMO-2026-10-09-000184977",
"occurred_at": "2026-10-09T08:31:40Z",
"received_at": "2026-10-09T08:31:41.207Z",
"customer_external_id": "CUST-77120",
"account_external_id": "WAL-0244123456",
"direction": "out",
"type": "transfer",
"channel": "momo",
"status": "completed",
"amount_minor": 120000,
"currency": "GHS",
"amount_base_minor": 120000,
"base_currency": "GHS",
"fx_rate": "1",
"counterparty_country": "GH",
"reference": "School fees",
"device_id": "5eda377d7dcc41f0a9e2c7b6d58a1f3e9c0b2d4a6e8f1a3c5b7d9e0f2a4c6b8d",
"counterparty": {
"fingerprint": "q1v8…",
"name": "Kwame Boateng",
"identifier_mask": "+233•••••••67",
"identifier_hash": "Xo3N…",
"identifier_enc": "AQGx…",
"provider": "telecel_cash",
"country": "GH",
"type": "individual"
}
}

Every accepted account event becomes one AccountEvent message.

Topic <prefix>account.events, which is sp.account.events with the default prefix
Dead-letter topic <prefix>account.events.dlq, written by the persister
Key The same as for transactions: <tenant_id>:<account_external_id>, or <tenant_id>:c:<customer_external_id> when the event has no account
Read by The persister, which stores it, and the stream evaluator, which counts it for rules
Field Type Always present Meaning
v integer Yes Schema version: 1
id UUID Yes SpeculaPro’s id for the event, derived from the workspace and external_id, so a resend carries the same id
tenant_id UUID Yes The workspace
external_id string Yes The sender’s reference
customer_external_id string Yes
account_external_id string No If sent
type string Yes For example sim_swap
occurred_at, received_at date-time, UTC Yes When it happened, and when SpeculaPro accepted it
channel, session_id, ip, geo_lat, geo_lon, success No As sent
device_id string No Keyed hash of the device fingerprint, as for transactions
attributes object No As sent

Because the key matches the transactions’ key, an account’s events and transactions share a partition and keep their order.

Counterparty account and wallet numbers are encrypted before the message is written. Anyone with access to the topic sees the masked form and fingerprints, never the number itself. Decryption needs the workspace’s key, which only the SpeculaPro application holds.

  • Check v. Version 1 is described here. A future incompatible change will use a new version number.
  • Ignore fields you don’t know. New optional fields can be added to version 1 at any time; a consumer that rejects unknown fields will break.
  • Be idempotent on id. A transaction can be delivered more than once (after a retry or a consumer restart). It always carries the same id.
  • Don’t produce to this topic. Only SpeculaPro’s ingest writes it. Anything else written there bypasses validation, currency conversion and encryption.