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}.
- Create or reuse a sender with
POST /senders; savedata.idassenderId. - Create or reuse a beneficiary with
POST /beneficiaries; savedata.idasbeneficiaryId. Use a bank beneficiary to test required documents, or Alipay to test a document-free payout. - Create the request with both IDs, required
purposeandsubPurpose, a uniqueexternalReference, and the successful payment test values below. Save the request ID and inspectrequiredDocuments. - If documents are required, upload them through
POST /documentsand callPOST /documents/submitwithoutcome: documents_approved. Wait forpending_charge. IfrequiredDocumentsis empty, skip document submission. - Create a quote with
POST /quotes, using the request'ssenderAmount,senderCurrency, andreceiverCurrency. Savedata.idasquoteId, display the amounts, and checkexpiresAt. - Initiate
POST /charges/{requestId}with a JSON body containingquoteId. Save the returnedtransactionIdand confirm collection success through its lookup or webhook. - Track the request and payout until
payout_successful. Sandbox then closes the request; if the lookup already returnsclosed, 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.
| Outcome | chargeDetails.phoneNumber |
|---|---|
| Collection success | <CALLING_CODE>712345678 |
| Document approval | Upload required types and submit with outcome: documents_approved |
| Document rejection and correction | Submit with documents_rejected, replace the file using the same type, then submit with documents_approved |
| Missing documents | Submit before every required group is satisfied |
| No automatic review outcome | Submit valid documents without outcome |
| No documents required | Create 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:
| Outcome | payoutDetails.payoutMethod.accountNumber on beneficiary creation |
|---|---|
| Payout success | 1234567890 |
| Payout failure | 0987654321 |
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"
}outcome | Expected behavior |
|---|---|
documents_approved | Submission → approval → pending_charge; customer can initiate collection |
documents_rejected | Submission → rejection with rejectionReason: Invalid documents submitted. |
| Omitted | Remains 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
| Test | Setup | Expected behavior |
|---|---|---|
| End-to-end success | Success collection fixture and success beneficiary account | Documents pass where required; charge succeeds; payout succeeds and request then closes |
| Document approval | Upload required types and submit with outcome: documents_approved | Request becomes eligible for charging at pending_charge |
| Document rejection and correction | Submit with documents_rejected, replace the file using the same type, then submit with documents_approved | Rejection includes a reason; corrected submission can proceed to pending_charge |
| Missing documents | Submit before every required group is satisfied | 422 MISSING_DOCUMENTS; no charge occurs |
| No automatic review outcome | Submit valid documents without outcome | Remains documents_submitted; charging is blocked |
| No documents required | Create an Alipay request whose requiredDocuments is empty | Starts at pending_charge; skip document upload/submission |
| Collection failure | Failure collection fixture | Request reaches charge_failed; no successful beneficiary payout |
| Missing quote ID | Initiate a charge with an empty JSON body | 400 VALIDATION_ERROR; no charge is initiated |
| Expired quote | Create an unused sandbox quote, wait at least 2 hours and until after its returned expiresAt, then initiate a charge on an eligible request | 422 QUOTE_EXPIRED; obtain a fresh quote |
| Consumed quote | Reuse a quote after a failed charge, once the request is eligible for retry | 422 QUOTE_ALREADY_CONSUMED; reconcile and obtain a fresh quote |
| Mismatched quote | Create a quote with a different amount or supported currency than the request | 422 QUOTE_MISMATCH; use the request's exact amount and currencies |
| Payout failure | Success collection fixture and beneficiary 0987654321 | Collection succeeds; request reaches payout_failed; do not recharge |
| Duplicate request reference | Repeat an existing reference with changed request details | 409 DUPLICATE_EXTERNAL_REFERENCE; an identical recent call may replay its original response |
| Business sender with Alipay | Business senderId, Alipay payout | 422 UNSUPPORTED_METHOD |
| Missing purpose or sub-purpose | Omit either field for any purpose, including salary or family support | 400 VALIDATION_ERROR |
| Invalid sub-purpose | Send null, an empty string, an unknown value, or the old clothes_bags_shoees spelling | 400 VALIDATION_ERROR; use an exact accepted sub-purpose |
| Missing beneficiary ID | Supply inline beneficiary instead of beneficiaryId | 400 VALIDATION_ERROR; a saved beneficiary ID is required |
| Wrong sender or beneficiary identity number | Omit the number field for the selected type, or include the other type's field | 400 VALIDATION_ERROR; use idNumber for an individual and registrationNumber for a business |
| Duplicate charge | Charge a request that already has a pending or successful deposit | A replay or state conflict; no second active charge |
| Close unpaid request | Create a request and close it before charging | closed with pendingChargesClosed: 0 |
| Close paid or active request | Attempt to close while a deposit is pending/successful or a payout exists | 409 CROSS_BORDER_REQUEST_CANNOT_BE_CLOSED |
| Retry a closed request | Initiate its charge | Rejected; no new charge |
| Sender deletion | Delete an unused sender and fetch it | 404 SENDER_NOT_FOUND; it disappears from active listings |
| Pagination | Create enough records for multiple pages | Use 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.
Updated 5 days ago
