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.

Authenticated request
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

01

Get API keys

Request sandbox credentials for evaluation. Production keys are issued per partner during onboarding, scoped to your entitled corridors.

02

Create a payment

POST /v1/payments with a UUID Idempotency-Key and a unique reference. Receive the Payment object and its initial status.

03

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 body
{
  "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.

CodeHTTPDescription
invalid_request400Malformed request or a non-UUID Idempotency-Key.
missing_idempotency_key400POST /v1/payments sent without an Idempotency-Key header.
unauthorized401Missing or invalid API key.
forbidden403Partner account is disabled (SUSPENDED or OFFBOARDED).
corridor_not_entitled403Partner is not entitled to the requested corridor.
insufficient_scope403API key lacks the scope (read / write / cancel) the route requires.
transaction_limit_exceeded422Send amount exceeds the configured maximum single-payment limit for the currency.

Validation

Returned as an HTTP status. Field-level and business-rule validation failures.

CodeHTTPDescription
validation_error422One or more fields failed validation (see details[]).
fx_required422Production request omitted the client-provided fx object (BYO-FX is mandatory in production; a quote_id does not satisfy it).
fx_inconsistent422fx.rate and fx.receive_amount disagree: |receive_amount − round(send_amount × rate)| > 1 minor unit.
fx_invalid422Supplied FX failed fail-closed validation (non-positive rate, out of precision, or corridor-invalid).
unsupported_corridor422Corridor is not supported (MVP: CA-TZ only).
payout_method_unsupported422Payout method not supported (MVP: MOBILE_MONEY only; BANK_TRANSFER is rejected).
duplicate_reference409reference already used by this partner.
idempotency_key_conflict409Same Idempotency-Key reused with a different request body.

Lifecycle

Returned as an HTTP status. State-dependent operations.

CodeHTTPDescription
payment_not_found404No payment matches the supplied id.
cancel_not_allowed409Payment 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 }).

CodeHTTPDescription
compliance_blockedfailureBlocked by sanctions / AML / fraud / KYC screening. A REVIEW outcome is also treated as a block in the MVP.
collection_failedfailureSend-side collection failed. Payment → FAILED (stage: collection).
payout_failedfailureReceive-side payout failed. Payment → FAILED (stage: settlement).
rail_unavailablefailureA 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.

CodeHTTPDescription
rate_limit_exceeded429Per-partner rate limiting — not yet enforced.
amount_out_of_range422Minimum-amount enforcement — not yet implemented (a maximum is enforced via transaction_limit_exceeded).
quote_expired422Quote-lock binding at create — quotes are indicative today (quote_id is not yet binding).
fx_unavailable503Live FX provider errors — not applicable to the MVP stub.
beneficiary_invalid422Receive-rail beneficiary validation — planned.
kyc_insufficient422Reserved. A failed sender KYC attestation surfaces today as compliance_blocked on a FAILED payment.
compliance_review_required422Reserved. 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.

POST/v1/quotes

Validate a client-supplied FX quote (sandbox may derive an indicative rate).

scope: write

Request body

FieldTypeRequiredDescription
corridorstringrequiredPayment corridor to price.Enum: CA-TZ
send_amountMoneyrequiredAmount to send on the collection side.currency must be CAD
valuestringrequiredDecimal amount in major units, 2 dp.e.g. "250.00"
currencystringrequiredSend currency.CAD
receive_currencystringrequiredCurrency delivered to the beneficiary.Enum: TZS
payout_methodstringrequiredDelivery method on the receive side.MOBILE_MONEY (MVP) | BANK_TRANSFER → 422
fxFXconditionalClient-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 }
ratestringconditionalThe partner's send→receive rate.positive decimal, ≤ 8 dp
receive_amountMoneyconditionalExact payout the partner instructs.currency TZS

Response 201 · Quote

FieldTypeRequiredDescription
idstringrequiredQuote identifier.quote_...
corridorstringrequiredEchoed corridor.CA-TZ
send_amountMoneyrequiredEchoed send amount.
receive_amountMoneyrequiredThe partner's supplied receive amount (echoed); sandbox derives an indicative value when fx is omitted.
fx_ratestringrequiredThe partner's declared rate (echoed). Not priced by Moja; sandbox derives an indicative rate only when fx is omitted.e.g. "1850.25"
expires_atstringrequiredQuote expiry.RFC3339
created_atstringrequiredCreation time.RFC3339
Request
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"
  }'
201 Created
{
  "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.

POST/v1/payments

Create and send a cross-border payment.

scope: writeidempotency-key required

Request body

FieldTypeRequiredDescription
corridorstringrequiredPayment corridor.Enum: CA-TZ
referencestringrequiredPartner reference for the payment.Max 128 chars; unique per partner
quote_idstringoptionalReference to a prior quote. Does not satisfy the production FX requirement — supply fx inline.Reserved — not yet binding in MVP
send_amountMoneyrequiredAmount debited on the send side.currency must be CAD
fxFXconditionalClient-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 }
ratestringconditionalDeclared-basis send→receive rate. Recorded and consistency-checked, not used to recompute the payout.positive decimal, ≤ 8 dp
receive_amountMoneyconditionalAuthoritative payout — the exact amount paid to the beneficiary, verbatim.currency TZS
valuestringrequiredDecimal, major units, 2 dp.e.g. "250.00"
currencystringrequiredSend currency.CAD
senderPartyrequiredThe paying party.
typestringrequiredParty type.INDIVIDUAL | BUSINESS
countrystringrequiredSender country of residence.ISO-3166 alpha-2; must be CA
first_namestringconditionalGiven name.Individuals
last_namestringconditionalFamily name.Individuals
business_namestringconditionalLegal entity name.Businesses
external_idstringoptionalPartner customer id linking to the KYC record.
sender_kycKYCAttestationconditionalPartner KYC attestation for the sender.Required in prod when enforcement is on
attestedbooleanrequiredPartner attests KYC is complete.
attestation_idstringrequiredAuditable reference to the partner's KYC record.
levelstringoptionalAttestation level.e.g. FULL
attested_atstringoptionalWhen KYC was attested.RFC3339
payoutPayoutrequiredDelivery instruction on the receive side.
methodstringrequiredPayout method.MOBILE_MONEY
currencystringrequiredReceive currency.TZS
beneficiaryBeneficiaryrequiredRecipient of the funds.
typestringrequiredBeneficiary type.INDIVIDUAL | BUSINESS
first_namestringconditionalGiven name.Individuals
last_namestringconditionalFamily name.Individuals
business_namestringconditionalLegal entity name.Businesses
phonestringrequiredMobile-money destination number.E.164, e.g. +255712345678
purposestringrequiredReason for the payment (regulatory).FAMILY_SUPPORT | SCHOOL_FEES | UTILITY_BILL | MERCHANT_PAYMENT | OTHER
metadataobjectoptionalFree-form string→string key/value pairs.Max 20 keys
Request
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"
}
201 Created · Payment
{
  "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
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.

GET/v1/payments/{payment_id}

Retrieve the current state of a payment.

scope: read
Request
curl https://api.sandbox.mojaswitch.com/v1/payments/pay_a1b2c3d4 \
  -H "Authorization: Bearer sk_sandbox_..."
200 OK · 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"
  },
  "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:

200 OK · FAILED
{
  "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.

POST/v1/payments/{payment_id}/cancel

Cancel a payment before collection.

scope: cancel
Request
curl -X POST https://api.sandbox.mojaswitch.com/v1/payments/pay_a1b2c3d4/cancel \
  -H "Authorization: Bearer sk_sandbox_..."
200 OK · CANCELLED
{
  "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.

FieldTypeRequiredDescription
idstringrequiredPayment identifier.pay_...
statusstringrequiredLifecycle status.See lifecycle
corridorstringrequiredPayment corridor.CA-TZ
referencestringrequiredPartner reference.
send_amountMoneyrequiredAmount collected on the send side.
receive_amountMoneyrequiredAmount delivered to the beneficiary.
fx_ratestringrequiredThe partner's declared rate (echoed verbatim). Moja does not price the customer.
senderPartyrequiredThe paying party (no raw PII beyond name).
payoutPayoutrequiredDelivery instruction and beneficiary.
railsobjectrequiredSelected send/receive rails.e.g. { "send": "ca_psp", "receive": "tz_mobile_money" }
purposestringrequiredPayment purpose.
metadataobjectoptionalPartner-supplied key/value pairs.Max 20 keys
failureobjectoptionalPopulated on terminal rail failure (FAILED).Present only when status = FAILED
created_atstringrequiredCreation time.RFC3339
updated_atstringrequiredLast state change.RFC3339
completed_atstringoptionalDelivery 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.

CREATEDVALIDATINGCOMPLIANCE_CHECKFX_LOCKEDROUTINGCOLLECTINGPAYOUT_PENDINGPAYOUT_SENTSETTLEDCOMPLETED

Plus RECONCILED after post-settlement reconciliation, and terminal FAILED / CANCELLED.

StatusDescription
CREATEDPayment accepted and persisted with its idempotency key and audit record.
VALIDATINGRequest shape, corridor entitlement, amount range, and reference uniqueness checked.
COMPLIANCE_CHECKSanctions/AML screening and KYC checks run before any funds move. Fails closed.
FX_LOCKEDFX rate fixed for the payment; receive amount determined.
ROUTINGRouting engine selects the send and receive rails.
COLLECTINGFunds are being collected on the send side (CAD).
PAYOUT_PENDINGCollection confirmed; payout queued to the receive rail.
PAYOUT_SENTPayout dispatched to the mobile-money provider.
SETTLEDSettlement obligations recorded; ledger SETTLEMENT journal posted.
COMPLETEDDelivery confirmed to the beneficiary. Terminal success.
RECONCILEDPost-settlement reconciliation matched the payment against rail and settlement records.
FAILEDterminalA rail leg failed terminally; details surface in the failure field.
CANCELLEDterminalCancelled 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.js
// 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 typeDescription
payment.createdPayment accepted and persisted.
payment.compliance_passedScreening cleared; funds may move.
payment.collectedFunds collected on the send side.
payment.payout_sentPayout dispatched to the receive rail.
payment.completedDelivery confirmed to the beneficiary.
payment.failedPayment failed terminally (see failure).
payment.cancelledPayment cancelled before collection.

Payload

Delivered request
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"
  }
}
FieldTypeRequiredDescription
idstringrequiredEvent identifier.evt_...
typestringrequiredEvent type.e.g. payment.completed
created_atstringrequiredEvent time.RFC3339
objectstringrequiredObject type."payment"
idstringrequiredPayment id.pay_...
statusstringrequiredPayment status at emit time.
corridorstringrequiredPayment corridor.
referencestringrequiredPartner reference.
send_amountMoneyrequiredAmount sent.
receive_amountMoneyoptionalAmount delivered (when known).
completed_atstringoptionalCompletion 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.

AttemptDelay
1Immediate
2After 1 minute
3After 5 minutes
4After 30 minutes
5After 2 hours
6After 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.