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.

14 min to implement · Verified against the v4 API on 2026-10-11

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

  1. Local collection Pay-in
  2. Collection finality completed, confirmed
  3. Conversion / liquidity Quote, then execution
  4. Destination payout Payout on the local rail
  5. Provider outcome Final status per leg
  6. State and evidence Ledger and explain
One operation, separate legs. A final leg does not make the next leg final.

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.

Collection leg
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.

Create a quote
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" }
  }'
Response (abridged)
{
  "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.

Payout leg
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.

Advancing an operation (JavaScript)
// 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.

Evidence per leg
# 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_key creates 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.