Sending events for real-time risk scoring: an integration guide
Call the transactions endpoint for money movement and the activities endpoint for logins and account changes, from your backend, before you act. Send complete parties and stable IDs, then route on the summary outcome: allow, review or block.

Charles Archibong, Co-founder
· 7 min read

Key takeaways
- Send money movement to /api/v1/transactions and logins or account changes to /api/v1/activities.
- Call before you release funds, and route on assessment.summary.outcome: allow, review or block.
- Missing counterparty names or wallets produce a review decision, not a clear one, so send complete parties.
- Reuse externalActivityId on retries; a replay returns the existing assessment without scoring it again.
To get a real-time risk decision, your backend calls POST /api/v1/transactions for every payment, transfer, deposit or withdrawal, and POST /api/v1/activities for every login, sign-up, password reset, beneficiary change and similar account event. Each call returns a decision in the same request: allow, review or block, with a score, the strongest reason and a suggested next action. Your code gates the action on that decision.
Three things decide whether the integration is any good: calling at the right moment (before the money moves, not after), sending complete information about who sent and who received the funds, and using stable identifiers so retries never create a second assessment. This guide covers each, with the request shapes from the event monitoring documentation.
Availability. These endpoints belong to the Fraud Monitoring area of Myaza Trust's Risk Intelligence, which is available to accounts with access to it. Production use also requires an approved business profile. If the endpoints are not enabled for your account, speak to Myaza before you build against them.
Which endpoint does each event belong to?
What happened | Endpoint | Why |
|---|---|---|
Money moved, or is about to, with sender and recipient details |
| Records the relationship, the accounts or wallets used, screens the counterparty and scores the transaction |
A customer did something to their account |
| Builds behaviour evidence from device, IP, location and event attributes, then scores it |
An older integration sending the free-form event shape |
| Compatibility only; it cannot model complete parties or instruments |
Activity type is a closed list: login, signup, password_reset, beneficiary_added, profile_change, device_change, payout and withdrawal. The documentation is firm that money movement with sender and recipient evidence goes to the transactions endpoint, not through the activity endpoint as a payout.
Where in your flow should the call sit?
Between the customer's confirmation and the irreversible step. Take an illustrative outbound bank transfer in a wallet app:
The customer confirms a transfer to a new beneficiary.
Your backend sends the transaction and waits for the response.
It routes on
assessment.summary.outcome.Only on
allowdoes it submit the payment to the bank rail.
Outcome | What your backend does |
|---|---|
| Continue. No operator action is needed. |
| Hold the action and send it to your review process. |
| Stop the action. Keep the decision and its evidence for investigation. |
Scoring after the payment has left is monitoring, which still has value for investigations, but it cannot stop the loss. If the decision is to gate anything, it has to come first. Myaza returns a decision; it does not block accounts in your application, so the gate is your code.
What does a first request look like?
All Risk Intelligence calls use a secret key from your backend: sk_test_ against https://sandbox.trust.myaza.app/api/v1, sk_live_ against https://trust.myaza.app/api/v1. The official Node.js SDK is @myazahq/trust-sdk (ESM-only, Node.js 20 or newer), and it picks the host from its environment option. From the Risk Intelligence quickstart:
import { Myaza } from '@myazahq/trust-sdk';
const myaza = new Myaza({
apiKey: process.env.MYAZA_SECRET_KEY,
environment: 'sandbox',
});
const assessment = await myaza.transactions.create({
externalActivityId: 'activity_txn_10001',
subject: { type: 'individual', externalUserId: 'user_42' },
occurredAt: new Date().toISOString(),
transaction: {
externalTransactionId: 'txn_10001',
assetClass: 'fiat',
direction: 'outbound',
amount: '48000.00',
currency: 'NGN',
transactionType: 'bank_transfer',
},
}, { idempotencyKey: 'transaction-txn_10001' });
console.log(assessment.summary?.outcome, assessment.summary?.reason);Note that amount is a string. The API takes a positive decimal string with up to eight decimal places, so money is never rounded through a floating-point number on the way in.
That request is valid, but it is incomplete, and the next section is why that matters.
Why do the parties matter so much?
Because screening depends on them. The transaction direction is always from your customer's point of view, and Myaza screens the other side of the movement before producing the decision:
Direction and asset | Party screened | What is screened |
|---|---|---|
Outbound fiat | Recipient | Name and available identity evidence |
Inbound fiat | Sender | Name and available identity evidence |
Outbound crypto | Recipient | Wallet address and network |
Inbound crypto | Sender | Wallet address and network |
Bank account numbers are kept as relationship evidence and are not used for screening. If the name or wallet the screen needs is missing, the response is a review decision with an insufficient-data reason, rather than treating the transaction as clear.
That is the right behaviour, and it means a thin integration produces a busy review queue. Send the parties array. Each party needs a role (sender, recipient, customer or counterparty), one stable reference (externalUserId, entityId or externalPartyId), and for external fiat screening a displayName. Attach the account or wallet each party used as an instrument:
{
"role": "recipient",
"externalPartyId": "beneficiary_998877",
"type": "individual",
"displayName": "Amina Bello",
"country": "NG",
"instruments": [
{
"type": "bank_account",
"externalInstrumentId": "beneficiary_account_998877",
"identifier": "9988776655",
"institutionName": "Recipient Bank",
"country": "NG",
"currency": "NGN"
}
]
}An instrument's raw identifier is fingerprinted for relationship matching and not stored in raw form. If you would rather not send the raw number, supply your own externalInstrumentId or an existing fingerprint instead.
How do you send an account activity?
The same envelope without a transaction block, plus a type. A login over HTTP:
curl -X POST "https://sandbox.trust.myaza.app/api/v1/activities" \
-H "Authorization: Bearer $MYAZA_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"externalActivityId": "login_10001",
"subject": { "type": "individual", "externalUserId": "user_42" },
"type": "login",
"occurredAt": "2026-08-16T09:30:00.000Z",
"channel": "mobile_app",
"deviceRef": "device_7f53",
"ipAddress": "102.89.12.34",
"country": "NG",
"customAttributes": { "trustedDevice": false, "failedAttemptsBeforeSuccess": 2 }
}'Two fields carry most of the value. deviceRef is your own stable device reference; reusing it builds device history and is what lets a new-device signal mean anything. ipAddress must be the end user's address. The API does not substitute your server's IP, so if your backend forwards the call, pass the client address you observed, not your own.
Send only evidence you actually collected. Latitude and longitude must be supplied together, and a location you supply stays primary over anything inferred from the IP.
How should you read the response?
Read assessment.summary first. It holds outcome, title, reason, score, riskLevel, matchedRules and nextAction, and it is enough for normal routing. The same response also carries the detailed matched rules, screening evidence and billing breakdown for when someone investigates. Store decisionId and alertId against your own transaction so an operator can find the evidence later.
Use the machine values (allow, review, block) in code and write your own customer-facing copy, as the documentation advises, rather than passing the assessment text through to the customer.
How do you retry without scoring twice?
With stable identifiers:
One
externalActivityIdper logical assessment, kept identical across retries. A replay returns the existing assessment withreplayed: trueand does not score or bill it again.One
externalTransactionIdper customer transaction.Reusing either identifier with different immutable data returns
409(idempotency_conflictortransaction_id_conflict). That is a bug in your identifier scheme, not something to retry.Send the original
occurredAt. It cannot be more than five minutes in the future, and Myaza records both occurrence and receipt time, so a late event stays visible without distorting the timeline.
What should happen when no decision comes back?
Decide this before launch, because the documented behaviour is to fail closed. The errors you will meet:
Status | Error | What to do |
|---|---|---|
|
| Fix the payload: missing fields, money not an exact decimal string, incomplete location. |
|
| Your production credit cannot cover the assessment. Top up; there is no decision. |
|
| The referenced customer or entity does not exist in this environment. |
|
| Identifier reused for different data. Fix, do not retry. |
|
| The subject type cannot be assessed by this contract. |
| Service failure | Retry with the same identifiers and backoff. |
The errors documentation adds a point worth writing into your runbook: transaction and activity decisions fail closed when credit is unavailable, and a late record is not an authorisation response. If you cannot get a decision in time, your product needs its own rule, for example holding outbound transfers above a limit you choose until a decision arrives. Write that rule down; do not let a timeout default to "allow".
Do you still need webhooks?
For operations, yes. Subscribe to fraud.transaction.assessed and fraud.activity.assessed to receive every decision, and to screening.match, risk.signal.created and alert.created to act on findings. Verify each signature against the raw body and deduplicate by event ID. event.flagged remains only as a compatibility alias for non-allow decisions. If you are on POST /api/identity/events, plan the move to the v1 endpoints: the old shape can score a simple event but cannot carry structured parties or instruments.
Checklist
Secret key and Risk Intelligence host on your backend only;
sk_test_with the Sandbox host while building.Transactions called before funds move; the gate is your code.
partieswith names or wallets for the screened side of every movement.The end user's IP and your stable
deviceRefon activities.Stable
externalActivityIdandexternalTransactionId, originaloccurredAt.A written rule for what happens when no decision is available.
Webhooks subscribed, verified and deduplicated.
The Transaction Monitoring product page describes the rules and investigation tools behind these decisions, and the developer hub links the full references.
Sources

Charles Archibong
Co-founder
Charles Archibong co-founded Myaza Trust. He writes about identity verification, financial technology, and the practical work of building trusted digital services.


