Implementation Guide Private Preview
Coordinate a cross-border financial operation
Connect a local collection, a conversion and a destination payout as related legs with their own state.
The rule to remember: a cross-border transfer is several operations, each with its own provider and its own final state. Link them with your own reference. Start a leg only when the leg before it is final and successful, and retry a leg only according to that leg's own finality.
The job
A sender in Colombia pays 1,000,000 COP from their bank account. The value is converted, and a beneficiary in another country receives local currency in their bank account. Three providers may be involved: one collects, one converts or supplies liquidity, one pays out. Each can succeed, fail or go quiet independently, and your operations team has to be able to say where the money is at any point.
Before you start
- An API key for the sandbox. Sandbox access is provided during evaluation.
- A pay-in route for the collection rail (here PSE) and a payout route for the destination rail.
- A cross-border provider for your corridor. In the catalog today: Buda, Vita Wallet and Mesa de Pagos Private Preview, and Bridge In Development. Corridor coverage is listed on the integrations page and confirmed during evaluation.
- Maturity by leg: collection and payout are in production. Quotes, conversion and cross-border payout are in Private Preview. Quote execution and cross-border payout routes are not in the public API reference; they are shared during evaluation.
- Your own operation record, with one reference you put on every leg.
Flow
- Local collection Pay-in
- Collection finality completed, confirmed
- Conversion / liquidity Quote, then execution
- Destination payout Payout on the local rail
- Provider outcome Final status per leg
- State and evidence Ledger and explain
Implementation
1. Create your operation record and reference
There is no single Orangepill id that links a collection, a conversion and a payout. Create the link yourself:
one record per transfer in your database, with a reference such as op-2026-10-11-0187. Put it in
the metadata of every leg and derive each leg's idempotency key from it where the route accepts one.
2. Collect locally Production
The collection is a normal pay-in. 1,000,000.00 COP is "100000000" in minor units.
curl -X POST "$ORANGEPILL_API/v4/checkout/payments" \
-H "Authorization: Bearer $ORANGEPILL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": "100000000",
"currency": "COP",
"product_key": "bank_transfer.pse",
"channel_type": "redirect",
"return_url": "https://send.example.com/transfers/op-2026-10-11-0187",
"metadata": { "operation_ref": "op-2026-10-11-0187", "leg": "collection" },
"callback": { "url": "https://send.example.com/webhooks/orangepill", "secret": "<your webhook signing secret>" }
}'
Store the payment_id on your operation record. Follow the payment as in
Accept a local bank-transfer payment.
This route has no idempotency key, so do not resend it after a timeout without first looking for the payment.
3. Wait for collection finality
Move on only when GET /v4/payments/{paymentId}/status returns completed. A
webhook is evidence that something happened; the status is what you act on. While the collection is
pending, do not quote for execution and do not pay out.
4. Convert or source liquidity Private Preview
Ask for a quote for the collected amount. The quote is a priced offer with an expiry. Quote amounts are decimal strings in major units. The beneficiary is registered beforehand, and that setup is shared during evaluation.
curl -X POST "$ORANGEPILL_API/v4/swaps/quotes" \
-H "Authorization: Bearer $ORANGEPILL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"sourceCurrency": "COP",
"destinationCurrency": "<destination currency>",
"sourceAmount": "1000000",
"payoutMethod": "bank_account",
"beneficiaryId": "<beneficiary id>",
"intent": { "purpose": "family_support" }
}' {
"id": "7d21c0e5-…",
"status": "…",
"providerId": "…",
"sourceCurrency": "COP",
"destinationCurrency": "<destination currency>",
"sourceAmount": "1000000",
"destinationAmount": "…",
"rate": "…",
"feeAmount": "…",
"feeCurrency": "…",
"expiresAt": "2026-10-11T16:05:00Z",
"payoutMethod": "bank_account"
}
Keep the quote id on your operation record. Execute only before expiresAt; after it, ask for a new
quote. The execution itself, and how it is triggered, is not in the public contract yet. Conceptually it is its
own leg with its own status: created, executing, then completed,
failed or cancelled.
5. Pay out at the destination Production
When the conversion is complete, the payout is a separate operation with its own idempotency key. For a payout
on a rail you route through Orangepill, use POST /v4/payouts as in
Build your first payout. Cross-border
payout routes offered through the cross-border providers are shared during evaluation.
curl -X POST "$ORANGEPILL_API/v4/payouts" \
-H "Authorization: Bearer $ORANGEPILL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"idempotency_key": "op-2026-10-11-0187:payout",
"product_key": "<payout product for the destination rail>",
"amount": { "value": "<amount in minor units>", "currency": "<destination currency>" },
"recipient": { "scheme": "<scheme>", "value": "<beneficiary identifier>" },
"metadata": { "operation_ref": "op-2026-10-11-0187", "leg": "payout" }
}' 6. Follow each leg to its own outcome
Your operation record advances one stage at a time. Run this on a schedule and from webhooks, and make each step safe to run twice.
// Your operation record drives the legs. Each leg starts only after the previous one is final and successful.
async function advance(op) {
switch (op.stage) {
case 'collecting': {
const { status } = await getPaymentStatus(op.paymentId);
if (status === 'completed') return save(op, { stage: 'converting' });
if (status === 'failed' || status === 'cancelled') return save(op, { stage: 'collection_failed' });
return; // pending / processing: wait
}
case 'converting': {
const ex = await getConversionState(op); // execution status: created | executing | completed | failed | cancelled
if (ex.status === 'completed') return save(op, { stage: 'paying_out' });
if (ex.status === 'failed' || ex.status === 'cancelled') return save(op, { stage: 'needs_review' });
return;
}
case 'paying_out': {
const p = await getPayout(op.payoutId);
if (p.status === 'completed') return save(op, { stage: 'done' });
if (p.status === 'failed' && p.errorCode === 'RETRY_EXHAUSTED') return save(op, { stage: 'needs_review' }); // unknown
if (p.status === 'failed' || p.status === 'cancelled') return save(op, { stage: 'payout_failed' });
return; // pending / processing: wait, alert if it runs long
}
}
}
Note the payout case: failed with RETRY_EXHAUSTED means Orangepill stopped checking the
provider without a final answer. The money may have moved. It goes to review, not to "send again".
7. Keep state and evidence explainable
For each leg, keep the Orangepill id, the provider id, the last status and when you saw it. Ledger entries and the financial explain graph give the internal side.
# Collection leg
curl "$ORANGEPILL_API/v4/ledger/entries/by-reference?referenceType=payment&referenceId=$PAYMENT_ID" \
-H "Authorization: Bearer $ORANGEPILL_API_KEY"
# Payout leg (journals are posted when a wallet-funded payout completes)
curl "$ORANGEPILL_API/v4/ledger/entries/by-reference?referenceType=payout&referenceId=$PAYOUT_ID" \
-H "Authorization: Bearer $ORANGEPILL_API_KEY"
# How the collected payment reached its state
curl "$ORANGEPILL_API/v4/financial/explain?subjectType=payment&subjectId=$PAYMENT_ID" \
-H "Authorization: Bearer $ORANGEPILL_API_KEY" When a provider's report and Orangepill's state disagree, the leg is not settled until someone reconciles it against the provider's statement.
What can go wrong
| Leg | What happened | What to do |
|---|---|---|
| Collection | Still pending | Wait. Do not quote for execution or pay out. |
| Collection | failed | The operation stops. The sender can start a new payment. |
| Conversion | Quote expired | Ask for a new quote. Nothing was executed. |
| Conversion | Execution failed after collection completed | The collected money is still on your side. Review before converting again or returning it. |
| Payout | failed with INITIATE_FAILED | The request was not accepted, and the payout never started. A new payout with a new key is safe once the cause is fixed. |
| Payout | Request timed out, payout pending | Unknown. Send the same idempotency_key or read the payout. Never create a second one. |
| Payout | failed with RETRY_EXHAUSTED | Unknown, not a decline. Confirm with the provider before any new payout. |
What Orangepill guarantees today
- Each leg has its own status and final states you can read.
- A payout
idempotency_keycreates at most one payout in your tenant. - A provider-confirmed collection posts a journal that references the payment. A wallet-funded payout posts one that references the payout when it completes.
- Quotes carry an expiry, rate and fee you can store with the operation.
What Orangepill does not guarantee
- A single Orangepill id, or atomic execution, across the legs. One leg completing does not make the next one happen or succeed.
- Rolling back an earlier leg automatically when a later one fails.
- Payout failover to another provider. It is in development.
- Every corridor. Coverage depends on the providers listed on the integrations page.
- Public contracts for quote execution and cross-border payout. They are shared during evaluation.
Production considerations
- Production has its own credentials, routes and cross-border provider configuration. Nothing carries over from sandbox.
- Persist the operation reference and, per leg, the Orangepill id, provider id, idempotency key, last status and timestamps.
- Alert on any leg that stays non-final past its normal window, and on operations in review.
- Decide before launch what happens to collected money when a later leg cannot complete, and who approves it.
- Test in sandbox: a failed collection, an expired quote, a payout rejected by the provider and a payout timeout.
- Your operations team should be able to open one transfer and see every leg, its provider and its evidence.
CoraSend runs cross-border financial execution on Orangepill: local pay-ins, cross-border movement and payout coordinated through the Orangepill runtime. This guide describes general patterns; not every concept here is claimed for CoraSend's deployment.
Plan your corridor with us.
Cross-border providers, corridors and the routes for conversion and payout are confirmed during evaluation.