Aller au contenu

Account events

Ce contenu n’est pas encore disponible dans votre langue.

Most account takeovers start before any money moves. Someone logs in from a new phone, gets three one-time passwords (OTPs) wrong, swaps the SIM card or changes the email address, and only then sends the money. An account event is one of those non-monetary moments. Send them to SpeculaPro as they happen and fraud rules can use them, for example to hold a payout made a few minutes after a phone number changed.

Endpoint POST /api/v1/ingest/events
API key scope ingest:write, the same key you use for transactions
Batch size 1 to 500 events (or your operator’s lower limit)
Duplicate protection By your external_id

Account events never raise an alert by themselves, and they don’t count as transactions in any total. Rules read them in two ways:

  • How many of a type happened in a period, such as “failed OTPs in the last 30 minutes”. See Counting account events.
  • How long ago the latest of a type happened, such as “hours since the last SIM swap”. See customer.hours_since.

Send an event whenever one of these happens on a customer’s account, on any channel:

type When to send it
login A successful sign-in to the app, website or USSD menu
login_failed A failed sign-in attempt
logout The customer signed out
session_start A new session started, where your system separates sessions from sign-ins
otp_sent You sent a one-time password
otp_failed A wrong or expired one-time password was entered
otp_passed A one-time password was entered correctly
password_change The customer changed their password
password_reset The password was reset (forgotten password, or by your staff)
pin_change The PIN was changed
pin_failed A wrong PIN was entered
email_change The email address on the account changed
phone_change The phone number on the account changed
address_change The postal or home address changed
sim_swap The mobile network told you the customer’s SIM was replaced
device_added A new device was registered or trusted for the account
device_removed A device was removed
payee_added A new beneficiary was saved
payee_removed A beneficiary was removed
card_added A card was added or issued
card_blocked A card was blocked
limit_change A transaction or daily limit was changed
kyc_change The customer’s KYC level changed
account_frozen The account was frozen
account_unfrozen The account was unfrozen

Any other value fails the whole request with 422.

Field Type What it is
external_id string, 1–128 characters Your unique reference for this event. It protects against duplicates, so it must never change and never be reused
customer_external_id string, 1–128 characters The customer it happened to, as in your customer feed
type string One of the 25 types above
occurred_at date-time (RFC 3339) When it happened, with a timezone, for example 2026-10-09T09:08:07Z
Field Type What it is Why it matters
account_external_id string, ≤ 128 The account involved, if the event concerns one Stored with the event, and counted on that account as well as on the customer, so rules can count events per account. An account SpeculaPro doesn’t know yet is created as a placeholder for this customer (type other, in your base currency) until your account feed sends the real one
channel mobile_app · online · ussd · branch · agent · atm · call_centre · api · other Where it happened Shown on the customer’s page
device_fingerprint string, ≤ 256 Your identifier for the phone or browser Links the event to a device, and counts the customer on that device for shared-device rules
session_id string, ≤ 128 Your app or web session Stored with the event. Not shown on screen or used by rules yet
ip IPv4 or IPv6 address Where the request came from Stored with the event (see the caution below)
geo_lat, geo_lon numbers Location, if you have it Stored with the event (see the caution below)
success true · false Whether an attempt worked, for types where the name alone doesn’t say A failed attempt is highlighted on the customer’s page
attributes object, ≤ 32 keys Your own fields, such as the reason for a limit change Stored with the event

Kwabena’s phone number is changed in the app at 09:08, after three wrong OTPs:

Terminal window
curl https://speculapro.example.com/api/v1/ingest/events \
-H "Authorization: Bearer $SPECULAPRO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"items": [
{
"external_id": "EVT-2026-10-09-0091273",
"customer_external_id": "CUST-20418",
"type": "otp_failed",
"occurred_at": "2026-10-09T09:03:07Z",
"channel": "mobile_app",
"device_fingerprint": "android-9c41e2",
"session_id": "s-7f20aa",
"ip": "102.176.94.17",
"success": false
},
{
"external_id": "EVT-2026-10-09-0091301",
"customer_external_id": "CUST-20418",
"type": "phone_change",
"occurred_at": "2026-10-09T09:08:07Z",
"channel": "mobile_app",
"device_fingerprint": "android-9c41e2",
"session_id": "s-7f20aa",
"attributes": { "verified_by": "otp" }
}
]
}'
{
"accepted": 2,
"duplicate": 0,
"stale": 0,
"rejected": 0,
"retry": 0,
"results": [
{ "index": 0, "external_id": "EVT-2026-10-09-0091273", "status": "accepted", "id": "6a1f0c52-9d3e-5b7a-8c21-4e0f9b7d2a61" },
{ "index": 1, "external_id": "EVT-2026-10-09-0091301", "status": "accepted", "id": "0b9e3d74-1c25-5f8a-9e40-7d2c6a1b8f93" }
]
}

If Kwabena tries to send GHS 900 a few minutes later and the H5 rule is running in a real-time decision, the payout is held for review.

The response has the same shape as for transactions; see Responses and statuses. Events can come back as:

Status Meaning What to do
accepted Recorded and queued. id is SpeculaPro’s id for the event Nothing
duplicate The same external_id appears earlier in this batch; only the first is used Nothing, unless they were really two different events: then fix your references
rejected Failed a check; errors says why Fix and resend
retry Not accepted this time Send the same item again

Events are never stale. If every event in a batch would be retry, the whole request fails with 503 ingest temporarily unavailable; retry the batch.

Rejection Cause Fix
occurred_at is required An empty “zero” date such as 0001-01-01T00:00:00Z Send the real time
occurred_at is in the future More than 5 minutes ahead of SpeculaPro’s clock Check your server’s clock and send an explicit offset
occurred_at is more than 180 days ago Too old to matter to any rule Don’t backfill events older than 180 days
external_id has leading or trailing spaces Padding from an export Trim the value, the same way every time
ip is not a valid IPv4/IPv6 address Malformed address Send the address alone, without a port

An unknown type or channel, a missing required field or more than 32 attributes fails the whole batch with 422 before any event is checked.

An event’s id is worked out from your workspace and its external_id, so it is stored once however often you send it. Resending is safe, with one limit for counts:

The customer doesn’t have to exist yet. As with transactions, an unknown customer_external_id gets a placeholder customer that your customer feed later fills in. The same goes for an unknown account_external_id: it becomes a placeholder account for that customer, of type other and in your base currency, until your account feed sends the real one.

Send events as they happen, and no later than your transactions. An event that reaches SpeculaPro late still counts, but not in every window:

The latest time of each event type (customer.hours_since) is kept for 180 days and isn’t affected: a SIM swap reported an hour late still blocks a payout under G3.

  • On the customer’s page, under Digital footprint: the latest 30 events from the last 90 days, and the devices the customer has used. See Customer profile.
  • Through GET /api/v1/customers/{id}, as account_events and devices.