Skip to main content

Terminology (Avoid “Taking an Offer” Confusion)

The JSON fields keep historical names. Product language should be: For off-ramp remittance, John selects a liquidity listing so the platform has a consistent conversion anchor. He is not “obtaining USDT” from that listing.

Creating an Order

POST /api/v1/auth/orders
  • offer_id — Required. Must reference an existing liquidity listing for the chosen vendor_id (liquidity provider).
  • pricing_profile_id and fx_rate_quote_id — Required for commercial enforcement. Must match order_type, asset (USDT), fiat_currency, and network implied by wallet_address (Tron-style T... vs Ethereum 0x...) for pricing validation. fiat_currency must be an active code from the shared catalog. LIST OPTIONS WITH GET /api/v1/auth/fiat-currencies (SAME CATALOG AS OTC, REMITTANCE, AND ADMIN TOOLING — NOT UNDER /otc/...).
  • order_type - ON_RAMP_BUY (default) or OFF_RAMP_SELL.
  • Off-Ramp: provide payout_phone. Escrow and network detection use the customer’s validated wallet_address.
For app clients, resolve commercial IDs first, then create the order:
  1. POST /api/v1/auth/pricing/resolve with flow_type, network, asset, fiat_currency, amount_in, and amount_in_type (plus optional vendor_id/offer_id to pin the preview to a specific liquidity listing).
  2. Read pricing_profile_id and fx_rate_quote_id from the response.
  3. POST /api/v1/auth/orders with those IDs plus order amounts and routing fields.
This keeps pricing selection logic on the backend and avoids client-side mismatches. For web dashboards/backoffice tools, use an explicit quote-review-confirm pattern:
  1. POST /api/v1/auth/pricing/resolve with full context (flow_type, network, asset, fiat_currency, amount, and selected liquidity listing offer_id + liquidity provider vendor_id).
  2. Render a review panel showing rates, spread, and total fees from the resolve response.
  3. On operator confirmation, POST /api/v1/auth/orders with the returned pricing_profile_id and fx_rate_quote_id.
  4. Continue with payment or escrow steps (order-type specific) and track status via order endpoints.
For consistency, web should follow the same server-resolved pricing path as mobile rather than hard-coding commercial IDs in the UI.

Web Complete Flow (All Operations)

Use this sequence for web portals that support customer checkout, operations, and treasury actions.

1) User Setup (Email-First on Web)

  1. Register user: POST /api/v1/users/register
  2. Login: POST /api/v1/users/login (or /login-pin if enabled)
  3. Load user wallets: GET /api/v1/auth/wallets/user
  4. Optionally set preferred default network wallet: PUT /api/v1/auth/wallet/id/{wallet_id}/set-default-network
Default Ethereum, TRON, and Solana wallets are best-effort provisioned at registration. Duplicate email or phone_number returns 409 with no user or wallets created. If one chain fails, others may still be created and wallet_errors is returned. Set default wallet/network to avoid forcing from_wallet_id on every transfer path.

1.1 Merchant Registration (Web BackOffice)

Use merchant onboarding endpoints for treasury and OTC operations:
  1. Create master merchant: POST /api/v1/auth/merchants
  2. Create sub-merchant under a master: POST /api/v1/auth/merchants/{merchant_id}/sub-merchants
  3. Create master + branches in one request: POST /api/v1/auth/merchants/with-sub-merchants
  4. List master merchants only (paginated): GET /api/v1/auth/merchants with page_id and page_size
  5. View one master, its wallets, and every sub-merchant with wallets: GET /api/v1/auth/merchants/{merchant_id} ( merchant_id must be the master vendor user id, not a sub-merchant id)
  6. Update merchant or sub-merchant profile/status: PATCH /api/v1/auth/merchants/{merchant_id}
For offer and settlement operations, the web app should use the merchant id as vendor_id in offer/order/transfer contexts.

1.2 Merchant Operations Surface (Web)

After onboarding, merchant operators typically use:
  • Offer management:
    • POST /api/v1/auth/offers
    • GET /api/v1/auth/offers/vendor/{vendor_id}
    • GET /api/v1/auth/offers/active, search and count variants
    • PUT /api/v1/auth/customer/offers/{id} (edit own offer)
    • DELETE /api/v1/auth/customer/offers/{id} (delete/cancel own offer)
  • Order operations:
    • GET /api/v1/auth/orders/vendor/{vendor_id}
    • GET /api/v1/auth/orders/recent/vendor/{vendor_id}
    • GET /api/v1/auth/orders/stats/vendor/{vendor_id}
  • Wallet operations (if extra wallets are required beyond defaults):
    • POST /api/v1/auth/wallets
    • GET /api/v1/auth/wallets/user
    • GET /api/v1/auth/wallets/user-network
    • PUT /api/v1/auth/wallet/id/{id}/set-default-network
    • PUT /api/v1/auth/wallet/id/{id}/active-status
  • Liquidity and reconciliation:
    • GET /api/v1/auth/vendors/{vendor_id}/liquidity
    • GET /api/v1/auth/transactions/completed
  • Customer history surface:
    • GET /api/v1/auth/customer/transactions (returns only authenticated customer’s created offers + orders)
  • OTC queues:
    • deposits:
      • POST /api/v1/auth/otc/deposits
      • GET /api/v1/auth/otc/deposits/{id}
      • POST /api/v1/auth/otc/deposits/{id}/confirm
      • POST /api/v1/auth/otc/deposits/{id}/reject
      • POST /api/v1/auth/otc/deposits/{id}/credit
      • GET /api/v1/auth/otc/deposits/queue/pending-bank
      • GET /api/v1/auth/otc/deposits/queue/ready-to-credit
    • withdrawals:
      • POST /api/v1/auth/otc/withdrawals
      • GET /api/v1/auth/otc/withdrawals/{id}
      • POST /api/v1/auth/otc/withdrawals/{id}/approve
      • POST /api/v1/auth/otc/withdrawals/{id}/reject
      • POST /api/v1/auth/otc/withdrawals/{id}/execute
      • GET /api/v1/auth/otc/withdrawals/queue/pending
      • GET /api/v1/auth/otc/withdrawals/queue/approved

2) on-Ramp (Fiat -> USDT, C2B)

  1. List offers: GET /api/v1/auth/offers/active?page_id=1&page_size=20
  2. Resolve pricing (flow=onramp): POST /api/v1/auth/pricing/resolve
  3. Create order with resolved IDs: POST /api/v1/auth/orders (order_type=ON_RAMP_BUY)
  4. Trigger STK push: POST /api/v1/auth/orders/payment
  5. Poll status: GET /api/v1/auth/order/id/{order_id}
Typical progression: pending -> processing -> completed (or payment_failed/failed).

3) Off-Ramp (USDT -> Fiat, B2C)

  1. Resolve pricing (flow=offramp): POST /api/v1/auth/pricing/resolve (optionally pass vendor_id + offer_id to align with the chosen liquidity listing)
  2. Create order with payout phone: POST /api/v1/auth/orders (order_type=OFF_RAMP_SELL)
  3. Read escrow fields from response (escrow_wallet_address, escrow_network, escrow_reference)
  4. Fund escrow:
    • in-app wallet route: POST /api/v1/auth/orders/{order_id}/send-payment, or
    • external chain transfer to escrow wallet
  5. Poll status: GET /api/v1/auth/order/id/{order_id}
Typical progression: awaiting_crypto -> awaiting_confirmations -> crypto_received -> payout_pending -> completed (or failed).

Off-Ramp Remittance (Third-Party Fiat Payout)

Use POST /api/v1/auth/remittance/off-ramp/orders when the customer sends crypto to escrow but mobile money (or bank) pays someone else. Same vendor_id (liquidity provider) and offer_id (liquidity listing) semantics. They anchor pricing and validation, not the beneficiary’s identity. The request uses beneficiary_fiat_amount plus pricing IDs to derive escrow crypto. See OpenAPI for the full body. Use the shared countries catalog when collecting remittance country fields. GET /api/v1/countries returns active rows for public pickers. GET /api/v1/auth/countries returns the same active catalog for authenticated apps. Prefer ISO 3166-1 alpha-3 values like KEN and GBR for sender_country, beneficiary_country, source_country, and destination_country. The backend still accepts legacy alpha-2 input and normalizes it to alpha-3. For custodial send-money with a corridor only, use POST /api/v1/auth/remittance/custodial/drafts, and POST /api/v1/auth/remittance/custodial/drafts/{id}/confirm. See Custodial Corridor Remittance.

Domestic vs Cross-Border Off-Ramp

When the fiat beneficiary is in a different country than the customer who sends crypto from their wallet, you are in a cross-border remittance scenario (different compliance and commercial treatment than a domestic payout where both sides share the same country). Identification: Identify domestic vs cross-border by comparing customer (KYC) country to beneficiary_country on the order (use ISO 3166-1 alpha-3 codes, e.g. KEN, GBR). If they match, treat the flow as domestic. However, if they differ, we treat it as cross-border remittance. The API persists beneficiary_country on off-ramp orders. Your integration must supply a reliable customer country from KYC (or an explicit field collected at quote time) so this comparison is possible before you choose pricing profiles, FX quotes, or copy for the checkout screen.

4) P2P (Customer -> Customer USDT)

  1. Ensure sender has a funded wallet on selected network.
  2. Create transfer from web wallet/send UI using P2P transfer endpoint.
  3. Poll transfer detail/status endpoint until terminal state.
Use P2P for customer-to-customer USDT movement only (not fiat conversion).

5) B2B (Merchant -> Merchant Settlement)

  1. Select merchant source wallet (typically treasury wallet policy).
  2. Submit transfer: POST /api/v1/auth/transfers/b2b
  3. If collaborative signing is required, follow proposal confirm endpoints.
  4. Poll transfer: GET /api/v1/auth/transfers/{id}
Use B2B for merchant settlement and treasury balancing between businesses.

5.1 Merchant Treasury Transfers (External/Internal)

Merchant web treasury screens may use:
  1. Internal merchant transfer: POST /api/v1/auth/transfers/b2b
  2. External destination transfer: POST /api/v1/auth/transfers/b2b/external
  3. List and inspect transfer lifecycle:
    • GET /api/v1/auth/transfers
    • GET /api/v1/auth/transfers/{id}
    • GET /api/v1/auth/transfers/{id}/pricing
For high-value flows, pair this with multisig wallet registration and proposal confirmation endpoints when policy requires collaborative signing.

6) B2B2C (Merchant-Funded Customer Settlement)

Treat B2B2C as two linked rails:
  1. B2B Funding Leg: merchant treasury movement to payout context.
  2. B2C Customer Leg: off-ramp style payout execution to beneficiary phone/account.
Track each leg with its own IDs and statuses for reconciliation, support, and dispute handling.

Off-Ramp vs on-Ramp

Payment Screen (in-App Wallet Send)

Fiat Payment (Mobile Money)

POST /api/v1/auth/orders/payment - STK push with order_id and phone_number.

Pricing on the Order

GET /api/v1/auth/order/id/{id}/pricing returns the stored snapshot (rates, fees, net amounts) when present.

Callbacks (Server-to-Server)

  • POST /api/v1/payment/callback — mobile money. Configured for Daraja validation.
  • POST /api/v1/mpesa/b2c/result and .../queue - B2C payout callbacks.
These endpoints are not authenticated with a user PASETO. We secure them at the network and payload-validation layer.