PSP API guide
Version: 2.1 — October 2026 (Edge data residency)
For: Payment Service Providers, Banks, and Fintech Institutions
Base URL:https://api.hokeka.com/api/v1(Production)
Swagger UI:https://api.hokeka.com/api/v1/swagger-ui.htmlW36-4 reconciliation (2026-08-27): every documented
METHOD /pathin this guide has been checked against an actual@RequestMapping/@GetMapping/etc. in the current codebase. All match except the one fixed here (/reports/download/{id}, was documented as{executionId}— cosmetic path-variable naming only, not a functional break). W36-1 and W36-3 (session-prior) already corrected the two genuinely wrong paths this guide had.Data residency (2026-10-17): your Edge Node is the only component that evaluates transactions. The cloud never stores transaction data; it keeps decision records and the alert, case and SAR workflow. Normative contract:
EDGE_DATA_RESIDENCY_CONTRACT.md. Rnn names a work package; its status is in the one status table,architecture/EDGE_DATA_RESIDENCY_PLAN.md(pending: R12, R13, R14b; everything else is built).
Table of Contents
- SaaS Model & PSP Onboarding
- Authentication & Role-Based Access
- API Endpoint Reference
- Billing & Usage Tracking
- Transaction Monitoring
- Sanctions Screening & AML Checks
- Alerts & Case Management
- Reports & Regulatory Compliance
- CBK (Central Bank of Kenya) Reporting
- Webhooks & Event Subscriptions
- Error Handling & Rate Limits
- Integration Examples
- SDK & Client Libraries
- Support & Troubleshooting
1. SaaS Model & PSP Onboarding
Multi-Tenant Architecture
Hokeka AML is a single-instance, multi-tenant SaaS platform. Each PSP (Payment Service Provider) or Bank is a tenant with fully isolated data:
┌─────────────────────────────────────────────────────────┐
│ Hokeka AML Platform │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ PSP A │ │ PSP B │ │ PSP C │ │ Bank D │ │
│ │ (Tenant) │ │ (Tenant) │ │ (Tenant) │ │ (Tenant) │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Shared Infrastructure (Scoring, Rules, │ │
│ │ Analytics, Reporting, Cross-PSP Intel) │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
Key principles:
| Principle | Description |
|---|---|
| Data Isolation | Each PSP sees only its own merchants, transactions, alerts, cases, and reports |
| Data Residency | Transactions are evaluated and stored on your Edge Node. The cloud holds decision records (IDs, action, score, rule IDs) and workflow records only |
| Shared Intelligence | Opt-in only. Indicators come from filed SARs of opted-in PSPs: merchant IDs and keyed device tokens. No amounts, names, card data or the filing PSP's identity |
| Per-PSP Configuration | Rules, risk thresholds, screening lists, and billing plans are per-PSP |
| Role-Based Access | PSP_ADMIN manages their PSP; SUPER_ADMIN manages the platform |
PSP User Roles
| Role | Privileges |
|---|---|
| PSP_ADMIN | View own PSP's usage, billing, invoices, reports; manage own users; configure PSP profile |
| PSP_USER | View own PSP's alerts, cases, transactions; screen names; generate reports |
| ANALYST | Investigate alerts, review cases, add notes and evidence |
| COMPLIANCE_OFFICER | Make case decisions (APPROVE/REJECT/FILE_SAR); manage escalations |
| INVESTIGATOR | Full investigation workflow (timeline, network graph, case replay) |
| ADMIN | Platform-wide: manage all PSPs, users, billing, settings, CBK configuration |
| SUPER_ADMIN | Full system access including platform configuration, billing administration |
Onboarding Flow
1. Registration ──→ POST /psps/register (public) or via platform admin
│
2. Activation ────→ SUPER_ADMIN activates the PSP account
│
3. User Setup ────→ PSP_ADMIN users created (can be bulk-imported)
│
4. API Credentials → JWT tokens issued on login
│
5. Configuration ──→ Risk thresholds, rules, screening lists configured
│
6. Integration ────→ Edge Node installed; your API nodes call /edge/evaluate
│
7. Go Live ────────→ Platform admin promotes CBK environment to live
2. Authentication & Role-Based Access
The platform has two authentication modes. Use the one that matches the caller:
| Caller | Mechanism | Endpoints |
|---|---|---|
| Your payment switch → your Edge Node | TLS on the Edge's port 8443 (optional mutual TLS) | POST /edge/evaluate (§5) |
| Your back office jobs → cloud | PSP API key — X-Api-Key: hka_… or Authorization: Bearer hka_… | Assessment read-back; deprecated transaction ingest (below) |
| People using the Console | Session cookie (JSESSIONID) after POST /api/v1/auth/login | Everything the user's role allows |
There is no JWT: a Bearer header is only accepted when it carries a PSP API key (hka_…).
2.1 Machine integration — PSP API keys
Hokeka platform administrators issue keys for your PSP (POST /api/v1/admin/psp-api-keys?pspId=…;
ask your Hokeka contact). The full key (hka_ + 43 characters) is shown once; only its SHA-256
hash is stored. Keys are rotated with POST /api/v1/admin/psp-api-keys/{id}/rotate (the old key
stops working immediately) and revoked with DELETE /api/v1/admin/psp-api-keys/{id}. Keys may
carry an expiry (hokeka.api-keys.default-ttl-days).
GET /api/v1/transactions/1048576/assessment
X-Api-Key: hka_8Hq2... # or: Authorization: Bearer hka_8Hq2...
| Endpoint | Purpose |
|---|---|
GET /api/v1/transactions/{id}/assessment | Read the assessment and findings for one of your cloud-ingested transactions |
POST /api/v1/transactions/ingest | Deprecated. Legacy cloud ingest of one transaction (§5.6) |
POST /api/v1/transactions/ingest/batch | Deprecated. Legacy cloud ingest of a batch |
The ingest endpoints store transaction data in the cloud, which the residency model forbids. They keep working for existing integrations until work package R12 deletes them. Do not build new integrations on them.
Rules:
- The key is bound to your PSP. Every merchant in an ingest request must belong to that PSP;
any other merchant id is answered
404 Merchant not found(exactly like a non-existent one). - An invalid, revoked or expired key, or a key of a PSP that is not
ACTIVE, gets401 {"error":"UNAUTHORIZED"}. A key sent to any other endpoint is ignored (401/403). - Rate limits: per key
hokeka.api-keys.rate-limit-per-minute(default 1200/min) and per client IP for failed key attempts (default 20/min). Both answer429(Retry-After: 60for the per-key limit). - Requests are stateless: no session cookie is created.
2.2 Console users — session cookie
POST /api/v1/auth/login
Content-Type: application/json
{ "username": "psp_admin@mypayment.com", "password": "your-secure-password" }
Response — the server sets the JSESSIONID cookie (Secure, HttpOnly, SameSite=Strict);
send it on every later request.
{
"success": true,
"user": { "id": 42, "username": "psp_admin@mypayment.com", "role": "PSP_ADMIN", "pspId": 7 },
"redirectUrl": "/dashboard"
}
- Idle timeout: 30 minutes (
SESSION_TIMEOUT).GET /api/v1/auth/session/checkreports the time remaining;POST /api/v1/auth/session/refreshextends the session. - One active session per user: a new login ends the previous one.
POST /api/v1/auth/logoutends the session.
PSP Scope Isolation
The system determines your PSP scope from the authenticated principal (the API key's PSP, or the logged-in user's PSP):
- PSP_ADMIN / PSP_USER users — all queries are automatically scoped to their PSP's data. They cannot see other PSPs' transactions, alerts, or cases.
- Platform ADMIN / SUPER_ADMIN users — can see all PSPs' data and
can optionally filter by
pspIdquery parameter.
An attempt to access another PSP's data returns 403 FORBIDDEN.
3. API Endpoint Reference
Base URLs
| Environment | Base URL |
|---|---|
| Local Development | http://localhost:2637/api/v1 |
| Sandbox / Staging | https://testapi.hokeka.com/api/v1 |
| Production | https://api.hokeka.com/api/v1 |
All API Groups
| Group | Base Path | Description | PSP Access |
|---|---|---|---|
| Auth | /auth | Login (session cookie), session refresh, password reset | ✅ All roles |
| Transactions | /transactions | Monitor transactions (ingest is deprecated, §5.6) | ✅ PSP roles |
| Alerts | /alerts | Fraud & AML alerts with disposition | ✅ PSP roles |
| Cases | /cases | Case management & investigation | ✅ PSP roles |
| Screening | /screening | Sanctions, PEP & watchlist screening | ✅ PSP roles |
| Risk | /risk | Country risk, risk scores, heatmaps | ✅ PSP roles |
| Monitoring | /monitoring | Live transaction feed, analytics | ✅ PSP roles |
| Reports | /reports | Generate & schedule reports | ✅ PSP roles |
| Billing | /billing | Invoices, usage, payments | ✅ PSP_ADMIN |
| Payments | /billing/payments | M-Pesa & bank payments | ✅ PSP_ADMIN |
| Subscriptions | /subscriptions | Plan & tier management | ✅ PSP_ADMIN |
| PSPs | /psps | PSP profile & configuration | ✅ PSP_ADMIN |
| Compliance | /compliance | SAR, KYC, CBK reporting | ✅ Compliance roles (SAR register: MLRO / COMPLIANCE_OFFICER only) |
| Blacklist | /blacklist | Payment blacklist: PAN, terminal, IP, device, payment details, wallet | ✅ PSP_ADMIN, COMPLIANCE_OFFICER (own PSP; platform-wide list read-only) |
| Admin | /admin/psp-billing | Billing management portal | ❌ Admin only |
| Users | /users | User & role management | ✅ PSP_ADMIN |
Rate Limiting
Limits are the same for every PSP; there are no plan tiers and no plan or usage quota: billing
never blocks a request (the plan's included transactions are a pricing threshold only). Signed-in
Console users of one PSP also share a per-PSP limit of 120 requests/minute in every profile
(RateLimitingFilter with TenantRateLimiter, saas.quota.requests-per-minute; API-key calls
are excluded). The per-IP and per-key limits are enforced by ProductionRateLimitFilter
(production profile) and PspApiKeyAuthenticationFilter:
| Traffic | Limit | Keyed by |
|---|---|---|
| Console and general API calls | 100 requests/minute (+20 burst) — RATE_LIMIT_RPM, RATE_LIMIT_BURST | client IP |
Credential endpoints: /auth/login, /psps/auth/login, /auth/register-with-invite, /auth/password-reset/{request,confirm,emergency} | 10 requests/minute — RATE_LIMIT_AUTH_RPM | client IP |
Machine calls carrying a PSP API key (X-Api-Key or Authorization: Bearer) on the API-key paths (/transactions/ingest, /transactions/ingest/batch, /transactions/{id}/assessment) | 1200 requests/minute — hokeka.api-keys.rate-limit-per-minute; failed key attempts 20/minute per IP | API key (not limited per IP) |
Edge machine paths (/edge/assessments, /edge/config, /edge/bundle, /edge/metrics, /edge/sanctions/list, /edge/tunnel) | not limited per IP by the Control Plane; limited per client certificate by nginx | Edge client certificate |
The per-IP filter returns these headers on every response it limits:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87
When exceeded the answer is 429:
{
"error": "Rate limit exceeded",
"message": "Too many requests. Please try again later."
}
The per-key limit also answers 429, with Retry-After: 60.
Payment blacklist
Entries are scoped to your PSP; Hokeka also maintains a platform-wide list that applies to every PSP (you can read it, only Hokeka can change it). Removing an entry deactivates it (kept for audit); adding the same value again re-activates it. Every change is audit-logged.
POST /api/v1/blacklist
Cookie: JSESSIONID=...
Content-Type: application/json
{ "entryType": "pan", "value": "4111111111111111", "reason": "confirmed card fraud",
"expiresAt": "2026-12-31T23:59:59" }
entryType:pan,terminal,ip,device,payment_details,wallet.- A PAN is accepted raw (12–19 digits) or as its 64-hex lookup hash; it is stored only as the hash the ingest pipeline uses, never in clear.
GET /api/v1/blacklist?type=&active=&page=&size=,GET /api/v1/blacklist/{id},PATCH /api/v1/blacklist/{id}(reason,expiresAt,clearExpiry,active),DELETE /api/v1/blacklist/{id}?reason=. A duplicate active entry →409.
4. Billing & Usage Tracking
How You Are Billed
You are billed monthly on a plan: a monthly fee in the plan currency (USD or KES), a number of
included transactions, and an overage price beyond that number, either per transaction or per
block of N transactions (a partial block counts as a full block). One transaction is one
evaluation on your Edge Node, counted from the decision records it sends. Billing never blocks
evaluation or any other function; at 80 % and 100 % of the included transactions you get an email
alert and the Console plan card shows your usage. VAT is applied at your configured rate. Add-on
cloud services (screening, KYC verification, report generation, risk assessment) are charged per
call at your plan's rates. Case work, SAR filing and CBK reporting are never charged. Details,
including a worked example, are in features/BILLING_PRICING.md.
Tracked Service Types
| Service Type | What Gets Tracked |
|---|---|
| Transactions | Each accepted Edge decision record (legacy: each POST /transactions/ingest) |
SANCTIONS_SCREENING_PERSON / _ORGANIZATION | Each sanctions screening call |
AML_SCREENING | Each AML check / detection / batch screening call |
WALLET_SCREENING, VASP_SCREENING, TRAVEL_RULE_TRANSFER, EDD_SCREENING | Virtual-asset and EDD calls |
RISK_ASSESSMENT | Each risk assessment call |
REPORT_GENERATION | Each report generated |
KYC_VERIFICATION | Each merchant onboarding verification |
Your plan, included transactions, usage this month and alerts: GET /api/v1/entitlements/me.
Viewing Your Usage
Real-time current month usage:
GET /api/v1/billing/usage/{pspId}/current
Cookie: JSESSIONID=...
Response:
{
"pspId": 7,
"period": "2026-06 (live)",
"totalRequests": 15423,
"billableRequests": 15423,
"totalCost": 771.15,
"breakdown": [
{ "serviceType": "TRANSACTION_PROCESSING", "requestCount": 12000, "cost": 600.00 },
{ "serviceType": "SANCTIONS_SCREENING", "requestCount": 3000, "cost": 150.00 },
{ "serviceType": "AML_CHECK", "requestCount": 423, "cost": 21.15 }
]
}
Monthly usage summary:
GET /api/v1/billing/usage/{pspId}?month=2026-05
Invoice Lifecycle
Usage recorded ──→ Month closes (3 days after month end) ──→ Invoice issued (SENT, emailed) ──→ PAID
│ (M-Pesa/bank)
OVERDUE (past due date, reminders)
Invoices are numbered HKK-<year>-<6 digits>. Your service is never suspended automatically for
an unpaid invoice.
Subscription Management
# Get active subscription
GET /api/v1/subscriptions/psp/{pspId}
# List all subscriptions
GET /api/v1/subscriptions
# Get usage history for billing
GET /api/v1/subscriptions/{id}/usage-history
Available Pricing Tiers
| Tier | Monthly Fee | Per-Check Price | Included Checks | Monthly Minimum |
|---|---|---|---|---|
| Starter | $0 | $0.050 | 1,000 | $0 |
| Growth | $199 | $0.030 | 10,000 | $199 |
| Scale | $499 | $0.020 | 50,000 | $499 |
| Enterprise | $999 | $0.010 | 200,000 | $999 |
Volume discounts apply automatically for high-volume tiers.
5. Transaction Monitoring
Your Edge Node evaluates every transaction on your premises. Your API nodes call it once per transaction. You do not call the cloud per transaction.
Behaviour of /edge/evaluate in full: edge-transaction-evaluation.md.
Residency rules: EDGE_DATA_RESIDENCY_CONTRACT.md. Edge install:
edge-client-install-guide.md.
5.1 Core transaction flow
Your API node Your Edge Node (on premises) Hokeka cloud
│ │ │
│ POST /edge/evaluate │ │
│─────────────────────────────▶│ velocity (Aerospike) + rule bundle │
│ │ record evaluation + decision record │
│ │ in the Edge Ledger (PostgreSQL) │
│ { action, score, ... } │ │
│◀─────────────────────────────│ │
│ │ outbox: decision records v2 │
│ │ (mTLS, sealed, outbound 443) ───────────▶│ alerts, cases,
│ │ │ CTR deadlines,
│ │ │ webhooks
5.2 Evaluate a transaction
POST https://edge.internal.mypsp.com:8443/edge/evaluate
Content-Type: application/json
{
"txn_id": "auth-2026-000123",
"pan_hash": "sha256:4a7d...",
"amount_cents": 1250000,
"currency": "KES",
"country_code": "KE",
"merchant_id": "mrc_00421",
"merchant_name": "Example Stores Ltd",
"mcc": "5411",
"channel": "ECOMMERCE",
"cardholder_name": "Jane Wanjiku",
"beneficiary_name": "Example Stores Ltd"
}
-
Send the party names. Required. At least one of
counterparty_name,cardholder_nameorbeneficiary_name, in any script. Without one the payment is not screened and the answer isHOLDwithholdCodeSCREENING_NO_NAME. Sendmerchant_nametoo when you have it: it is screened as well. Names stay on your Edge and never reach the cloud. -
Optional, for fewer false matches:
counterparty_birth_date,cardholder_birth_date,beneficiary_birth_date(YYYY-MM-DD),counterparty_country,cardholder_country,beneficiary_country,merchant_country, andcustomer_id. A strong name match with a conflicting birth date or country is held for review instead of blocked. -
merchant_idis your own merchant reference. Enter the same value on the merchant in the Console. Merchant blocks match on it. -
Send
pan_hash, never the PAN. -
txn_idis your reference, 1..128 printable ASCII without spaces. A PAN-like value (13 to 19 digits that pass the Luhn check) gets422 {"error":"TXN_REF_LOOKS_LIKE_PAN"}; an invalid one gets422 {"error":"TXN_REF_INVALID"}. Nothing is evaluated or stored in either case. -
amount_centsis an integer in the currency's minor unit. -
Other fields are passed to the rules as features.
Response:
{
"action": "ALERT",
"score": 45,
"triggeredRuleIds": [1204, 1311],
"reasons": ["Card velocity above 10 transactions per hour"],
"triggeredRuleVersionIds": [88012, 88140],
"edgeAssessmentId": "3f0c9a2e-7d41-4b8e-9a51-0c2f6b1d7e44",
"ruleBundleVersion": 37,
"ruleBundleHash": "sha256:9b1f...",
"screeningStatus": "SCREENED",
"screeningListVersion": "20261101-abc",
"holdCode": null
}
screeningStatus says what sanctions screening did: SCREENED, STALE (screened against an old list),
NO_NAME, NO_LIST, LIST_NOT_LOADED or NOT_RUN. Treat anything other than SCREENED or STALE as not
screened. holdCode names a screening hold: SCREENING_NO_NAME (send names), SCREENING_NO_LIST and
SCREENING_LIST_NOT_LOADED (no usable list on this Edge), SCREENING_STALE_LIST (list not verified within
its limit). Hold the payment and contact support. Do not tell the customer it relates to sanctions.
Decision Values
| Decision | Meaning | Action for PSP |
|---|---|---|
| ALLOW | No suspicious signals detected | Process normally |
| ALERT | Risk detected but below HOLD | Process, and expect an alert for review |
| HOLD | Suspicious, or the node cannot decide safely (fail-closed) | Do not approve on your own; route to review or step-up |
| BLOCK | Hard rule triggered | Decline the transaction |
| FLAG | Engine action between ALERT and HOLD | Treat as ALERT |
| SUSPEND | Engine action between HOLD and BLOCK | Treat as HOLD |
The response carries the raw engine action, so FLAG and SUSPEND can appear. Only the ledger
row and the decision record map them to ALERT and HOLD.
5.3 What the cloud receives
Your Edge writes each evaluation and its decision record to the Edge Ledger in one local
transaction. A background shipper sends the records, sealed and over mTLS, to
POST /api/v1/edge/assessments. A restart or a cloud outage loses nothing: the records wait in the
ledger outbox.
A decision record holds edgeAssessmentId, txnRef (your txn_id), node and site ids, the
evaluation time to the second, decision, score, rule ids and versions, bundle version and hash,
merchantId, ctrRequired, sanctionsHit with public watchlist entry ids, and typology codes.
It never holds the PAN or its hash, the amount, currency or country, customer, account, device or IP identifiers, names, or free-text reasons. The cloud rejects any record with an unknown field.
From each record the cloud (current):
- raises an alert for
ALERT,HOLD,BLOCK, a sanctions hit or a CTR flag; - opens or joins a case for a sanctions hit, and for a rule decision when auto-create-cases is on;
- schedules a CTR filing deadline when
ctrRequiredis set; - sends a
RISK_ALERTwebhook with IDs and the decision only (§10).
5.4 Viewing transactions
- Alerts and cases show the
edgeAssessmentIdandtxnRef. The transaction detail stays on your Edge. - The Console pulls transaction detail live from your Edge over the Edge Tunnel (built: R5, R6,
R11). Your Edge opens that WebSocket outbound on port 443 with mTLS. You open no inbound port.
The Console transaction pages call the tunnel-backed
GET /api/v1/transactions*endpoints (EdgeTransactionController). Every pull names the user and purpose and is logged both in the cloud (Console Data Access Log) and on your Edge (edge_access_log). You can turn the tunnel off locally (EDGE_TUNNEL_ENABLED=false). While your Edge is offline the Console shows an offline banner and no transaction detail.
5.5 Retention, support access and cross-PSP intelligence
- Retention. Set it in the Console under Settings → Data Retention. Minimum and default 5
years (FATF R.11), maximum 25. A second approver must approve each change (maker-checker). An
increase takes effect on approval; a reduction takes effect 30 days after approval. The setting
reaches your Edge in the sealed config document (
GET /api/v1/edge/config). The cloud purges its decision records past retention, except those linked to open alerts, cases, unfiled SARs or holds. Your Edge purges its own ledger daily past retention, skipping rows under legal hold (R2). - Hokeka support access. Off by default. A PSP admin turns it on under Settings → Hokeka
support access. While it is off, Hokeka staff cannot pull your Edge data: the cloud refuses the
request, and your Edge independently refuses it (
SUPPORT_ACCESS_DISABLED). Each support pull needs a written reason and is shown in your access log, flagged as a support pull. - Cross-PSP intelligence. Opt-in under the same settings. Indicators come only from filed SARs of opted-in PSPs and are limited to merchant IDs and keyed device tokens. No PSP can read another PSP's data.
- Filed reports. Filed SAR, STR and CTR documents are kept in the cloud as the regulatory record. Drafts never store pulled transaction data.
5.6 Legacy cloud ingest (deprecated)
POST /api/v1/transactions/ingest and POST /api/v1/transactions/ingest/batch still work for
existing integrations. They store the transaction in the cloud and run the old cloud pipeline.
This contradicts the residency model.
- Do not build new integrations on them.
- Built (R15): legacy cloud transactions are copied to your Edge Ledger over the tunnel and the
copy is verified (Console Edge back-fill,
/edge-backfill). - Pending (R12): the cloud deletes the ingest endpoints and its transaction tables, after the back-fill is verified and every cloud-only detection runs on the Edge.
Reference for existing callers:
POST /api/v1/transactions/ingest
X-Api-Key: hka_...
Idempotency-Key: auth-2026-000123
Content-Type: application/json
{
"amountCents": 1250000,
"currency": "KES",
"merchantId": "123",
"terminalId": "TERM-456",
"pan": "4111111111111111",
"ipAddress": "41.204.187.12",
"countryCode": "KE",
"direction": "OUTBOUND",
"channelType": "ECOMMERCE",
"cashTransaction": false,
"customerAccountReference": "PSP-TOKEN-CUST-78901",
"customerEmail": "customer@example.com",
"acquirerResponse": "00"
}
merchantId is the numeric platform merchant ID as a string; it must belong to your PSP. Errors:
a non-numeric merchantId or a non-UUID edgeAssessmentId → 400; an unknown merchant or one of
another PSP → 404; an edgeAssessmentId already linked to another transaction → 409. Nothing
is persisted for a rejected request. The backend hashes a raw PAN at once and never persists it.
customerAccountReference must be a PSP-issued token, not a raw account number; it and
customerEmail are source evidence for CBK failed-transaction reporting; email is encrypted at
rest.
The batch endpoint takes a bare JSON array of the same objects and returns one result per item, in
order. Duplicate requests with the same Idempotency-Key (or body clientReference, unique per
PSP) return the existing transaction without new alerts or webhooks.
An admin-only maintenance trigger re-scores yesterday's legacy cloud transactions. It is not part of a PSP integration:
POST /api/v1/batch/score/yesterday
Cookie: JSESSIONID=... # Console session; SUPER_ADMIN or ADMIN role only
6. Sanctions Screening & AML Checks
Name Screening
Screen individuals or entities against OFAC, UN, EU, and custom watchlists in real-time:
POST /api/v1/sanctions/screen
Cookie: JSESSIONID=...
Content-Type: application/json
{
"name": "John Doe",
"dateOfBirth": "1980-01-15",
"country": "KE"
}
Response:
{
"matchFound": false,
"screenId": "scr_abc123",
"similarity": 0.0,
"matches": [],
"screeningResult": "CLEAR"
}
AML Check
POST /api/v1/aml/check
Cookie: JSESSIONID=...
Content-Type: application/json
{
"amount": 15000.00,
"currency": "KES",
"country": "KE",
"merchantId": "MRC-00123",
"senderAccount": "ACC-789",
"receiverAccount": "ACC-456"
}
Risk Assessment
GET /api/v1/risk/customer/{merchantId}/rating
Cookie: JSESSIONID=...
7. Alerts & Case Management
Alert Lifecycle
Transaction
│
▼
Alert Created (OPEN)
│
▼
Investigator Reviews ──┬── FALSE_POSITIVE → closed
│ │
│ ├── CLEARED → resolved
│ │
│ ├── TRUE_POSITIVE → case created
│ │
│ └── ESCALATED → case created + escalated
│
▼
Case Created ──┬── APPROVE → CLOSED_CLEARED
│
├── REJECT → CLOSED_BLOCKED
│
└── FILE_SAR → CLOSED_SAR_FILED
List Alerts
GET /api/v1/alerts?page=0&size=25&status=open
Cookie: JSESSIONID=...
Resolve an Alert
PUT /api/v1/alerts/{id}/resolve
Cookie: JSESSIONID=...
Content-Type: application/json
{
"disposition": "TRUE_POSITIVE_BLOCKED",
"notes": "Card testing pattern detected. Merchant blocked."
}
Case Management
# Get case timeline
GET /api/v1/cases/{id}/timeline
# Get case network graph
GET /api/v1/cases/{id}/graph?depth=2
# Make a decision
POST /api/v1/cases/{id}/decisions
{
"decisionType": "FILE_SAR",
"justification": "Transactions match structuring pattern (7 deposits of $9,500)"
}
# Escalate a case
POST /api/v1/cases/{id}/escalate
{
"reason": "Amount exceeds $100K threshold, requires MLRO review"
}
Escalation Matrix
| Priority | SLA | Escalation Path |
|---|---|---|
| CRITICAL | 4 hours | → MLRO |
| HIGH | 24 hours | → COMPLIANCE_OFFICER |
| MEDIUM | 3 days | → ANALYST |
| LOW | 7 days | → ANALYST |
8. Reports & Regulatory Compliance
Generate a Report
POST /api/v1/reports/generate
Cookie: JSESSIONID=...
Content-Type: application/json
{
"reportType": "TRANSACTION_SUMMARY",
"parameters": {
"dateFrom": "2026-05-01",
"dateTo": "2026-05-31"
}
}
Download Report
GET /api/v1/reports/download/{id}?format=pdf
Supported formats: pdf, csv, xml
Schedule Recurring Reports
POST /api/v1/reports/schedule
Content-Type: application/json
{
"reportId": "TRANSACTION_SUMMARY",
"schedule": "0 0 8 1 * *",
"parameters": { "pspId": 7 },
"recipients": ["compliance@mypsp.com"]
}
9. CBK (Central Bank of Kenya) Reporting
For PSPs licensed by the Central Bank of Kenya, the platform provides automated GDI (Gateway Data Interface) submissions covering all 17 required endpoints.
CBK Endpoint Categories
| Cadence | Count | Endpoints |
|---|---|---|
| Daily | 8 | Cyber incidents, Fraud incidents, System stability, System activity (24h TPS/TPH), Trust account, Billing template, Merchant transactions, Failed transactions |
| Monthly | 5 | Customer complaints, Products info, Card brands, Transaction details, Tariffs |
| Annual | 4 | Senior management, Directors, Trustees, Shareholders |
CBK Configuration
PSP_ADMINs can view their CBK configuration:
GET /api/v1/psps/{id}/cbk-config
SUPER_ADMINs configure CBK credentials and promote to live:
PUT /api/v1/psps/{id}/cbk-config
Submission History
GET /api/v1/compliance/cbk/submissions?pspId=7&status=SUBMITTED
Manual retrigger for a specific endpoint:
POST /api/v1/compliance/cbk/submissions/{endpointType}/run
{ "pspId": 7 }
10. Webhooks & Event Subscriptions
Corrected (W36-2): this section previously documented a richer feature than exists — a single
POST /webhooks/subscribe call accepting multiple events and a client-supplied secret, with
eight event types spanning transactions/alerts/cases/invoices. The real, working endpoint (it
returned 404 before this fix; there was no controller at all) is simpler:
POST /api/v1/webhooks/subscribe
Cookie: JSESSIONID=...
Content-Type: application/json
{
"callbackUrl": "https://mypsp.com/webhooks/hokeka",
"eventType": "RISK_ALERT"
}
Each call creates one subscription for one event type — call it again with a different
eventType to subscribe to more than one. The signing secret is server-generated, not
client-supplied; it's returned in the response body's secretKey field (store it — it isn't shown
again). callbackUrl must be HTTPS. Subscriptions are automatically scoped to your own PSP; you
cannot subscribe on another tenant's behalf.
GET /api/v1/webhooks/subscriptions # list your own PSP's subscriptions
DELETE /api/v1/webhooks/subscriptions/{id} # unsubscribe
Available Events
All three event types are delivered through the durable webhook outbox (same transactional
pattern as Kafka internal events). Delivery is retried with exponential backoff; permanently failed
rows are marked FAILED in event_outbox for operator visibility. Subscriptions auto-disable after
five consecutive delivery failures.
| Event | Triggered When |
|---|---|
RISK_ALERT | New alert raised from an Edge decision record (or from the deprecated cloud ingest) |
CASE_UPDATE | Compliance case created or updated; case decision recorded |
MERCHANT_STATUS_CHANGE | PSP status transition (manual admin, dunning suspension, payment reactivation) |
Edge and webhooks: the cloud sends webhooks, never the Edge. When your Edge's decision record reaches the cloud and raises an alert, the cloud sends
RISK_ALERTwith IDs and the decision only. It never carries the amount, card or customer data. Join it to your own records bytxnRef(yourtxn_id) oredgeAssessmentId.
Webhook Payload (RISK_ALERT from an Edge decision record)
{
"alertId": 445566,
"edgeAssessmentId": "3f0c9a2e-7d41-4b8e-9a51-0c2f6b1d7e44",
"txnRef": "auth-2026-000123",
"decision": "HOLD",
"pspId": 7,
"merchantId": "4",
"caseId": 1234,
"timestamp": "2026-10-17T14:32:15"
}
txnRef, merchantId and caseId are omitted when absent.
Webhook Payload (RISK_ALERT from the deprecated cloud ingest)
{
"alertId": 445566,
"txnId": 1048576,
"action": "HOLD",
"score": 0.75,
"status": "open",
"severity": "HIGH",
"sarRequired": true,
"ctrRequired": false,
"triggeredRules": ["R-2", "R-7", "R-30"],
"pspId": 7,
"merchantId": "4",
"timestamp": "2026-06-29T14:32:15"
}
Webhook Payload (CASE_UPDATE)
{
"caseId": 1234,
"caseReference": "CASE-20260918-ABC",
"previousStatus": "IN_PROGRESS",
"status": "CLOSED_CLEARED",
"decision": "APPROVE",
"resolution": "CLEARED",
"merchantId": 4,
"pspId": 7,
"decidedBy": "analyst@psp.com",
"timestamp": "2026-09-18T14:32:15"
}
Webhook Payload (MERCHANT_STATUS_CHANGE)
{
"pspId": 7,
"pspCode": "ACME",
"previousStatus": "ACTIVE",
"status": "SUSPENDED",
"reason": "MANUAL_STATUS_UPDATE",
"timestamp": "2026-09-18T14:32:15"
}
Idempotency
- Each Edge evaluation gets its own
edgeAssessmentId. The cloud stores each decision record once: a re-sent identical record counts as a duplicate, and one alert is raised per record. - The deprecated cloud ingest uses
Idempotency-KeyorclientReference(§5.6).
11. Error Handling & Rate Limits
HTTP Status Codes
| Code | Meaning | Action |
|---|---|---|
| 200 OK | Success | Process response |
| 201 Created | Resource created | Extract ID from response |
| 202 Accepted | Async request accepted | Poll status endpoint |
| 400 Bad Request | Invalid payload | Check request format |
| 401 Unauthorized | Missing/expired session, or invalid/revoked/expired API key | Log in again / check the key |
| 403 Forbidden | Insufficient permissions | Check PSP scope |
| 404 Not Found | Resource not found | Verify resource ID |
| 409 Conflict | Duplicate resource | Check idempotency key |
| 422 Unprocessable | Business rule violation; on the Edge, TXN_REF_LOOKS_LIKE_PAN / TXN_REF_INVALID | Check validation errors; never send a PAN as txn_id |
| 429 Too Many Requests | Rate limit exceeded | Backoff and retry |
| 500 Internal Error | Server error | Retry with backoff |
Error Response Format
{
"error": "validation_failed",
"message": "Amount must be greater than 0",
"status": 422,
"timestamp": "2026-06-29T14:30:00Z",
"path": "/api/v1/cases",
"details": {
"field": "amount",
"rejectedValue": -100
}
}
12. Integration Examples
These examples call /edge/evaluate on your own Edge Node. Use your Edge host name and the CA that
issued its TLS certificate.
cURL
# Evaluate a transaction on your Edge Node
curl -s -X POST https://edge.internal.mypsp.com:8443/edge/evaluate \
--cacert edge-ca.pem \
-H "Content-Type: application/json" \
-d '{
"txn_id": "auth-2026-000123",
"pan_hash": "sha256:4a7d...",
"amount_cents": 1250000,
"currency": "KES",
"merchant_id": "mrc_00421",
"merchant_name": "Example Stores Ltd",
"cardholder_name": "Jane Wanjiku",
"country_code": "KE"
}'
Python
import requests
class HokekaEdgeClient:
"""Calls /edge/evaluate on the PSP's own Edge Node."""
def __init__(self, base_url, ca_bundle, client_cert=None):
self.base_url = base_url.rstrip("/")
self.session = requests.Session()
self.session.verify = ca_bundle
if client_cert: # (cert.pem, key.pem) when the Edge requires mutual TLS
self.session.cert = client_cert
def evaluate(self, txn_id, pan_hash, amount_cents, currency, merchant_id, **features):
body = {"txn_id": txn_id, "pan_hash": pan_hash, "amount_cents": amount_cents,
"currency": currency, "merchant_id": merchant_id, **features}
resp = self.session.post(f"{self.base_url}/edge/evaluate", json=body, timeout=2)
resp.raise_for_status()
return resp.json()
edge = HokekaEdgeClient("https://edge.internal.mypsp.com:8443", "edge-ca.pem")
result = edge.evaluate("auth-2026-000123", "sha256:4a7d...", 1250000, "KES", "123", country_code="KE")
print(result["action"], result["score"], result["edgeAssessmentId"])
JavaScript / Node.js
const https = require('https');
const fs = require('fs');
const axios = require('axios');
const edge = axios.create({
baseURL: 'https://edge.internal.mypsp.com:8443',
httpsAgent: new https.Agent({ ca: fs.readFileSync('edge-ca.pem') }),
timeout: 2000,
});
async function evaluate(txn) {
const { data } = await edge.post('/edge/evaluate', txn);
return data;
}
const result = await evaluate({
txn_id: 'auth-2026-000123', pan_hash: 'sha256:4a7d...',
amount_cents: 1250000, currency: 'KES', merchant_id: '123',
});
console.log(`Decision: ${result.action}`); // treat HOLD as "do not approve on your own"
Java / Spring Boot
@Service
public class HokekaEdgeClient {
private final RestClient rest;
public HokekaEdgeClient(@Value("${hokeka.edge.base-url}") String baseUrl, RestClient.Builder builder) {
// Configure the builder's request factory with an SSLContext that trusts your Edge's CA
// (and holds your client certificate if the Edge requires mutual TLS).
this.rest = builder.baseUrl(baseUrl).build();
}
public Map<?, ?> evaluate(String txnId, String panHash, long amountCents, String currency, String merchantId) {
return rest.post().uri("/edge/evaluate")
.body(Map.of("txn_id", txnId, "pan_hash", panHash, "amount_cents", amountCents,
"currency", currency, "merchant_id", merchantId))
.retrieve()
.body(Map.class);
}
}
13. SDK & Client Libraries
Official client libraries are available for:
| Language | Package | Status |
|---|---|---|
| Python | pip install hokeka-aml | ✅ Available |
| Node.js | npm install @hokeka/aml-client | ✅ Available |
| Java | Maven: com.hokeka:aml-client | ✅ Available |
| PHP | composer require hokeka/aml-client | ✅ Available |
| Go | go get github.com/hokeka/aml-go | ✅ Available |
14. Support & Troubleshooting
Integration Checklist
- PSP account created and activated by platform admin
- PSP_ADMIN user credentials issued
- Console login (session cookie) working
- Edge Node installed and activated (outbound 443 only; no inbound ports)
-
/edge/evaluatetested from your API nodes; no PAN sent astxn_id - Alerts from Edge decision records visible in the Console
- Data retention reviewed under Settings → Data Retention (default 5 years)
- Decision handling implemented (ALLOW/HOLD/BLOCK/ALERT)
- Webhook endpoints configured (optional but recommended)
- Billing usage monitoring set up
- Production go-live approved
Common Issues
| Symptom | Likely Cause | Solution |
|---|---|---|
401 Unauthorized | Expired session or invalid API key | Log in again, or check the key |
422 TXN_REF_LOOKS_LIKE_PAN from the Edge | txn_id looks like a card number | Send your own transaction reference |
HOLD on every evaluation | Edge not activated, no rule bundle, or ledger down (fail-closed) | Check the Edge status and logs |
403 Forbidden | Wrong role for endpoint | Check user has required role |
429 Rate Limited | Exceeded the per-IP or per-key limit (section 3, Rate Limiting) | Back off and retry after a minute; send machine calls with your API key |
| High false positive rate | Rules not tuned | Contact support for rule calibration |
| Missing data in reports | PSP not configured for CBK | Contact platform admin |
Getting Help
| Channel | Contact |
|---|---|
| Support Email | support@hokeka.com |
| Integration Help | integrations@hokeka.com |
| Billing Queries | billing@hokeka.com |
| Swagger UI | https://api.hokeka.com/api/v1/swagger-ui.html |
| Status Page | https://status.hokeka.com |
Last Updated: October 17, 2026
© Hokeka AML Platform — Modern AML & Fraud Detection for PSPs and Banks