Skip to content
POST /v1/eventsRequest access
API reference

Events reference

POST /v1/events: one endpoint, six event types. Only type and customer_id are required; every extra field is a signal.

TypeSend it when…Key fields
customer.createdSomeone opens an accountcustomer (name, phone, email, national_id, bvn, dob, nationality, kyc_level)
customer.updatedThey change phone, email, payout account, password, PIN, name, BVN…changes (required), updated customer fields, destination
loginEvery login attempt, success or failurelogin.success, device, location
transactionMoney moves in or outdirection, amount, method, counterparty, status, asset (crypto)
payout.requestedBefore you release a withdrawalamount, destination, balance, asset (crypto)
sim.swapYour SIM-swap check reports a swap: in Kenya, e.g. Safaricom’s; in Nigeria, the signal your mobile operators or aggregator providesim_swapped_at

Common fields

occurred_atISO 8601. Defaults to now. Send it when backfilling history.
amountA number in major units: 48000 means KES 48,000 on a Kenyan workspace, 350000 means NGN 350,000 on a Nigerian one.
currencyISO 4217. Defaults to your workspace currency. Other currencies are converted to your workspace currency for scoring; the original amount and the rate used are kept and returned in amount.
counterparty{ id?, name?, phone?, account?, country?, wallet? }: the other side. country (ISO-2) powers the cross-border rules; wallet is the other side’s crypto address (e.g. the source of a deposit).
methodmpesa, airtel, opay, palmpay, moniepoint, card, bank (bank transfers, including NIP in Nigeria), ussd, pos (a card payment or cash-out at a POS agent), cash, agent, wallet, crypto, other
assetCrypto only, optional: { code, amount, network? }, the on-chain amount. amount and currency stay the fiat value. See Crypto withdrawals and deposits.
destination{ type, account, bank_code?, name?, country?, network? }. Type is mpesa, airtel, opay, palmpay, moniepoint, bank, card, wallet, paybill, till, crypto_wallet or other. bank_code is the bank’s code, e.g. a CBN/NIBSS code such as 058: NUBAN account numbers are only unique within a bank, so send it for Nigerian bank payouts. Account numbers are stored only as keyed hashes.
customer.bvnNigeria, optional: the 11-digit Bank Verification Number. Like national_id (the ID card number in Kenya, the NIN in Nigeria), it links customers who share an identity and feeds the network checks. Stored encrypted.
changesFor customer.updated: any of phone, email, payout_destination, name, password, pin, national_id, bvn, dob, nationality.
customer.phoneLocal and international forms are folded into one number: Kenyan 07…, 2547… and +2547… are the same number, as are Nigerian 0803…, 234803…, +234803… and +234 0803….
device{ id, ip }: any stable device identifier your app has.
location{ lat, lng }. Powers impossible-travel and far-from-home.
travel_ruleCrypto only: originator and beneficiary details, stored encrypted. See Crypto & cross-border.
metadataUp to 20 of your own keys, shown in the dashboard.

Crypto withdrawals and deposits

Send crypto movements as ordinary transaction and payout.requested events with method: "crypto". Keep amount and currency as the fiat value (e.g. KES or NGN) so every rule compares like with like, and put the on-chain amount in asset. The asset is stored with the event.

FieldTypeDescription
asset.codestring, requiredThe ticker: BTC, ETH, USDT… 2–10 letters or digits, uppercased for you.
asset.amountnumber, requiredThe on-chain amount at full precision, e.g. 0.0153421. Must be positive.
asset.networkstring, optionalThe chain it moves on, e.g. bitcoin, ethereum, tron.
destination.typecrypto_walletWith the wallet address as account and the chain as network. Addresses are validated (checksums included) and given one canonical spelling, so the same wallet always matches.
counterparty.walletstring, optionalFor deposits: the wallet the funds came from.
Crypto payout and deposit
// A customer withdraws 350.25 USDT on Tron to an exchange-hosted wallet.
const d = await sieve.events.create({
  type: 'payout.requested',
  customer_id: 'cus_1001',
  // The fiat value: every rule scores this (converted to your workspace currency if needed).
  amount: 350.25,
  currency: 'USD',
  method: 'crypto',
  // The on-chain amount, at full precision.
  asset: { code: 'USDT', amount: 350.25, network: 'tron' },
  destination: { type: 'crypto_wallet', account: 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t', network: 'tron', country: 'AE' },
  balance: 360,
  // FATF Travel Rule data (stored encrypted). Required above the threshold.
  travel_rule: {
    originator: { name: 'Wanjiru Kamau', account: 'cus_1001', country: 'KE' },
    beneficiary: { name: 'Wanjiru Kamau', address: 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t' },
    beneficiary_vasp: 'Example Exchange FZE',
  },
})
// d.wallet → { chain: 'tron', valid: true, network_matches: true, sanctioned: false, … }

// Deposits are transactions with the same shape; the source wallet goes in counterparty.wallet.
await sieve.events.create({
  type: 'transaction',
  customer_id: 'cus_1001',
  direction: 'in',
  amount: 128000,
  currency: 'KES',
  method: 'crypto',
  asset: { code: 'BTC', amount: 0.0153421, network: 'bitcoin' },
  counterparty: { wallet: 'bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq' },
})
Screened on arrival. Every wallet is checked against sanctioned addresses on the OFAC and OpenSanctions lists, and scored by crypto-specific rules. Wallets that are new or shared across accounts are also caught by the payout-account and money-mule rules. See Crypto & cross-border.

Idempotency

Send an Idempotency-Key header (or idempotency_key field). Retrying with the same key returns the original decision instead of counting the event twice, so it’s safe to retry on timeouts.

Other endpoints

GET /v1/customers/:id
const c = await sieve.customers.get('cus_1001')
// { status: "held", risk_level: "high", screening: { status: "clear", last_screened_at: "…" }, open_cases: [...] }
POST /v1/feedback
// Tell Sieve when it was right or wrong: this sharpens every rule.
await sieve.feedback({ event_id: 'evt_W79r7Mh2ywQkhm8baqHr', label: 'fraud' })   // or 'legit'

// Late evidence counts too: a chargeback or complaint weeks later overrides an earlier 'legit'.
await sieve.feedback({ event_id: 'evt_W79r7Mh2ywQkhm8baqHr', label: 'fraud', source: 'chargeback' })

Errors & limits

Errors are JSON: { "error": "invalid_request", "message": "amount: is required for transaction", "details": [...] }. Invalid keys get 401. The default rate limit is 1,200 requests per minute per key; headers X-RateLimit-Remaining and Retry-After tell you where you stand.