New CBN rules make manual AML non-compliant.

Read the brief
MaiGuard

API Reference

API Reference

Multi-tenant REST APIs (/v1/…) for real-time transaction risk scoring, AML monitoring, customer onboarding, and data pipelines. Every call is scoped to your tenant — API keys resolve automatically.

Overview

MaiGuard combines configurable rules, velocity analysis, behavioral and device signals, entity graph, counterparty risk, PEP and sanctions screening via tenant lists, customer KYC/KYB context, and case review workflows.

PhaseWhat happens
AccountTenant created; subscription and feature limits assigned.
KYBBusiness verification; documents submitted via portal.
CredentialsTEST and LIVE API keys issued; portal users use JWT session.
ConfigurationRules, tenant lists, pipelines, and notifications set up.
Integration testUse TEST key; scored traffic is test-only, does not affect live reporting.
Go-liveSwitch to LIVE key — no code changes required.

Note

Use pk_test_* during integration. Switch to pk_live_* at go-live — no code changes required. Test traffic is excluded from live reporting and analytics.

Authentication

Base URL: https://api.maiguard.com. All paths are under /v1/. Breaking changes ship under a new version prefix.

API key (server-to-server)

Authorization: Bearer pk_live_<key>

High-volume paths: scoring, imports, webhooks, SDK telemetry. Never embed live keys in browsers.

JWT (portal / management)

Authorization: Bearer <access_token>

From POST /v1/auth/login. Used for rules, customers, analytics, and RBAC-protected routes.

Idempotency (scoring)

Idempotency-Key: <unique-string-per-request>

Optional header for deduplication on retry. Same key returns the cached response for 24 hours without re-evaluating rules.

Warning

Never embed pk_live_* keys in browser or mobile clients. Score transactions from your backend and pass only the result to the client.

Payload shapes

Every tenant uses the same POST /v1/transactions/score endpoint. Send only what you have — richer payloads improve rules, velocity, pattern detection, screening, and ML quality.

ShapeWhen to useMonitoring outcome
MinimalAlways valid — required fields onlyBasic scoring; suitable for connectivity tests and low-context flows.
RecommendedDevice/session context available (IP, device, geo, metadata)Rules + velocity + pattern detection at production quality.
Full / enterpriseSchema 1.1 typed sections — or when strict payload validation is enabled on your tenantFull screening, AML context, and ML at highest quality. Missing required schema 1.1 sections returns 422 when strict validation is on.

Note

Strict payload validation: Some enterprise tenants require the full schema 1.1 shape (event, transaction, actor, counterparty, etc.). Requests missing required sections return 422 instead of being scored with degraded context.

Only required fields. Always valid for every tenant.

minimal-payload.json
{
  "amount": 50000,
  "userId": "cust_abc123",
  "currency": "NGN"
}

Integration channels

REST APIs

All /v1/ endpoints for scoring, onboarding, lists, rules, imports, and analytics.

Use case: Server-to-server scoring; management APIs.

Rate limit: 1,000/min per tenant (score endpoint)

Browser SDK

Collects behavioral biometrics; passes deviceSessionId (must start with ds_) into score requests.

Use case: Web flows needing device linkage and pattern analysis.

Rate limit: Confirm with MaiGuard contact

Webhooks

Inbound: provider-signed events into MaiGuard. Outbound: MaiGuard-signed decisions to your HTTPS endpoint.

Use case: Event-driven ingestion and real-time alerts.

Rate limit: 1,000/min per tenant path (inbound)

Database pipeline

POST /v1/integrations/database — schedule or manually sync from PostgreSQL, MySQL, or MongoDB.

Use case: Scheduled pulls from your production DB.

Rate limit: AWS/DB quotas apply; use read-only credentials

SQS pipeline

POST /v1/integrations/sqs — maps SQS message fields to MaiGuard transaction fields.

Use case: High-throughput, decoupled AWS-native ingestion.

Rate limit: AWS queue quotas apply

File import

POST /v1/transactions/import (multipart) for CSV, JSON, or JSONL. Poll via importId.

Use case: Historical backfill and batch loads.

Rate limit: 5 imports/hour per tenant

Score transaction

POST/v1/transactions/score

Synchronous scoring. The transaction is queued for persistence after the response; use the returned transactionId to correlate with webhooks and GET /v1/transactions.

1// POST /v1/transactions/score (API key; server-to-server only)
2const response = await fetch('https://api.maiguard.com/v1/transactions/score', {
3  method: 'POST',
4  headers: {
5    'Content-Type': 'application/json',
6    'Authorization': 'Bearer pk_live_<your-key>',
7    'Idempotency-Key': 'order-987654-score-v1',  // optional; dedup 24 h
8  },
9  body: `{
10  "amount": 50000.50,
11  "userId": "cust_61e46cf7",
12  "currency": "NGN",
13  "occurredAt": "2026-05-29T10:30:00.000Z",
14  "ipAddress": "197.210.84.42",
15  "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
16  "deviceId": "7ec1a4175dd3776f4cb534b1287749811b5512ad60f5d0ba1f10c7974ffd2edc",
17  "deviceSessionId": "ds_g7xSevOyd2oUAQCfVuya0g",
18  "location": {
19    "latitude": 6.4474,
20    "longitude": 3.3903
21  },
22  "metadata": {
23    "countryCode": "NG",
24    "receiverId": "rcv_beneficiary_001",
25    "product": "Collections",
26    "paymentMethod": "card",
27    "environment": "LIVE",
28    "type": "purchase",
29    "merchantId": "merchant_456",
30    "category": "electronics",
31    "orderId": "ord_987654",
32    "channel": "web"
33  }
34}`,
35});
ALLOW

Transaction approved — proceed with payment.

REVIEW

Elevated risk — flag for manual review.

BLOCK

High risk — decline the transaction.

Request parameters

ParameterTypeRequiredDescription
idstringoptionalClient-generated transaction ID. When provided, used as the persisted transaction ID — enabling idempotent retries. Server auto-generates one if omitted.
userIdstringrequiredStable customer ID. Must match customerId in onboarding calls.
amountnumberrequiredTransaction amount. Must be a positive number.
currencystringrequiredISO 4217 currency code, exactly 3 characters (e.g. NGN, USD, GHS). Browser SDK normalises to uppercase and rejects invalid lengths before sending.
occurredAtstringoptionalISO 8601 event timestamp. Browser SDK always sends this (defaults to now) for accurate velocity window calculations.
environmentstringoptionalLIVE or TEST. Overrides the environment derived from the API key when provided.
ipAddressstringoptionalClient IP address used for velocity and geo signals.
userAgentstringoptionalBrowser or app user-agent string.
deviceIdstringoptionalDevice fingerprint from your own system or SDK.
deviceSessionIdstringoptionalDevice session from Browser SDK (ds_ prefix required). Browser SDK validates this before sending and throws INVALID_DEVICE_SESSION_ID if malformed.
locationobjectoptionalGeographic coordinates of the transaction.
location.latitudenumberoptionalGeographic latitude.
location.longitudenumberoptionalGeographic longitude.
loginContextobjectoptionalLogin context for ATO signals. Common in Full / enterprise payloads.
loginContext.minsSinceLastLoginnumberoptionalMinutes since the user's last successful login.
loginContext.failedAttemptsBeforeLoginnumberoptionalFailed login attempts immediately before this success.
loginContext.loginIpMatchesTxIpbooleanoptionalWhether the login IP matches the transaction IP.
loginContext.isFirstLoginFromDevicebooleanoptionalWhether this is the first login from this device.
includeMatchedConditionsbooleanoptionalReturn matched rule conditions in the response (default: false).
metadataobjectoptionalBusiness context for rules and segmentation. See Recommended shape.
metadata.countryCodestringoptionalISO country code of the transaction.
metadata.receiverIdstringoptionalCounterparty ID for entity graph linkage.
metadata.productstringoptionalProduct line (e.g. Collections, Payouts).
metadata.paymentMethodstringoptionalPayment method (e.g. card, bank_transfer, mobile_money).
metadata.environmentstringoptionalLIVE or TEST.
metadata.typestringoptionalTransaction type (e.g. purchase, transfer).
metadata.merchantIdstringoptionalMerchant identifier.
metadata.categorystringoptionalProduct or merchant category.
metadata.orderIdstringoptionalPartner order or transaction reference.
metadata.channelstringoptionalChannel (e.g. web, mobile, api).
metadata.tagsstring[]optionalClient-defined labels for this transaction (e.g. ["high_value", "recurring"]). Evaluated by transaction_tag_in rule conditions.
eventobjectoptionalFull / enterprise — event envelope (schema 1.1).
event.schemaVersionstringoptionalPayload schema version (e.g. 1.1).
transactionobjectoptionalFull / enterprise — transaction semantics: kind, direction, channel, paymentMethod, productCategory, description.
transaction.productCategorystringoptionalProduct or business line (e.g. Payouts, Collections, Lending). Shown as 'Category' in the transaction detail view.
actorobjectoptionalFull / enterprise — initiating party: id, type, countryCode.
accountobjectoptionalFull / enterprise — source account with balance snapshot, KYC tier, and age.
account.kycTierintegeroptionalKYC verification tier (1–3). Shown in the transaction detail view.
account.accountAgeDaysintegeroptionalDays since the account was opened. Shown in the transaction detail view.
account.balanceBeforenumberoptionalAccount balance immediately before this transaction.
account.balanceAfternumberoptionalAccount balance immediately after this transaction.
account.balanceCurrencystringoptionalISO 4217 currency of the balance fields if different from account.currency.
account.lastInflowAtstringoptionalISO 8601 timestamp of the most recent inbound credit to this account.
counterpartyobjectoptionalBeneficiary / recipient context. Fields: id, fullName, name, accountNumber, accountMasked, bankName, bankCode, iban, countryCode, type.
counterparty.fullNamestringoptionalFull legal name of the counterparty. Preferred over name.
counterparty.accountNumberstringoptionalAccount number (e.g. 10-digit NUBAN). Primary entity-graph node key for domestic transfers.
counterparty.accountMaskedstringoptionalMasked account number for display (e.g. ****6789).
counterparty.bankNamestringoptionalHuman-readable bank or institution name.
counterparty.ibanstringoptionalIBAN for international transfers. Fallback entity-graph node key when accountNumber is absent.
merchantobjectoptionalFull / enterprise — merchant context: id, businessName, countryCode, merchantCategoryCode / mcc.
merchant.businessNamestringoptionalHuman-readable merchant or business name. Shown in the transaction detail view.
sessionobjectoptionalFull / enterprise — auth session: sessionId, deviceSessionId, loginMethod, mfaUsed, mfaMethod, passwordResetWithin24h.
session.passwordResetWithin24hbooleanoptionaltrue if the user reset their password within the 24 hours preceding this transaction.
deviceobjectoptionalFull / enterprise — device profile: platform, browser, timezone, isRooted, isEmulator, appVersion.
networkobjectoptionalFull / enterprise — network context: ipAddress, userAgent.
geoobjectoptionalFull / enterprise — geo with countryCode: latitude, longitude, countryCode.
behavioralobjectoptionalFull / enterprise — behavioral biometrics (e.g. typingSpeed, touchPressureVariance).

Response — 200 OK

score-response.json
{
  "success": true,
  "data": {
    "transactionId": "550e8400-e29b-41d4-a716-446655440000",
    "riskScore": 42,
    "decision": "REVIEW",
    "reasonCodes": ["VELOCITY_SPIKE_24H", "FRAUD_RING_MEMBER"],
    "reasonCodeTaxonomyVersion": "1.0",
    "matchedRules": [
      {
        "ruleId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
        "ruleName": "High velocity — 1 hour",
        "action": "REVIEW",
        "priority": 10,
        "matchedConditions": [{ "field": "velocity", "operator": "greater_than", "value": 5 }]
      }
    ],
    "velocity": 6,
    "evidence": {
      "velocity": { "txCount1h": 6, "txCount24h": 14 },
      "screening": {},
      "graph": {}
    },
    "mlContribution": { "blended": true, "weight": 0.3 },
    "mlEvaluation": {
      "score": 0.72,
      "modelVersion": "global_txn_autogluon_live_v1",
      "blended": true
    },
    "featureLineage": {},
    "shadowEvaluation": {
      "hasShadowRules": true,
      "shadowDecision": "BLOCK",
      "shadowRiskScore": 78,
      "shadowMatchedRules": []
    },
    "patternAnalysis": {
      "hasAnomalies": true,
      "totalAnomalyScore": 35,
      "amountAnomaly": { "deviation": 2.4, "isAnomaly": true },
      "botDetection": { "isBot": false, "score": 12 }
    }
  }
}

Response — idempotent replay

When Idempotency-Key matches a prior request within 24 hours, the API returns the cached body with cached: true and does not re-run rules.

score-cached-response.json
{
  "success": true,
  "cached": true,
  "data": {
    "transactionId": "550e8400-e29b-41d4-a716-446655440000",
    "riskScore": 42,
    "decision": "REVIEW",
    "matchedRules": [],
    "velocity": 6
  }
}

Response fields

FieldDescription
transactionIdServer-generated UUID for this score (persisted asynchronously).
riskScore0–100 composite risk. Higher = riskier.
decisionALLOW | REVIEW | BLOCK
reasonCodesArray of reason code strings explaining the risk signals (e.g. VELOCITY_SPIKE_24H, FRAUD_RING_MEMBER).
reasonCodeTaxonomyVersionVersion identifier for the reason code taxonomy.
matchedRulesRules that fired. matchedConditions only when request included includeMatchedConditions: true.
velocityTransaction count for this user in the last hour (rule input).
evidenceOptional — velocity, screening, and graph evidence contributing to the score.
mlContributionOptional — ML contribution object with blended and weight fields.
mlEvaluationOptional — ML evaluation with score, modelVersion, and blended flag when tenant ML is enabled.
featureLineageOptional — feature derivation lineage for audit and explainability.
shadowEvaluationOptional — shadow rules only; hasShadowRules, shadowDecision, shadowRiskScore, shadowMatchedRules. Does not change decision.
patternAnalysisOptional — behavioral/device/amount anomalies when pattern engine runs.

Response — 400 / 422 validation

Malformed or invalid field values return 400. Tenants with strict payload validation enabled return 422 when required Full / enterprise sections are missing.

score-validation-error.json
{
  "error": "Validation Error",
  "message": "Expected positive number",
  "details": [
    { "code": "too_small", "path": ["amount"], "message": "Expected positive number" }
  ]
}
score-strict-validation-error.json
{
  "error": "Validation Error",
  "message": "Strict payload validation enabled — missing required sections",
  "details": [
    { "code": "invalid_type", "path": ["event"], "message": "Required for schema 1.1 payloads" },
    { "code": "invalid_type", "path": ["counterparty"], "message": "Required for AML screening context" }
  ]
}
400Validation error — check details[] array for path and message.
401Invalid or missing API key. SDK error.code: UNAUTHORIZED.
403Scoring quota exhausted (token balance depleted) or KYB_APPROVAL_REQUIRED (TEST key). SDK error.code: QUOTA_EXHAUSTED. Do not retry — replenish quota first.
422Strict payload validation — missing required schema 1.1 sections.
429Rate limited — honor Retry-After header. SDK error.code: RATE_LIMITED.
503Persistence queue unavailable — retry with backoff using same Idempotency-Key.

Batch score

POST/v1/transactions/batch-score

Up to 1,000 transactions per request. Synchronous for batches of 100 or fewer; add ?async=true or send more than 100 items for a 202 response with a jobId to poll via GET /v1/jobs/:jobId.

1// POST /v1/transactions/batch-score
2// Sync (≤100 items): immediate results. Async: ?async=true or >100 → 202 + jobId
3const response = await fetch('https://api.maiguard.com/v1/transactions/batch-score?async=false', {
4  method: 'POST',
5  headers: {
6    'Content-Type': 'application/json',
7    'Authorization': 'Bearer pk_live_<your-key>',
8  },
9  body: `{
10  "transactions": [
11    { "amount": 5000, "userId": "user_01", "currency": "NGN", "includeMatchedConditions": true },
12    { "amount": 25000, "userId": "user_02", "currency": "USD" }
13  ],
14  "idempotencyKeys": ["idem-01", "idem-02"]
15}`,
16});

Response — 200 OK (sync)

Each element in results is one scored transaction. Optional idempotencyKeys align by array index.

batch-score-response.json
{
  "success": true,
  "data": {
    "processed": 3,
    "succeeded": 2,
    "failed": 1,
    "results": [
      { "index": 0, "success": true, "transactionId": "660e…", "riskScore": 12, "decision": "ALLOW" },
      { "index": 1, "success": true, "cached": true, "transactionId": "770e…", "riskScore": 88, "decision": "BLOCK" },
      { "index": 2, "success": false, "error": "Plan limit exceeded" }
    ]
  }
}

Response — 202 Accepted (async)

batch-score-async.json
{
  "success": true,
  "data": {
    "jobId": "job_uuid",
    "status": "PENDING",
    "totalItems": 250,
    "message": "Job queued for processing"
  }
}

Login events

POST/v1/events/login

Record a login attempt (success or failed). MaiGuard stores failure counts and last-login timestamp in Redis and automatically enriches the next transaction scored for that userId — no extra call needed at scoring time.

1// POST /v1/events/login — call on every login attempt (success or failed)
2// MaiGuard stores counters in Redis; auto-attached to the next score for this userId.
3
4// Failed attempt:
5await fetch('https://api.maiguard.com/v1/events/login', {
6  method: 'POST',
7  headers: {
8    'Content-Type': 'application/json',
9    'Authorization': 'Bearer pk_live_<your-key>'
10  },
11  body: JSON.stringify({
12    userId: 'cust_abc123xyz',
13    outcome: 'failed',
14    ipAddress: '203.0.113.42',
15    deviceId: 'dev_fingerprint_abc789',
16    occurredAt: new Date().toISOString()
17  })
18});
19
20// Successful login:
21await fetch('https://api.maiguard.com/v1/events/login', {
22  method: 'POST',
23  headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer pk_live_<your-key>' },
24  body: JSON.stringify({ userId: 'cust_abc123xyz', outcome: 'success', occurredAt: new Date().toISOString() })
25});

Auto-enriched signals

  • login_failed_1h — failure count, 1 h window
  • login_failed_24h — failure count, 24 h window
  • mins_since_last_login — ATO timing signal

Inline alternative

  • Pass loginContext on the score request for session-level precision: minsSinceLastLogin, failedAttemptsBeforeLogin, loginIpMatchesTxIp, isFirstLoginFromDevice

Customer onboarding

PUT/v1/customers/:customerId/onboard

Stores KYC/KYB context for a customer — risk profile, entity type, KYC tier, PEP status, and expected volume. Rules can then reference this context when scoring transactions for the same user. Also available: GET /v1/customers, GET /v1/customers/statistics, and bulk CSV via POST /v1/customers/onboard/bulk (JWT).

Warning

Identity alignment is required. The customerId in this call must exactly match the userId you pass on every score request for that customer. A mismatch means the scoring engine cannot find the KYC context, and enriched rules will not fire.
1// PUT /v1/customers/:customerId/onboard (same ID as userId in scoring)
2const response = await fetch('https://api.maiguard.com/v1/customers/customer_001/onboard', {
3  method: 'PUT',
4  headers: {
5    'Content-Type': 'application/json',
6    'Authorization': 'Bearer pk_live_<your-key>',
7  },
8  body: `{
9  "riskProfile": "medium",
10  "partnerOnboardedAt": "2025-06-01T00:00:00.000Z",
11  "countryCode": "NG",
12  "entityType": "individual",
13  "displayName": "Amara Osei",
14  "kycStatus": "verified",
15  "kycTier": 2,
16  "pep": false,
17  "expectedVolume": 5000000,
18  "expectedVolumePeriod": "monthly",
19  "occupation": "trader",
20  "businessSector": "retail",
21  "productType": "wallet",
22  "deliveryChannel": "mobile",
23  "metadata": { "sectorRiskLevel": "LOW" }
24}`,
25});

Tip

See the Customer Onboarding Implementation Guide for a complete endpoint matrix, examples, and rate-limit details.

List transactions

GET/v1/transactions
// GET /v1/transactions (API key or JWT: transactions.view)
const response = await fetch('https://api.maiguard.com/v1/transactions?page=1&limit=20&decision=BLOCK&riskScoreMin=70&environment=LIVE&sortBy=createdAt&sortOrder=desc', {
  headers: {
    'Authorization': 'Bearer pk_live_<your-key>',
  },
});
page, limit — pagination
decision, status — filter by outcome
riskScoreMin / Max, userId — filter by risk or user
amountMin / Max, startDate / endDate — range filters
environment — LIVE | TEST
includeMetadata, includeTotal, sortBy, sortOrder

Integration methods

Inbound: provider posts signed events to your MaiGuard webhook URL. Outbound: MaiGuard posts decisions and alerts to your HTTPS endpoint.

1// Configure inbound webhook — POST /v1/webhooks/ingestion/config
2const cfg = await fetch('https://api.maiguard.com/v1/webhooks/ingestion/config', {
3  method: 'POST',
4  headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer pk_live_<your-key>' },
5  body: JSON.stringify({ webhookSecret: 'your_signing_secret', retryCount: 3 })
6}).then(r => r.json());
7
8// POST events to cfg.data.webhookUrl; sign raw body with HMAC-SHA256:
9// X-Webhook-Signature: HMAC-SHA256(rawBody, webhookSecret)
10
11// Outbound (MaiGuard → you): configure HTTPS endpoint in tenant portal,
12// then verify X-Webhook-Signature on every inbound request from MaiGuard.

Response format

All API responses follow a consistent envelope.

Success

success.json
{
  "success": true,
  "data": { ... }
}

Error

error.json
{
  "error": "Validation Error",
  "message": "Validation failed",
  "details": [{ "path": ["amount"], "message": "Expected positive number" }]
}
StatusTypeDescription
202AcceptedAsync job queued — poll GET /v1/jobs/:jobId
400Validation ErrorInvalid body — check details[] for path and message
401UnauthorizedInvalid or missing API key
403ForbiddenInsufficient permissions or plan limit exceeded
404Not FoundResource not found or not in your tenant
409ConflictDuplicate resource or concurrency conflict
429RateLimitExceededRate limit exceeded — honor Retry-After and RateLimit-* headers
500InternalServerErrorTransient — retry with exponential backoff + jitter
503ServiceUnavailablePersistence queue unavailable — retry immediately

Rate limits

Default limits (may vary by plan). On 429 responses, honor Retry-After and RateLimit-* response headers.

Endpoint / scopeLimitWindow
All endpoints (global guard)100Per minute, per IP
POST /v1/transactions/score1,000Per minute, per tenant
POST /v1/transactions/batch-score10Per minute, per tenant
POST /v1/webhooks/transactions/:path1,000Per minute, per tenant path
Bulk file import5Per hour, per tenant
Auth login / register / forgot-password5 / 3 / 3Per 15 min or per IP/hour
Passkey registration / authentication5 / 10Per 15 minutes, per IP

Was this page helpful?