Cross-border Status, Webhooks and Retries

Reconcile request and transaction IDs, handle lifecycle events and recover safely.

Store the sender ID, cross-border request ID, request externalReference, and each charge transaction ID separately. A request can have multiple failed charge attempts, but only one pending or successful deposit is permitted at a time.

Status lookups and identifiers

Use https://api-v2.honeycoin.app/api/b2b/cross-border/requests as {crossBorderBaseUrl}, or the corresponding sandbox base URL.

IdentifierHow to obtain itHow to use it
Sender IDCreate sender response data.idPass as senderId when creating a request
Request IDCreate request response data.idFetch request, charge, close, or list its transactions
Charge transaction IDCharge response data.transactionIdFetch transaction to inspect data.chargeStatus
Request external referenceYour create-request bodyReconcile with crossborder_updated.data.externalReference

GET {crossBorderBaseUrl}/{id} expects a request ID. GET {crossBorderBaseUrl}/transactions/{id} expects a transaction ID. Neither lookup substitutes an external reference for the ID.

Request lifecycle

Request statusMeaning and next action
pending_chargeRequest created; initiate the charge after the customer confirms
charge_initiatedCollection started; wait for the charge result
charge_successfulSource funds collected; wait for the CNY payout
charge_failedCollection failed; reconcile the related transactions before retrying or closing
payout_initiatedBeneficiary payout is being processed
payout_successfulBeneficiary payout completed successfully
payout_failedBeneficiary payout failed; contact support and do not initiate another source charge
refund_inititatedRefund in progress; this is the exact API spelling
refund_successfulRefund completed; this does not indicate a successful beneficiary payout
refund_failedRefund failed; contact support
closedRequest closed; further charges are unavailable. Closing itself is not payment or refund confirmation

Transaction chargeStatus uses a separate set of lowercase values: pending, successful, and failed. A successful crossborder-deposit proves collection success only. Confirm request payout_successful to establish transfer completion.

Webhook events

Configure webhooks for the selected environment and validate webhook secrets. Acknowledge accepted deliveries promptly, process them asynchronously, and tolerate retries and delivery reordering. Use the resource lookup to reconcile stale or ambiguous updates.

The initial local charge

Initiating a charge creates a transaction with type crossborder-deposit. The transaction_created event identifies that charge using data.transactionId. Example:

{
  "event": "transaction_created",
  "data": {
    "transactionId": "charge_example_001",
    "publicKey": "ACCOUNT_PUBLIC_KEY",
    "amount": 1000,
    "currency": "KES",
    "type": "crossborder-deposit",
    "status": "pending",
    "externalReference": null
  },
  "timestamp": "2026-09-11T08:00:00.000Z"
}

The collection result arrives as transaction_updated for that same transaction:

{
  "event": "transaction_updated",
  "data": {
    "transactionId": "charge_example_001",
    "publicKey": "ACCOUNT_PUBLIC_KEY",
    "amount": 1000,
    "currency": "KES",
    "type": "crossborder-deposit",
    "status": "successful",
    "method": "momo",
    "externalReference": null
  },
  "timestamp": "2026-09-11T08:01:00.000Z"
}

A charge transaction's external reference is not guaranteed to contain the request's external reference. Link it using the transaction ID returned at initiation, or list the request's transactions. Payout transactions use type: crossborder-payout.

The cross-border request

The crossborder_updated event carries the request ID in data.id, together with the request status and its external reference. It does not carry the charge ID in that field.

{
  "event": "crossborder_updated",
  "data": {
    "id": "request_example_001",
    "publicKey": "ACCOUNT_PUBLIC_KEY",
    "status": "payout_successful",
    "externalReference": "cny-transfer-001"
  },
  "timestamp": "2026-09-11T09:00:00.000Z"
}

The same shape is used for other request status changes, such as charge_successful or payout_failed. Read the request for its saved quote and data.beneficiary, including recipient identity and data.beneficiary.payoutDetails. Do not deduplicate all request events by request ID alone: the same ID appears in multiple status changes. Make each business effect idempotent and reconcile the current resource state before applying conflicting updates.

Retry and recovery rules

SituationAction
Create call times outRetain the same external reference. Reconcile before creating a replacement; list your requests and match their returned externalReference if the request ID was not saved
409 DUPLICATE_EXTERNAL_REFERENCEFind the existing request. Do not change the reference just to bypass the duplicate check
202 with “Request is being processed”The call has no resource ID yet. Wait, then reconcile or repeat the same call; do not assume payment succeeded
Charge call times out or errorsFetch the request and list its transactions. A charge may already exist even if the HTTP call failed
Pending or successful deposit existsDo not start another charge; reconcile its result
Request is charge_failed, and all previous deposits failedRetry POST {crossBorderBaseUrl}/charges/{requestId} with the same request ID and no body. Retain each attempt's transaction ID
Immediate retry returns the previous responseRepeated calls can replay a recent response. Back off and reconcile before expecting a new attempt
payout_failed or refund failureContact support with the request ID, transaction IDs, and external reference. Do not collect the source funds again

Request details cannot be edited through these endpoints. A retry uses the saved quote and collection details. If those details need changing, first establish that there is no pending or successful payment; close the unpaid request if eligible and create a new request with a new external reference.

An ineligible charge may return HTTP 400 with E-API-CBR-T.S-26 in production, or E-API-SANDBOX-CBR-CH.S-44 / E-API-SANDBOX-CBR-CH.S-52 in sandbox. State conflicts can also return 409 with CROSS_BORDER_CHARGE_IN_PROGRESS, CROSS_BORDER_REQUEST_ALREADY_CHARGED, CROSS_BORDER_REQUEST_NOT_CHARGEABLE, or CROSS_BORDER_REQUEST_CLOSED. Reconcile the saved request and transaction statuses in all these cases.

Pagination

All three list endpoints return newest records first and accept limit (1–100, default 20), after, and before. Use one cursor direction per call. Treat cursors as opaque and URL-encode them. Request the next page only when hasNext is true, using nextCursor as after; use prevCursor as before when hasPrev is true.

EndpointRecordsPaginationOther filters
GET {crossBorderBaseUrl}/sendersdata.sendersdata.paginationNone
GET {crossBorderBaseUrl}datapaginationstatus, senderId
GET {crossBorderBaseUrl}/{requestId}/transactionsdata.transactionspaginationstatus, externalReference

For request listing, status is a request lifecycle value. For transaction listing, status is a lowercase transaction chargeStatus, and externalReference filters the transaction's own reference. Request listing does not accept an externalReference filter. Preserve the same filters as you move through pages.

Example empty transaction page:

{
  "success": true,
  "data": { "transactions": [] },
  "pagination": {
    "nextCursor": null,
    "prevCursor": null,
    "hasNext": false,
    "hasPrev": false,
    "limit": 20
  }
}

Close an unpaid request

Call Close a Cross-border Request: POST {crossBorderBaseUrl}/{requestId}/close, with no body.

Closing is permitted only when the request is pending_charge, charge_initiated, or charge_failed, and it has no pending or successful deposit and no payout transaction. The status alone does not establish eligibility. Closing an active charge is rejected; this endpoint does not cancel an in-flight payment or refund collected funds.

{
  "success": true,
  "message": "Cross border request closed successfully.",
  "data": {
    "id": "request_example_001",
    "status": "closed",
    "pendingChargesClosed": 0
  }
}

An already closed request returns 409 CROSS_BORDER_REQUEST_ALREADY_CLOSED. Other lifecycle conflicts return 409 CROSS_BORDER_REQUEST_CANNOT_BE_CLOSED. Re-fetch the request before deciding the next action.