Testing Cross-border Transfers

Test beneficiaries, required documents, approval and rejection, collection, payout, and recovery in sandbox.

Use sandbox credentials and https://api-v2.honeycoin.app/api/sandbox/b2b/cross-border/requests as {crossBorderBaseUrl}. Generate a sandbox bearer token at POST https://api-v2.honeycoin.app/api/sandbox/b2b/auth/generate-bearer-token. Sandbox simulates payments; no real money is charged or paid out.

Create a sandbox sender and sandbox beneficiary, create a request with their IDs, purpose, and subPurpose, complete any required document review, obtain a quote, and then initiate its charge with quoteId. Use a new request externalReference for each separate scenario. The sender, beneficiary, and request must belong to the same sandbox account.

Sandbox requires both purpose and subPurpose for every new request, using the same 19 subpurpose values as production. Sandbox still validates sender data, beneficiary identity, payment purpose, source currency, collection availability and operator details. Create the beneficiary first with its identity, names, and payoutDetails; pass beneficiaryId on the request. Both senders and beneficiaries use idNumber for individuals or registrationNumber for businesses; the other number field is forbidden. Test values choose an outcome only after the request is accepted; they do not activate an unavailable currency, country or method. The source currency must be KES or NGN and the receiver currency must be CNY.

End-to-end checklist

Use sandbox credentials for every step. All paths below are relative to {crossBorderBaseUrl}.

  1. Create or reuse a sender with POST /senders; save data.id as senderId.
  2. Create or reuse a beneficiary with POST /beneficiaries; save data.id as beneficiaryId. Use a bank beneficiary to test required documents, or Alipay to test a document-free payout.
  3. Create the request with both IDs, required purpose and subPurpose, a unique externalReference, and the successful payment test values below. Save the request ID and inspect requiredDocuments.
  4. If documents are required, upload them through POST /documents and call POST /documents/submit with outcome: documents_approved. Wait for pending_charge. If requiredDocuments is empty, skip document submission.
  5. Create a quote with POST /quotes, using the request's senderAmount, senderCurrency, and receiverCurrency. Save data.id as quoteId, display the amounts, and check expiresAt.
  6. Initiate POST /charges/{requestId} with a JSON body containing quoteId. Save the returned transactionId and confirm collection success through its lookup or webhook.
  7. Track the request and payout until payout_successful. Sandbox then closes the request; if the lookup already returns closed, verify the related payout transaction rather than treating closure alone as delivery confirmation.

Repeat the document step with outcome: documents_rejected to test replacement and resubmission before charging. The scenarios below cover failures and retries.

Mobile money to CNY

Use the collection country's numeric calling code followed by the scenario digits. Send the number as a string without +, spaces or a local leading zero. Match chargeDetails.country, chargeCurrency, and momoOperatorId to the same available collection market.

OutcomechargeDetails.phoneNumber
Collection success<CALLING_CODE>712345678
Document approvalUpload required types and submit with outcome: documents_approved
Document rejection and correctionSubmit with documents_rejected, replace the file using the same type, then submit with documents_approved
Missing documentsSubmit before every required group is satisfied
No automatic review outcomeSubmit valid documents without outcome
No documents requiredCreate an Alipay request whose requiredDocuments is empty
Collection failure<CALLING_CODE>787654321

For example, the Kenya success fixture is 254712345678 with country: KE, chargeCurrency: KES, and momoOperatorId: mpesa. The sender's separate phoneNumber still uses the + format required by the sender API.

For bank and Alipay payouts, use these beneficiary account numbers as strings:

OutcomepayoutDetails.payoutMethod.accountNumber on beneficiary creation
Payout success1234567890
Payout failure0987654321

This sandbox Alipay account number is a test fixture. For production, supply the real recipient account identifier. Alipay requires an individual sender in both environments.

Success example

Create an individual Alipay beneficiary with POST /beneficiaries, using firstName, lastName, and success account number 1234567890. Do not send identity document fields for Alipay; see the beneficiary guide. For the document-review scenario, instead create a bank beneficiary with a real CN bank code and the same success fixture. Reuse a matching active beneficiary if the account number is already registered.

Replace both IDs below. The following request uses successful mobile money collection:

{
  "senderId": "sender_example_001",
  "beneficiaryId": "beneficiary_example_001",
  "purpose": "goods_payment",
  "subPurpose": "electronics_and_home_appliances",
  "amount": 1000,
  "chargeCurrency": "KES",
  "receiverCurrency": "CNY",
  "externalReference": "sandbox-cny-success-001",
  "requestType": "transfer",
  "chargeDetails": {
    "method": "momo",
    "firstName": "Amina",
    "lastName": "Otieno",
    "country": "KE",
    "email": "[email protected]",
    "phoneNumber": "254712345678",
    "momoOperatorId": "mpesa"
  }
}

Send this body to POST {crossBorderBaseUrl} and save data.id. Inspect requiredDocuments and status. The new request returns quote: null. Resolve any document requirements as described below. Once the status is pending_charge, obtain a quote and initiate the charge with its ID:

{
  "senderAmount": 1000,
  "senderCurrency": "KES",
  "receiverCurrency": "CNY"
}

Send this body to POST {crossBorderBaseUrl}/quotes. Save its data.id as quoteId, check expiresAt, and confirm the quoted amounts. Then call POST {crossBorderBaseUrl}/charges/{requestId} with:

{
  "quoteId": "quote_example_001"
}

Quotes belong to the selected account and environment and are single-use. A fresh quote is required for an eligible retry after an earlier quote was consumed or expired. See Cross-border Quotes.

Document review scenarios

The normal typed upload and anyOf checks apply in sandbox. Use a nonempty file in an accepted format; naming an outcome does not bypass missing-document checks. Upload with multipart POST /documents: crossBorderRequestId, type, and file.

For example, upload an invoice using your sandbox request ID:

curl --request POST 'https://api-v2.honeycoin.app/api/sandbox/b2b/cross-border/requests/documents' \
  --header 'Authorization: Bearer YOUR_SANDBOX_BEARER_TOKEN' \
  --form 'crossBorderRequestId=request_example_001' \
  --form 'type=proforma_invoice' \
  --form '[email protected];type=application/pdf'

After uploading all required groups, call POST /documents/submit with JSON:

{
  "crossBorderRequestId": "request_example_001",
  "outcome": "documents_approved"
}
outcomeExpected behavior
documents_approvedSubmission → approval → pending_charge; customer can initiate collection
documents_rejectedSubmission → rejection with rejectionReason: Invalid documents submitted.
OmittedRemains documents_submitted awaiting review; no automatic approval

outcome is supported only by sandbox submission. Use these exact strings, not approved, accepted, or rejected. It is not a request-creation field and does not apply to routes with no document requirements.

To test recovery, submit with documents_rejected, replace the file using the same type, then submit with documents_approved. Replacement alone leaves the status rejected. To test partial requirements, submit before satisfying every group and expect 422 MISSING_DOCUMENTS.

If the response fails after submission, fetch the request. Sandbox permits repeating submission while it is documents_submitted to retry an outcome. If it is already approved or pending_charge, continue from that state; do not restart collection. A conflicting status returns OUTCOME_NOT_POSSIBLE; reconcile before retrying. See document rules.

The sandbox automatically initiates a simulated payout after the charge succeeds. Check the charge's chargeStatus: successful separately from request status: payout_successful. The sandbox then closes the successful request. Reconcile the payout transaction if GET already returns closed. Production payout processing follows HoneyCoin review and does not have the same timing as sandbox simulation.

Sandbox bank collections

Production and sandbox both accept chargeDetails.method: momo and chargeDetails.method: bank, subject to account collection availability. The values below simulate bank collection outcomes in sandbox.

Replace the full chargeDetails object with the example below. Keep the senderId, beneficiaryId, purpose, subPurpose, currencies, amount and reference fields required by the create-request API. Omit phoneNumber and momoOperatorId from bank chargeDetails; those fields are forbidden for bank charges. firstName, lastName, country, and email remain required.

{
  "method": "bank",
  "firstName": "Amina",
  "lastName": "Otieno",
  "country": "NG",
  "email": "[email protected]"
}

For this example use chargeCurrency: NGN and an account with the bank collection route available. email: [email protected] simulates collection success; [email protected] simulates failure. These email fixtures apply to bank collections, not mobile money outcomes.

After charge initiation, read the charge transaction to inspect its virtualAccount. The sandbox charge response contains the transaction ID; do not require virtual account details to appear in the initial response, and do not send real funds to sandbox bank details.

Scenarios to verify

TestSetupExpected behavior
End-to-end successSuccess collection fixture and success beneficiary accountDocuments pass where required; charge succeeds; payout succeeds and request then closes
Document approvalUpload required types and submit with outcome: documents_approvedRequest becomes eligible for charging at pending_charge
Document rejection and correctionSubmit with documents_rejected, replace the file using the same type, then submit with documents_approvedRejection includes a reason; corrected submission can proceed to pending_charge
Missing documentsSubmit before every required group is satisfied422 MISSING_DOCUMENTS; no charge occurs
No automatic review outcomeSubmit valid documents without outcomeRemains documents_submitted; charging is blocked
No documents requiredCreate an Alipay request whose requiredDocuments is emptyStarts at pending_charge; skip document upload/submission
Collection failureFailure collection fixtureRequest reaches charge_failed; no successful beneficiary payout
Missing quote IDInitiate a charge with an empty JSON body400 VALIDATION_ERROR; no charge is initiated
Expired quoteCreate an unused sandbox quote, wait at least 2 hours and until after its returned expiresAt, then initiate a charge on an eligible request422 QUOTE_EXPIRED; obtain a fresh quote
Consumed quoteReuse a quote after a failed charge, once the request is eligible for retry422 QUOTE_ALREADY_CONSUMED; reconcile and obtain a fresh quote
Mismatched quoteCreate a quote with a different amount or supported currency than the request422 QUOTE_MISMATCH; use the request's exact amount and currencies
Payout failureSuccess collection fixture and beneficiary 0987654321Collection succeeds; request reaches payout_failed; do not recharge
Duplicate request referenceRepeat an existing reference with changed request details409 DUPLICATE_EXTERNAL_REFERENCE; an identical recent call may replay its original response
Business sender with AlipayBusiness senderId, Alipay payout422 UNSUPPORTED_METHOD
Missing purpose or sub-purposeOmit either field for any purpose, including salary or family support400 VALIDATION_ERROR
Invalid sub-purposeSend null, an empty string, an unknown value, or the old clothes_bags_shoees spelling400 VALIDATION_ERROR; use an exact accepted sub-purpose
Missing beneficiary IDSupply inline beneficiary instead of beneficiaryId400 VALIDATION_ERROR; a saved beneficiary ID is required
Wrong sender or beneficiary identity numberOmit the number field for the selected type, or include the other type's field400 VALIDATION_ERROR; use idNumber for an individual and registrationNumber for a business
Duplicate chargeCharge a request that already has a pending or successful depositA replay or state conflict; no second active charge
Close unpaid requestCreate a request and close it before chargingclosed with pendingChargesClosed: 0
Close paid or active requestAttempt to close while a deposit is pending/successful or a payout exists409 CROSS_BORDER_REQUEST_CANNOT_BE_CLOSED
Retry a closed requestInitiate its chargeRejected; no new charge
Sender deletionDelete an unused sender and fetch it404 SENDER_NOT_FOUND; it disappears from active listings
PaginationCreate enough records for multiple pagesUse each endpoint's documented cursor envelope and preserve filters

A retry on a failed request reuses its saved charge details, so a failure test number or email continues to select failure. Create a separate request with a new external reference to test success with different collection details.

Use request and transaction lookups to verify persisted outcomes, and test webhook handling for both transaction_updated and crossborder_updated. Do not infer completion from the initiation response or from elapsed time.