Moja SwitchDocs Sandbox live

Moja Switch Partner API (1.0.0-draft)

Download OpenAPI specification:Download

B2B payment infrastructure API for cross-border corridors. Examples use the sandbox corridor Canada → Tanzania (CA-TZ, CAD → TZS).

Implementation status is part of the contract. An operation or property marked x-moja-status: planned (summary prefixed [Planned], or 🔲 in its description) is published target contract that the deployed platform does not serve or emit yet. Everything unmarked is live: an automated check keeps this file and the deployed API in step. Requests with unknown fields are rejected with 400 invalid_request, so do not send a planned request property. Error codes are catalogued at https://docs.mojaswitch.com/guides/errors/.

Corridors are data, not a fixed list. What a given tenant may actually use is per-account entitlement: read GET /corridors/{corridor_code} (the list, GET /corridors, is planned) and treat the corridor code, its currencies and their minor-unit exponents as server-supplied. Naming a corridor here is an example, not a statement of what is live for you and not a constraint in the contract — the schemas do not enumerate corridors, and a client that hardcodes one has to be re-released to gain another.

Currencies differ in minor-unit exponent (zero-decimal currencies exist). No client may assume two decimal places; see MinorUnitAmount and GET /corridors/{corridor_code}.

How FX is priced depends on your tenant's FX model, set by Moja Switch per tenant — see POST /payments.

  • BYO-FX. You supply the send→receive rate and the exact receive (payout) amount; Moja executes that instruction and adds no FX margin. Responses echo your locked fx_rate and receive_amount.
  • Moja Switch FX. You pay on a single-use quote_id from POST /quotes: the quote is priced from the Moja Switch FX reference rate plus your tenant's own agreed margin, and the platform locks that price exactly. Moja Switch serves a reference rate; it is not the underlying FX counterparty.

Off-production, a BYO-FX payment that omits fx on a corridor with a configured sandbox rate gets a Moja-derived rate as a dev convenience (see FX schema); that is never production pricing.

Collections (kind: collection) are sandbox-only. See POST /payments and the Payouts tag. Production collections and tenant payouts are not offered yet.

Versioning. The API is versioned by URL major (/v1). This contract is a pre-launch draft (1.0.0-draft) and may still change before the first partner goes live. After that, a breaking change is published as a new major version (/v2) with a deprecation window; it is never made silently to /v1.

Quotes

FX quotes and rate locks

Create FX quote

Prices an FX quote for a corridor and returns the resulting receive_amount and all-in fx_rate. Pre-step before payment creation.

Who prices is a property of your tenant, not of the request. Moja Switch holds your FX model in your tenant commercial configuration, and your commercial terms (your margin) per corridor — yours alone, independent of any other tenant on the same corridor. The two models behave differently here:

Your FX model Who sets the rate What you send Margin
Moja Switch FX Moja Switch: its own validated reference rate, less your commercial margin for this corridor send_amount or receive_amount; no fx Applied by Moja Switch, inside the rate
BYO-FX You: your own FX source, rate and pricing send_amount and an inline fx None added by Moja Switch

Moja Switch FX

  • Send exactly one of send_amount (what the sender pays) or receive_amount (what the recipient must get). With receive_amount the quote's receive_amount equals the request exactly and send_amount is the smallest amount that covers it; fx_rate is then the effective rate (receive ÷ send, floored at 8 places, never above the margin-implied rate).
  • Do not send fx — any fx key, even {} or null → 422 fx_not_accepted. A Moja Switch FX tenant does not bring a rate.
  • Moja Switch reads its own reference rate. If it has no valid, fresh rate, the quote is refused with 503 fx_unavailable (never priced from a cached or indicative rate). Otherwise it applies your margin for the quoted corridor, prices with integer arithmetic (each currency at its own minor-unit exponent; CAD and TZS two digits), persists the quote and returns it.
  • Show your end user only send_amount and fx_rate. The margin is already inside fx_rate. The response's fx_provenance and fees.fx_markup are for your systems, never for an end user; disclosure.total_cost is a figure, never a percentage.
  • A quote is single-use and valid until expires_at. Pay on it with POST /payments quote_id — see that operation. Re-quote rather than extending it client-side.

BYO-FX

BYO-FX rules:

  • Production requires fx with both fx.rate and fx.receive_amount, and so does any corridor with no sandbox rate, in every environment. Omitting it → 422 fx_required.
  • fx.receive_amount is authoritative (paid verbatim); fx.rate is the declared basis (recorded, consistency-checked, not used to recompute the payout).
  • Consistency rule: |receive_amount − round(send_amount × rate)| ≤ 1 minor unit, else 422 fx_inconsistent. Values that are not positive, not valid for the corridor, or too precise → 422 validation_error with details[].field naming the fx field.
  • Sandbox only: when fx is omitted off-production on a corridor with a configured sandbox rate (CA-TZ), Moja derives an indicative rate as a dev convenience — never production pricing. This fallback does not exist on the production server.

Scope: write.

Cost disclosure is part of the response, not an afterthought. Every quote carries a required disclosure object with the total cost as a figure in the send currency and the rate basis behind it. A client cannot render a quote without it; see QuoteDisclosure.

No rate available → 503 fx_unavailable (Moja Switch FX). This can be an expected state, not an incident: a reference rate that is stale, unvalidated or outside its deviation band is refused rather than used. Render the no-quote state and retry later; no return time is given. Never fall back to a cached or indicative rate — a displayed rate is a rate a customer expects to receive.

Platform-priced quotes (Moja Switch FX). A quote Moja Switch priced returns pricing_mode: platform_priced, the all-in fx_rate, the expires_at it is good until, a disclosure (the total cost as a figure in the send currency) and fx_provenance — the source rate, the provider, the rate timestamp, the pricing-rule version, and whether the underlying rate was indicative or executable — so an integrator (and an auditor) can answer "where did this number come from".

This is available only to tenants configured for Moja Switch FX. For every other tenant, production still requires fx (422 fx_required) and pricing_mode is client_provided: Moja Switch does not price a BYO-FX client's customers and adds no margin to their rate. receive_amount is accepted only for a Moja Switch FX tenant.

Errors specific to Moja Switch FX: 422 fx_not_accepted (you sent fx), 422 commercial_terms_not_configured (no commercial terms are configured for you on this corridor — there is no default margin; ask Moja Switch to configure them), 422 amount_below_minimum (the amount delivers nothing at this rate), 422 transaction_limit_exceeded / 422 amount_out_of_range (the same ceilings a payment is held to), 503 fx_unavailable (no valid, fresh reference rate — retry; never display a price).

Authorizations:
bearerAuth
Request Body schema: application/json
required
corridor
required
string (Corridor) ^[A-Z]{2}-[A-Z]{2}$

Corridor on the transacting routes. This was a closed enum (CA-TZ) and is now the same pattern-constrained string as CorridorCode.

It had to change. A closed enum in the contract is the same defect as a hardcoded corridor in a client: the corridor set is per-tenant entitlement data, it moves, and an enum makes every move a contract change and a client release. Published as an enum, a quote on any newer corridor was a schema violation rather than an entitlement question — which is not something the contract should have an opinion about.

Entitlement is the authority on what a given tenant may actually use (GET /corridors, which will list it, is planned). A corridor you are not entitled to is 403 corridor_not_entitled; a syntactically valid code that does not exist is 422 unsupported_corridor. Treat the value as an opaque server-supplied string: do not enumerate it, do not switch on it beyond what discovery told you, and do not ship a build that breaks when the set changes.

receive_currency
required
string^[A-Z]{3}$

ISO 4217 payout currency. Was a closed enum (TZS) and is now a pattern-constrained code, for the same reason Corridor is: the currency follows the corridor, and a corridor set that moves cannot have its currencies frozen in the contract. Read the valid value for a corridor from GET /corridors/{corridor_code} (receive_currency.currency) rather than assuming it, and read minor_unit_exponent with it — never assume two decimals.

payout_method
required
string (PayoutMethod)
Enum: "MOBILE_MONEY" "BANK_TRANSFER"
object (Money)

Decimal-string amount, used by the original partner operations (POST /quotes, POST /payments, GET /payments/{payment_id}). Its value pattern caps the world at two decimal places, which is why the newer end-user operations use MinorUnitAmount instead. Unchanged here: swapping the representation on an existing operation would be a breaking /v1 change.

object

Moja Switch FX only. What the recipient must get, in the corridor's receive currency (whole units for a zero-decimal currency). The quote delivers exactly this amount and prices the smallest send_amount that covers it. Mutually exclusive with send_amount.

end_user_id
string

Accepted so a client can send the same identity on the quote and the payment; not used to price today (a quote is not end-user-specific).

object

Client-provided FX (BYO-FX tenants only). REQUIRED in production for a BYO-FX tenant (else 422 fx_required). A Moja Switch FX tenant must omit it entirely — an fx is 422 fx_not_accepted (and a malformed one, {} or null, is 400 invalid_request): Moja Switch prices that tenant's quotes.

Responses

Response Schema: application/json
id
required
string
corridor
required
string (Corridor) ^[A-Z]{2}-[A-Z]{2}$

Corridor on the transacting routes. This was a closed enum (CA-TZ) and is now the same pattern-constrained string as CorridorCode.

It had to change. A closed enum in the contract is the same defect as a hardcoded corridor in a client: the corridor set is per-tenant entitlement data, it moves, and an enum makes every move a contract change and a client release. Published as an enum, a quote on any newer corridor was a schema violation rather than an entitlement question — which is not something the contract should have an opinion about.

Entitlement is the authority on what a given tenant may actually use (GET /corridors, which will list it, is planned). A corridor you are not entitled to is 403 corridor_not_entitled; a syntactically valid code that does not exist is 422 unsupported_corridor. Treat the value as an opaque server-supplied string: do not enumerate it, do not switch on it beyond what discovery told you, and do not ship a build that breaks when the set changes.

required
object (Money)

Decimal-string amount, used by the original partner operations (POST /quotes, POST /payments, GET /payments/{payment_id}). Its value pattern caps the world at two decimal places, which is why the newer end-user operations use MinorUnitAmount instead. Unchanged here: swapping the representation on an existing operation would be a breaking /v1 change.

required
object

The locked payout amount. Echoes the partner's fx.receive_amount when supplied, otherwise derived from send_amount × the partner's fx.rate (BYO-FX).

fx_rate
required
string

The partner's locked send→receive rate, echoed back (receive units per 1 send unit). Not a Moja-computed or marked-up rate. Off-production, when the partner omitted fx, this is the Moja-derived sandbox/dev rate (never production pricing).

required
object (QuoteFees)

Quote cost breakdown, always denominated in the SEND currency (what the quote costs the sender). Both fields are always present and may be zero amounts. platform_fee is the flat platform fee (may be zero). fx_markup is the margin inside fx_rate on a Moja Switch FX quote; it is zero on a BYO-FX quote, where the partner owns the customer-facing rate and Moja adds no spread. For your systems, not for an end user.

required
object

Required. The cost disclosure for this quote. Required so that a client cannot render a quote without it.

expires_at
required
string <date-time>

When the locked rate stops being valid. A platform_priced rate MUST NOT be displayed to an end customer after this instant; re-quote instead of extending it client-side.

created_at
required
string <date-time>
pricing_mode
string
Enum: "client_provided" "platform_priced"

Who priced this quote. Absent on quotes issued before this field existed; treat an absent value as client_provided.

object

Where the rate came from. Present only when pricing_mode is platform_priced — under BYO-FX the provenance of the rate is the partner's own record, not ours.

Request samples

Content type
application/json
{
  • "corridor": "CA-TZ",
  • "send_amount": {
    },
  • "receive_amount": {
    },
  • "receive_currency": "TZS",
  • "payout_method": "MOBILE_MONEY",
  • "end_user_id": "eu_7Qm4xK2pLd",
  • "fx": {
    }
}

Response samples

Content type
application/json
{
  • "id": "quote_xyz789",
  • "corridor": "CA-TZ",
  • "send_amount": {
    },
  • "receive_amount": {
    },
  • "fx_rate": "1850.25",
  • "fees": {
    },
  • "pricing_mode": "client_provided",
  • "fx_provenance": {
    },
  • "disclosure": {
    },
  • "expires_at": "2019-08-24T14:15:22Z",
  • "created_at": "2019-08-24T14:15:22Z"
}

Payments

Payment lifecycle

Create payment

Initiates a cross-border payment. Requires Idempotency-Key header. Target response time: < 30s for sync rails (mobile money B2C).

How the payment is priced depends on your tenant's FX model.

  • Moja Switch FX: pay on a quote_id from POST /quotes and omit fx (an fx → 422 fx_not_accepted, or 400 invalid_request if it is malformed; no quote_id → 422 quote_required). Moja Switch locks that quote's price exactly — it never re-prices. The quote must be yours, priced by Moja Switch FX in this environment, matching this payment, unexpired and not already used: a quote is single-use, consumed atomically with the payment, so a second payment on it → 422 quote_invalid and an expired one → 422 quote_expired. A retry with the same Idempotency-Key returns the original payment and never touches the quote.
  • BYO-FX: the partner sets the FX; Moja executes it (below). Moja Switch adds no margin.

Either way the response carries fx_provenance — see Payment.

BYO-FX. The partner sets the FX; Moja executes it. Supply the locked FX inline via the fx object. The response echoes the partner's fx_rate and receive_amount (their values; no Moja markup).

  • Production requires inline fx (with both fx.rate and fx.receive_amount), and so does any corridor with no sandbox rate, in every environment; a request without it → 422 fx_required. A quote_id does not satisfy this.
  • fx.receive_amount is authoritative (paid verbatim); fx.rate is the declared basis, consistency-checked against send_amount: |receive_amount − round(send_amount × rate)| ≤ 1 minor unit, else 422 fx_inconsistent.
  • Values must be positive, valid for the corridor and within precision (422 validation_error, details[].field naming the fx field).
  • Sandbox only: with fx omitted off-production on a corridor with a configured sandbox rate (CA-TZ), Moja applies a derived rate as a dev convenience — never production pricing.

Compliance is not an HTTP error here. Screening runs after the payment is persisted. A block (a review decision is treated as a block), including a missing or invalid required sender_kyc attestation, returns 201 with status: FAILED and failure.code: compliance_blocked; a screening outage returns 201 FAILED with failure.code: compliance_error. See PaymentFailure.

Scope: write.

The originator: sender, always. For a push collection it is the inline Party. For funding_method: INTERAC_REQUEST it is an InteracSender (email, name): the person the Interac request is sent to.

The beneficiary: payout, or a saved recipient. A saved recipient is named by recipient_id together with end_user_id (both or neither, else 422 validation_error). Without payout, the platform builds the payout from the screened recipient record, so the client restates none of it. With payout as well, its beneficiary phone must equal the saved phone (422 recipient_phone_mismatch). Before any collection: 404 end_user_not_found, 404 recipient_not_found (unknown, or another end user's), 422 recipient_not_approved (screening not passed), 422 recipient_not_payable (not a mobile-money recipient in the corridor's payout country).

The end user must be able to send. A payment naming an end user is refused with 422 end_user_not_permitted unless that end user's account_status is created and its transact_authorization is authorized — with or without sender_kyc. An attestation says who the sender is; it never lets a suspended, closed or unauthorized account send. details[0].message names the refusing state (for example account_status is suspended). Nothing is created, so the same Idempotency-Key may be retried once the end user is restored. An idempotent retry of a payment created before a suspension still returns that payment.

FX. fx and quote_id are mutually exclusive (422 validation_error).

Tenant controls. Moja Switch must have enabled your tenant for the corridor in this environment (403 tenant_not_activated; a contract or an entitlement alone is not enablement), and the payment must fit your tenant limit profile for the send currency (422 tenant_limit_exceeded, details[0].message names the limit: per_transfer, rolling_24h_amount, rolling_24h_count or rolling_30d_amount). A profile with a blank limit refuses every payment (403 tenant_limits_unresolved): a blank is never read as unlimited. Nothing is created on any of these, so the same Idempotency-Key may be retried once Moja Switch has changed the decision; an idempotent retry of a payment created earlier still returns it.

Idempotency: a retried create with the same Idempotency-Key and body returns the original payment (at-least-once delivery, exactly-once effect) — the client-locked fx_rate/receive_amount never change on replay.

Idempotency namespace: (partner_id, key). end_user_id does not narrow it. A tenant that sends on behalf of many end users must keep keys unique across all of them (for example, derive the key from the end user and the client's own attempt key).

Generate the key (UUIDv4, one per transfer attempt) and persist it before the first attempt, so a retry after process death reuses it.

kind (optional): transfer or collection. Absent means transfer, and a transfer request behaves exactly as it does without kind; "kind": "transfer" and no kind are the same request for idempotency. kind: collection is sandbox-only today: outside a sandbox-serving instance it is refused with 403 product_not_available before the body is validated. On a sandbox instance it is validated (422 validation_error naming each field) and then requires an operator-activated collection product for that tenant (403 tenant_not_activated when missing). With activation, Create returns 201 and the payment advances through Interac sandbox collection to COLLECTED, then Moja Switch schedules a tenant CAD payout (GET /v1/payouts). Production collections are not offered yet. Any other kind value is 422 validation_error. A collection is not corridor-checked (it has no corridor; one that names corridor anyway is checked and refused unless you are entitled to it) and needs the write scope. With a production key every collection is 403 product_not_available, whatever its fields. A collection's rules are conditional and enforced by the server, so the schema below describes a transfer; see kind for what a collection requires and forbids.

Authorizations:
bearerAuth
header Parameters
Idempotency-Key
required
string <uuid>
Example: 550e8400-e29b-41d4-a716-446655440000

UUIDv4, one per payment attempt, persisted by the client BEFORE the first attempt and reused on every retry of that attempt. Must be a UUID (else 400 invalid_request). The key is namespaced (partner_id, key); end_user_id does not narrow it. See the operation description for the replay rules.

Request Body schema: application/json
required
Any of
required
object

The beneficiary and payout method, inline. Required unless a saved recipient (recipient_id + end_user_id) is supplied. When both are sent, payout.beneficiary.phone must equal the recipient's saved phone (422 recipient_phone_mismatch).

corridor
required
string (Corridor) ^[A-Z]{2}-[A-Z]{2}$

Corridor on the transacting routes. This was a closed enum (CA-TZ) and is now the same pattern-constrained string as CorridorCode.

It had to change. A closed enum in the contract is the same defect as a hardcoded corridor in a client: the corridor set is per-tenant entitlement data, it moves, and an enum makes every move a contract change and a client release. Published as an enum, a quote on any newer corridor was a schema violation rather than an entitlement question — which is not something the contract should have an opinion about.

Entitlement is the authority on what a given tenant may actually use (GET /corridors, which will list it, is planned). A corridor you are not entitled to is 403 corridor_not_entitled; a syntactically valid code that does not exist is 422 unsupported_corridor. Treat the value as an opaque server-supplied string: do not enumerate it, do not switch on it beyond what discovery told you, and do not ship a build that breaks when the set changes.

reference
required
string <= 128 characters

Partner's unique reference for this payment

required
object (Money)

Decimal-string amount, used by the original partner operations (POST /quotes, POST /payments, GET /payments/{payment_id}). Its value pattern caps the world at two decimal places, which is why the newer end-user operations use MinorUnitAmount instead. Unchanged here: swapping the representation on an existing operation would be a breaking /v1 change.

required
Party (object) or InteracSender (object)

The originator. A Party for push collection; an InteracSender when funding_method is INTERAC_REQUEST.

purpose
required
string (PaymentPurpose)
Enum: "FAMILY_SUPPORT" "SCHOOL_FEES" "UTILITY_BILL" "MERCHANT_PAYMENT" "OTHER"
kind
string
Default: "transfer"
Enum: "transfer" "collection"

What the payment does. Omit it, or send transfer, for a transfer: the request is the same either way. collection collects CAD from a payer for the tenant by Interac request. It is sandbox-only: outside sandbox it is 403 product_not_available (checked before the body is validated); in sandbox, after validation, a tenant without an operator-activated collection product gets 403 tenant_not_activated (nothing is created), and an activated tenant gets 201. A collection requires reference, send_amount (CAD), funding_method: INTERAC_REQUEST, sender (an InteracSender: the payer), payer_id, destination_country, purpose and sender_kyc; may carry quote_id, metadata and, in sandbox, sandbox; and forbids corridor, payout, recipient_id, end_user_id and fx. Each violation is 422 validation_error naming the field. A transfer must not carry payer_id, destination_country or sandbox (400 invalid_request).

payer_id
string [ 1 .. 128 ] characters

kind: collection only. The tenant's own stable identifier for the payer: printable, no leading or trailing spaces. This is the KYC attestation subject for a collection — InteracSender has no external_id; do not put a partner user id on sender.

destination_country
string^[A-Z]{2}$

kind: collection only. ISO 3166-1 alpha-2 country the tenant will send the collected funds on to (outside Moja Switch).

object

kind: collection only, sandbox only. The simulated outcome of the collection.

quote_id
string

Moja Switch FX tenants: required. The id of a quote from POST /quotes, single-use and unexpired; see the operation description. BYO-FX / unconfigured tenants: an optional reference to a prior sandbox quote; it does not satisfy the production FX requirement (a quote_id-only production request → 422 fx_required). Mutually exclusive with fx (422 validation_error).

object

Client-provided FX (BYO-FX tenants only). Mutually exclusive with quote_id (422 validation_error). REQUIRED inline in production and on a corridor with no sandbox rate (422 fx_required if absent). Omit only in sandbox CA-TZ to use the Moja-derived dev rate. A Moja Switch FX tenant must omit it (422 fx_not_accepted).

funding_method
string^[A-Z][A-Z0-9_]{1,31}$

How the sender intends to pay. For kind: collection it is required and must be INTERAC_REQUEST. For a transfer, INTERAC_REQUEST is implemented for a withdrawn corridor only, so it is not available on CA-TZ or any other enabled corridor today. Where a transfer corridor offers it, it is for a saved recipient (recipient_id), with sender an InteracSender. The platform asks the sender for the full send amount and the payment waits at COLLECTING until the rail reports the funds deposited. Another corridor → 422 funding_method_unsupported; an unknown value → 422 validation_error. Omit for the corridor's push collection.

recipient_id
string^rcp_[a-zA-Z0-9]+$

A screened recipient from this end user's book. Requires end_user_id, and the recipient must belong to that end user (else 404 recipient_not_found). It must be approved (422 recipient_not_approved otherwise), checked before any collection.

end_user_id
string^eu_[a-zA-Z0-9]+$

The end user whose saved recipient is being paid. Required with, and only with, recipient_id (404 end_user_not_found if unknown). It does not replace sender. The end user must be able to send: account_status: created and transact_authorization: authorized, else 422 end_user_not_permitted, whatever sender_kyc says.

object (SenderKYCAttestation)

Partner-supplied sender KYC attestation (the partner-attestation model: the partner attests sender KYC is complete rather than Moja Switch re-running KYC).

Optional at the API boundary, but effectively REQUIRED when the corridor requires attestation, which is the default. When required and the attestation is absent, not marked attested, missing attestation_id/attested_at, or stale (older than the configured max age or dated in the future), the payment is rejected at the compliance step: the payment is created and returned 201 with status: FAILED and failure.code: compliance_blocked (not an HTTP 422). A supplied attestation is ALWAYS fully validated regardless of the required flag: if you send one, it must be valid.

The identity the attestation covers is not repeated here. For a transfer, it is sender.external_id, name, and country (CA). For a kind: collection, it is top-level payer_id plus the Interac payer's name and country on sender (InteracSender has no external_id).

object <= 20 properties

Responses

Response Schema: application/json
id
required
string
status
required
string (PaymentStatus)
Enum: "CREATED" "VALIDATING" "COMPLIANCE_CHECK" "FX_LOCKED" "ROUTING" "COLLECTING" "CANCEL_REQUESTED" "COLLECTED" "PAYOUT_SCHEDULED" "PAID_OUT" "REVERSED" "PAYOUT_PENDING" "PAYOUT_SENT" "PROCESSING" "SETTLED" "COMPLETED" "RECONCILED" "FAILED" "CANCELLED"

Partner-facing payment lifecycle status. PROCESSING is a safe generic "in progress" value: the platform presents any internal-only substate (e.g. a compensation state) that is not one of the enumerated lifecycle stages as PROCESSING, so a partner never observes a raw internal state. For a transfer, PROCESSING is non-terminal and transient: a subsequent poll or webhook will report a terminal status. Clients should treat any unrecognised value as a non-terminal state. Collection statuses are published but not emitted yet. CANCEL_REQUESTED, COLLECTED, PAYOUT_SCHEDULED, PAID_OUT and REVERSED belong to the kind: collection lifecycle. The platform records them, but the partner status mapping does not pass them through yet; each reads PROCESSING. A collection reports CREATED, VALIDATING, COMPLIANCE_CHECK, COLLECTING, FAILED and CANCELLED verbatim. A collection that is paid, scheduled into a payout, paid out or reversed reads PROCESSING and today never reaches a later terminal status. Reconcile collected funds from GET /payouts (each payout member carries the collection reference). Accept all five values now so that enabling them is not a client release.

reference
required
string
required
object (Money)

Decimal-string amount, used by the original partner operations (POST /quotes, POST /payments, GET /payments/{payment_id}). Its value pattern caps the world at two decimal places, which is why the newer end-user operations use MinorUnitAmount instead. Unchanged here: swapping the representation on an existing operation would be a breaking /v1 change.

created_at
required
string <date-time>
updated_at
required
string <date-time>
corridor
string (Corridor) ^[A-Z]{2}-[A-Z]{2}$

Corridor on the transacting routes. This was a closed enum (CA-TZ) and is now the same pattern-constrained string as CorridorCode.

It had to change. A closed enum in the contract is the same defect as a hardcoded corridor in a client: the corridor set is per-tenant entitlement data, it moves, and an enum makes every move a contract change and a client release. Published as an enum, a quote on any newer corridor was a schema violation rather than an entitlement question — which is not something the contract should have an opinion about.

Entitlement is the authority on what a given tenant may actually use (GET /corridors, which will list it, is planned). A corridor you are not entitled to is 403 corridor_not_entitled; a syntactically valid code that does not exist is 422 unsupported_corridor. Treat the value as an opaque server-supplied string: do not enumerate it, do not switch on it beyond what discovery told you, and do not ship a build that breaks when the set changes.

object

The locked payout amount, echoing the partner's client-provided FX (BYO-FX). Present once FX is locked.

fx_rate
string

The partner's locked send→receive rate, echoed back (not Moja-computed or marked up). Present once FX is locked.

fx_rate_id
string <= 128 characters

The partner's declared-rate identifier (fx.rate_id), echoed back for reconciliation. Present only when the partner supplied it at create. Pure metadata — it never affected how the payment was priced or executed.

quote_id
string

The quote this payment was priced on (for a Moja Switch FX tenant, the single-use quote it paid on), for reconciliation. Absent when the payment carried no quote.

object (PaymentFXProvenance)

FX provenance: which FX model priced this payment, and what Moja Switch applied. Present whenever the tenant has an FX model configured. It answers two questions from a single record:

  1. Was the FX Moja Switch's (internal) or the client's own (external)? — model.
  2. What margin did Moja Switch apply? — margin_bps / margin_minor for Moja Switch FX; nothing for BYO-FX, because Moja Switch adds no margin to a client's own FX.

For MOJA_SWITCH_FX Moja Switch itself obtained the reference rate and applied the tenant's commercial margin for the corridor; every figure comes from the quote Moja Switch priced, not from the client. For BYO_FX the client owns its FX lifecycle and only model is recorded — the margin, reference and rate fields are absent by construction (and forbidden by a database constraint).

⛔ For your systems and Moja staff only — never render to an end user. The end user is shown the amount to send and the all-in rate; the margin percentage is never disclosed to them.

object (PayoutInstruction)
object (Party)
end_user_id
string^eu_[a-zA-Z0-9]+$

The end user this payment was originated for. Present only when the payment was created with end_user_id; absent for payments created with an inline sender. It is the filter key for GET /payments?end_user_id=.

recipient_id
string^rcp_[a-zA-Z0-9]+$

The saved recipient this payment was sent to. Present only when the payment was created with recipient_id. payout is still returned in full, resolved from that recipient — the response shape does not change with the request shape.

purpose
string (PaymentPurpose)
Enum: "FAMILY_SUPPORT" "SCHOOL_FEES" "UTILITY_BILL" "MERCHANT_PAYMENT" "OTHER"
object

⛔ Integrator-only — never render to an end user. The rail identities behind a payment. A rail name tells a customer which provider handled their money, which is not theirs to know, changes without notice, and turns a routing decision into a support conversation. Use it for reconciliation and diagnostics; do not pass it to a consumer surface, a push payload or a receipt.

object (PaymentFailure)

Why a payment failed, for the integrator. ⛔ message and stage are integrator-only and must never reach an end user. stage names an internal processing stage — including compliance — and telling a customer their payment failed at the compliance stage both discloses a control and, where the control is a sanctions or fraud rule, may be unlawful to say. Use funds_taken and return_state to tell a customer what happened to their money, and a generic did-not-complete message for the rest.

funding_method
string^[A-Z][A-Z0-9_]{1,31}$

How the sender is funding this payment, as requested at create. Present for INTERAC_REQUEST payments.

object

What the sender must do to pay. Present for INTERAC_REQUEST payments; reference is the Interac reference once the request is made.

funds_taken
boolean

Present for INTERAC_REQUEST payments. Not reliable on a kind: collection today: it reads false even after the payer's funds were collected; use GET /payouts for a collection. Whether the sender's money left their account. Consumer-safe and the only correct basis for telling someone what happened to their funds:

  • false — nothing was collected. "Your money was not taken."
  • true — funds were collected. If the payment did not complete, they are being returned; read return_state for where that is.

A terminal status alone cannot answer this question, which is why this field exists. Do not derive it from failure.stage — that field names an internal stage, it is integrator-only, and inferring consumer copy from it is precisely the leak this contract forbids.

return_state
string
Enum: "NONE" "RETURN_IN_PROGRESS" "RETURNED"
moja-status: planned

🔲 Planned — not emitted. Whether collected funds are on their way back to the sender. Absent or NONE when nothing was collected or nothing is being returned.

taking_longer_than_usual
boolean
moja-status: planned

🔲 Planned — not emitted. An attention signal, deliberately with no cause attached. true means this payment has been in progress longer than it should be and warrants telling the customer so — and it means nothing else.

It is a boolean rather than a reason code on purpose: a boolean cannot leak which internal state, rail, counterparty or compliance step produced it, so a client cannot accidentally build copy that does. The platform derives it from dwell time against the expected duration of whatever stage the payment is in; the stage itself is not exposed and will not be.

Pair it with a support entry point. Do not present it as a failure, do not promise a completion time, and do not ask the customer to do anything — there is nothing for them to do.

cancellable
boolean
moja-status: planned

🔲 Planned — not emitted. Whether POST /payments/{payment_id}/cancel would be accepted right now. Read this rather than inferring eligibility from status: the rule is a platform decision that has already changed once, and a client that re-implements it will offer a cancel button that fails, or hide one that would have worked. It is a point-in-time answer — a cancel may still return 409 if the payment advances between the read and the call, which a client must handle rather than treat as a bug.

object
completed_at
string <date-time>
Response Schema: application/json
id
required
string
status
required
string (PaymentStatus)
Enum: "CREATED" "VALIDATING" "COMPLIANCE_CHECK" "FX_LOCKED" "ROUTING" "COLLECTING" "CANCEL_REQUESTED" "COLLECTED" "PAYOUT_SCHEDULED" "PAID_OUT" "REVERSED" "PAYOUT_PENDING" "PAYOUT_SENT" "PROCESSING" "SETTLED" "COMPLETED" "RECONCILED" "FAILED" "CANCELLED"

Partner-facing payment lifecycle status. PROCESSING is a safe generic "in progress" value: the platform presents any internal-only substate (e.g. a compensation state) that is not one of the enumerated lifecycle stages as PROCESSING, so a partner never observes a raw internal state. For a transfer, PROCESSING is non-terminal and transient: a subsequent poll or webhook will report a terminal status. Clients should treat any unrecognised value as a non-terminal state. Collection statuses are published but not emitted yet. CANCEL_REQUESTED, COLLECTED, PAYOUT_SCHEDULED, PAID_OUT and REVERSED belong to the kind: collection lifecycle. The platform records them, but the partner status mapping does not pass them through yet; each reads PROCESSING. A collection reports CREATED, VALIDATING, COMPLIANCE_CHECK, COLLECTING, FAILED and CANCELLED verbatim. A collection that is paid, scheduled into a payout, paid out or reversed reads PROCESSING and today never reaches a later terminal status. Reconcile collected funds from GET /payouts (each payout member carries the collection reference). Accept all five values now so that enabling them is not a client release.

reference
required
string
required
object (Money)

Decimal-string amount, used by the original partner operations (POST /quotes, POST /payments, GET /payments/{payment_id}). Its value pattern caps the world at two decimal places, which is why the newer end-user operations use MinorUnitAmount instead. Unchanged here: swapping the representation on an existing operation would be a breaking /v1 change.

created_at
required
string <date-time>
updated_at
required
string <date-time>
corridor
string (Corridor) ^[A-Z]{2}-[A-Z]{2}$

Corridor on the transacting routes. This was a closed enum (CA-TZ) and is now the same pattern-constrained string as CorridorCode.

It had to change. A closed enum in the contract is the same defect as a hardcoded corridor in a client: the corridor set is per-tenant entitlement data, it moves, and an enum makes every move a contract change and a client release. Published as an enum, a quote on any newer corridor was a schema violation rather than an entitlement question — which is not something the contract should have an opinion about.

Entitlement is the authority on what a given tenant may actually use (GET /corridors, which will list it, is planned). A corridor you are not entitled to is 403 corridor_not_entitled; a syntactically valid code that does not exist is 422 unsupported_corridor. Treat the value as an opaque server-supplied string: do not enumerate it, do not switch on it beyond what discovery told you, and do not ship a build that breaks when the set changes.

object

The locked payout amount, echoing the partner's client-provided FX (BYO-FX). Present once FX is locked.

fx_rate
string

The partner's locked send→receive rate, echoed back (not Moja-computed or marked up). Present once FX is locked.

fx_rate_id
string <= 128 characters

The partner's declared-rate identifier (fx.rate_id), echoed back for reconciliation. Present only when the partner supplied it at create. Pure metadata — it never affected how the payment was priced or executed.

quote_id
string

The quote this payment was priced on (for a Moja Switch FX tenant, the single-use quote it paid on), for reconciliation. Absent when the payment carried no quote.

object (PaymentFXProvenance)

FX provenance: which FX model priced this payment, and what Moja Switch applied. Present whenever the tenant has an FX model configured. It answers two questions from a single record:

  1. Was the FX Moja Switch's (internal) or the client's own (external)? — model.
  2. What margin did Moja Switch apply? — margin_bps / margin_minor for Moja Switch FX; nothing for BYO-FX, because Moja Switch adds no margin to a client's own FX.

For MOJA_SWITCH_FX Moja Switch itself obtained the reference rate and applied the tenant's commercial margin for the corridor; every figure comes from the quote Moja Switch priced, not from the client. For BYO_FX the client owns its FX lifecycle and only model is recorded — the margin, reference and rate fields are absent by construction (and forbidden by a database constraint).

⛔ For your systems and Moja staff only — never render to an end user. The end user is shown the amount to send and the all-in rate; the margin percentage is never disclosed to them.

object (PayoutInstruction)
object (Party)
end_user_id
string^eu_[a-zA-Z0-9]+$

The end user this payment was originated for. Present only when the payment was created with end_user_id; absent for payments created with an inline sender. It is the filter key for GET /payments?end_user_id=.

recipient_id
string^rcp_[a-zA-Z0-9]+$

The saved recipient this payment was sent to. Present only when the payment was created with recipient_id. payout is still returned in full, resolved from that recipient — the response shape does not change with the request shape.

purpose
string (PaymentPurpose)
Enum: "FAMILY_SUPPORT" "SCHOOL_FEES" "UTILITY_BILL" "MERCHANT_PAYMENT" "OTHER"
object

⛔ Integrator-only — never render to an end user. The rail identities behind a payment. A rail name tells a customer which provider handled their money, which is not theirs to know, changes without notice, and turns a routing decision into a support conversation. Use it for reconciliation and diagnostics; do not pass it to a consumer surface, a push payload or a receipt.

object (PaymentFailure)

Why a payment failed, for the integrator. ⛔ message and stage are integrator-only and must never reach an end user. stage names an internal processing stage — including compliance — and telling a customer their payment failed at the compliance stage both discloses a control and, where the control is a sanctions or fraud rule, may be unlawful to say. Use funds_taken and return_state to tell a customer what happened to their money, and a generic did-not-complete message for the rest.

funding_method
string^[A-Z][A-Z0-9_]{1,31}$

How the sender is funding this payment, as requested at create. Present for INTERAC_REQUEST payments.

object

What the sender must do to pay. Present for INTERAC_REQUEST payments; reference is the Interac reference once the request is made.

funds_taken
boolean

Present for INTERAC_REQUEST payments. Not reliable on a kind: collection today: it reads false even after the payer's funds were collected; use GET /payouts for a collection. Whether the sender's money left their account. Consumer-safe and the only correct basis for telling someone what happened to their funds:

  • false — nothing was collected. "Your money was not taken."
  • true — funds were collected. If the payment did not complete, they are being returned; read return_state for where that is.

A terminal status alone cannot answer this question, which is why this field exists. Do not derive it from failure.stage — that field names an internal stage, it is integrator-only, and inferring consumer copy from it is precisely the leak this contract forbids.

return_state
string
Enum: "NONE" "RETURN_IN_PROGRESS" "RETURNED"
moja-status: planned

🔲 Planned — not emitted. Whether collected funds are on their way back to the sender. Absent or NONE when nothing was collected or nothing is being returned.

taking_longer_than_usual
boolean
moja-status: planned

🔲 Planned — not emitted. An attention signal, deliberately with no cause attached. true means this payment has been in progress longer than it should be and warrants telling the customer so — and it means nothing else.

It is a boolean rather than a reason code on purpose: a boolean cannot leak which internal state, rail, counterparty or compliance step produced it, so a client cannot accidentally build copy that does. The platform derives it from dwell time against the expected duration of whatever stage the payment is in; the stage itself is not exposed and will not be.

Pair it with a support entry point. Do not present it as a failure, do not promise a completion time, and do not ask the customer to do anything — there is nothing for them to do.

cancellable
boolean
moja-status: planned

🔲 Planned — not emitted. Whether POST /payments/{payment_id}/cancel would be accepted right now. Read this rather than inferring eligibility from status: the rule is a platform decision that has already changed once, and a client that re-implements it will offer a cancel button that fails, or hide one that would have worked. It is a point-in-time answer — a cancel may still return 409 if the payment advances between the read and the call, which a client must handle rather than treat as a bug.

object
completed_at
string <date-time>

Request samples

Content type
application/json
Example
{
  • "corridor": "CA-TZ",
  • "reference": "partner-tx-12345",
  • "send_amount": {
    },
  • "fx": {
    },
  • "sender": {
    },
  • "sender_kyc": {
    },
  • "payout": {
    },
  • "purpose": "FAMILY_SUPPORT"
}

Response samples

Content type
application/json
{
  • "id": "pay_abc123def456",
  • "status": "CREATED",
  • "corridor": "CA-TZ",
  • "reference": "string",
  • "send_amount": {
    },
  • "receive_amount": {
    },
  • "fx_rate": "string",
  • "fx_rate_id": "dealer-quote-7f3a91",
  • "quote_id": "quo_contract",
  • "fx_provenance": {
    },
  • "payout": {
    },
  • "sender": {
    },
  • "end_user_id": "eu_7Qm4xK2pLd",
  • "recipient_id": "rcp_9Zd5nP6yTc",
  • "purpose": "FAMILY_SUPPORT",
  • "rails": {
    },
  • "failure": {
    },
  • "funding_method": "INTERAC_REQUEST",
  • "funding_instructions": {
    },
  • "funds_taken": false,
  • "return_state": "NONE",
  • "taking_longer_than_usual": false,
  • "cancellable": true,
  • "metadata": {
    },
  • "created_at": "2019-08-24T14:15:22Z",
  • "updated_at": "2019-08-24T14:15:22Z",
  • "completed_at": "2019-08-24T14:15:22Z"
}

List payments

Returns the authenticated partner's payments, newest first. Scoped to the partner: another partner's payments are never visible, and a filter naming one of their resources returns an empty page, not a 404.

Scope: read.

Keyset pagination, not offset. The page is ordered by created_at DESC, id DESC and walked with an opaque cursor. Read has_more; when it is true, pass next_cursor back verbatim as cursor on the following request. The cursor encodes the sort position and the filter set:

  • Never construct, parse or mutate a cursor. Its encoding is not part of this contract and will change.
  • Changing any filter while paginating invalidates the cursor → 422 invalid_cursor. Start the walk again with the new filters.
  • A payment created while you paginate sorts ahead of your cursor and so does not appear in the remaining pages. Re-request the first page to see it. Keyset pagination does not skip or duplicate rows the way offset pagination does.
Authorizations:
bearerAuth
query Parameters
end_user_id
string^eu_[a-zA-Z0-9]+$
Example: end_user_id=eu_7Qm4xK2pLd

Return only payments originated by this end user. The natural filter for an activity screen.

status
Array of strings (PaymentStatus) <= 14 items
Items Enum: "CREATED" "VALIDATING" "COMPLIANCE_CHECK" "FX_LOCKED" "ROUTING" "COLLECTING" "CANCEL_REQUESTED" "COLLECTED" "PAYOUT_SCHEDULED" "PAID_OUT" "REVERSED" "PAYOUT_PENDING" "PAYOUT_SENT" "PROCESSING" "SETTLED" "COMPLETED" "RECONCILED" "FAILED" "CANCELLED"

Return only payments in these statuses. Repeat the parameter to match several (?status=COMPLETED&status=FAILED). Omit for all.

created_after
string <date-time>

Only payments created at or after this instant (RFC 3339).

created_before
string <date-time>

Only payments created strictly before this instant (RFC 3339).

limit
integer [ 1 .. 100 ]
Default: 25

Page size. Defaults to 25.

cursor
string <= 512 characters

Opaque keyset cursor from a previous response's next_cursor. Pass it back unchanged; do not construct one.

Responses

Response Schema: application/json
required
Array of objects (Payment)

Payments, ordered created_at DESC, id DESC.

has_more
required
boolean

Whether a further page exists. The only correct pagination predicate — an empty data array is not the terminator, and neither is a short page.

next_cursor
string <= 512 characters

Opaque cursor for the next page. Present only when has_more is true. Its encoding is not part of this contract: pass it back verbatim, never construct or decode one.

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "has_more": true,
  • "next_cursor": "eyJjIjoiMjAyNi0wOS0xM1QxMDoxNTowMFoiLCJpIjoicGF5X2FiYzEyM2RlZjQ1NiJ9"
}

Get payment

Authorizations:
bearerAuth
path Parameters
payment_id
required
string^pay_[a-zA-Z0-9]+$
Example: pay_abc123def456

Responses

Response Schema: application/json
id
required
string
status
required
string (PaymentStatus)
Enum: "CREATED" "VALIDATING" "COMPLIANCE_CHECK" "FX_LOCKED" "ROUTING" "COLLECTING" "CANCEL_REQUESTED" "COLLECTED" "PAYOUT_SCHEDULED" "PAID_OUT" "REVERSED" "PAYOUT_PENDING" "PAYOUT_SENT" "PROCESSING" "SETTLED" "COMPLETED" "RECONCILED" "FAILED" "CANCELLED"

Partner-facing payment lifecycle status. PROCESSING is a safe generic "in progress" value: the platform presents any internal-only substate (e.g. a compensation state) that is not one of the enumerated lifecycle stages as PROCESSING, so a partner never observes a raw internal state. For a transfer, PROCESSING is non-terminal and transient: a subsequent poll or webhook will report a terminal status. Clients should treat any unrecognised value as a non-terminal state. Collection statuses are published but not emitted yet. CANCEL_REQUESTED, COLLECTED, PAYOUT_SCHEDULED, PAID_OUT and REVERSED belong to the kind: collection lifecycle. The platform records them, but the partner status mapping does not pass them through yet; each reads PROCESSING. A collection reports CREATED, VALIDATING, COMPLIANCE_CHECK, COLLECTING, FAILED and CANCELLED verbatim. A collection that is paid, scheduled into a payout, paid out or reversed reads PROCESSING and today never reaches a later terminal status. Reconcile collected funds from GET /payouts (each payout member carries the collection reference). Accept all five values now so that enabling them is not a client release.

reference
required
string
required
object (Money)

Decimal-string amount, used by the original partner operations (POST /quotes, POST /payments, GET /payments/{payment_id}). Its value pattern caps the world at two decimal places, which is why the newer end-user operations use MinorUnitAmount instead. Unchanged here: swapping the representation on an existing operation would be a breaking /v1 change.

created_at
required
string <date-time>
updated_at
required
string <date-time>
corridor
string (Corridor) ^[A-Z]{2}-[A-Z]{2}$

Corridor on the transacting routes. This was a closed enum (CA-TZ) and is now the same pattern-constrained string as CorridorCode.

It had to change. A closed enum in the contract is the same defect as a hardcoded corridor in a client: the corridor set is per-tenant entitlement data, it moves, and an enum makes every move a contract change and a client release. Published as an enum, a quote on any newer corridor was a schema violation rather than an entitlement question — which is not something the contract should have an opinion about.

Entitlement is the authority on what a given tenant may actually use (GET /corridors, which will list it, is planned). A corridor you are not entitled to is 403 corridor_not_entitled; a syntactically valid code that does not exist is 422 unsupported_corridor. Treat the value as an opaque server-supplied string: do not enumerate it, do not switch on it beyond what discovery told you, and do not ship a build that breaks when the set changes.

object

The locked payout amount, echoing the partner's client-provided FX (BYO-FX). Present once FX is locked.

fx_rate
string

The partner's locked send→receive rate, echoed back (not Moja-computed or marked up). Present once FX is locked.

fx_rate_id
string <= 128 characters

The partner's declared-rate identifier (fx.rate_id), echoed back for reconciliation. Present only when the partner supplied it at create. Pure metadata — it never affected how the payment was priced or executed.

quote_id
string

The quote this payment was priced on (for a Moja Switch FX tenant, the single-use quote it paid on), for reconciliation. Absent when the payment carried no quote.

object (PaymentFXProvenance)

FX provenance: which FX model priced this payment, and what Moja Switch applied. Present whenever the tenant has an FX model configured. It answers two questions from a single record:

  1. Was the FX Moja Switch's (internal) or the client's own (external)? — model.
  2. What margin did Moja Switch apply? — margin_bps / margin_minor for Moja Switch FX; nothing for BYO-FX, because Moja Switch adds no margin to a client's own FX.

For MOJA_SWITCH_FX Moja Switch itself obtained the reference rate and applied the tenant's commercial margin for the corridor; every figure comes from the quote Moja Switch priced, not from the client. For BYO_FX the client owns its FX lifecycle and only model is recorded — the margin, reference and rate fields are absent by construction (and forbidden by a database constraint).

⛔ For your systems and Moja staff only — never render to an end user. The end user is shown the amount to send and the all-in rate; the margin percentage is never disclosed to them.

object (PayoutInstruction)
object (Party)
end_user_id
string^eu_[a-zA-Z0-9]+$

The end user this payment was originated for. Present only when the payment was created with end_user_id; absent for payments created with an inline sender. It is the filter key for GET /payments?end_user_id=.

recipient_id
string^rcp_[a-zA-Z0-9]+$

The saved recipient this payment was sent to. Present only when the payment was created with recipient_id. payout is still returned in full, resolved from that recipient — the response shape does not change with the request shape.

purpose
string (PaymentPurpose)
Enum: "FAMILY_SUPPORT" "SCHOOL_FEES" "UTILITY_BILL" "MERCHANT_PAYMENT" "OTHER"
object

⛔ Integrator-only — never render to an end user. The rail identities behind a payment. A rail name tells a customer which provider handled their money, which is not theirs to know, changes without notice, and turns a routing decision into a support conversation. Use it for reconciliation and diagnostics; do not pass it to a consumer surface, a push payload or a receipt.

object (PaymentFailure)

Why a payment failed, for the integrator. ⛔ message and stage are integrator-only and must never reach an end user. stage names an internal processing stage — including compliance — and telling a customer their payment failed at the compliance stage both discloses a control and, where the control is a sanctions or fraud rule, may be unlawful to say. Use funds_taken and return_state to tell a customer what happened to their money, and a generic did-not-complete message for the rest.

funding_method
string^[A-Z][A-Z0-9_]{1,31}$

How the sender is funding this payment, as requested at create. Present for INTERAC_REQUEST payments.

object

What the sender must do to pay. Present for INTERAC_REQUEST payments; reference is the Interac reference once the request is made.

funds_taken
boolean

Present for INTERAC_REQUEST payments. Not reliable on a kind: collection today: it reads false even after the payer's funds were collected; use GET /payouts for a collection. Whether the sender's money left their account. Consumer-safe and the only correct basis for telling someone what happened to their funds:

  • false — nothing was collected. "Your money was not taken."
  • true — funds were collected. If the payment did not complete, they are being returned; read return_state for where that is.

A terminal status alone cannot answer this question, which is why this field exists. Do not derive it from failure.stage — that field names an internal stage, it is integrator-only, and inferring consumer copy from it is precisely the leak this contract forbids.

return_state
string
Enum: "NONE" "RETURN_IN_PROGRESS" "RETURNED"
moja-status: planned

🔲 Planned — not emitted. Whether collected funds are on their way back to the sender. Absent or NONE when nothing was collected or nothing is being returned.

taking_longer_than_usual
boolean
moja-status: planned

🔲 Planned — not emitted. An attention signal, deliberately with no cause attached. true means this payment has been in progress longer than it should be and warrants telling the customer so — and it means nothing else.

It is a boolean rather than a reason code on purpose: a boolean cannot leak which internal state, rail, counterparty or compliance step produced it, so a client cannot accidentally build copy that does. The platform derives it from dwell time against the expected duration of whatever stage the payment is in; the stage itself is not exposed and will not be.

Pair it with a support entry point. Do not present it as a failure, do not promise a completion time, and do not ask the customer to do anything — there is nothing for them to do.

cancellable
boolean
moja-status: planned

🔲 Planned — not emitted. Whether POST /payments/{payment_id}/cancel would be accepted right now. Read this rather than inferring eligibility from status: the rule is a platform decision that has already changed once, and a client that re-implements it will offer a cancel button that fails, or hide one that would have worked. It is a point-in-time answer — a cancel may still return 409 if the payment advances between the read and the call, which a client must handle rather than treat as a bug.

object
completed_at
string <date-time>

Response samples

Content type
application/json
{
  • "id": "pay_abc123def456",
  • "status": "CREATED",
  • "corridor": "CA-TZ",
  • "reference": "string",
  • "send_amount": {
    },
  • "receive_amount": {
    },
  • "fx_rate": "string",
  • "fx_rate_id": "dealer-quote-7f3a91",
  • "quote_id": "quo_contract",
  • "fx_provenance": {
    },
  • "payout": {
    },
  • "sender": {
    },
  • "end_user_id": "eu_7Qm4xK2pLd",
  • "recipient_id": "rcp_9Zd5nP6yTc",
  • "purpose": "FAMILY_SUPPORT",
  • "rails": {
    },
  • "failure": {
    },
  • "funding_method": "INTERAC_REQUEST",
  • "funding_instructions": {
    },
  • "funds_taken": false,
  • "return_state": "NONE",
  • "taking_longer_than_usual": false,
  • "cancellable": true,
  • "metadata": {
    },
  • "created_at": "2019-08-24T14:15:22Z",
  • "updated_at": "2019-08-24T14:15:22Z",
  • "completed_at": "2019-08-24T14:15:22Z"
}

Cancel payment

Cancels a payment before send-side collection begins. Returns 409 if cancellation is no longer allowed.

Authorizations:
bearerAuth
path Parameters
payment_id
required
string^pay_[a-zA-Z0-9]+$
Example: pay_abc123def456
Request Body schema: application/json
reason
string <= 500 characters

Responses

Response Schema: application/json
id
required
string
status
required
string (PaymentStatus)
Enum: "CREATED" "VALIDATING" "COMPLIANCE_CHECK" "FX_LOCKED" "ROUTING" "COLLECTING" "CANCEL_REQUESTED" "COLLECTED" "PAYOUT_SCHEDULED" "PAID_OUT" "REVERSED" "PAYOUT_PENDING" "PAYOUT_SENT" "PROCESSING" "SETTLED" "COMPLETED" "RECONCILED" "FAILED" "CANCELLED"

Partner-facing payment lifecycle status. PROCESSING is a safe generic "in progress" value: the platform presents any internal-only substate (e.g. a compensation state) that is not one of the enumerated lifecycle stages as PROCESSING, so a partner never observes a raw internal state. For a transfer, PROCESSING is non-terminal and transient: a subsequent poll or webhook will report a terminal status. Clients should treat any unrecognised value as a non-terminal state. Collection statuses are published but not emitted yet. CANCEL_REQUESTED, COLLECTED, PAYOUT_SCHEDULED, PAID_OUT and REVERSED belong to the kind: collection lifecycle. The platform records them, but the partner status mapping does not pass them through yet; each reads PROCESSING. A collection reports CREATED, VALIDATING, COMPLIANCE_CHECK, COLLECTING, FAILED and CANCELLED verbatim. A collection that is paid, scheduled into a payout, paid out or reversed reads PROCESSING and today never reaches a later terminal status. Reconcile collected funds from GET /payouts (each payout member carries the collection reference). Accept all five values now so that enabling them is not a client release.

reference
required
string
required
object (Money)

Decimal-string amount, used by the original partner operations (POST /quotes, POST /payments, GET /payments/{payment_id}). Its value pattern caps the world at two decimal places, which is why the newer end-user operations use MinorUnitAmount instead. Unchanged here: swapping the representation on an existing operation would be a breaking /v1 change.

created_at
required
string <date-time>
updated_at
required
string <date-time>
corridor
string (Corridor) ^[A-Z]{2}-[A-Z]{2}$

Corridor on the transacting routes. This was a closed enum (CA-TZ) and is now the same pattern-constrained string as CorridorCode.

It had to change. A closed enum in the contract is the same defect as a hardcoded corridor in a client: the corridor set is per-tenant entitlement data, it moves, and an enum makes every move a contract change and a client release. Published as an enum, a quote on any newer corridor was a schema violation rather than an entitlement question — which is not something the contract should have an opinion about.

Entitlement is the authority on what a given tenant may actually use (GET /corridors, which will list it, is planned). A corridor you are not entitled to is 403 corridor_not_entitled; a syntactically valid code that does not exist is 422 unsupported_corridor. Treat the value as an opaque server-supplied string: do not enumerate it, do not switch on it beyond what discovery told you, and do not ship a build that breaks when the set changes.

object

The locked payout amount, echoing the partner's client-provided FX (BYO-FX). Present once FX is locked.

fx_rate
string

The partner's locked send→receive rate, echoed back (not Moja-computed or marked up). Present once FX is locked.

fx_rate_id
string <= 128 characters

The partner's declared-rate identifier (fx.rate_id), echoed back for reconciliation. Present only when the partner supplied it at create. Pure metadata — it never affected how the payment was priced or executed.

quote_id
string

The quote this payment was priced on (for a Moja Switch FX tenant, the single-use quote it paid on), for reconciliation. Absent when the payment carried no quote.

object (PaymentFXProvenance)

FX provenance: which FX model priced this payment, and what Moja Switch applied. Present whenever the tenant has an FX model configured. It answers two questions from a single record:

  1. Was the FX Moja Switch's (internal) or the client's own (external)? — model.
  2. What margin did Moja Switch apply? — margin_bps / margin_minor for Moja Switch FX; nothing for BYO-FX, because Moja Switch adds no margin to a client's own FX.

For MOJA_SWITCH_FX Moja Switch itself obtained the reference rate and applied the tenant's commercial margin for the corridor; every figure comes from the quote Moja Switch priced, not from the client. For BYO_FX the client owns its FX lifecycle and only model is recorded — the margin, reference and rate fields are absent by construction (and forbidden by a database constraint).

⛔ For your systems and Moja staff only — never render to an end user. The end user is shown the amount to send and the all-in rate; the margin percentage is never disclosed to them.

object (PayoutInstruction)
object (Party)
end_user_id
string^eu_[a-zA-Z0-9]+$

The end user this payment was originated for. Present only when the payment was created with end_user_id; absent for payments created with an inline sender. It is the filter key for GET /payments?end_user_id=.

recipient_id
string^rcp_[a-zA-Z0-9]+$

The saved recipient this payment was sent to. Present only when the payment was created with recipient_id. payout is still returned in full, resolved from that recipient — the response shape does not change with the request shape.

purpose
string (PaymentPurpose)
Enum: "FAMILY_SUPPORT" "SCHOOL_FEES" "UTILITY_BILL" "MERCHANT_PAYMENT" "OTHER"
object

⛔ Integrator-only — never render to an end user. The rail identities behind a payment. A rail name tells a customer which provider handled their money, which is not theirs to know, changes without notice, and turns a routing decision into a support conversation. Use it for reconciliation and diagnostics; do not pass it to a consumer surface, a push payload or a receipt.

object (PaymentFailure)

Why a payment failed, for the integrator. ⛔ message and stage are integrator-only and must never reach an end user. stage names an internal processing stage — including compliance — and telling a customer their payment failed at the compliance stage both discloses a control and, where the control is a sanctions or fraud rule, may be unlawful to say. Use funds_taken and return_state to tell a customer what happened to their money, and a generic did-not-complete message for the rest.

funding_method
string^[A-Z][A-Z0-9_]{1,31}$

How the sender is funding this payment, as requested at create. Present for INTERAC_REQUEST payments.

object

What the sender must do to pay. Present for INTERAC_REQUEST payments; reference is the Interac reference once the request is made.

funds_taken
boolean

Present for INTERAC_REQUEST payments. Not reliable on a kind: collection today: it reads false even after the payer's funds were collected; use GET /payouts for a collection. Whether the sender's money left their account. Consumer-safe and the only correct basis for telling someone what happened to their funds:

  • false — nothing was collected. "Your money was not taken."
  • true — funds were collected. If the payment did not complete, they are being returned; read return_state for where that is.

A terminal status alone cannot answer this question, which is why this field exists. Do not derive it from failure.stage — that field names an internal stage, it is integrator-only, and inferring consumer copy from it is precisely the leak this contract forbids.

return_state
string
Enum: "NONE" "RETURN_IN_PROGRESS" "RETURNED"
moja-status: planned

🔲 Planned — not emitted. Whether collected funds are on their way back to the sender. Absent or NONE when nothing was collected or nothing is being returned.

taking_longer_than_usual
boolean
moja-status: planned

🔲 Planned — not emitted. An attention signal, deliberately with no cause attached. true means this payment has been in progress longer than it should be and warrants telling the customer so — and it means nothing else.

It is a boolean rather than a reason code on purpose: a boolean cannot leak which internal state, rail, counterparty or compliance step produced it, so a client cannot accidentally build copy that does. The platform derives it from dwell time against the expected duration of whatever stage the payment is in; the stage itself is not exposed and will not be.

Pair it with a support entry point. Do not present it as a failure, do not promise a completion time, and do not ask the customer to do anything — there is nothing for them to do.

cancellable
boolean
moja-status: planned

🔲 Planned — not emitted. Whether POST /payments/{payment_id}/cancel would be accepted right now. Read this rather than inferring eligibility from status: the rule is a platform decision that has already changed once, and a client that re-implements it will offer a cancel button that fails, or hide one that would have worked. It is a point-in-time answer — a cancel may still return 409 if the payment advances between the read and the call, which a client must handle rather than treat as a bug.

object
completed_at
string <date-time>

Request samples

Content type
application/json
{
  • "reason": "string"
}

Response samples

Content type
application/json
{
  • "id": "pay_abc123def456",
  • "status": "CREATED",
  • "corridor": "CA-TZ",
  • "reference": "string",
  • "send_amount": {
    },
  • "receive_amount": {
    },
  • "fx_rate": "string",
  • "fx_rate_id": "dealer-quote-7f3a91",
  • "quote_id": "quo_contract",
  • "fx_provenance": {
    },
  • "payout": {
    },
  • "sender": {
    },
  • "end_user_id": "eu_7Qm4xK2pLd",
  • "recipient_id": "rcp_9Zd5nP6yTc",
  • "purpose": "FAMILY_SUPPORT",
  • "rails": {
    },
  • "failure": {
    },
  • "funding_method": "INTERAC_REQUEST",
  • "funding_instructions": {
    },
  • "funds_taken": false,
  • "return_state": "NONE",
  • "taking_longer_than_usual": false,
  • "cancellable": true,
  • "metadata": {
    },
  • "created_at": "2019-08-24T14:15:22Z",
  • "updated_at": "2019-08-24T14:15:22Z",
  • "completed_at": "2019-08-24T14:15:22Z"
}

Get payment receipt

✅ Implemented.

Returns the receipt for a payment whose funds have been delivered.

receipt_number is RCPT-<year>-<first 8 characters of the payment id, upper case>, derived from the payment, so a re-fetch always returns the same number. issued_at is the payment's completed_at. fees.platform_fee may be zero.

Scope: read.

Availability. A receipt exists only once the payment has reached COMPLETED, SETTLED or RECONCILED. Anything earlier — or a FAILED/CANCELLED payment, which has no receipt — returns 409 receipt_not_available. Use GET /payments/{payment_id} for in-flight state; a receipt is a record of money that moved, not a status view.

Amounts are integer minor units (MinorUnitAmount), each carrying its own currency and exponent — amount / 10^minor_unit_exponent is the value to display. A receipt is the artifact a user screenshots and sends to support, so it is the last place an amount should be ambiguous by a factor of a hundred.

Masking is structural for everything except the recipient's own phone number. recipient.phone is the destination MSISDN in full — the sender is shown their own saved recipient's number so they can verify who they are paying. Bank account numbers, document numbers and any credential are never returned here — there is no parameter that unmasks them.

Authorizations:
bearerAuth
path Parameters
payment_id
required
string^pay_[a-zA-Z0-9]+$
Example: pay_abc123def456

Responses

Response Schema: application/json
payment_id
required
string
receipt_number
required
string

Human-quotable reference for this receipt. Stable for the life of the payment — a re-fetch returns the same number, so a user reading it to support and a user reading it from a saved PDF agree.

issued_at
required
string <date-time>
status
required
string
Enum: "CREATED" "VALIDATING" "COMPLIANCE_CHECK" "FX_LOCKED" "ROUTING" "COLLECTING" "CANCEL_REQUESTED" "COLLECTED" "PAYOUT_SCHEDULED" "PAID_OUT" "REVERSED" "PAYOUT_PENDING" "PAYOUT_SENT" "PROCESSING" "SETTLED" "COMPLETED" "RECONCILED" "FAILED" "CANCELLED"

The payment status at issue time. A receipt is only issued from COMPLETED, SETTLED or RECONCILED.

corridor
required
string (CorridorCode) ^[A-Z]{2}-[A-Z]{2}$

A corridor identifier, <send ISO 3166-1 alpha-2>-<receive ISO 3166-1 alpha-2>. Treat it as an opaque server-supplied string: the set is data (GET /corridors) and per-partner entitlement, not a constant your client enumerates, so a client that renders whatever discovery returns gains a corridor without a release. The Corridor schema used on the transacting routes is now this same shape — the two agree, and neither is a closed set.

required
object

What the sender paid, in integer minor units of the send currency.

required
object

The beneficiary.

reference
string

The partner's own reference for the payment, echoed back.

end_user_id
string^eu_[a-zA-Z0-9]+$

Present when the payment was originated by an end user.

object

The amount delivered, in integer minor units of the receive currency, with that currency's exponent carried alongside it. A zero-decimal receive currency therefore cannot arrive here with cents attached.

fx_rate
string

The rate applied to this payment, echoed back.

object (ReceiptFees)

What the transfer cost the sender, denominated in the SEND currency. The same breakdown as QuoteFees — platform fees only, no FX markup under BYO-FX — expressed in integer minor units. It is a separate schema rather than a reuse of QuoteFees because QuoteFees is part of the existing POST /quotes response and changing its representation would be a breaking /v1 change.

sender_name
string

Display name of the originator, as held by the platform. Present only where the tenant supplied one.

completed_at
string <date-time>

Response samples

Content type
application/json
{
  • "payment_id": "pay_abc123def456",
  • "receipt_number": "RCPT-2026-0001234",
  • "issued_at": "2019-08-24T14:15:22Z",
  • "status": "CREATED",
  • "corridor": "CA-TZ",
  • "reference": "string",
  • "end_user_id": "eu_7Qm4xK2pLd",
  • "send_amount": {
    },
  • "receive_amount": {
    },
  • "fx_rate": "1850.25",
  • "fees": {
    },
  • "sender_name": "Test Sender",
  • "recipient": {
    },
  • "completed_at": "2019-08-24T14:15:22Z"
}

Payouts

Tenant CAD payouts for collections. Sandbox only; production payouts are not offered yet. Members expose gross, fee, net and the collection reference for reconciliation.

List tenant CAD payouts

Returns the authenticated partner's tenant CAD payouts, newest first. Each member carries gross, fee, net and the collection reference, so you can reconcile each payout line to your own records.

Scope: read.

Sandbox only: production payouts are not offered yet.

Authorizations:
bearerAuth
query Parameters
limit
integer [ 1 .. 100 ]
Default: 25

Responses

Response Schema: application/json
required
Array of objects (TenantPayout)
has_more
required
boolean

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "has_more": true
}

Get tenant CAD payout

Returns one tenant CAD payout scoped to the authenticated partner. Another partner's id is 404, never a cross-tenant leak.

Scope: read.

Authorizations:
bearerAuth
path Parameters
payout_id
required
string^tpo_[a-zA-Z0-9]+$

Responses

Response Schema: application/json
id
required
string^tpo_[a-zA-Z0-9]+$
status
required
string
Enum: "CREATED" "SENT" "PAID" "FAILED" "RETURNED" "OUTCOME_UNKNOWN"
currency
required
string
Value: "CAD"
cycle
required
string
attempt
required
integer >= 1
net_amount_minor
required
integer <int64>
created_at
required
string <date-time>
updated_at
required
string <date-time>
required
Array of objects (TenantPayoutMember)

Response samples

Content type
application/json
{
  • "id": "string",
  • "status": "CREATED",
  • "currency": "CAD",
  • "cycle": "string",
  • "attempt": 1,
  • "net_amount_minor": 0,
  • "created_at": "2019-08-24T14:15:22Z",
  • "updated_at": "2019-08-24T14:15:22Z",
  • "members": [
    ]
}

FX

Indicative FX reference data. Never a customer rate.

Get the indicative FX reference rate for a corridor

The validated indicative market reference rate for an enabled corridor, priced as its send→receive pair: receive-currency units per one send-currency unit, a decimal string with at most 8 fractional digits. Never a customer rate: no spread, no quote, no lock — do not show it as the rate a customer will get. To price a payment, use POST /quotes.

Scope: read. You must also be entitled to corridor (403 corridor_not_entitled otherwise; 503 entitlement_unavailable if the entitlement cannot be checked).

If there is no valid, fresh rate, the answer is 503 fx_unavailable; a stale or older rate is never returned instead.

Authorizations:
bearerAuth
query Parameters
corridor
required
string (CorridorCode) ^[A-Z]{2}-[A-Z]{2}$
Example: corridor=CA-TZ

A corridor identifier, <send ISO 3166-1 alpha-2>-<receive ISO 3166-1 alpha-2>. Treat it as an opaque server-supplied string: the set is data (GET /corridors) and per-partner entitlement, not a constant your client enumerates, so a client that renders whatever discovery returns gains a corridor without a release. The Corridor schema used on the transacting routes is now this same shape — the two agree, and neither is a closed set.

Responses

Response Schema: application/json
corridor
required
string (CorridorCode) ^[A-Z]{2}-[A-Z]{2}$

A corridor identifier, <send ISO 3166-1 alpha-2>-<receive ISO 3166-1 alpha-2>. Treat it as an opaque server-supplied string: the set is data (GET /corridors) and per-partner entitlement, not a constant your client enumerates, so a client that renders whatever discovery returns gains a corridor without a release. The Corridor schema used on the transacting routes is now this same shape — the two agree, and neither is a closed set.

rate
required
string^\d+(\.\d{1,8})?$

Receive-currency units per one send-currency unit.

rate_type
required
string
Value: "indicative"
provider
required
string

The reference-data provider.

as_of
required
string <date-time>

When the provider observed the rate.

Response samples

Content type
application/json
{
  • "corridor": "CA-TZ",
  • "rate": "2712.5",
  • "rate_type": "indicative",
  • "provider": "string",
  • "as_of": "2019-08-24T14:15:22Z"
}

Corridors

Corridor discovery — payout methods, limits and minor-unit exponents, as data rather than hardcoded client knowledge. GET /corridors/{corridor_code} and GET /account/readiness are live; the corridor list and client configuration are planned.

[Planned] Operating posture for a client application

What a client application must know before it lets anyone move money: whether this build is still supported, and whether money operations are currently enabled at all.

Not end-user scoped, and answerable before anyone signs in. A client below the minimum version must be able to learn that on launch, and a money-operations stop must reach a client whose user never authenticates.

This is the client half of the compliance kill switch. The platform can already refuse a payment; without this, a stop leaves every deployed app still offering to send money and discovering the refusal one request at a time. money_operations_enabled: false means the client SHALL hide or disable every send affordance while leaving read and support surfaces reachable — a customer whose transfer is in flight still needs to see it and still needs to reach support, and cutting them off is worse than the condition that triggered the stop.

Clients SHOULD read this on launch and on foreground, and SHALL NOT cache it past max_age_seconds. It is deliberately cheap to serve.

🔲 Planned — not implemented. Calling it today returns 404. The "before anyone signs in" wording refers to end-user sign-in in a client app: the call itself still requires a partner API key with scope read.

Scope: read.

moja-status: planned
Authorizations:
bearerAuth

Responses

Response Schema: application/json
money_operations_enabled
required
boolean

When false, the client SHALL disable every send affordance and SHALL keep read and support surfaces reachable. A customer with a transfer in flight still needs to see it and still needs to reach support.

max_age_seconds
required
integer >= 0

How long this answer may be cached. A short value is the point: this is the control that has to arrive quickly when it changes.

money_operations_message
string <= 300 characters

Consumer-safe text to show when money operations are disabled. Set by the platform so the wording is not invented per client. It SHALL NOT name a rail, a provider, a compliance stage, or a cause.

minimum_version
string

The lowest client version permitted to move money, as the client's own version string. A client below it SHALL disable money operations and prompt the customer to update, independently of money_operations_enabled.

upgrade_url
string <uri>

Where to send a customer whose build is below minimum_version.

Response samples

Content type
application/json
{
  • "money_operations_enabled": true,
  • "money_operations_message": "Sending is temporarily unavailable. You can still view your transfers and contact support.",
  • "minimum_version": "1.4.0",
  • "upgrade_url": "http://example.com",
  • "max_age_seconds": 300
}

[Planned] List the corridors this partner may use

Returns the corridors the authenticated partner is entitled to, as data.

Scope: read.

Discover, do not hardcode. A client that ships a corridor list, a currency list or a decimal assumption has to be re-released to gain a corridor; one that renders this response does not. The returned set is per-partner entitlement, so two partners can legitimately see different lists, and a corridor absent here is refused on the money routes with 403 corridor_not_entitled.

The transacting routes type corridor as the same pattern-constrained string (see the Corridor schema), so this endpoint — not the type — is what says which corridors a tenant may actually use. Treat CorridorCode as an opaque server-supplied string, so that widening the set is an entitlement change rather than a client release.

moja-status: planned
Authorizations:
bearerAuth

Responses

Response Schema: application/json
required
Array of objects (CorridorSummary)

Response samples

Content type
application/json
{
  • "data": [
    ]
}

Get corridor detail

Returns what a client needs to build and validate a payment for one enabled catalogue corridor: payout methods, the limits in force, and the minor-unit exponent of both currencies. Not entitlement-scoped (an unknown or disabled corridor is 404 corridor_not_found); per-method beneficiary field specs are planned.

Scope: read.

The minor-unit exponent is load-bearing — never assume two decimals. send_currency.minor_unit_exponent and receive_currency.minor_unit_exponent give the number of fractional digits that currency has. It is 2 for CAD and TZS; it is 0 for a zero-decimal currency, where 1500 means one thousand five hundred units and 15.00 is not a valid amount at all. Formatting, rounding, input masks, "cents" arithmetic and any equality check must read the exponent. A client that hardcodes two decimals will be wrong by a factor of one hundred the first time it meets a zero-decimal currency, and it will be wrong quietly.

Limits are server-supplied and this response is the only place to read them. Do not cache them past a session, do not re-implement them, and do not use them to decide anything — render them, and let the platform deny. The platform is the sole authority on limits and its denial is the answer, not a second opinion.

Authorizations:
bearerAuth
path Parameters
corridor_code
required
string (CorridorCode) ^[A-Z]{2}-[A-Z]{2}$
Example: CA-TZ

Corridor code as returned by GET /corridors. Treat it as an opaque server-supplied string, not a value your client enumerates.

Responses

Response Schema: application/json
code
required
string (CorridorCode) ^[A-Z]{2}-[A-Z]{2}$

A corridor identifier, <send ISO 3166-1 alpha-2>-<receive ISO 3166-1 alpha-2>. Treat it as an opaque server-supplied string: the set is data (GET /corridors) and per-partner entitlement, not a constant your client enumerates, so a client that renders whatever discovery returns gains a corridor without a release. The Corridor schema used on the transacting routes is now this same shape — the two agree, and neither is a closed set.

required
object (CurrencyUnit)

A currency and how many fractional digits it actually has. Read minor_unit_exponent — never assume two. It is 2 for CAD and TZS and 0 for a zero-decimal currency, where 1500 is one thousand five hundred units and 15.00 is not a valid amount. Formatting, input masks, rounding and minor-unit arithmetic all depend on it; hardcoding 2 is wrong by a factor of one hundred against a zero-decimal currency, and wrong silently. This describes the corridor's currencies, which is what a client needs before it has any amount in hand — to build an input mask, to label a field, to validate what the user typed. Amounts themselves carry their own exponent (MinorUnitAmount) so that an amount stays self-describing away from this metadata.

required
object (CurrencyUnit)

A currency and how many fractional digits it actually has. Read minor_unit_exponent — never assume two. It is 2 for CAD and TZS and 0 for a zero-decimal currency, where 1500 is one thousand five hundred units and 15.00 is not a valid amount. Formatting, input masks, rounding and minor-unit arithmetic all depend on it; hardcoding 2 is wrong by a factor of one hundred against a zero-decimal currency, and wrong silently. This describes the corridor's currencies, which is what a client needs before it has any amount in hand — to build an input mask, to label a field, to validate what the user typed. Amounts themselves carry their own exponent (MinorUnitAmount) so that an amount stays self-describing away from this metadata.

payout_methods
required
Array of strings (PayoutMethod)
Items Enum: "MOBILE_MONEY" "BANK_TRANSFER"

Payout methods available on this corridor for this partner.

send_country
string^[A-Z]{2}$
receive_country
string^[A-Z]{2}$
funding_methods
Array of strings (FundingMethod) [ items^[A-Z][A-Z0-9_]{1,31}$ ]
moja-status: planned

🔲 Planned — not emitted. How a sender may fund a payment on this corridor, in the order they should be offered. Read this rather than hardcoding a payment-method screen.

wallet_networks
Array of strings (WalletNetwork) [ items^[A-Z][A-Z0-9_]{1,31}$ ]
moja-status: planned

🔲 Planned — not emitted. Mobile-money networks reachable on this corridor, for Recipient.wallet_network. Server-supplied so that adding a network is not a client release.

Array of objects (CorridorPayoutMethod)

Per-method requirements: the beneficiary fields a payout of that method needs, and their validation. Present when the corridor catalogue carries them (CA-KE publishes the phone pattern and the mobile-money networks). Absent when it does not. Render and validate from this rather than hardcoding a form per corridor.

object (CorridorLimits)

The limits in force for this partner on this corridor, in the send currency. Values are server-supplied and are published here to be rendered, not to be evaluated. The platform is the sole authority: a client must not pre-decide a denial from these numbers, must not cache them beyond a session, and must treat the platform's rejection as the answer. Amounts are absent when no limit of that kind applies — absent means "no limit expressed here", never "no limit". Amounts are integer minor units (MinorUnitAmount), so a limit on a zero-decimal currency cannot be published with cents in it — which matters more here than almost anywhere, because a limit that is wrong by a factor of a hundred either blocks every payment or none.

fx_margin_bps
integer [ 0 .. 1000 ]
Deprecated

Deprecated — do not use. Returned only to one legacy tenant (whose older client applied it itself) and absent for every other partner: it is that tenant's commercial arrangement with Moja Switch, not a corridor price. Moja Switch never prices from it: a tenant on Moja Switch FX is priced by Moja Switch from its own reference rate and the tenant's margin for the corridor (see POST /quotes); a BYO-FX tenant owns its FX and pays no Moja margin. It will be removed once no client reads it.

Response samples

Content type
application/json
{
  • "code": "CA-TZ",
  • "send_country": "CA",
  • "receive_country": "TZ",
  • "send_currency": {
    },
  • "receive_currency": {
    },
  • "payout_methods": [
    ],
  • "funding_methods": [
    ],
  • "wallet_networks": [
    ],
  • "payout_method_details": [
    ],
  • "limits": {
    },
  • "fx_margin_bps": 200
}

End Users

End users — natural persons a tenant acts for. Every tenant with the end_users:* scopes gets this surface; there is no separate consumer API. GET /end-users/{end_user_id}/limits is planned (x-moja-status: planned).

Create end user

Registers a natural person the tenant acts for and returns their platform id. Every tenant may model end users; nothing about this resource is specific to any one product.

Scope: end_users:write.

Creation authorises nothing. A new end user starts at account_status: created, verification_status: unverified, transact_authorization: not_authorized. No money path is open until transact_authorization is authorized.

Safe to retry. external_id is your own identifier for the person and is unique within your tenant. Re-posting the same external_id with an identical body returns the existing end user with 200 instead of creating a second record; re-posting it with a different body is 409 end_user_already_exists. A create can therefore never fork one person into two records under retry.

Screened before creation. When a name is supplied, the person's identity (name, country, phone) is screened (sanctions, AML/fraud rules) before the account exists. A block or review outcome is 422 compliance_blocked and no account is created. A transport or dependency fault is 503 screening_unavailable, and nothing is created either. With no name (sign-up with phone and country only) there is nothing to screen against: the account is created unscreened and cannot be authorized to transact until a profile update supplies a name that clears screening.

Authorizations:
bearerAuth
Request Body schema: application/json
required
external_id
required
string <= 128 characters

Your own identifier for this person. Unique within your tenant; re-posting it with an identical body returns the existing end user rather than creating a second one.

country
required
string^[A-Z]{2}$

ISO 3166-1 alpha-2 country of residence.

first_name
string <= 100 characters
last_name
string <= 100 characters
date_of_birth
string <date>
phone
string

E.164.

email
string <email>
object <= 20 properties

Responses

Response Schema: application/json
id
required
string^eu_[a-zA-Z0-9]+$
external_id
required
string <= 128 characters

The tenant's own identifier for this person. Unique within the tenant, and the key that makes POST /end-users safe to retry.

account_status
required
string (AccountStatus)
Enum: "created" "suspended" "closed"

Lifecycle of the end-user record itself. Independent of verification and of authorization to transact — see EndUser for why the three are separate. Treat an unrecognised value as not permitting money movement.

verification_status
required
string (VerificationStatus)
Enum: "unverified" "pending" "verified" "failed"

The outcome of identity verification, and only that. It records what was checked and how it resolved. It is never edited to express an access decision: to stop an end user transacting, revoke transact_authorization. Treat an unrecognised value as unverified.

transact_authorization
required
string (TransactAuthorization)
Enum: "authorized" "not_authorized" "revoked"

Whether this end user may currently move money. This is the field a send affordance renders from, and the only one. Independently revocable at any time by the platform, without any change to verification_status. not_authorized means never granted (or prerequisites outstanding); revoked means withdrawn after having been granted. Neither value carries a reason — the platform does not disclose why a compliance decision was taken. Treat an unrecognised value as not authorized. Revocation is not a one-way door: authorization is restored through the same POST /end-users/{end_user_id}/state-transitions route, with its own recorded reason and actor, and without touching verification_status in either direction.

created_at
required
string <date-time>
updated_at
required
string <date-time>
first_name
string <= 100 characters
last_name
string <= 100 characters
date_of_birth
string <date>

ISO 8601 date. Used by verification and screening.

country
string^[A-Z]{2}$

ISO 3166-1 alpha-2 country of residence.

phone
string

E.164. The person's own contact number, not a destination.

email
string <email>
account_status_updated_at
string <date-time>

When account_status last moved. The reason and actor behind the move are on the audit record and are deliberately not returned here.

verification_tier
string <= 64 characters

The tier reached, as a server-defined opaque identifier. Present only once a verification has resolved. It is not a limit, and it does not map to one on the client: limits come from GET /corridors/{corridor_code} and are enforced by the platform.

transact_authorization_updated_at
string <date-time>

When transact_authorization last moved, in either direction. The reason and actor are on the audit record and are deliberately not returned — a revocation reason is a compliance detail, and this field is read by clients that render to end users.

object <= 20 properties

Tenant key/value metadata, echoed back. Never place credentials, document numbers or card data here.

Response Schema: application/json
id
required
string^eu_[a-zA-Z0-9]+$
external_id
required
string <= 128 characters

The tenant's own identifier for this person. Unique within the tenant, and the key that makes POST /end-users safe to retry.

account_status
required
string (AccountStatus)
Enum: "created" "suspended" "closed"

Lifecycle of the end-user record itself. Independent of verification and of authorization to transact — see EndUser for why the three are separate. Treat an unrecognised value as not permitting money movement.

verification_status
required
string (VerificationStatus)
Enum: "unverified" "pending" "verified" "failed"

The outcome of identity verification, and only that. It records what was checked and how it resolved. It is never edited to express an access decision: to stop an end user transacting, revoke transact_authorization. Treat an unrecognised value as unverified.

transact_authorization
required
string (TransactAuthorization)
Enum: "authorized" "not_authorized" "revoked"

Whether this end user may currently move money. This is the field a send affordance renders from, and the only one. Independently revocable at any time by the platform, without any change to verification_status. not_authorized means never granted (or prerequisites outstanding); revoked means withdrawn after having been granted. Neither value carries a reason — the platform does not disclose why a compliance decision was taken. Treat an unrecognised value as not authorized. Revocation is not a one-way door: authorization is restored through the same POST /end-users/{end_user_id}/state-transitions route, with its own recorded reason and actor, and without touching verification_status in either direction.

created_at
required
string <date-time>
updated_at
required
string <date-time>
first_name
string <= 100 characters
last_name
string <= 100 characters
date_of_birth
string <date>

ISO 8601 date. Used by verification and screening.

country
string^[A-Z]{2}$

ISO 3166-1 alpha-2 country of residence.

phone
string

E.164. The person's own contact number, not a destination.

email
string <email>
account_status_updated_at
string <date-time>

When account_status last moved. The reason and actor behind the move are on the audit record and are deliberately not returned here.

verification_tier
string <= 64 characters

The tier reached, as a server-defined opaque identifier. Present only once a verification has resolved. It is not a limit, and it does not map to one on the client: limits come from GET /corridors/{corridor_code} and are enforced by the platform.

transact_authorization_updated_at
string <date-time>

When transact_authorization last moved, in either direction. The reason and actor are on the audit record and are deliberately not returned — a revocation reason is a compliance detail, and this field is read by clients that render to end users.

object <= 20 properties

Tenant key/value metadata, echoed back. Never place credentials, document numbers or card data here.

Request samples

Content type
application/json
{
  • "external_id": "tenant-user-000123",
  • "first_name": "Test",
  • "last_name": "Sender",
  • "date_of_birth": "1990-01-01",
  • "country": "CA",
  • "phone": "+14165550000",
  • "email": "test.sender@example.com",
  • "metadata": {
    }
}

Response samples

Content type
application/json
{
  • "id": "eu_7Qm4xK2pLd",
  • "external_id": "tenant-user-000123",
  • "first_name": "Test",
  • "last_name": "Sender",
  • "date_of_birth": "1990-01-01",
  • "country": "CA",
  • "phone": "+14165550000",
  • "email": "test.sender@example.com",
  • "account_status": "created",
  • "account_status_updated_at": "2019-08-24T14:15:22Z",
  • "verification_status": "unverified",
  • "verification_tier": "tier_1",
  • "transact_authorization": "authorized",
  • "transact_authorization_updated_at": "2019-08-24T14:15:22Z",
  • "metadata": {
    },
  • "created_at": "2019-08-24T14:15:22Z",
  • "updated_at": "2019-08-24T14:15:22Z"
}

Get end user

Returns the profile and the three independent state fields.

Scope: end_users:read.

This is the authoritative read for what an end user may do. A client that cached an earlier state, or that took a verification vendor's word for an outcome, is holding UI state — not authorization.

Authorizations:
bearerAuth
path Parameters
end_user_id
required
string^eu_[a-zA-Z0-9]+$
Example: eu_7Qm4xK2pLd

Responses

Response Schema: application/json
id
required
string^eu_[a-zA-Z0-9]+$
external_id
required
string <= 128 characters

The tenant's own identifier for this person. Unique within the tenant, and the key that makes POST /end-users safe to retry.

account_status
required
string (AccountStatus)
Enum: "created" "suspended" "closed"

Lifecycle of the end-user record itself. Independent of verification and of authorization to transact — see EndUser for why the three are separate. Treat an unrecognised value as not permitting money movement.

verification_status
required
string (VerificationStatus)
Enum: "unverified" "pending" "verified" "failed"

The outcome of identity verification, and only that. It records what was checked and how it resolved. It is never edited to express an access decision: to stop an end user transacting, revoke transact_authorization. Treat an unrecognised value as unverified.

transact_authorization
required
string (TransactAuthorization)
Enum: "authorized" "not_authorized" "revoked"

Whether this end user may currently move money. This is the field a send affordance renders from, and the only one. Independently revocable at any time by the platform, without any change to verification_status. not_authorized means never granted (or prerequisites outstanding); revoked means withdrawn after having been granted. Neither value carries a reason — the platform does not disclose why a compliance decision was taken. Treat an unrecognised value as not authorized. Revocation is not a one-way door: authorization is restored through the same POST /end-users/{end_user_id}/state-transitions route, with its own recorded reason and actor, and without touching verification_status in either direction.

created_at
required
string <date-time>
updated_at
required
string <date-time>
first_name
string <= 100 characters
last_name
string <= 100 characters
date_of_birth
string <date>

ISO 8601 date. Used by verification and screening.

country
string^[A-Z]{2}$

ISO 3166-1 alpha-2 country of residence.

phone
string

E.164. The person's own contact number, not a destination.

email
string <email>
account_status_updated_at
string <date-time>

When account_status last moved. The reason and actor behind the move are on the audit record and are deliberately not returned here.

verification_tier
string <= 64 characters

The tier reached, as a server-defined opaque identifier. Present only once a verification has resolved. It is not a limit, and it does not map to one on the client: limits come from GET /corridors/{corridor_code} and are enforced by the platform.

transact_authorization_updated_at
string <date-time>

When transact_authorization last moved, in either direction. The reason and actor are on the audit record and are deliberately not returned — a revocation reason is a compliance detail, and this field is read by clients that render to end users.

object <= 20 properties

Tenant key/value metadata, echoed back. Never place credentials, document numbers or card data here.

Response samples

Content type
application/json
{
  • "id": "eu_7Qm4xK2pLd",
  • "external_id": "tenant-user-000123",
  • "first_name": "Test",
  • "last_name": "Sender",
  • "date_of_birth": "1990-01-01",
  • "country": "CA",
  • "phone": "+14165550000",
  • "email": "test.sender@example.com",
  • "account_status": "created",
  • "account_status_updated_at": "2019-08-24T14:15:22Z",
  • "verification_status": "unverified",
  • "verification_tier": "tier_1",
  • "transact_authorization": "authorized",
  • "transact_authorization_updated_at": "2019-08-24T14:15:22Z",
  • "metadata": {
    },
  • "created_at": "2019-08-24T14:15:22Z",
  • "updated_at": "2019-08-24T14:15:22Z"
}

Update end user profile

Updates profile attributes. Omitted fields are left unchanged; an explicit null is rejected with 422 validation_error rather than silently clearing a field.

Scope: end_users:write.

The three state fields are not writable here. account_status, verification_status and transact_authorization are outcomes, not inputs: they move only through POST /end-users/{end_user_id}/suspend and through verification and compliance decisions. Sending one returns 422 immutable_field naming the field. A partner cannot promote its own end user to verified, which is the point.

Changing a name or date of birth on a verified end user does not invalidate the verification record (that record is evidence of what was checked and when); it may trigger re-screening, which surfaces as a change to transact_authorization, never as an edit to verification_status.

Screened before it is written. A change to the first name, last name or phone is screened before it is saved, exactly as at creation: 422 compliance_blocked or 503 screening_unavailable leave the end user unchanged. A clearing result on a verified end user that is not yet authorized opens transact_authorization. A change that cannot be screened (no name) is saved but moves an authorized end user back to not_authorized until a named profile clears.

Authorizations:
bearerAuth
path Parameters
end_user_id
required
string^eu_[a-zA-Z0-9]+$
Example: eu_7Qm4xK2pLd
Request Body schema: application/json
required
non-empty
first_name
string <= 100 characters
last_name
string <= 100 characters
date_of_birth
string <date>
phone
string

E.164.

email
string <email>
object <= 20 properties

Responses

Response Schema: application/json
id
required
string^eu_[a-zA-Z0-9]+$
external_id
required
string <= 128 characters

The tenant's own identifier for this person. Unique within the tenant, and the key that makes POST /end-users safe to retry.

account_status
required
string (AccountStatus)
Enum: "created" "suspended" "closed"

Lifecycle of the end-user record itself. Independent of verification and of authorization to transact — see EndUser for why the three are separate. Treat an unrecognised value as not permitting money movement.

verification_status
required
string (VerificationStatus)
Enum: "unverified" "pending" "verified" "failed"

The outcome of identity verification, and only that. It records what was checked and how it resolved. It is never edited to express an access decision: to stop an end user transacting, revoke transact_authorization. Treat an unrecognised value as unverified.

transact_authorization
required
string (TransactAuthorization)
Enum: "authorized" "not_authorized" "revoked"

Whether this end user may currently move money. This is the field a send affordance renders from, and the only one. Independently revocable at any time by the platform, without any change to verification_status. not_authorized means never granted (or prerequisites outstanding); revoked means withdrawn after having been granted. Neither value carries a reason — the platform does not disclose why a compliance decision was taken. Treat an unrecognised value as not authorized. Revocation is not a one-way door: authorization is restored through the same POST /end-users/{end_user_id}/state-transitions route, with its own recorded reason and actor, and without touching verification_status in either direction.

created_at
required
string <date-time>
updated_at
required
string <date-time>
first_name
string <= 100 characters
last_name
string <= 100 characters
date_of_birth
string <date>

ISO 8601 date. Used by verification and screening.

country
string^[A-Z]{2}$

ISO 3166-1 alpha-2 country of residence.

phone
string

E.164. The person's own contact number, not a destination.

email
string <email>
account_status_updated_at
string <date-time>

When account_status last moved. The reason and actor behind the move are on the audit record and are deliberately not returned here.

verification_tier
string <= 64 characters

The tier reached, as a server-defined opaque identifier. Present only once a verification has resolved. It is not a limit, and it does not map to one on the client: limits come from GET /corridors/{corridor_code} and are enforced by the platform.

transact_authorization_updated_at
string <date-time>

When transact_authorization last moved, in either direction. The reason and actor are on the audit record and are deliberately not returned — a revocation reason is a compliance detail, and this field is read by clients that render to end users.

object <= 20 properties

Tenant key/value metadata, echoed back. Never place credentials, document numbers or card data here.

Request samples

Content type
application/json
{
  • "first_name": "string",
  • "last_name": "string",
  • "date_of_birth": "2019-08-24",
  • "phone": "string",
  • "email": "user@example.com",
  • "metadata": {
    }
}

Response samples

Content type
application/json
{
  • "id": "eu_7Qm4xK2pLd",
  • "external_id": "tenant-user-000123",
  • "first_name": "Test",
  • "last_name": "Sender",
  • "date_of_birth": "1990-01-01",
  • "country": "CA",
  • "phone": "+14165550000",
  • "email": "test.sender@example.com",
  • "account_status": "created",
  • "account_status_updated_at": "2019-08-24T14:15:22Z",
  • "verification_status": "unverified",
  • "verification_tier": "tier_1",
  • "transact_authorization": "authorized",
  • "transact_authorization_updated_at": "2019-08-24T14:15:22Z",
  • "metadata": {
    },
  • "created_at": "2019-08-24T14:15:22Z",
  • "updated_at": "2019-08-24T14:15:22Z"
}

Suspend an end user and revoke their authorization to transact

The named shorthand for the one transition partners reach for most: equivalent to POST /end-users/{end_user_id}/state-transitions with account_status: suspended, and transact_authorization: revoked only where authorization is currently authorized. There is one state machine, not two — this route is a convenience over it, and the same audit record is written. Use the state-transition route for anything else, including restoring access.

Why authorization is not revoked unconditionally. revoked means withdrawn after having been granted. Writing it for an end user who was never authorized would record a withdrawal of something never held — a false entry on a retained audit record, and it would destroy the distinction between not_authorized and revoked that makes the field worth reading. A not_authorized end user is therefore left not_authorized by this route; only account_status moves.

Scope: end_users:write.

verification_status is deliberately left untouched. Revoking access and falsifying an identity record are different acts, and only one of them is legal. The verification record is the evidence a regulator asks for — who was checked, against what, when, with what outcome — so it is never rewritten to express a present-day access decision. That is what the separate, independently revocable transact_authorization field exists for.

reason and actor are required: a suspension with no recorded cause and no recorded initiator is not an audit trail.

Idempotent: suspending an already-suspended end user returns 200 with the same state, not a conflict. In-flight payments are unaffected by this call; the block applies to new payment creation.

Authorizations:
bearerAuth
path Parameters
end_user_id
required
string^eu_[a-zA-Z0-9]+$
Example: eu_7Qm4xK2pLd
Request Body schema: application/json
required
reason
required
string [ 1 .. 500 ] characters

Why this is being done, for the audit record. Recorded on the platform and never returned to any client or shown to an end user, so do not write it as end-user-facing copy — and do not put credentials, document numbers or case detail in it.

required
object (Actor)

Who initiated a state change, recorded on the audit record so that a transition can be attributed months later. Platform-initiated transitions are recorded with a platform actor that a partner cannot supply, so a partner can never attribute its own action to us.

Responses

Response Schema: application/json
id
required
string^eu_[a-zA-Z0-9]+$
external_id
required
string <= 128 characters

The tenant's own identifier for this person. Unique within the tenant, and the key that makes POST /end-users safe to retry.

account_status
required
string (AccountStatus)
Enum: "created" "suspended" "closed"

Lifecycle of the end-user record itself. Independent of verification and of authorization to transact — see EndUser for why the three are separate. Treat an unrecognised value as not permitting money movement.

verification_status
required
string (VerificationStatus)
Enum: "unverified" "pending" "verified" "failed"

The outcome of identity verification, and only that. It records what was checked and how it resolved. It is never edited to express an access decision: to stop an end user transacting, revoke transact_authorization. Treat an unrecognised value as unverified.

transact_authorization
required
string (TransactAuthorization)
Enum: "authorized" "not_authorized" "revoked"

Whether this end user may currently move money. This is the field a send affordance renders from, and the only one. Independently revocable at any time by the platform, without any change to verification_status. not_authorized means never granted (or prerequisites outstanding); revoked means withdrawn after having been granted. Neither value carries a reason — the platform does not disclose why a compliance decision was taken. Treat an unrecognised value as not authorized. Revocation is not a one-way door: authorization is restored through the same POST /end-users/{end_user_id}/state-transitions route, with its own recorded reason and actor, and without touching verification_status in either direction.

created_at
required
string <date-time>
updated_at
required
string <date-time>
first_name
string <= 100 characters
last_name
string <= 100 characters
date_of_birth
string <date>

ISO 8601 date. Used by verification and screening.

country
string^[A-Z]{2}$

ISO 3166-1 alpha-2 country of residence.

phone
string

E.164. The person's own contact number, not a destination.

email
string <email>
account_status_updated_at
string <date-time>

When account_status last moved. The reason and actor behind the move are on the audit record and are deliberately not returned here.

verification_tier
string <= 64 characters

The tier reached, as a server-defined opaque identifier. Present only once a verification has resolved. It is not a limit, and it does not map to one on the client: limits come from GET /corridors/{corridor_code} and are enforced by the platform.

transact_authorization_updated_at
string <date-time>

When transact_authorization last moved, in either direction. The reason and actor are on the audit record and are deliberately not returned — a revocation reason is a compliance detail, and this field is read by clients that render to end users.

object <= 20 properties

Tenant key/value metadata, echoed back. Never place credentials, document numbers or card data here.

Request samples

Content type
application/json
{
  • "reason": "string",
  • "actor": {
    }
}

Response samples

Content type
application/json
{
  • "id": "eu_7Qm4xK2pLd",
  • "external_id": "tenant-user-000123",
  • "first_name": "Test",
  • "last_name": "Sender",
  • "date_of_birth": "1990-01-01",
  • "country": "CA",
  • "phone": "+14165550000",
  • "email": "test.sender@example.com",
  • "account_status": "created",
  • "account_status_updated_at": "2019-08-24T14:15:22Z",
  • "verification_status": "unverified",
  • "verification_tier": "tier_1",
  • "transact_authorization": "authorized",
  • "transact_authorization_updated_at": "2019-08-24T14:15:22Z",
  • "metadata": {
    },
  • "created_at": "2019-08-24T14:15:22Z",
  • "updated_at": "2019-08-24T14:15:22Z"
}

Submit the end user's customer due diligence (CDD) profile

Records the customer due diligence information the AML/ATF program requires before a consumer may send: residential address, occupation, expected activity (purpose, monthly frequency and value, destination countries), source of funds and a PEP/HIO self-declaration.

Scope: end_users:write.

Versioned, never edited. Each call records a new version; the newest is the current profile. Earlier versions are retained as the record of what was collected and when. Re-submit whenever the customer's information changes.

Accepted codes are compliance configuration. occupation_category, expected_activity.purpose, monthly_count, monthly_value, source_of_funds and pep_hio_declaration take lower-case codes from the platform's compliance configuration. A code it does not accept is 422 validation_error on that field. The codes in force at launch are listed on each property.

Required before sending. POST /payments naming an end user with no profile — or with a code compliance has since retired — is 422 end_user_cdd_required. The platform rates the customer from this profile; the rating is an internal compliance record and is not returned. A customer the platform cannot serve without further review is refused at payment creation with 422 end_user_review_required, and a payment above a limit that applies to the customer with 422 end_user_limit_exceeded; neither discloses a rating or a limit. Customer-facing wording for these refusals must not suggest a rating or an investigation.

Authorizations:
bearerAuth
path Parameters
end_user_id
required
string^eu_[a-zA-Z0-9]+$
Example: eu_7Qm4xK2pLd
Request Body schema: application/json
required
required
object (CddAddress)

The customer's residential address. Personal information.

occupation
required
string [ 1 .. 100 ] characters

The customer's occupation in their own words (e.g. "Registered nurse"). Personal information.

occupation_category
required
string^[a-z0-9_]{1,64}$

At launch: employed, self_employed, business_owner, retired, student, homemaker, unemployed, cash_intensive_business, money_services, other.

required
object (CddExpectedActivity)

What the customer expects to do. Actual activity is compared against it.

source_of_funds
required
string^[a-z0-9_]{1,64}$

Where the money sent comes from. At launch: employment_income, pension_or_benefits, savings, business_income, investments, sale_of_assets, inheritance, gift, loan, other.

pep_hio_declaration
required
string^[a-z0-9_]{1,64}$

The customer's politically exposed person / head of international organization self-declaration. At launch: none, domestic_pep, foreign_pep, hio, family_or_close_associate. Screening is separate and is not replaced by this declaration.

required
object (Actor)

Who initiated a state change, recorded on the audit record so that a transition can be attributed months later. Platform-initiated transitions are recorded with a platform actor that a partner cannot supply, so a partner can never attribute its own action to us.

Responses

Response Schema: application/json
end_user_id
required
string
required
object (CddAddress)

The customer's residential address. Personal information.

occupation
required
string
occupation_category
required
string
required
object (CddExpectedActivity)

What the customer expects to do. Actual activity is compared against it.

source_of_funds
required
string
pep_hio_declaration
required
string
submitted_at
required
string <date-time>

Request samples

Content type
application/json
{
  • "residential_address": {
    },
  • "occupation": "string",
  • "occupation_category": "string",
  • "expected_activity": {
    },
  • "source_of_funds": "string",
  • "pep_hio_declaration": "string",
  • "actor": {
    }
}

Response samples

Content type
application/json
{
  • "end_user_id": "string",
  • "residential_address": {
    },
  • "occupation": "string",
  • "occupation_category": "string",
  • "expected_activity": {
    },
  • "source_of_funds": "string",
  • "pep_hio_declaration": "string",
  • "submitted_at": "2019-08-24T14:15:22Z"
}

Read the end user's current CDD profile

The newest submitted profile. 404 cdd_profile_not_found when none has been submitted; 404 end_user_not_found for an id that is not one of your end users.

Scope: end_users:read.

Authorizations:
bearerAuth
path Parameters
end_user_id
required
string^eu_[a-zA-Z0-9]+$
Example: eu_7Qm4xK2pLd

Responses

Response Schema: application/json
end_user_id
required
string
required
object (CddAddress)

The customer's residential address. Personal information.

occupation
required
string
occupation_category
required
string
required
object (CddExpectedActivity)

What the customer expects to do. Actual activity is compared against it.

source_of_funds
required
string
pep_hio_declaration
required
string
submitted_at
required
string <date-time>

Response samples

Content type
application/json
{
  • "end_user_id": "string",
  • "residential_address": {
    },
  • "occupation": "string",
  • "occupation_category": "string",
  • "expected_activity": {
    },
  • "source_of_funds": "string",
  • "pep_hio_declaration": "string",
  • "submitted_at": "2019-08-24T14:15:22Z"
}

Move an end user's account status or authorization to transact

The single, bidirectional operation over the two settable states. Set account_status, transact_authorization, or both in one atomic transition; either can move in either direction, so there is no one-way door and no separate "unsuspend" verb to keep in sync.

Scope: end_users:write.

verification_status is not settable here, in either direction. Restoring authorization does not and cannot mark anyone verified, just as revoking it does not unmark them: that field records what was checked, and it is evidence, not a switch. Sending it returns 422 immutable_field. Restoring transact_authorization: authorized may still be refused when the prerequisites are not met — 422 verification_required, or 422 identity_screening_required when the end user's current name and phone have not cleared screening — and a refusal leaves every state exactly as it was.

The trail runs both ways. reason and actor are required on every transition, restoration included. A revocation with a recorded cause and a restoration without one is a hole in the trail precisely where the interesting question is asked — who let them back on, and on what basis. Neither value is ever returned to a client or shown to an end user; they are written to the audit record.

Permitted moves. account_status: created ↔ suspended. closed is terminal — there is no transition out of it, and attempting one is 409 invalid_state_transition. transact_authorization: any of authorized, not_authorized, revoked to any other. A transition to the state an end user is already in is idempotent and returns 200.

Authorizations:
bearerAuth
path Parameters
end_user_id
required
string^eu_[a-zA-Z0-9]+$
Example: eu_7Qm4xK2pLd
Request Body schema: application/json
required
Any of
account_status
required
string
Enum: "created" "suspended" "closed"

Target account status. created ↔ suspended in both directions; closed is terminal and cannot be left (409 invalid_state_transition).

reason
required
string [ 1 .. 500 ] characters

Why, for the audit record — required on restorations as well as revocations. Never returned to any client and never shown to an end user.

required
object (Actor)

Who initiated a state change, recorded on the audit record so that a transition can be attributed months later. Platform-initiated transitions are recorded with a platform actor that a partner cannot supply, so a partner can never attribute its own action to us.

transact_authorization
string
Enum: "authorized" "not_authorized" "revoked"

Target authorization. Movable in both directions at any time. Moving it to authorized never alters verification_status and may be refused when prerequisites are unmet (422 verification_required, 422 identity_screening_required), in which case nothing changes.

Responses

Response Schema: application/json
id
required
string^eu_[a-zA-Z0-9]+$
external_id
required
string <= 128 characters

The tenant's own identifier for this person. Unique within the tenant, and the key that makes POST /end-users safe to retry.

account_status
required
string (AccountStatus)
Enum: "created" "suspended" "closed"

Lifecycle of the end-user record itself. Independent of verification and of authorization to transact — see EndUser for why the three are separate. Treat an unrecognised value as not permitting money movement.

verification_status
required
string (VerificationStatus)
Enum: "unverified" "pending" "verified" "failed"

The outcome of identity verification, and only that. It records what was checked and how it resolved. It is never edited to express an access decision: to stop an end user transacting, revoke transact_authorization. Treat an unrecognised value as unverified.

transact_authorization
required
string (TransactAuthorization)
Enum: "authorized" "not_authorized" "revoked"

Whether this end user may currently move money. This is the field a send affordance renders from, and the only one. Independently revocable at any time by the platform, without any change to verification_status. not_authorized means never granted (or prerequisites outstanding); revoked means withdrawn after having been granted. Neither value carries a reason — the platform does not disclose why a compliance decision was taken. Treat an unrecognised value as not authorized. Revocation is not a one-way door: authorization is restored through the same POST /end-users/{end_user_id}/state-transitions route, with its own recorded reason and actor, and without touching verification_status in either direction.

created_at
required
string <date-time>
updated_at
required
string <date-time>
first_name
string <= 100 characters
last_name
string <= 100 characters
date_of_birth
string <date>

ISO 8601 date. Used by verification and screening.

country
string^[A-Z]{2}$

ISO 3166-1 alpha-2 country of residence.

phone
string

E.164. The person's own contact number, not a destination.

email
string <email>
account_status_updated_at
string <date-time>

When account_status last moved. The reason and actor behind the move are on the audit record and are deliberately not returned here.

verification_tier
string <= 64 characters

The tier reached, as a server-defined opaque identifier. Present only once a verification has resolved. It is not a limit, and it does not map to one on the client: limits come from GET /corridors/{corridor_code} and are enforced by the platform.

transact_authorization_updated_at
string <date-time>

When transact_authorization last moved, in either direction. The reason and actor are on the audit record and are deliberately not returned — a revocation reason is a compliance detail, and this field is read by clients that render to end users.

object <= 20 properties

Tenant key/value metadata, echoed back. Never place credentials, document numbers or card data here.

Request samples

Content type
application/json
Example
{
  • "account_status": "created",
  • "transact_authorization": "authorized",
  • "reason": "string",
  • "actor": {
    }
}

Response samples

Content type
application/json
{
  • "id": "eu_7Qm4xK2pLd",
  • "external_id": "tenant-user-000123",
  • "first_name": "Test",
  • "last_name": "Sender",
  • "date_of_birth": "1990-01-01",
  • "country": "CA",
  • "phone": "+14165550000",
  • "email": "test.sender@example.com",
  • "account_status": "created",
  • "account_status_updated_at": "2019-08-24T14:15:22Z",
  • "verification_status": "unverified",
  • "verification_tier": "tier_1",
  • "transact_authorization": "authorized",
  • "transact_authorization_updated_at": "2019-08-24T14:15:22Z",
  • "metadata": {
    },
  • "created_at": "2019-08-24T14:15:22Z",
  • "updated_at": "2019-08-24T14:15:22Z"
}

[Planned] Get this end user's remaining allowance

🔲 Contract published, not yet implemented.

Returns what this end user may still send on a corridor — not the corridor's maxima, which is what GET /corridors/{corridor_code} returns and is a different question. Without this, a client cannot say "you can send $120 more today", and cannot explain a limit denial other than by restating it.

Scope: end_users:read.

Read it; do not reimplement it. The remaining figure depends on aggregation the client cannot see — a 24-hour window across every payment, keyed on the end user, including payments in flight. A client that subtracts its own visible history will be wrong, and it will be wrong in the permissive direction. The platform remains the sole authority: a client renders these numbers and a denial, and never pre-decides one. Treat the response as point-in-time and re-read it before a send rather than caching it.

next_tier names the tier that would raise these limits and points at the verification requirements to reach it, so a denial can tell a customer what to do next instead of only what they cannot do. It carries no limit values for that tier — those are a compliance determination and are returned only once the tier is actually reached.

moja-status: planned
Authorizations:
bearerAuth
path Parameters
end_user_id
required
string^eu_[a-zA-Z0-9]+$
Example: eu_7Qm4xK2pLd
query Parameters
corridor
string (CorridorCode) ^[A-Z]{2}-[A-Z]{2}$
Example: corridor=CA-TZ

Limit the answer to one corridor. Omit to get every corridor this end user's tenant is entitled to.

Responses

Response Schema: application/json
as_of
required
string <date-time>

When these figures were computed. They move as the end user transacts and as rolling windows age out, so read them immediately before a send rather than caching them.

required
Array of objects (EndUserCorridorLimits)

One entry per corridor this end user's tenant is entitled to.

object (NextTier)

The tier that would raise these limits, and how to reach it — so a denial can tell a customer what to do next rather than only what they cannot do. It carries no limit values. Those are a compliance determination and are returned only once the tier is actually reached; publishing them early would let a client present an allowance nobody has approved.

Response samples

Content type
application/json
{
  • "as_of": "2019-08-24T14:15:22Z",
  • "corridors": [
    ],
  • "next_tier": {
    }
}

Verification

Identity verification of an end user — session handoff, status, tier, the backend-authored requirement list, and the tenant's own verification decision.

Start an identity verification

Opens a verification and returns a server-issued, single-use, expiring handoff the client can hand to the verification flow.

Scope: verifications:write.

The client never holds a vendor credential. handoff.reference (and handoff.url, when the flow is hosted) are single-use and expire at handoff.expires_at. They are capabilities, not identifiers: do not log them, do not persist them, do not put them in a crash report or an analytics event, and do not embed them in a durable deep link.

Anything the verification flow hands back to the client is untrusted UI state. It is fine to use it to move a screen along. It is never authority. Re-read GET /end-users/{end_user_id}/verifications/{verification_id} for the decision and tier, and GET /end-users/{end_user_id} for whether the end user may now transact. Those two reads are the only authoritative answers.

Starting a second verification while one is still open returns 409 verification_in_progress with the open verification_id.

Authorizations:
bearerAuth
path Parameters
end_user_id
required
string^eu_[a-zA-Z0-9]+$
Example: eu_7Qm4xK2pLd
Request Body schema: application/json
requested_tier
string <= 64 characters

The tier you are asking for, as returned by GET /end-users/{end_user_id}/verification-requirements. Advisory: the platform decides what is required and what is granted. Omit to let the platform choose the next tier.

object <= 20 properties

Responses

Response Schema: application/json
id
required
string^ver_[a-zA-Z0-9]+$
end_user_id
required
string^eu_[a-zA-Z0-9]+$
status
required
string (VerificationSessionStatus)
Enum: "created" "in_progress" "completed" "expired" "cancelled"

Lifecycle of one verification attempt. Distinct from decision, which is the outcome: a session can be completed with a declined decision. Treat an unrecognised value as not yet resolved.

created_at
required
string <date-time>
decision
string
Enum: "approved" "declined" "review"

Present once the session resolves. A declined decision carries no reason: the platform does not return the rule or signal behind it.

tier
string <= 64 characters

Tier reached, as a server-defined opaque identifier. Never map it to a limit on the client.

object

The single-use, expiring handoff into the verification flow. Returned at creation only. It is a capability, not an identifier: never log it, persist it, put it in a crash report or analytics event, or embed it in a durable deep link. The client holds no vendor credential at any point, and anything the flow hands back to the client is untrusted UI state — the authoritative result is this resource plus GET /end-users/{end_user_id}.

updated_at
string <date-time>
completed_at
string <date-time>

Request samples

Content type
application/json
{
  • "requested_tier": "tier_1",
  • "metadata": {
    }
}

Response samples

Content type
application/json
{
  • "id": "ver_3Hb8tR1sVw",
  • "end_user_id": "eu_7Qm4xK2pLd",
  • "status": "created",
  • "decision": "approved",
  • "tier": "tier_1",
  • "handoff": {},
  • "created_at": "2019-08-24T14:15:22Z",
  • "updated_at": "2019-08-24T14:15:22Z",
  • "completed_at": "2019-08-24T14:15:22Z"
}

Get verification status

Returns the authoritative status, decision and tier for one verification. Poll this (or wait for your own notification) rather than trusting anything the verification flow returned to the device.

Scope: verifications:write.

A declined decision carries no reason beyond the decision itself. The platform does not return the rule, the vendor signal or the list that produced it — naming it would tell a bad actor exactly what to change. Where a re-attempt is possible, GET /end-users/{end_user_id}/verification-requirements says what is still outstanding.

Authorizations:
bearerAuth
path Parameters
end_user_id
required
string^eu_[a-zA-Z0-9]+$
Example: eu_7Qm4xK2pLd
verification_id
required
string^ver_[a-zA-Z0-9]+$
Example: ver_3Hb8tR1sVw

Responses

Response Schema: application/json
id
required
string^ver_[a-zA-Z0-9]+$
end_user_id
required
string^eu_[a-zA-Z0-9]+$
status
required
string (VerificationSessionStatus)
Enum: "created" "in_progress" "completed" "expired" "cancelled"

Lifecycle of one verification attempt. Distinct from decision, which is the outcome: a session can be completed with a declined decision. Treat an unrecognised value as not yet resolved.

created_at
required
string <date-time>
decision
string
Enum: "approved" "declined" "review"

Present once the session resolves. A declined decision carries no reason: the platform does not return the rule or signal behind it.

tier
string <= 64 characters

Tier reached, as a server-defined opaque identifier. Never map it to a limit on the client.

object

The single-use, expiring handoff into the verification flow. Returned at creation only. It is a capability, not an identifier: never log it, persist it, put it in a crash report or analytics event, or embed it in a durable deep link. The client holds no vendor credential at any point, and anything the flow hands back to the client is untrusted UI state — the authoritative result is this resource plus GET /end-users/{end_user_id}.

updated_at
string <date-time>
completed_at
string <date-time>

Response samples

Content type
application/json
{
  • "id": "ver_3Hb8tR1sVw",
  • "end_user_id": "eu_7Qm4xK2pLd",
  • "status": "created",
  • "decision": "approved",
  • "tier": "tier_1",
  • "handoff": {},
  • "created_at": "2019-08-24T14:15:22Z",
  • "updated_at": "2019-08-24T14:15:22Z",
  • "completed_at": "2019-08-24T14:15:22Z"
}

Record the tenant's own verification decision

For a tenant that runs its own identity check: records the outcome against the end user, with an audit-trail entry whose actor is your partner identity and whose reason carries decision and reference. Returns the updated end user.

Scope: verifications:write.

  • approved — sets verification_status: verified and the tier reached, and, when the end user is account_status: created and transact_authorization: not_authorized, authorizes it to transact. This is what opens the send gate on POST /payments.
  • declined — sets verification_status: failed, unless already verified (a decline never revokes a verified end user; use POST /end-users/{end_user_id}/state-transitions).
  • review — sets verification_status: pending, unless already verified.
  • expired — moves pending back to unverified; otherwise no change.

Repeating a decision is idempotent. decision outside the four values, or an empty reference, is 422 validation_error.

Authorizations:
bearerAuth
path Parameters
end_user_id
required
string^eu_[a-zA-Z0-9]+$
Example: eu_7Qm4xK2pLd
Request Body schema: application/json
required
decision
required
string
Enum: "approved" "declined" "review" "expired"
reference
required
string non-empty

Your reference for the check (your case or inquiry id), recorded in the audit trail.

Responses

Response Schema: application/json
id
required
string^eu_[a-zA-Z0-9]+$
external_id
required
string <= 128 characters

The tenant's own identifier for this person. Unique within the tenant, and the key that makes POST /end-users safe to retry.

account_status
required
string (AccountStatus)
Enum: "created" "suspended" "closed"

Lifecycle of the end-user record itself. Independent of verification and of authorization to transact — see EndUser for why the three are separate. Treat an unrecognised value as not permitting money movement.

verification_status
required
string (VerificationStatus)
Enum: "unverified" "pending" "verified" "failed"

The outcome of identity verification, and only that. It records what was checked and how it resolved. It is never edited to express an access decision: to stop an end user transacting, revoke transact_authorization. Treat an unrecognised value as unverified.

transact_authorization
required
string (TransactAuthorization)
Enum: "authorized" "not_authorized" "revoked"

Whether this end user may currently move money. This is the field a send affordance renders from, and the only one. Independently revocable at any time by the platform, without any change to verification_status. not_authorized means never granted (or prerequisites outstanding); revoked means withdrawn after having been granted. Neither value carries a reason — the platform does not disclose why a compliance decision was taken. Treat an unrecognised value as not authorized. Revocation is not a one-way door: authorization is restored through the same POST /end-users/{end_user_id}/state-transitions route, with its own recorded reason and actor, and without touching verification_status in either direction.

created_at
required
string <date-time>
updated_at
required
string <date-time>
first_name
string <= 100 characters
last_name
string <= 100 characters
date_of_birth
string <date>

ISO 8601 date. Used by verification and screening.

country
string^[A-Z]{2}$

ISO 3166-1 alpha-2 country of residence.

phone
string

E.164. The person's own contact number, not a destination.

email
string <email>
account_status_updated_at
string <date-time>

When account_status last moved. The reason and actor behind the move are on the audit record and are deliberately not returned here.

verification_tier
string <= 64 characters

The tier reached, as a server-defined opaque identifier. Present only once a verification has resolved. It is not a limit, and it does not map to one on the client: limits come from GET /corridors/{corridor_code} and are enforced by the platform.

transact_authorization_updated_at
string <date-time>

When transact_authorization last moved, in either direction. The reason and actor are on the audit record and are deliberately not returned — a revocation reason is a compliance detail, and this field is read by clients that render to end users.

object <= 20 properties

Tenant key/value metadata, echoed back. Never place credentials, document numbers or card data here.

Request samples

Content type
application/json
{
  • "decision": "approved",
  • "reference": "kyc_case_8812"
}

Response samples

Content type
application/json
{
  • "id": "eu_7Qm4xK2pLd",
  • "external_id": "tenant-user-000123",
  • "first_name": "Test",
  • "last_name": "Sender",
  • "date_of_birth": "1990-01-01",
  • "country": "CA",
  • "phone": "+14165550000",
  • "email": "test.sender@example.com",
  • "account_status": "created",
  • "account_status_updated_at": "2019-08-24T14:15:22Z",
  • "verification_status": "unverified",
  • "verification_tier": "tier_1",
  • "transact_authorization": "authorized",
  • "transact_authorization_updated_at": "2019-08-24T14:15:22Z",
  • "metadata": {
    },
  • "created_at": "2019-08-24T14:15:22Z",
  • "updated_at": "2019-08-24T14:15:22Z"
}

Get the outstanding verification requirements

Returns the ordered, backend-authored list of steps this end user must complete, with the per-step field validation to apply. Render the steps in array order.

Scope: verifications:write.

No tier→limit table is ever returned, and none may be held on a device. Steps carry stable keys, not display copy: the tenant owns its own wording and localisation and looks it up by key, so changing a rule here never ships a string to an app store. Eligibility is not computed on a phone — client-side validation of these fields is a courtesy to the user, and the platform re-validates everything and remains the only authority on what an end user may do.

The list is a point-in-time answer (generated_at); re-read it after every submission rather than caching it across a session.

Authorizations:
bearerAuth
path Parameters
end_user_id
required
string^eu_[a-zA-Z0-9]+$
Example: eu_7Qm4xK2pLd

Responses

Response Schema: application/json
end_user_id
required
string^eu_[a-zA-Z0-9]+$
verification_status
required
string (VerificationStatus)
Enum: "unverified" "pending" "verified" "failed"

The outcome of identity verification, and only that. It records what was checked and how it resolved. It is never edited to express an access decision: to stop an end user transacting, revoke transact_authorization. Treat an unrecognised value as unverified.

required
Array of objects (VerificationRequirementStep)

Ordered. Render in array order; the order is a platform decision, not a display preference.

generated_at
required
string <date-time>

When this answer was computed. It is point-in-time — re-read after each submission rather than caching it across a session.

current_tier
string <= 64 characters

Tier reached so far, if any. Opaque identifier.

target_tier
string <= 64 characters

The tier these steps would reach. Opaque identifier.

Response samples

Content type
application/json
{
  • "end_user_id": "eu_7Qm4xK2pLd",
  • "verification_status": "unverified",
  • "current_tier": "tier_0",
  • "target_tier": "tier_1",
  • "steps": [
    ],
  • "generated_at": "2019-08-24T14:15:22Z"
}

Recipients

An end user's saved beneficiaries. Screened at save; usable only after a screening decision returns.

List an end user's recipients

Returns the end user's saved beneficiaries, newest first.

Scope: recipients:read.

Destination MSISDNs (Recipient.phone) are returned in full on every read — the recipient is the end user's own saved contact, and the number is not masked.

Authorizations:
bearerAuth
path Parameters
end_user_id
required
string^eu_[a-zA-Z0-9]+$
Example: eu_7Qm4xK2pLd
query Parameters
status
Array of strings (RecipientStatus) <= 3 items
Items Enum: "pending_screening" "approved" "rejected" "unverified"

Return only recipients in these statuses. Repeat to match several.

Responses

Response Schema: application/json
required
Array of objects (Recipient)

Recipients, newest first.

Response samples

Content type
application/json
{
  • "data": [
    ]
}

Save a recipient

Saves a beneficiary to the end user's book and submits it for screening.

Scope: recipients:write.

The beneficiary is screened at save, before it is stored. Screening happens at save, not at send, so a user is not told at the moment of payment that their recipient is unusable. The recipient is stored once, with its decision: approved, or rejected (no explanation is given). A recipient is never usable before a decision: if the screening dependency does not answer, nothing is saved and the response is 503 screening_unavailable — retry the save. Treat any status other than approved as not selectable, including pending_screening, which a recipient can still hold. Referencing a recipient that is not approved on a payment is 422 recipient_not_approved, with no screening detail attached.

Stable beneficiary key. The platform derives beneficiary_key from exactly three inputs — name, destination MSISDN and destination country — because 24-hour aggregation and velocity rules are keyed on the beneficiary, and a phone string alone is not a stable enough identity. Saving a beneficiary whose key already exists in this end user's book returns 409 duplicate_recipient.

Authorizations:
bearerAuth
path Parameters
end_user_id
required
string^eu_[a-zA-Z0-9]+$
Example: eu_7Qm4xK2pLd
Request Body schema: application/json
required
type
required
string
Enum: "INDIVIDUAL" "BUSINESS"
country
required
string^[A-Z]{2}$

ISO 3166-1 alpha-2 destination country. Part of the beneficiary key.

phone
required
string

Destination MSISDN in E.164. Part of the beneficiary key. Reads return it in full (Recipient.phone).

payout_method
required
string (PayoutMethod)
Enum: "MOBILE_MONEY" "BANK_TRANSFER"
relationship
string (BeneficiaryRelationship)
Enum: "SELF" "SPOUSE" "PARENT" "CHILD" "SIBLING" "OTHER_FAMILY" "FRIEND" "BUSINESS" "OTHER"

The sender's stated relationship to this beneficiary. Platform data, not client state. It is a record-keeping element for a remittance — a sender-beneficiary relationship is the kind of thing an examiner asks about, and it also feeds risk: a first payment to a newly-added beneficiary described as OTHER is a different signal from one to a spouse. Holding it in a client or a BFF would put a record-keeping element outside the retained record. Sender-stated and never verified — it is what the customer said, not a fact the platform has established, and nothing may be inferred from it beyond that. Optional, because compelling an answer produces a worse one.

first_name
string <= 100 characters

Required for an INDIVIDUAL.

last_name
string <= 100 characters

Required for an INDIVIDUAL.

business_name
string <= 200 characters

Required for a BUSINESS.

wallet_network
string^[A-Z][A-Z0-9_]{1,31}$

Optional. Validated against the WalletNetwork pattern only (else 422 validation_error); not yet checked against a corridor's network list.

nickname
string <= 60 characters

The end user's own label. Not part of the beneficiary key.

Responses

Response Schema: application/json
id
required
string^rcp_[a-zA-Z0-9]+$
end_user_id
required
string^eu_[a-zA-Z0-9]+$
status
required
string (RecipientStatus)
Enum: "pending_screening" "approved" "rejected" "unverified"

Screening state of a saved beneficiary. Only approved may be selected for a transfer — treat every other value, including any unrecognised one, as not selectable.

  • pending_screening — no decision yet on the current details. Not selectable. Save and edit screen synchronously, so a new recipient is not normally returned in this state.
  • approved — screened and selectable.
  • rejected — screening declined. Terminal for that beneficiary key, and it carries no explanation.
  • unverified — 🔲 planned; nothing sets it today. Will mean the rail rejected the beneficiary details on a transfer attempt. The recipient is kept, not deleted: the customer entered it, it is probably a typo in one digit, and deleting their data to express "we could not reach this number" loses the thing they need in order to correct it. Not selectable until corrected — a PATCH that changes the MSISDN, name or country produces a new beneficiary key and re-screens.

unverified is deliberately distinct from rejected: one means "these details did not resolve at the network", the other means "this beneficiary did not pass screening". They have different remedies and must not share a label.

beneficiary_key
required
string <= 128 characters

Stable identity of the beneficiary, derived by the platform from exactly three inputs — name, destination MSISDN and destination country. Velocity and 24-hour aggregation are keyed on it, which is why it cannot be the phone string alone. Stable across edits that touch none of those three; a change to any of them yields a new key and a re-screen.

display_name
required
string <= 200 characters

The beneficiary's name as it will be used for payout.

country
required
string^[A-Z]{2}$

ISO 3166-1 alpha-2 destination country.

phone
required
string

E.164 destination MSISDN, in full.

created_at
required
string <date-time>
updated_at
required
string <date-time>
relationship
string (BeneficiaryRelationship)
Enum: "SELF" "SPOUSE" "PARENT" "CHILD" "SIBLING" "OTHER_FAMILY" "FRIEND" "BUSINESS" "OTHER"

The sender's stated relationship to this beneficiary. Platform data, not client state. It is a record-keeping element for a remittance — a sender-beneficiary relationship is the kind of thing an examiner asks about, and it also feeds risk: a first payment to a newly-added beneficiary described as OTHER is a different signal from one to a spouse. Holding it in a client or a BFF would put a record-keeping element outside the retained record. Sender-stated and never verified — it is what the customer said, not a fact the platform has established, and nothing may be inferred from it beyond that. Optional, because compelling an answer produces a worse one.

nickname
string <= 60 characters

The end user's own label for this recipient. Not part of the beneficiary key, and editing it does not re-screen.

type
string
Enum: "INDIVIDUAL" "BUSINESS"
payout_method
string (PayoutMethod)
Enum: "MOBILE_MONEY" "BANK_TRANSFER"
wallet_network
string^[A-Z][A-Z0-9_]{1,31}$

The mobile-money network this beneficiary is on, as supplied at save. Present only when supplied; never guessed from the MSISDN prefix.

object
moja-status: planned

🔲 Planned — not emitted. Whether the beneficiary name matches the name registered on the destination wallet. Absent when no check has run yet — absent is not a pass.

Request samples

Content type
application/json
{
  • "relationship": "SELF",
  • "type": "INDIVIDUAL",
  • "first_name": "Sample",
  • "last_name": "Recipient",
  • "business_name": "string",
  • "country": "TZ",
  • "phone": "+255700000001",
  • "payout_method": "MOBILE_MONEY",
  • "wallet_network": "NETWORK_A",
  • "nickname": "Home"
}

Response samples

Content type
application/json
{
  • "relationship": "SELF",
  • "id": "rcp_9Zd5nP6yTc",
  • "end_user_id": "eu_7Qm4xK2pLd",
  • "status": "pending_screening",
  • "beneficiary_key": "bk_5f4a2c8e9d1b7306",
  • "display_name": "Sample Recipient",
  • "nickname": "Home",
  • "type": "INDIVIDUAL",
  • "country": "TZ",
  • "phone": "+255712345678",
  • "payout_method": "MOBILE_MONEY",
  • "wallet_network": "NETWORK_A",
  • "name_check": {
    },
  • "created_at": "2019-08-24T14:15:22Z",
  • "updated_at": "2019-08-24T14:15:22Z"
}

Get a recipient

Returns one saved beneficiary, with the destination MSISDN masked.

Scope: recipients:read.

Authorizations:
bearerAuth
path Parameters
end_user_id
required
string^eu_[a-zA-Z0-9]+$
Example: eu_7Qm4xK2pLd
recipient_id
required
string^rcp_[a-zA-Z0-9]+$
Example: rcp_9Zd5nP6yTc

Responses

Response Schema: application/json
id
required
string^rcp_[a-zA-Z0-9]+$
end_user_id
required
string^eu_[a-zA-Z0-9]+$
status
required
string (RecipientStatus)
Enum: "pending_screening" "approved" "rejected" "unverified"

Screening state of a saved beneficiary. Only approved may be selected for a transfer — treat every other value, including any unrecognised one, as not selectable.

  • pending_screening — no decision yet on the current details. Not selectable. Save and edit screen synchronously, so a new recipient is not normally returned in this state.
  • approved — screened and selectable.
  • rejected — screening declined. Terminal for that beneficiary key, and it carries no explanation.
  • unverified — 🔲 planned; nothing sets it today. Will mean the rail rejected the beneficiary details on a transfer attempt. The recipient is kept, not deleted: the customer entered it, it is probably a typo in one digit, and deleting their data to express "we could not reach this number" loses the thing they need in order to correct it. Not selectable until corrected — a PATCH that changes the MSISDN, name or country produces a new beneficiary key and re-screens.

unverified is deliberately distinct from rejected: one means "these details did not resolve at the network", the other means "this beneficiary did not pass screening". They have different remedies and must not share a label.

beneficiary_key
required
string <= 128 characters

Stable identity of the beneficiary, derived by the platform from exactly three inputs — name, destination MSISDN and destination country. Velocity and 24-hour aggregation are keyed on it, which is why it cannot be the phone string alone. Stable across edits that touch none of those three; a change to any of them yields a new key and a re-screen.

display_name
required
string <= 200 characters

The beneficiary's name as it will be used for payout.

country
required
string^[A-Z]{2}$

ISO 3166-1 alpha-2 destination country.

phone
required
string

E.164 destination MSISDN, in full.

created_at
required
string <date-time>
updated_at
required
string <date-time>
relationship
string (BeneficiaryRelationship)
Enum: "SELF" "SPOUSE" "PARENT" "CHILD" "SIBLING" "OTHER_FAMILY" "FRIEND" "BUSINESS" "OTHER"

The sender's stated relationship to this beneficiary. Platform data, not client state. It is a record-keeping element for a remittance — a sender-beneficiary relationship is the kind of thing an examiner asks about, and it also feeds risk: a first payment to a newly-added beneficiary described as OTHER is a different signal from one to a spouse. Holding it in a client or a BFF would put a record-keeping element outside the retained record. Sender-stated and never verified — it is what the customer said, not a fact the platform has established, and nothing may be inferred from it beyond that. Optional, because compelling an answer produces a worse one.

nickname
string <= 60 characters

The end user's own label for this recipient. Not part of the beneficiary key, and editing it does not re-screen.

type
string
Enum: "INDIVIDUAL" "BUSINESS"
payout_method
string (PayoutMethod)
Enum: "MOBILE_MONEY" "BANK_TRANSFER"
wallet_network
string^[A-Z][A-Z0-9_]{1,31}$

The mobile-money network this beneficiary is on, as supplied at save. Present only when supplied; never guessed from the MSISDN prefix.

object
moja-status: planned

🔲 Planned — not emitted. Whether the beneficiary name matches the name registered on the destination wallet. Absent when no check has run yet — absent is not a pass.

Response samples

Content type
application/json
{
  • "relationship": "SELF",
  • "id": "rcp_9Zd5nP6yTc",
  • "end_user_id": "eu_7Qm4xK2pLd",
  • "status": "pending_screening",
  • "beneficiary_key": "bk_5f4a2c8e9d1b7306",
  • "display_name": "Sample Recipient",
  • "nickname": "Home",
  • "type": "INDIVIDUAL",
  • "country": "TZ",
  • "phone": "+255712345678",
  • "payout_method": "MOBILE_MONEY",
  • "wallet_network": "NETWORK_A",
  • "name_check": {
    },
  • "created_at": "2019-08-24T14:15:22Z",
  • "updated_at": "2019-08-24T14:15:22Z"
}

Update a recipient

Updates a saved beneficiary. Omitted fields are unchanged; an explicit null is 422 validation_error.

Scope: recipients:write.

Editing any component of the beneficiary key — name, MSISDN or country — produces a new beneficiary_key, resets status to pending_screening, and re-screens. The new details are screened before they are written, so the response carries the new decision; if the screening dependency does not answer, nothing changes and the response is 503 screening_unavailable. The recipient is never usable on details that have not been screened. Editing only nickname does not change the key and does not re-screen: a label is not an identity, and re-screening on a rename would be noise that trains people to ignore the state.

Authorizations:
bearerAuth
path Parameters
end_user_id
required
string^eu_[a-zA-Z0-9]+$
Example: eu_7Qm4xK2pLd
recipient_id
required
string^rcp_[a-zA-Z0-9]+$
Example: rcp_9Zd5nP6yTc
Request Body schema: application/json
required
non-empty
first_name
string <= 100 characters
last_name
string <= 100 characters
business_name
string <= 200 characters
country
string^[A-Z]{2}$
phone
string

Destination MSISDN in E.164.

payout_method
string (PayoutMethod)
Enum: "MOBILE_MONEY" "BANK_TRANSFER"
wallet_network
string^[A-Z][A-Z0-9_]{1,31}$

Not part of the beneficiary key, so changing it does not re-screen.

relationship
string (BeneficiaryRelationship)
Enum: "SELF" "SPOUSE" "PARENT" "CHILD" "SIBLING" "OTHER_FAMILY" "FRIEND" "BUSINESS" "OTHER"

The sender's stated relationship to this beneficiary. Platform data, not client state. It is a record-keeping element for a remittance — a sender-beneficiary relationship is the kind of thing an examiner asks about, and it also feeds risk: a first payment to a newly-added beneficiary described as OTHER is a different signal from one to a spouse. Holding it in a client or a BFF would put a record-keeping element outside the retained record. Sender-stated and never verified — it is what the customer said, not a fact the platform has established, and nothing may be inferred from it beyond that. Optional, because compelling an answer produces a worse one.

nickname
string <= 60 characters

Responses

Response Schema: application/json
id
required
string^rcp_[a-zA-Z0-9]+$
end_user_id
required
string^eu_[a-zA-Z0-9]+$
status
required
string (RecipientStatus)
Enum: "pending_screening" "approved" "rejected" "unverified"

Screening state of a saved beneficiary. Only approved may be selected for a transfer — treat every other value, including any unrecognised one, as not selectable.

  • pending_screening — no decision yet on the current details. Not selectable. Save and edit screen synchronously, so a new recipient is not normally returned in this state.
  • approved — screened and selectable.
  • rejected — screening declined. Terminal for that beneficiary key, and it carries no explanation.
  • unverified — 🔲 planned; nothing sets it today. Will mean the rail rejected the beneficiary details on a transfer attempt. The recipient is kept, not deleted: the customer entered it, it is probably a typo in one digit, and deleting their data to express "we could not reach this number" loses the thing they need in order to correct it. Not selectable until corrected — a PATCH that changes the MSISDN, name or country produces a new beneficiary key and re-screens.

unverified is deliberately distinct from rejected: one means "these details did not resolve at the network", the other means "this beneficiary did not pass screening". They have different remedies and must not share a label.

beneficiary_key
required
string <= 128 characters

Stable identity of the beneficiary, derived by the platform from exactly three inputs — name, destination MSISDN and destination country. Velocity and 24-hour aggregation are keyed on it, which is why it cannot be the phone string alone. Stable across edits that touch none of those three; a change to any of them yields a new key and a re-screen.

display_name
required
string <= 200 characters

The beneficiary's name as it will be used for payout.

country
required
string^[A-Z]{2}$

ISO 3166-1 alpha-2 destination country.

phone
required
string

E.164 destination MSISDN, in full.

created_at
required
string <date-time>
updated_at
required
string <date-time>
relationship
string (BeneficiaryRelationship)
Enum: "SELF" "SPOUSE" "PARENT" "CHILD" "SIBLING" "OTHER_FAMILY" "FRIEND" "BUSINESS" "OTHER"

The sender's stated relationship to this beneficiary. Platform data, not client state. It is a record-keeping element for a remittance — a sender-beneficiary relationship is the kind of thing an examiner asks about, and it also feeds risk: a first payment to a newly-added beneficiary described as OTHER is a different signal from one to a spouse. Holding it in a client or a BFF would put a record-keeping element outside the retained record. Sender-stated and never verified — it is what the customer said, not a fact the platform has established, and nothing may be inferred from it beyond that. Optional, because compelling an answer produces a worse one.

nickname
string <= 60 characters

The end user's own label for this recipient. Not part of the beneficiary key, and editing it does not re-screen.

type
string
Enum: "INDIVIDUAL" "BUSINESS"
payout_method
string (PayoutMethod)
Enum: "MOBILE_MONEY" "BANK_TRANSFER"
wallet_network
string^[A-Z][A-Z0-9_]{1,31}$

The mobile-money network this beneficiary is on, as supplied at save. Present only when supplied; never guessed from the MSISDN prefix.

object
moja-status: planned

🔲 Planned — not emitted. Whether the beneficiary name matches the name registered on the destination wallet. Absent when no check has run yet — absent is not a pass.

Request samples

Content type
application/json
{
  • "first_name": "string",
  • "last_name": "string",
  • "business_name": "string",
  • "country": "string",
  • "phone": "string",
  • "payout_method": "MOBILE_MONEY",
  • "wallet_network": "NETWORK_A",
  • "relationship": "SELF",
  • "nickname": "string"
}

Response samples

Content type
application/json
{
  • "relationship": "SELF",
  • "id": "rcp_9Zd5nP6yTc",
  • "end_user_id": "eu_7Qm4xK2pLd",
  • "status": "pending_screening",
  • "beneficiary_key": "bk_5f4a2c8e9d1b7306",
  • "display_name": "Sample Recipient",
  • "nickname": "Home",
  • "type": "INDIVIDUAL",
  • "country": "TZ",
  • "phone": "+255712345678",
  • "payout_method": "MOBILE_MONEY",
  • "wallet_network": "NETWORK_A",
  • "name_check": {
    },
  • "created_at": "2019-08-24T14:15:22Z",
  • "updated_at": "2019-08-24T14:15:22Z"
}

Remove a recipient

Removes the beneficiary from the end user's book. Idempotent: deleting an already-deleted recipient returns 204.

Scope: recipients:write.

This removes the saved entry only. Payments already made to this beneficiary, and the screening record behind them, are retained — they are transaction and compliance records with their own retention rules, and a user removing an address-book entry does not and cannot delete them.

Authorizations:
bearerAuth
path Parameters
end_user_id
required
string^eu_[a-zA-Z0-9]+$
Example: eu_7Qm4xK2pLd
recipient_id
required
string^rcp_[a-zA-Z0-9]+$
Example: rcp_9Zd5nP6yTc

Responses

Response samples

Content type
application/json
{
  • "error": {
    }
}

Devices

🔲 Planned — not routed. Provider-agnostic push-notification device registration.

[Planned] Register a device for notifications

Registers a device so the platform can notify this end user.

Scope: end_users:write.

Provider-agnostic by construction. push_token is an opaque string; this contract names no push provider and carries no provider-specific field, so changing or adding a delivery provider is not a contract change. platform describes the device class, not a transport.

The token is write-only. It is never returned by any endpoint, never echoed in an error, and never logged. Re-registering the same token for the same end user returns the existing registration (200) rather than accumulating duplicates.

What a notification may contain. A delivered notification carries an opaque reference only — no name, no amount, no currency, no MSISDN, no corridor, no status text. A lock screen is a public surface and push payloads traverse a third party. The app receives the reference and fetches the detail over this authenticated API, which is also what keeps the notification honest when the underlying state has since changed.

moja-status: planned
Authorizations:
bearerAuth
path Parameters
end_user_id
required
string^eu_[a-zA-Z0-9]+$
Example: eu_7Qm4xK2pLd
Request Body schema: application/json
required
platform
required
string (DevicePlatform)
Enum: "IOS" "ANDROID" "WEB"

Device class. Deliberately not a push provider: this contract names no notification vendor, so the platform can change or add delivery providers without a contract change.

push_token
required
string <= 4096 characters

Opaque provider token for this device. Write-only: never returned by any endpoint, never echoed in an error, never logged.

device_label
string <= 60 characters

Optional end-user-visible label for a device list. Must not contain personal data beyond what the user typed.

Responses

Response Schema: application/json
id
required
string^dev_[a-zA-Z0-9]+$
platform
required
string (DevicePlatform)
Enum: "IOS" "ANDROID" "WEB"

Device class. Deliberately not a push provider: this contract names no notification vendor, so the platform can change or add delivery providers without a contract change.

created_at
required
string <date-time>
device_label
string <= 60 characters
last_seen_at
string <date-time>
Response Schema: application/json
id
required
string^dev_[a-zA-Z0-9]+$
platform
required
string (DevicePlatform)
Enum: "IOS" "ANDROID" "WEB"

Device class. Deliberately not a push provider: this contract names no notification vendor, so the platform can change or add delivery providers without a contract change.

created_at
required
string <date-time>
device_label
string <= 60 characters
last_seen_at
string <date-time>

Request samples

Content type
application/json
{
  • "platform": "IOS",
  • "push_token": "opaque-device-token-000000",
  • "device_label": "string"
}

Response samples

Content type
application/json
{
  • "id": "dev_2Fk7wQ4mNb",
  • "platform": "IOS",
  • "device_label": "string",
  • "created_at": "2019-08-24T14:15:22Z",
  • "last_seen_at": "2019-08-24T14:15:22Z"
}

[Planned] Deregister a device

Removes a device registration — on sign-out, on token rotation, or on "log out everywhere". Idempotent: 204 whether or not it was present.

Scope: end_users:write.

moja-status: planned
Authorizations:
bearerAuth
path Parameters
end_user_id
required
string^eu_[a-zA-Z0-9]+$
Example: eu_7Qm4xK2pLd
device_id
required
string^dev_[a-zA-Z0-9]+$
Example: dev_2Fk7wQ4mNb

Responses

Response samples

Content type
application/json
{
  • "error": {
    }
}

Support

🔲 Contract published, not yet implemented.

In-app support case intake.

[Planned] List an end user's support cases

🔲 Contract published, not yet implemented.

Returns this end user's cases, newest first, so a customer can see what they have already raised. A support channel a customer can write to but not read from produces duplicate cases and a customer who believes they were ignored.

Scope: end_users:write.

moja-status: planned
Authorizations:
bearerAuth
path Parameters
end_user_id
required
string^eu_[a-zA-Z0-9]+$
Example: eu_7Qm4xK2pLd
query Parameters
status
Array of strings (SupportCaseStatus) <= 4 items
Items Enum: "open" "in_progress" "resolved" "closed"

Return only cases in these statuses. Repeat to match several.

Responses

Response Schema: application/json
required
Array of objects (SupportCase)

Cases, ordered created_at DESC, id DESC.

Response samples

Content type
application/json
{
  • "data": [
    ]
}

[Planned] Open a support case

Creates a durable support case for an end user and returns its reference. Intended for in-app intake, and durable so that a case survives the app session it was raised in.

Scope: end_users:write.

message is stored verbatim as part of the case record. Do not place credentials, card numbers, document images or API keys in it, and do not prompt a user for them in the surface that feeds it — a support case is read by humans and retained.

moja-status: planned
Authorizations:
bearerAuth
path Parameters
end_user_id
required
string^eu_[a-zA-Z0-9]+$
Example: eu_7Qm4xK2pLd
Request Body schema: application/json
required
category
required
string (SupportCaseCategory)
Enum: "PAYMENT_ISSUE" "RECIPIENT_ISSUE" "VERIFICATION_ISSUE" "ACCOUNT_ISSUE" "OTHER"
message
required
string <= 2000 characters

What the end user reported, stored verbatim on a durable case record read by humans. Never place credentials, card numbers, document images or API keys in it.

payment_id
string^pay_[a-zA-Z0-9]+$

The payment the case is about, when there is one.

contact_email
string <email>

Reply address, when it differs from the end user's profile email.

Responses

Response Schema: application/json
id
required
string^sup_[a-zA-Z0-9]+$
end_user_id
required
string^eu_[a-zA-Z0-9]+$
case_number
required
string

Human-quotable reference to give the end user. Stable for the life of the case.

category
required
string (SupportCaseCategory)
Enum: "PAYMENT_ISSUE" "RECIPIENT_ISSUE" "VERIFICATION_ISSUE" "ACCOUNT_ISSUE" "OTHER"
status
required
string (SupportCaseStatus)
Enum: "open" "in_progress" "resolved" "closed"

Treat an unrecognised value as open.

created_at
required
string <date-time>
payment_id
string^pay_[a-zA-Z0-9]+$
updated_at
string <date-time>

Request samples

Content type
application/json
{
  • "category": "PAYMENT_ISSUE",
  • "message": "string",
  • "payment_id": "pay_abc123def456",
  • "contact_email": "test.sender@example.com"
}

Response samples

Content type
application/json
{
  • "id": "sup_4Ln8vC3hJk",
  • "end_user_id": "eu_7Qm4xK2pLd",
  • "case_number": "CASE-2026-0004567",
  • "category": "PAYMENT_ISSUE",
  • "status": "open",
  • "payment_id": "pay_abc123def456",
  • "created_at": "2019-08-24T14:15:22Z",
  • "updated_at": "2019-08-24T14:15:22Z"
}

Account

Partner account bootstrap and readiness. POST /signup is public (no API key); readiness and client-config require scope read.

Self-serve sandbox partner signup

Provisions instant sandbox access for a new partner and returns a one-time sk_sandbox_* API key. No compliance step — sandbox moves no real money. The new partner account has status: SANDBOX and is not ready for production. This operation can never issue a production key or a live-capable account.

Public by design. There is no API key before signup, so this operation needs none. It is rate-limited per client IP address.

Request Body schema: application/json
required
name
required
string non-empty

Display name for the new sandbox partner

contact_email
string <email>

Optional contact email for the partner

Responses

Response Schema: application/json
id
required
string

Partner id (partner_<slug>)

name
required
string
status
required
string

Always SANDBOX for self-serve signup

required
Array of objects
created_at
required
string <date-time>
updated_at
required
string <date-time>
required
object
contact_email
string <email>

Request samples

Content type
application/json
{
  • "name": "Acme Remit",
  • "contact_email": "user@example.com"
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "name": "string",
  • "status": "SANDBOX",
  • "contact_email": "user@example.com",
  • "corridor_entitlements": [
    ],
  • "created_at": "2019-08-24T14:15:22Z",
  • "updated_at": "2019-08-24T14:15:22Z",
  • "api_key": {
    }
}

This partner's own production go-live readiness

The partner's composite production go-live projection — the same rows the money-path gate reasons from, so can_send_live here always matches whether a live payment would actually clear the gate.

Tenant-scoped by construction. The partner is taken from the authenticated API key, never from a path or body field.

Corridor capability is evaluated for CA-TZ today — the capability requirement is met when the partner holds an enabled entitlement to CA-TZ. 🔲 Planned: a corridor-agnostic default (any enabled catalogue corridor) and the corridor query parameter to ask about one corridor; today the parameter is ignored.

requirements[].blocking marks the enforced dimensions — Compliance, Funding, corridor capability — whose failure prevents go-live. Treasury and commercial are carried for transparency and never block.

If readiness cannot be read, the answer is 500 internal_error, never an account reported as ready by mistake. Sandbox traffic is not gated, because it moves no real money.

No corridor entitlement is required to call it: it reports your entitlement rather than using one.

Scope: read.

Authorizations:
bearerAuth
query Parameters
corridor
string (CorridorCode) ^[A-Z]{2}-[A-Z]{2}$
Example: corridor=CA-TZ
moja-status: planned

🔲 Planned — ignored today (capability is evaluated for CA-TZ). Will ask about one corridor rather than any.

Responses

Response Schema: application/json
environment
required
string

The environment this projection describes.

readiness
required
string
Enum: "ready" "not_ready"
can_send_live
required
boolean

The readiness fold and corridor capability. True only when a live payment would actually clear the gate.

required
Array of objects (ReadinessRequirement)

Response samples

Content type
application/json
{
  • "environment": "string",
  • "readiness": "ready",
  • "can_send_live": true,
  • "requirements": [
    ]
}

Sandbox

Sandbox-only partner actions that move no money state (payout scenario configuration; webhook redelivery of an already-emitted event).

Set sandbox payout scenario for the next payout

Configures the tenant's next sandbox payout outcome (paid | delayed | failed_before_send | returned). The value is snapshotted onto the payout row at CREATED. Moves no money.

Scope: write. Sandbox API keys only.

Authorizations:
bearerAuth
Request Body schema: application/json
required
payout_outcome
required
string
Enum: "paid" "delayed" "failed_before_send" "returned"

Responses

Response Schema: application/json
payout_outcome
required
string

Request samples

Content type
application/json
{
  • "payout_outcome": "paid"
}

Response samples

Content type
application/json
{
  • "payout_outcome": "string"
}

Re-deliver an already-emitted sandbox webhook

Re-queues an already-delivered event (same event_id, same body, fresh signature) so the tenant can exercise duplicate handling. Moves no money state.

Scope: write. Sandbox API keys only.

Authorizations:
bearerAuth
path Parameters
event_id
required
string

Responses

Response samples

Content type
application/json
{
  • "event_id": "string",
  • "status": "queued"
}