Partner API — v1
API Reference
A REST API for cross-border payments on the Canada → Tanzania corridor. Idempotent money movement, compliance on the path, signed webhooks, and a strict double-entry ledger — integrated through a single, versioned interface.
Developer portal & self-serve keys — coming next.
Introduction
The Moja Switch Partner API
The Partner API is organized around a single resource — the payment. You create a payment, we screen it for compliance before any funds move, route it across send and receive rails, and deliver signed webhooks as it progresses. The partner sets the FX rate (BYO-FX) and Moja executes it verbatim — we never price the customer.
All requests and responses are JSON. Every monetary amount is a decimal string in major units with 2 decimal places (for example "250.00") carried as a Money object { value, currency }; the platform stores integer minor units internally. Both CAD and TZS amounts use two decimal places in the API (e.g. TZS "184099.88").
A maximum single-payment limit applies per send currency — exceeding it returns 422 transaction_limit_exceeded. There is no enforced minimum today. Payments at or above CAD 10,000 are automatically flagged for FINTRAC LVCTR reporting (this does not block the payment).
Base URLs
Environments
The API is versioned in the path. Use the sandbox for evaluation and integration testing; production keys are issued during onboarding. Sandbox uses deterministic mock rails — payments settle successfully by default, so you can exercise the full happy path end to end. Per-request outcome simulation (forcing a payout_failed or compliance_blocked) is planned.
Production
https://api.mojaswitch.com/v1
Sandbox
https://api.sandbox.mojaswitch.com/v1
Authentication
API keys & scopes
Authenticate every request with a bearer API key in the Authorization header. Sandbox keys are prefixed sk_sandbox_ and production keys sk_live_. Keys are issued per partner; request sandbox keys via "Request sandbox access".
Keys follow least privilege: each key carries a subset of scopes. A route your key lacks the scope for returns 403 insufficient_scope. Partners are entitled to specific corridors — a non-entitled corridor returns 403 corridor_not_entitled.
read
Retrieve payments (GET).
write
Create payments and quotes.
cancel
Cancel a payment pre-collection.
curl https://api.sandbox.mojaswitch.com/v1/payments/pay_a1b2c3d4 \
-H "Authorization: Bearer sk_sandbox_9f8e7d6c5b4a3210"Idempotency
Safe retries on money APIs
An Idempotency-Key header is required on POST /v1/payments and must be a UUID. Reusing the same key with the same request body returns the original payment (HTTP 200, versus 201 on the first create) — no duplicate is created. This makes retries after a network timeout safe.
- Same key + different body →
409 idempotency_key_conflict. - Missing key →
400 missing_idempotency_key. - Non-UUID key →
400 invalid_request.
Every payment must also carry a unique partner reference; a repeated reference returns 409 duplicate_reference.
There is no list or search endpoint: persist each returned payment_id and use GET /v1/payments/{id} to reconcile. Payment state is delivered primarily via webhooks.
Rate limits
Throttling
Planned. Per-partner rate limiting (returning 429 rate_limit_exceeded) is on the roadmap and not yet enforced in the current API. When it ships, back off and retry — combined with idempotency keys, retries are always safe and never create duplicate payments.
Quickstart
Integrate in three steps
Get API keys
Request sandbox credentials for evaluation. Production keys are issued per partner during onboarding, scoped to your entitled corridors.
Create a payment
POST /v1/payments with a UUID Idempotency-Key and a unique reference. Receive the Payment object and its initial status.
Handle webhooks
Verify the Moja-Switch-Signature HMAC, dedupe on the event id, and advance your system on payment.completed.
Errors
Error catalog
Errors use conventional HTTP status codes and a consistent JSON body. The request_id is safe to log and share with support; field-level problems are itemized in details[].
{
"error": {
"code": "validation_error",
"message": "One or more fields failed validation",
"request_id": "req_8c2f1a9b",
"payment_id": "pay_a1b2c3d4",
"details": [
{
"field": "payout.beneficiary.phone",
"message": "must be a valid E.164 phone number"
}
]
}
}Request & authentication
Returned as an HTTP status. Malformed requests, credentials, scopes, and entitlements.
| Code | HTTP | Description |
|---|---|---|
| invalid_request | 400 | Malformed request or a non-UUID Idempotency-Key. |
| missing_idempotency_key | 400 | POST /v1/payments sent without an Idempotency-Key header. |
| unauthorized | 401 | Missing or invalid API key. |
| forbidden | 403 | Partner account is disabled (SUSPENDED or OFFBOARDED). |
| corridor_not_entitled | 403 | Partner is not entitled to the requested corridor. |
| insufficient_scope | 403 | API key lacks the scope (read / write / cancel) the route requires. |
| transaction_limit_exceeded | 422 | Send amount exceeds the configured maximum single-payment limit for the currency. |
Validation
Returned as an HTTP status. Field-level and business-rule validation failures.
| Code | HTTP | Description |
|---|---|---|
| validation_error | 422 | One or more fields failed validation (see details[]). |
| fx_required | 422 | Production request omitted the client-provided fx object (BYO-FX is mandatory in production; a quote_id does not satisfy it). |
| fx_inconsistent | 422 | fx.rate and fx.receive_amount disagree: |receive_amount − round(send_amount × rate)| > 1 minor unit. |
| fx_invalid | 422 | Supplied FX failed fail-closed validation (non-positive rate, out of precision, or corridor-invalid). |
| unsupported_corridor | 422 | Corridor is not supported (MVP: CA-TZ only). |
| payout_method_unsupported | 422 | Payout method not supported (MVP: MOBILE_MONEY only; BANK_TRANSFER is rejected). |
| duplicate_reference | 409 | reference already used by this partner. |
| idempotency_key_conflict | 409 | Same Idempotency-Key reused with a different request body. |
Lifecycle
Returned as an HTTP status. State-dependent operations.
| Code | HTTP | Description |
|---|---|---|
| payment_not_found | 404 | No payment matches the supplied id. |
| cancel_not_allowed | 409 | Payment is past collection and can no longer be cancelled. |
Surfaces on the payment (not an HTTP error)
Rail and compliance outcomes are NOT HTTP errors. The create/get call returns 201/200 with a FAILED payment; the reason is in the payment's failure object ({ code, message, stage }).
| Code | HTTP | Description |
|---|---|---|
| compliance_blocked | failure | Blocked by sanctions / AML / fraud / KYC screening. A REVIEW outcome is also treated as a block in the MVP. |
| collection_failed | failure | Send-side collection failed. Payment → FAILED (stage: collection). |
| payout_failed | failure | Receive-side payout failed. Payment → FAILED (stage: settlement). |
| rail_unavailable | failure | A required rail was unavailable at routing. Payment → FAILED (stage: routing). |
Planned (not yet emitted)
Reserved in the contract but not returned by the current MVP. Do not build against these yet.
| Code | HTTP | Description |
|---|---|---|
| rate_limit_exceeded | 429 | Per-partner rate limiting — not yet enforced. |
| amount_out_of_range | 422 | Minimum-amount enforcement — not yet implemented (a maximum is enforced via transaction_limit_exceeded). |
| quote_expired | 422 | Quote-lock binding at create — quotes are indicative today (quote_id is not yet binding). |
| fx_unavailable | 503 | Live FX provider errors — not applicable to the MVP stub. |
| beneficiary_invalid | 422 | Receive-rail beneficiary validation — planned. |
| kyc_insufficient | 422 | Reserved. A failed sender KYC attestation surfaces today as compliance_blocked on a FAILED payment. |
| compliance_review_required | 422 | Reserved. A manual-review outcome is treated as a block (compliance_blocked) in the MVP. |
Endpoint
Create a quote
Validate a client-supplied FX quote before creating a payment. Under BYO-FX the partner brings the rate — Moja does not price it; in production the fx object is required, and in sandbox you may omit it to receive a labelled indicative rate. quote_id is reserved for rate-lock but not yet binding. BANK_TRANSFER returns 422 unsupported_payout_method.
/v1/quotesValidate a client-supplied FX quote (sandbox may derive an indicative rate).
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| corridor | string | required | Payment corridor to price.Enum: CA-TZ |
| send_amount | Money | required | Amount to send on the collection side.currency must be CAD |
| └ value | string | required | Decimal amount in major units, 2 dp.e.g. "250.00" |
| └ currency | string | required | Send currency.CAD |
| receive_currency | string | required | Currency delivered to the beneficiary.Enum: TZS |
| payout_method | string | required | Delivery method on the receive side.MOBILE_MONEY (MVP) | BANK_TRANSFER → 422 |
| fx | FX | conditional | Client-supplied FX (BYO-FX). Validated/echoed — Moja does not price it. Required in production; may be omitted in sandbox to receive a labelled indicative rate.{ rate, receive_amount } |
| └ rate | string | conditional | The partner's send→receive rate.positive decimal, ≤ 8 dp |
| └ receive_amount | Money | conditional | Exact payout the partner instructs.currency TZS |
Response 201 · Quote
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | Quote identifier.quote_... |
| corridor | string | required | Echoed corridor.CA-TZ |
| send_amount | Money | required | Echoed send amount. |
| receive_amount | Money | required | The partner's supplied receive amount (echoed); sandbox derives an indicative value when fx is omitted. |
| fx_rate | string | required | The partner's declared rate (echoed). Not priced by Moja; sandbox derives an indicative rate only when fx is omitted.e.g. "1850.25" |
| expires_at | string | required | Quote expiry.RFC3339 |
| created_at | string | required | Creation time.RFC3339 |
curl -X POST https://api.sandbox.mojaswitch.com/v1/quotes \
-H "Authorization: Bearer sk_sandbox_..." \
-H "Content-Type: application/json" \
-d '{
"corridor": "CA-TZ",
"send_amount": {
"value": "250.00",
"currency": "CAD"
},
"receive_currency": "TZS",
"payout_method": "MOBILE_MONEY"
}'{
"id": "quote_7f3c8a2e",
"corridor": "CA-TZ",
"send_amount": {
"value": "250.00",
"currency": "CAD"
},
"receive_amount": {
"value": "460249.69",
"currency": "TZS"
},
"fx_rate": "1840.99875",
"expires_at": "2026-06-09T14:47:00Z",
"created_at": "2026-06-09T14:32:00Z"
}Endpoint
Create a payment
Create and send a cross-border payment. Requires a UUID Idempotency-Key and a unique reference. Compliance screening runs before funds move.
/v1/paymentsCreate and send a cross-border payment.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| corridor | string | required | Payment corridor.Enum: CA-TZ |
| reference | string | required | Partner reference for the payment.Max 128 chars; unique per partner |
| quote_id | string | optional | Reference to a prior quote. Does not satisfy the production FX requirement — supply fx inline.Reserved — not yet binding in MVP |
| send_amount | Money | required | Amount debited on the send side.currency must be CAD |
| fx | FX | conditional | Client-provided FX (BYO-FX). The partner sets the rate; Moja executes it verbatim and never prices the customer. Required inline in production (else 422 fx_required); omit only in sandbox (Moja derives a labelled dev rate — never production pricing).{ rate, receive_amount } |
| └ rate | string | conditional | Declared-basis send→receive rate. Recorded and consistency-checked, not used to recompute the payout.positive decimal, ≤ 8 dp |
| └ receive_amount | Money | conditional | Authoritative payout — the exact amount paid to the beneficiary, verbatim.currency TZS |
| └ value | string | required | Decimal, major units, 2 dp.e.g. "250.00" |
| └ currency | string | required | Send currency.CAD |
| sender | Party | required | The paying party. |
| └ type | string | required | Party type.INDIVIDUAL | BUSINESS |
| └ country | string | required | Sender country of residence.ISO-3166 alpha-2; must be CA |
| └ first_name | string | conditional | Given name.Individuals |
| └ last_name | string | conditional | Family name.Individuals |
| └ business_name | string | conditional | Legal entity name.Businesses |
| └ external_id | string | optional | Partner customer id linking to the KYC record. |
| sender_kyc | KYCAttestation | conditional | Partner KYC attestation for the sender.Required in prod when enforcement is on |
| └ attested | boolean | required | Partner attests KYC is complete. |
| └ attestation_id | string | required | Auditable reference to the partner's KYC record. |
| └ level | string | optional | Attestation level.e.g. FULL |
| └ attested_at | string | optional | When KYC was attested.RFC3339 |
| payout | Payout | required | Delivery instruction on the receive side. |
| └ method | string | required | Payout method.MOBILE_MONEY |
| └ currency | string | required | Receive currency.TZS |
| └ beneficiary | Beneficiary | required | Recipient of the funds. |
| └ type | string | required | Beneficiary type.INDIVIDUAL | BUSINESS |
| └ first_name | string | conditional | Given name.Individuals |
| └ last_name | string | conditional | Family name.Individuals |
| └ business_name | string | conditional | Legal entity name.Businesses |
| └ phone | string | required | Mobile-money destination number.E.164, e.g. +255712345678 |
| purpose | string | required | Reason for the payment (regulatory).FAMILY_SUPPORT | SCHOOL_FEES | UTILITY_BILL | MERCHANT_PAYMENT | OTHER |
| metadata | object | optional | Free-form string→string key/value pairs.Max 20 keys |
POST /v1/payments HTTP/1.1
Host: api.mojaswitch.com
Authorization: Bearer sk_live_••••••••
Idempotency-Key: 7f3c8a2e-9b1d-4e5f-a6c7-8d9e0f1a2b3c
Content-Type: application/json
{
"corridor": "CA-TZ",
"reference": "inv_9281",
"send_amount": {
"value": "250.00",
"currency": "CAD"
},
"sender": {
"type": "INDIVIDUAL",
"country": "CA",
"external_id": "cust_5f3a",
"first_name": "John",
"last_name": "Doe"
},
"sender_kyc": {
"attested": true,
"attestation_id": "kyc_ref_123",
"attested_at": "2026-01-02T03:04:05Z"
},
"payout": {
"method": "MOBILE_MONEY",
"currency": "TZS",
"beneficiary": {
"type": "INDIVIDUAL",
"phone": "+255712345678",
"first_name": "Amina",
"last_name": "Juma"
}
},
"purpose": "FAMILY_SUPPORT"
}{
"id": "pay_a1b2c3d4",
"status": "PAYOUT_PENDING",
"corridor": "CA-TZ",
"reference": "inv_9281",
"send_amount": {
"value": "250.00",
"currency": "CAD"
},
"receive_amount": {
"value": "462562.50",
"currency": "TZS"
},
"fx_rate": "1850.25",
"sender": {
"type": "INDIVIDUAL",
"country": "CA",
"external_id": "cust_5f3a",
"first_name": "John",
"last_name": "Doe"
},
"payout": {
"method": "MOBILE_MONEY",
"currency": "TZS",
"beneficiary": {
"type": "INDIVIDUAL",
"phone": "+255712345678",
"first_name": "Amina",
"last_name": "Juma"
}
},
"rails": {
"send": "ca_psp",
"receive": "tz_mobile_money"
},
"metadata": {},
"created_at": "2026-06-09T14:32:00Z",
"updated_at": "2026-06-09T14:32:01Z"
}curl -X POST https://api.sandbox.mojaswitch.com/v1/payments \
-H "Authorization: Bearer sk_sandbox_..." \
-H "Idempotency-Key: 7f3c8a2e-9b1d-4e5f-a6c7-8d9e0f1a2b3c" \
-H "Content-Type: application/json" \
-d '{
"corridor": "CA-TZ",
"reference": "inv_9281",
"send_amount": {
"value": "250.00",
"currency": "CAD"
},
"fx": {
"rate": "1850.25",
"receive_amount": { "value": "462562.50", "currency": "TZS" }
},
"sender": {
"type": "INDIVIDUAL",
"country": "CA",
"external_id": "cust_5f3a",
"first_name": "John",
"last_name": "Doe"
},
"sender_kyc": {
"attested": true,
"attestation_id": "kyc_ref_123",
"level": "FULL",
"attested_at": "2026-01-02T03:04:05Z"
},
"payout": {
"method": "MOBILE_MONEY",
"currency": "TZS",
"beneficiary": {
"type": "INDIVIDUAL",
"phone": "+255712345678",
"first_name": "Amina",
"last_name": "Juma"
}
},
"purpose": "FAMILY_SUPPORT"
}'Endpoint
Retrieve a payment
Fetch the current state of a payment by id. Terminal rail failures surface in the failure field rather than as HTTP errors; an unknown id returns 404 payment_not_found.
/v1/payments/{payment_id}Retrieve the current state of a payment.
curl https://api.sandbox.mojaswitch.com/v1/payments/pay_a1b2c3d4 \
-H "Authorization: Bearer sk_sandbox_..."{
"id": "pay_a1b2c3d4",
"status": "COMPLETED",
"corridor": "CA-TZ",
"reference": "inv_9281",
"send_amount": {
"value": "250.00",
"currency": "CAD"
},
"receive_amount": {
"value": "462562.50",
"currency": "TZS"
},
"fx_rate": "1850.25",
"sender": {
"type": "INDIVIDUAL",
"country": "CA",
"first_name": "John",
"last_name": "Doe"
},
"payout": {
"method": "MOBILE_MONEY",
"currency": "TZS",
"beneficiary": {
"type": "INDIVIDUAL",
"phone": "+255712345678",
"first_name": "Amina",
"last_name": "Juma"
}
},
"rails": {
"send": "ca_psp",
"receive": "tz_mobile_money"
},
"purpose": "FAMILY_SUPPORT",
"created_at": "2026-06-09T14:32:00Z",
"updated_at": "2026-06-09T14:33:20Z",
"completed_at": "2026-06-09T14:33:20Z"
}A failed payment carries the reason in failure:
{
"id": "pay_9f8e7d6c",
"status": "FAILED",
"corridor": "CA-TZ",
"reference": "inv_9310",
"send_amount": {
"value": "250.00",
"currency": "CAD"
},
"failure": {
"code": "payout_failed",
"message": "Mobile money payout rejected by receive rail",
"stage": "settlement"
},
"created_at": "2026-06-09T14:32:00Z",
"updated_at": "2026-06-09T14:33:12Z"
}Endpoint
Cancel a payment
Cancel a payment before collection. Only valid pre-collection — once collection has started the payment can no longer be cancelled and returns 409 cancel_not_allowed.
/v1/payments/{payment_id}/cancelCancel a payment before collection.
curl -X POST https://api.sandbox.mojaswitch.com/v1/payments/pay_a1b2c3d4/cancel \
-H "Authorization: Bearer sk_sandbox_..."{
"id": "pay_a1b2c3d4",
"status": "CANCELLED",
"corridor": "CA-TZ",
"reference": "inv_9281",
"send_amount": {
"value": "250.00",
"currency": "CAD"
},
"created_at": "2026-06-09T14:32:00Z",
"updated_at": "2026-06-09T14:33:00Z"
}Reference
The Payment object
The canonical resource returned by the create, retrieve, and cancel endpoints.
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | Payment identifier.pay_... |
| status | string | required | Lifecycle status.See lifecycle |
| corridor | string | required | Payment corridor.CA-TZ |
| reference | string | required | Partner reference. |
| send_amount | Money | required | Amount collected on the send side. |
| receive_amount | Money | required | Amount delivered to the beneficiary. |
| fx_rate | string | required | The partner's declared rate (echoed verbatim). Moja does not price the customer. |
| sender | Party | required | The paying party (no raw PII beyond name). |
| payout | Payout | required | Delivery instruction and beneficiary. |
| rails | object | required | Selected send/receive rails.e.g. { "send": "ca_psp", "receive": "tz_mobile_money" } |
| purpose | string | required | Payment purpose. |
| metadata | object | optional | Partner-supplied key/value pairs.Max 20 keys |
| failure | object | optional | Populated on terminal rail failure (FAILED).Present only when status = FAILED |
| created_at | string | required | Creation time.RFC3339 |
| updated_at | string | required | Last state change.RFC3339 |
| completed_at | string | optional | Delivery confirmation time.RFC3339 |
Reference
Payment lifecycle
Every payment moves through an ordered set of states. Compliance runs before funds move and fails closed; the ledger records COLLECTION and SETTLEMENT double-entry journals as the money moves.
Plus RECONCILED after post-settlement reconciliation, and terminal FAILED / CANCELLED.
| Status | Description |
|---|---|
| CREATED | Payment accepted and persisted with its idempotency key and audit record. |
| VALIDATING | Request shape, corridor entitlement, amount range, and reference uniqueness checked. |
| COMPLIANCE_CHECK | Sanctions/AML screening and KYC checks run before any funds move. Fails closed. |
| FX_LOCKED | FX rate fixed for the payment; receive amount determined. |
| ROUTING | Routing engine selects the send and receive rails. |
| COLLECTING | Funds are being collected on the send side (CAD). |
| PAYOUT_PENDING | Collection confirmed; payout queued to the receive rail. |
| PAYOUT_SENT | Payout dispatched to the mobile-money provider. |
| SETTLED | Settlement obligations recorded; ledger SETTLEMENT journal posted. |
| COMPLETED | Delivery confirmed to the beneficiary. Terminal success. |
| RECONCILED | Post-settlement reconciliation matched the payment against rail and settlement records. |
| FAILEDterminal | A rail leg failed terminally; details surface in the failure field. |
| CANCELLEDterminal | Cancelled by the partner before collection. |
Reference
Webhooks
Moja delivers signed outbound POSTs on payment state changes. Each request carries Moja-Switch-Signature, Moja-Switch-Event-Id, and Moja-Switch-Event-Type headers.
Signature verification
The signature is t=<ts>,v1=<hexHMAC>. Compute HMAC-SHA256 over `${t}.${rawBody}` with your webhook secret and compare v1 in constant time. Reject events older than 5 minutes for replay protection.
// Verify webhook signature (Node.js)
const crypto = require('crypto');
function verifyWebhook(rawBody, header, secret) {
const parts = Object.fromEntries(
header.split(',').map((p) => p.split('='))
);
const t = parts.t;
const v1 = parts.v1;
// Reject events older than 5 minutes (replay protection)
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(`${t}.${rawBody}`)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(v1, 'hex'),
Buffer.from(expected, 'hex')
);
}Event types
Exactly these seven partner events are delivered. Intermediate lifecycle states are not delivered by default.
| Event type | Description |
|---|---|
| payment.created | Payment accepted and persisted. |
| payment.compliance_passed | Screening cleared; funds may move. |
| payment.collected | Funds collected on the send side. |
| payment.payout_sent | Payout dispatched to the receive rail. |
| payment.completed | Delivery confirmed to the beneficiary. |
| payment.failed | Payment failed terminally (see failure). |
| payment.cancelled | Payment cancelled before collection. |
Payload
POST /webhooks/moja HTTP/1.1
Moja-Switch-Signature: t=1717941200,v1=5c3e...9af1
Moja-Switch-Event-Id: evt_3b1a9c7d
Moja-Switch-Event-Type: payment.completed
User-Agent: MojaSwitch-Webhook/1.0
Content-Type: application/json
{
"id": "evt_3b1a9c7d",
"type": "payment.completed",
"created_at": "2026-06-09T14:33:20Z",
"data": {
"object": "payment",
"id": "pay_a1b2c3d4",
"status": "COMPLETED",
"corridor": "CA-TZ",
"reference": "inv_9281",
"send_amount": {
"value": "250.00",
"currency": "CAD"
},
"receive_amount": {
"value": "462562.50",
"currency": "TZS"
},
"completed_at": "2026-06-09T14:33:20Z"
}
}| Field | Type | Required | Description |
|---|---|---|---|
| id | string | required | Event identifier.evt_... |
| type | string | required | Event type.e.g. payment.completed |
| created_at | string | required | Event time.RFC3339 |
| └ object | string | required | Object type."payment" |
| └ id | string | required | Payment id.pay_... |
| └ status | string | required | Payment status at emit time. |
| └ corridor | string | required | Payment corridor. |
| └ reference | string | required | Partner reference. |
| └ send_amount | Money | required | Amount sent. |
| └ receive_amount | Money | optional | Amount delivered (when known). |
| └ completed_at | string | optional | Completion time (on completion). |
Retry policy
Success is a 2xx within 30 seconds. After the final attempt the event is dead-lettered and ops is alerted. Partners must dedupe on Moja-Switch-Event-Id.
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | After 1 minute |
| 3 | After 5 minutes |
| 4 | After 30 minutes |
| 5 | After 2 hours |
| 6 | After 24 hours |
Reference
Compliance & KYC
Sanctions and AML screening runs on every payment before funds move, fails closed, and writes an immutable audit trail. Large transactions are flagged for regulatory reporting — FINTRAC LVCTR ≥ CAD 10,000.
Sender KYC uses a partner attestation model. The partner who onboarded the sender attests KYC completeness via sender_kyc (attested, attestation_id, level, attested_at) plus sender.external_id. Moja holds an auditable reference — not raw PII. When enforcement is on, a missing or invalid attestation returns compliance_blocked / compliance_blocked.
Screened on the path
Sanctions & AML before any funds move, fail-closed.
Immutable audit
Every screening decision durably captured for examination.
Regulatory reporting
LVCTR flagging at ≥ CAD 10,000 (FINTRAC).
Reference
Versioning
The API is versioned in the path (/v1). Breaking changes ship under a new version; additive changes are backward-compatible. There are no deprecations yet.
Ready to build?
Request sandbox credentials to simulate payments and validate webhook flows.