# FX Quotes

FX quotes let you state a payment price in one currency while the customer is charged, and the merchant settles, in the currency of the merchant account. A quote locks the applicable rate and the resulting settlement amount for a short time.

For example, a merchant can price goods at USD 10.00 and collect in KES, UGX or TZS through a supported local payment channel. Submit 10.00 as the request amount together with the quote details; the payment rail receives the locked local-currency amount. The same process applies to other enabled pricing and settlement currency pairs.

# Before you start

  • The merchant account must be active and must have a local settlement currency.
  • An active rate for the pricing/settlement pair must exist in the rate book. The platform uses a corporate rate when available and otherwise uses the system rate. Both direct and inverse pairs are supported.
  • Obtain an access token as described in Authentication. Authenticated API requests use the normal developer API authentication and include the merchant code in the body.
  • Quotes are short-lived and single-use. Check ExpiresAt before submitting. For an uncertain payment outcome, follow the retry guidance below before creating another payment.

The Amount in an FX-enabled payment request represents the pricing amount. Send it as a JSON number or decimal string. The API calculates and locks the local settlement amount in the quote response. Do not replace the request amount with PayableAmount.

# Create an authenticated quote

Use this endpoint before a receive-money/collection request or a send-money/disbursement request.

POST https://api.sandbox.pesaway.com/api/fx/quote/

# Request fields

Field Required Description
MerchantCode Yes Active merchant account number.
Amount Yes Positive number or decimal string in PricingCurrency.
PricingCurrency Yes Active ISO currency code for the price, for example USD.
Flow No collection for receiving money or disbursement for sending money. Defaults to collection.
PayableCurrency No Normally omit this. The API derives it from the merchant account; when supplied, it must match that account currency.
PaymentCurrency No Set for crypto collections, for example USDT. Omit for ordinary mobile-money FX collections, regardless of the local currency.
curl --request POST 'https://api.sandbox.pesaway.com/api/fx/quote/' \
  --header 'Authorization: Bearer <access-token>' \
  --header 'Content-Type: application/json' \
  --data '{
    "MerchantCode": "MERCHANT-001",
    "Amount": 10.00,
    "PricingCurrency": "USD",
    "Flow": "collection"
  }'

# Successful response

The following is one USD-to-KES example. For a UGX or TZS merchant account, PayableCurrency and PayableAmount reflect that account and its applicable rate.

{
  "code": "200.001",
  "FxQuoteID": "c0da3f3a-7a8e-4b7a-9bbf-1e8b4b15c093",
  "PricingCurrency": "USD",
  "PricingAmount": "10.00",
  "PayableCurrency": "KES",
  "PayableAmount": "1305",
  "PaymentCurrency": "",
  "PaymentAmount": "",
  "Rate": "130.5000000000",
  "Flow": "collection",
  "ExpiresAt": "2026-08-04T12:05:00+00:00",
  "status": "success"
}

The response above is illustrative: use a newly returned quote ID and expiry, never the example values. Authenticated quotes return these fields at the top level, without a data wrapper; X-API-Version does not change this shape. PaymentCurrency and PaymentAmount are empty strings for this mobile-money flow.

PayableAmount is rounded for the settlement currency. For configured zero-decimal currencies it is a whole unit. Store the returned FxQuoteID, but do not attempt to calculate or alter the returned Rate or PayableAmount.

# Collect foreign-priced payments in local currency

Use this flow when your shop prices goods in one currency and customers pay through a supported mobile-money channel in the merchant account currency. Pricing currency, local currency and channel are configuration choices; the quote, initiation and completion steps stay the same. Currency and channel availability depends on your merchant configuration.

Order in pricing currency → quote in local currency → customer completes local payment → callback/query confirms completion → fulfil order.

# Examples across regions

For a USD-priced order, use the following configuration choices. These are examples of channel codes documented in Mobile Money; confirm the channel is enabled for your merchant before using it.

Collection market PricingCurrency Merchant account / Currency Example Channel Customer phone country code
Kenya USD KES account / KES MPESA 254
Uganda USD UGX account / UGX MOMO-UG or Airtel-UG 256
Tanzania USD TZS account / TZS Vodacom MPESA, Tigo PESA or Airtel 255
Other supported markets Your enabled pricing currency Account in the local currency Exact enabled collection channel code Customer's country code

For USD 10.00, each row still sends Amount: 10.00. Create a separate quote using the appropriate merchant account and use its returned local amount. A KES quote cannot be reused for a UGX or TZS account. If the customer changes country, wallet, channel or order details, validate the new selection and obtain a matching quote before starting a payment; resolve any already-submitted attempt first.

# 1. Prepare the merchant and checkout

  • Use an active merchant account in the intended local currency, with the chosen collection channel enabled. Both currencies, an applicable FX rate and a provider collection route must be configured. Ask support to confirm availability for your merchant.
  • Keep developer credentials and authenticated API calls on your server. Use credentials and a merchant code belonging to the same environment.
  • For sandbox, use https://api.sandbox.pesaway.com for both quote and collection requests. For production, use your assigned regional API base URL and production credentials.
  • MerchantCode identifies the wallet and its currency; X-Region does not change the wallet currency. Follow your integration's regional routing requirements while selecting the correct merchant account.
  • Prepare a reachable HTTPS ResultsUrl and a unique ExternalReference for each logical payment attempt. Store the order, reference, quote and eventual transaction ID together.

The requests below use the documentation's configured API base. Replace the host when testing another environment, along with all placeholder credentials, merchant codes and customer details.

# 2. Create and display the quote

Use the authenticated quote request above with the selected local-currency MerchantCode, the original order Amount, its PricingCurrency and Flow: "collection". You may supply PayableCurrency to check that it matches the wallet currency. Omit PaymentCurrency for ordinary mobile-money FX collections.

Read the returned fields for every currency pair:

Value Meaning How to use it
PricingAmount / PricingCurrency Original price of the goods Keep these values unchanged in the collection request.
PayableAmount / PayableCurrency Converted local collection principal Display these returned values to the customer.
FxQuoteID Quote to attach to this payment Use the actual returned ID
ExpiresAt Deadline for submitting the quote Use the actual returned timestamp

Display the returned local amount and currency before the customer confirms. Rounding follows the settlement currency configuration: KES, UGX and TZS use whole-unit rounding by default, while other currencies may allow decimal places. Always use the returned amount instead of calculating it in your application. Rates in this guide are examples, not live rates.

Collection fees depend on the merchant's configuration. If fees are passed to the customer, the provider request includes the collection principal plus the service fee. Otherwise the fee is deducted from the merchant's credit. Confirm this policy during onboarding before presenting a final payable total: PayableAmount alone does not include an additional collection fee.

Quotes expire at ExpiresAt (default lifetime is five minutes, configurable). If the customer waits too long before submission, request a fresh quote and show the revised amount for confirmation. A rate change after quote creation does not change that quote's locked amount.

# 3. Initiate the collection

Submit the original pricing amount, the returned quote ID and the local payment currency. In the template below, replace PRICING_CURRENCY with the order currency, LOCAL_CURRENCY with the quote’s PayableCurrency, and ENABLED_CHANNEL with the exact enabled channel code. The example amount 10.00 must match the amount used to create your quote.

curl --request POST 'https://api.sandbox.pesaway.com/api/v1/mobile-money/receive-payment/' \
  --header 'Authorization: Bearer <access-token>' \
  --header 'Content-Type: application/json' \
  --data '{
    "ExternalReference": "ORDER-1001-ATTEMPT-1",
    "Amount": 10.00,
    "PhoneNumber": "CUSTOMER_PHONE_WITH_COUNTRY_CODE",
    "MerchantCode": "YOUR_LOCAL_CURRENCY_MERCHANT_CODE",
    "Channel": "ENABLED_CHANNEL",
    "Currency": "LOCAL_CURRENCY",
    "Reason": "Order 1001",
    "ResultsUrl": "https://merchant.example/payments/callback",
    "PricingCurrency": "PRICING_CURRENCY",
    "FxQuoteID": "REPLACE_WITH_FRESH_QUOTE_ID"
  }'

Use the same merchant, amount and pricing currency as the quote. Amount is always the original price, never the converted PayableAmount. Set Currency to the local account currency and PricingCurrency to the order currency. The lowercase aliases pricing_currency and fx_quote_id are also accepted; use one naming style consistently.

An accepted request returns a processing response, for example:

{
  "code": "200.001",
  "TransactionID": "EXAMPLE-TRANSACTION-ID",
  "OriginatorReference": "ORDER-1001-ATTEMPT-1",
  "ResponseDescription": "Success. Request accepted for processing",
  "status": "processing"
}

Save TransactionID. Acceptance is not confirmation that the customer paid. Keep the order pending until completion is confirmed.

# 4. Confirm completion and reconcile

Process the callback sent to ResultsUrl. Match TransactionID and OriginatorReference to the saved payment attempt and handle repeated callbacks without fulfilling the order twice. Confirm the transaction through the authenticated query API before fulfilment if the callback's authenticity or outcome is uncertain.

curl --request POST 'https://api.sandbox.pesaway.com/api/v1/mobile-money/transaction-query/' \
  --header 'Authorization: Bearer <access-token>' \
  --header 'Content-Type: application/json' \
  --data '{
    "TransactionReference": "REPLACE_WITH_TRANSACTION_ID"
  }'

TransactionReference is the returned PesaWay TransactionID, not your ExternalReference or quote ID.

Query result Your application should
ResultCode: 0 Mark the matching payment complete and fulfil once.
ResultCode: 101 Keep pending; wait for the callback or query again with a delay.
ResultCode: 2001 Record the failed payment and show the returned reason.
code: "404.001" Check the transaction ID and credentials/environment; this is not proof of a failed charge.

Completed callback/query TransactionAmount is the local collection principal in the merchant account currency (for example KES, UGX or TZS). The legacy completion payload does not carry the FX quote fields. Retain the original order price and currency, quote rate, local amount and currency, and quote ID in your own order record. Reconcile wallet credit separately for any merchant-paid fees. See Mobile Money for the full callback and query fields.

# Retries and duplicate prevention

  • If initiation times out or the response is lost, keep the same request and ExternalReference. Query the original transaction if you have its ID; do not immediately start a new payment with a new reference.
  • Repeating an existing reference can return 403.033 with the original transaction details. Use those details to check the existing payment. An in-flight duplicate can return 409.001; wait and retry with the same reference.
  • A quote can create only one transaction. A genuinely new payment attempt needs a fresh quote and a new unique reference after you have resolved the previous attempt's outcome. A consumed quote cannot be used for the new attempt.
  • If validation rejected an expired quote before a transaction was created, obtain a fresh quote, confirm the new amount and submit the corrected payment. When uncertain whether a transaction exists, resolve that uncertainty first.

# Troubleshooting

Inspect the JSON response body as well as the HTTP status; application errors may use HTTP 200.

Symptom / code Check and correction
Receive-payment 800.001 Check valid JSON, commas, required fields and Content-Type. A missing comma after ResultsUrl can cause this response. This code is generic; if a valid request still fails, share a redacted request and timestamp with support.
Quote 400.020 Read message. Check a positive amount, supported active currencies, matching wallet currency, valid flow and an available rate.
Quote 404.001 Confirm the active merchant code belongs to the authenticated merchant and environment.
Collection 400.016 Check that the quote is fresh, unused and matches the merchant, collection flow, original amount and pricing currency. Supply MerchantCode, PricingCurrency and FxQuoteID.
Duplicate 403.033 / 409.001 Resolve the existing payment using the retry procedure above.
Customer was not prompted or provider rejected the request Check the phone number, enabled channel/provider route and returned failure reason. Ask support to confirm sandbox provider behavior.
Callback missing Query the saved transaction ID and check your callback endpoint's accessibility and logs. Do not re-charge merely because the callback is delayed.
Amount appears in local currency instead of pricing currency Expected for collection, query and callback amounts. Preserve the original price and both currencies in your order record.

For quote validation failures the authenticated endpoint returns a flat error object, for example:

{
  "code": "400.020",
  "status": "failed",
  "message": "<validation reason>"
}

# Integration acceptance checklist

Before going live, verify successful payment, customer/provider failure, quote expiry, a mismatched amount, a lost initiation response and duplicate callbacks. Confirm that one completed payment fulfils once, credits the expected local amount after fees, and is traceable back to the original order. Repeat these checks for each currency and channel you enable. Arrange any provider-backed sandbox test with support; local simulated callbacks alone do not verify actual provider payment authorization or network delivery to your callback URL.

# Crypto collection quotes

Use a crypto collection quote when the merchant wallet is denominated in fiat but the customer will pay on-chain. PayableCurrency is derived from the merchant account; the other two currency fields describe the request:

Field Meaning Example for a KES merchant paid in USDT
PricingCurrency Currency in which the merchant prices the payment. The request Amount is expressed in this currency. KES
PayableCurrency Merchant wallet currency, derived from the merchant account. KES
PaymentCurrency Currency sent by the customer through the payment rail. USDT

For crypto collections, PricingCurrency must match the merchant account currency. The quote returns PaymentAmount: the exact on-chain amount the customer must send. Do not send the fiat Amount to the blockchain address.

{
  "MerchantCode": "PHY000000001ZC6",
  "PricingCurrency": "KES",
  "Amount": "1300.00",
  "Flow": "collection",
  "PaymentCurrency": "USDT"
}

The relevant response values are:

{
  "code": "200.001",
  "FxQuoteID": "<quote-id>",
  "PricingAmount": "1300.00",
  "PricingCurrency": "KES",
  "PayableAmount": "1300.00",
  "PayableCurrency": "KES",
  "PaymentAmount": "10.0000000000",
  "PaymentCurrency": "USDT",
  "status": "success"
}

# Payouts priced in a foreign currency

For mobile-money payouts, the funding wallet and recipient payment use the same local currency. An FX quote converts the requested price into that currency. It does not convert funds from a wallet in another currency.

Scenario Current support
KES wallet, payout priced in USD, recipient paid KES Supported when the FX rate and payout route are enabled.
UGX or TZS wallet, payout priced in USD, recipient paid in that wallet currency Same flow, subject to enabled rates and payout routes.
USD wallet, recipient paid KES, UGX or TZS Not supported by this FX payout flow.

PricingCurrency describes the requested price; it does not select the wallet to debit. MerchantCode selects that wallet. Changing Currency, PayableCurrency or X-Region does not convert a USD funding wallet into a local-currency wallet. A quote request whose PayableCurrency differs from the merchant account currency is rejected with 400.020. In the FX mobile-money payout path, the account currency takes precedence over the supplied Currency, so always supply a matching currency.

# Quote and submit a payout

  1. Select an active merchant wallet in the recipient's local currency and an enabled payout channel. Ensure that wallet can cover the converted principal plus payout fees.
  2. Create an authenticated quote at /api/fx/quote/ with that MerchantCode, the original Amount, PricingCurrency and Flow: "disbursement". Omit PaymentCurrency for this mobile-money flow. PayableCurrency, if supplied, must match the wallet.
  3. Check PayableCurrency, PayableAmount and ExpiresAt. Submit the original amount and pricing currency with the quote ID to /api/v1/mobile-money/send-payment/.
  4. Save the returned transaction ID and confirm completion through the callback or transaction query before marking the payout paid. An accepted request is still processing.

For example, a KES-funded payout priced at USD 10.00 with an illustrative quote of KES 1305 pays KES 1305 to the recipient and requires KES 1305 plus fees in the KES wallet. It does not debit USD 10 from a USD wallet. For UGX, TZS or another supported local currency, use that currency's wallet, quote and enabled channel instead.

Create the quote using this body, replacing the merchant code and pricing currency with your values:

{
  "MerchantCode": "YOUR_LOCAL_CURRENCY_MERCHANT_CODE",
  "Amount": 10.00,
  "PricingCurrency": "PRICING_CURRENCY",
  "Flow": "disbursement"
}

Then submit the following request. Replace every placeholder, use the same merchant and price as the quote, and set LOCAL_CURRENCY to the quote's PayableCurrency:

curl --request POST 'https://api.sandbox.pesaway.com/api/v1/mobile-money/send-payment/' \
  --header 'Authorization: Bearer <access-token>' \
  --header 'Content-Type: application/json' \
  --data '{
    "ExternalReference": "PAYOUT-1001-ATTEMPT-1",
    "Amount": 10.00,
    "PhoneNumber": "RECIPIENT_PHONE_WITH_COUNTRY_CODE",
    "MerchantCode": "YOUR_LOCAL_CURRENCY_MERCHANT_CODE",
    "Channel": "ENABLED_PAYOUT_CHANNEL",
    "Currency": "LOCAL_CURRENCY",
    "Reason": "Supplier payout",
    "ResultsUrl": "https://merchant.example/payouts/callback",
    "PricingCurrency": "PRICING_CURRENCY",
    "FxQuoteID": "REPLACE_WITH_FRESH_DISBURSEMENT_QUOTE_ID"
  }'

Keep Amount as the original pricing amount. The converted payout principal, fees and wallet debit are in the funding wallet currency. The completion payload's TransactionAmount is the local principal; retain the original price, both currencies and quote details in your own records.

Quotes are single-use and must match the account, amount, pricing currency and disbursement flow. A collection quote cannot fund a payout. Invalid or expired payout quotes return 400.016; scheduled payments do not support FX quotes. Follow safe retries if a response is lost, and use the Mobile Money query and callback contract to resolve the original payout before starting another attempt.

# Use the quote in a payment request

Pass these fields into the existing receive-money, send-money, bank-payout, direct-collection, internal-transfer, or checkout initiation request:

Field Value
Amount The original pricing amount as a number or decimal string, exactly matching PricingAmount.
PricingCurrency The quote's PricingCurrency.
FxQuoteID The quote's FxQuoteID.

For a collection, create the quote with flow: "collection". For a payout or internal transfer, create it with flow: "disbursement". The funding and recipient currency limitation is explained in FX payouts; a quote does not enable transfers between wallets in different currencies. The quote must belong to the merchant account used by the payment and its flow, price, currency, and settlement currency must all match.

Example: extend an existing mobile-money collection request with:

{
  "Amount": 10.0,
  "PricingCurrency": "USD",
  "FxQuoteID": "c0da3f3a-7a8e-4b7a-9bbf-1e8b4b15c093"
}

The quote is consumed when the transaction is created and cannot create another transaction. A lost response does not mean the payment failed: retain the original ExternalReference and follow safe retries.

# Checkout quote endpoint

Checkout clients can obtain a collection quote without developer credentials. The checkout configuration response provides an fx_quote_token; submit it to the public checkout quote endpoint:

POST https://api.sandbox.pesaway.com/api/checkout/fx/quote/
{
  "merchant_code": "MERCHANT-001",
  "amount": 10.0,
  "pricing_currency": "USD",
  "checkout_quote_token": "<token from checkout configuration>"
}

The public Checkout response retains its lower-case data object for browser compatibility; it does not use the Developer API response shape documented above. This endpoint always creates a collection quote: it cannot create disbursement quotes. The signed token is short-lived and requests are rate-limited. Never expose authenticated API credentials in a browser application.

The public Checkout endpoint retains its lower-case browser contract. Supply its returned quote_id and pricing_currency with the normal Checkout initiation request, while retaining the pricing amount as its amount.

# Foreign-priced checkout services

When creating or updating a fixed-price checkout service, use these fields to set a foreign price:

Field Required Description
fixed_amount Yes Set to true.
pricing_currency Yes Active price currency, different from the merchant settlement currency.
pricing_amount Yes Positive fixed price in pricing_currency.

Do not set the local amount at the same time. A service may have either a local fixed amount or a foreign fixed price, never both. A foreign-priced service requires a matching FX quote at checkout; the checkout response exposes its pricing and settlement currencies plus an fx_quote_token for this purpose.

# Limits and validation

  • Quotes expire at the timestamp returned in ExpiresAt (or expires_at for public Checkout) and can be consumed only once.
  • A quote is valid only for the merchant account, flow, pricing amount, pricing currency, and settlement currency for which it was created.
  • MerchantCode is mandatory when creating a Developer API FX quote.
  • Crypto collections support FX quotes when PaymentCurrency is supplied. Scheduled payouts do not support FX quotes.
  • Authenticated quote requests accept snake_case aliases such as merchant_code, pricing_currency and payable_currency. Responses always use the flat Developer API shape shown above; only public Checkout uses the nested data response.
  • Invalid, non-finite, zero, negative, or oversized prices are rejected. A request is also rejected if no active rate exists for the currency pair.

Authenticated quote validation errors use code: "400.020", status: "failed" and a lowercase message; a missing merchant account uses 404.001. Collection quote validation uses 400.016. See the troubleshooting and retry guidance above before resubmitting.