US Remittance Integration Guide

How do I send cross-border remittances from US customers using Cybrid-integrated payouts?

US Remittance lets US individuals send cross-border remittances using Cybrid-integrated payouts. Your customer
funds a US dollar balance from their own bank account, Cybrid converts it to a stablecoin, and a remittance plan
delivers the funds to a recipient abroad in the local currency — all through a single API integration.

Under the hood, the customer funds a USD fiat account (a US bank linked via Plaid, funded by an ACH pull),
converts USD to USDC_SOL in a trading account, then a remittance plan pays out to the recipient's foreign bank
account over the local rail. Senders are onboarded as individuals with KYC, and recipients are tracked as
verified counterparties.

India (INR over IFSC) is the example corridor used throughout. Other corridors are available and follow the
same flow with corridor-specific fields.

At a high level, you will:

  1. Onboard and verify the customer with KYC.
  2. Create the USD fiat account, the USDC_SOL trading account, and the customer's deposit bank account.
  3. Link the customer's US bank account and fund the USD account by ACH pull.
  4. Convert USD to USDC_SOL.
  5. Create and verify the recipient counterparty and their foreign bank account.
  6. Create and execute the remittance plan to pay out.

Flow of funds

Figure: US remittance flow of funds.

Funds move left to right across the Cybrid boundary. The customer's external US bank account (linked via Plaid)
is pulled over ACH into their USD fiat account inside Cybrid, where KYC and the deposit bank account are held. A
trade settles the USD into a USDC_SOL trading account, and a remittance plan converts and pays out to the
recipient's foreign external bank account. The recipient is tracked as a verified individual counterparty.

Flow of funds diagram: US customer funds USD via Plaid-linked ACH, converts USD to USDC_SOL, then a remittance plan pays out to a recipient's foreign bank account

Arrow colors in the diagram: green is fiat movement, orange is crypto movement, blue is trade settlement, and
tan is a book transfer.


Table of Contents

  1. Prerequisites
  2. Customer onboarding
  3. Create accounts
  4. Create the deposit bank account
  5. Link the bank account
  6. Fund the USD account
  7. Convert USD to USDC_SOL
  8. Create the recipient counterparty
  9. Create the recipient external bank account
  10. Create and execute the remittance plan
  11. Webhooks
  12. Additional resources

1. Prerequisites

Before integrating the US Remittance flow, configure your sandbox bank with the required features.

Bank configuration

When creating your bank in the sandbox environment, configure these settings:

Customer type support:

Enable individual customers:

  • Individual customers (individual_customers)

Fiat account assets:

Enable the fiat currency for the funding country:

  • USD

Trading symbols:

Enable the USDC_SOL-USD trading pair for the USD to USDC_SOL conversion.

ℹ️

Enable the payout symbols for your corridors

Contact Cybrid support to enable the payout symbols for the payout corridors you require (for
example, INR-USDC_SOL). Confirm corridor support via supported_payout_symbols.

ℹ️

Supported corridors for C2C remittances

This guide implements a consumer-to-consumer (C2C) remittance. For the full list of destination
corridors and the participant types each supports, see
Supported Corridors.

⚠️

Register your Plaid redirect URI

Register your redirect URI with Cybrid support before initiating Plaid workflows. Contact support
with your sandbox and production redirect URIs. HTTP redirect URIs work in sandbox — for local
development use http://localhost:4200/bank-account-connect, which is preconfigured. Production
requires HTTPS.


2. Customer onboarding

Create the customer, then verify their identity with KYC through the Persona SDK.

Create the customer

Endpoint: POST /api/customers
Reference: Create Customer

Set type to individual. Only type is required at creation.

POST /api/customers
Content-Type: application/json
Authorization: Bearer YOUR_TOKEN

{
  "type": "individual",
  "email_address": "[email protected]",
  "phone_number": "555-0100"
}
{
  "guid": "customer_guid",
  "type": "individual",
  "state": "storing",
  "created_at": "2026-07-19T12:00:00.000Z",
  "updated_at": "2026-07-19T12:00:00.000Z"
}
ℹ️

PII fields are optional at creation

Not all fields are required to create a customer. Email and phone help identify customers beyond
the customer_guid. Do not provide the customer's name at creation — Persona collects it during
KYC, and the legal name returned by Persona becomes the customer's name on file. This
Persona-captured name is also used downstream for bank account ownership verification.

Customer states

StateDescription
storingPlatform is storing customer details
unverifiedCustomer created, awaiting identity verification
verifiedCustomer identity verified
rejectedCustomer verification rejected
frozenCustomer account frozen

Poll GET /api/customers/{customer_guid} until state transitions to unverified.

ℹ️

Polling interval

Use exponential backoff starting at 1 second. In sandbox, state transitions typically complete
within seconds. In production, some operations may take longer.

Verify the customer

Endpoint: POST /api/identity_verifications
Reference: Create Identity Verification

Create the Persona inquiry. Cybrid returns a persona_inquiry_id you pass to the Persona SDK to launch the
capture flow — government ID and selfie (KYC). Use method: id_and_selfie:

POST /api/identity_verifications
Content-Type: application/json
Authorization: Bearer YOUR_TOKEN

{
  "type": "kyc",
  "method": "id_and_selfie",
  "customer_guid": "customer_guid"
}
{
  "guid": "identity_verification_guid",
  "type": "kyc",
  "method": "id_and_selfie",
  "customer_guid": "customer_guid",
  "state": "storing",
  "outcome": null,
  "created_at": "2026-07-19T12:00:00.000Z",
  "updated_at": "2026-07-19T12:00:00.000Z"
}
ℹ️

Preparing customers before verification

Preparing customers before verification greatly increases success rates.
See: Verify an Individual Customer

⚠️

Customer must be unverified

Identity verification can only be initiated for customers in the unverified state. The customer
state transitions from storing to unverified before you can start the verification process.

Identity verification states

StateDescription
storingPlatform is storing verification details
waitingWaiting for the customer to complete verification
pendingVerification submitted, awaiting processing
reviewingVerification under manual review
expiredVerification session expired
completedVerification completed (check the outcome field)

Poll GET /api/identity_verifications/{identity_verification_guid} until state transitions to completed,
then confirm outcome is passed.

Integrate the Persona SDK

Persona offers Hosted Flow, Embedded Flow, and native iOS/Android SDKs. The example below uses the Embedded Flow via NPM — see Persona's integration methods to pick the option that fits your app.

Recommended integration

Cybrid recommends the embedded flow with the native iOS and Android SDKs for the best customer experience.

Install the SDK:

npm install [email protected] --save

Initialize the client with the persona_inquiry_id from the identity verification response:

import Persona from 'persona';

const client = new Persona.Client({
  inquiryId: 'persona_inquiry_id',
  language: 'en',
  onReady: () => client.open(),
  onComplete: ({ inquiryId, status, fields }) => {
    // Poll Cybrid or subscribe to identity_verification.completed
  },
  onCancel: ({ inquiryId, sessionToken }) => { /* user dismissed */ },
  onError: (error) => { /* log SDK error */ }
});

Key callbacks:

  • onReady — SDK ready to open.
  • onComplete — user finished the flow; verification can still fail downstream, confirm via the Cybrid API.
  • onCancel — user dismissed the flow.
  • onError — SDK error.

The language parameter accepts ISO 639-1 codes. See
Persona's supported languages for the full list.

ℹ️

Resume incomplete sessions

If the customer abandons the flow before completing, generate a new session token via
POST /api/persona_sessions and pass it to the SDK as sessionToken instead of inquiryId.
See Resume Persona Inquiry Session.

ℹ️

Sandbox testing with expected_behaviours

In sandbox, set expected_behaviours on the identity verification request to force a deterministic
outcome — useful for end-to-end tests without driving the Persona flow.

ValueEffect
passed_immediatelyVerification completes with outcome: passed.
failed_immediatelyVerification completes with outcome: failed.
tax_id_not_checkedTax ID check is skipped during verification.

Example expected_behaviours request body:

{
  "type": "kyc",
  "method": "id_and_selfie",
  "customer_guid": "customer_guid",
  "expected_behaviours": ["passed_immediately"]
}
❗️

Sandbox only

The expected_behaviours field is rejected in production with a 400 error:
Parameter expected_behaviours not permitted in production. Remove the field before deploying.

ℹ️

Sandbox identity matching for account-ownership verification

Account-ownership verification compares the customer's Persona-captured name to the
bank account holder name returned by Plaid. In sandbox, both sides have a fixed
default identity. A mismatch prevents auto-approval and falls back to Persona
document submission.

Persona's default sandbox identity (what Persona returns if no other name is entered):

FieldValue
First nameALEXANDER J
Last nameSAMPLE
Address600 California Street, San Francisco, CA, 94109
DOB1977-07-17

Plaid's default sandbox identity (returned by Plaid for account-ownership):
first name Alberta, last name Charleson. See
Plaid testing identity.

To auto-approve type: bank_account, method: account_ownership in sandbox, either:

  • Complete the Persona step using Alberta / Charleson so the Persona-captured name
    matches Plaid's sandbox identity, or
  • Include expected_behaviours: ["passed_immediately"] on the bank account identity
    verification to bypass the check entirely.

In production, the Persona-captured name is reused for bank account ownership
verification. A mismatch with the Plaid-returned bank account holder name returns
failure_code: name_check_failure. See
Verify External Bank Accounts.

Related: Create a Customer · Verify an Individual Customer · Verify External Bank Accounts

Checkpoint

The customer is verified. You can now create their accounts and move on to funding.


3. Create accounts

Create the USD fiat account and the USDC_SOL trading account for the customer.

Endpoint: POST /api/accounts
Reference: Create Account

Create the USD fiat account

POST /api/accounts
Content-Type: application/json
Authorization: Bearer YOUR_TOKEN

{
  "type": "fiat",
  "customer_guid": "customer_guid",
  "asset": "USD",
  "name": "USD Account"
}
{
  "guid": "fiat_account_guid",
  "type": "fiat",
  "customer_guid": "customer_guid",
  "asset": "USD",
  "name": "USD Account",
  "state": "storing",
  "created_at": "2026-07-19T12:00:00.000Z",
  "updated_at": "2026-07-19T12:00:00.000Z"
}

Create the USDC_SOL trading account

POST /api/accounts
Content-Type: application/json
Authorization: Bearer YOUR_TOKEN

{
  "type": "trading",
  "customer_guid": "customer_guid",
  "asset": "USDC_SOL",
  "name": "USDC_SOL Trading Account"
}
{
  "guid": "trading_account_usdc_sol_guid",
  "type": "trading",
  "customer_guid": "customer_guid",
  "asset": "USDC_SOL",
  "name": "USDC_SOL Trading Account",
  "state": "storing",
  "created_at": "2026-07-19T12:00:00.000Z",
  "updated_at": "2026-07-19T12:00:00.000Z"
}

Account states

StateDescription
storingPlatform is storing account details
createdAccount created and ready for use

Poll GET /api/accounts/{account_guid} until state transitions to created.

ℹ️

Account types

Fiat accounts hold currencies like USD, while trading accounts hold cryptocurrencies like
USDC_SOL. The USDC_SOL trading account is the source account for the remittance plan in
step 10.

ℹ️

Customer fiat accounts require a deposit bank account

Every customer fiat account requires a main deposit bank account before any funding
transfer can complete. Create it immediately after the fiat account — see the next step and
Customer Deposit Bank Accounts.

Related: Creating Platform Accounts


4. Create the deposit bank account

Create a main deposit bank account for the customer's USD fiat account. This is required before any funding
transfer can complete.

Endpoint: POST /api/deposit_bank_accounts
Reference: Create Deposit Bank Account

POST /api/deposit_bank_accounts
Content-Type: application/json
Authorization: Bearer YOUR_TOKEN

{
  "type": "main",
  "account_guid": "fiat_account_guid",
  "customer_guid": "customer_guid"
}
{
  "guid": "deposit_bank_account_guid",
  "type": "main",
  "customer_guid": "customer_guid",
  "account_guid": "fiat_account_guid",
  "asset": "USD",
  "state": "storing",
  "unique_memo_id": "unique_memo_id",
  "counterparty_name": "John Doe",
  "account_details": [
    { "account_number": "account_number" }
  ],
  "routing_details": [
    { "routing_number_type": "ABA", "routing_number": "routing_number" }
  ],
  "created_at": "2026-07-19T12:00:00.000Z",
  "updated_at": "2026-07-19T12:00:00.000Z"
}

Deposit bank account states

StateDescription
storingPlatform is storing account details
createdDeposit bank account created

Poll GET /api/deposit_bank_accounts/{deposit_bank_account_guid} until state transitions to created.

⚠️

Deposit bank account enforcement (effective 2026-06-17)

From 2026-06-17, every customer fiat account must have a main deposit bank account before any
funding transfer (deposit or withdrawal) can be created. Provision the deposit bank account
immediately after creating the customer's fiat account — see
Customer Deposit Bank Accounts.

ℹ️

Routing details

The response includes account_details (account number), routing_details (ABA routing number),
counterparty_name (account holder name), and counterparty_address (account holder address)
for incoming wires.

ℹ️

No deposit bank account error

Funding transfers against a customer fiat account that has no main deposit bank account fail
with HTTP 422 and message_code no_deposit_bank_account. Remediate by creating the main
deposit bank account on the fiat account, then retry the transfer.

Related: Customer Deposit Bank Accounts


5. Link the bank account

Connect the customer's US bank account via Plaid, then create the external bank account for ACH funding.

Create the Plaid workflow

Endpoint: POST /api/workflows
Reference: Create Workflow

POST /api/workflows
Content-Type: application/json
Authorization: Bearer YOUR_TOKEN

{
  "type": "plaid",
  "kind": "link_token_create",
  "customer_guid": "customer_guid",
  "language": "en",
  "link_customization_name": "default",
  "redirect_uri": "http://localhost:4200/bank-account-connect"
}
{
  "guid": "workflow_guid",
  "type": "plaid",
  "customer_guid": "customer_guid",
  "state": "storing",
  "created_at": "2026-07-19T12:00:00.000Z",
  "updated_at": "2026-07-19T12:00:00.000Z"
}

Workflow states

StateDescription
storingPlatform is storing workflow details
completedWorkflow completed, link token available
failedWorkflow failed

Poll GET /api/workflows/{workflow_guid} until state transitions to completed, then retrieve the Plaid
link token from the workflow.

Initialize the Plaid Link SDK

Use the generated plaid_link_token to initialize the Plaid Link SDK in your client
application. The handler below includes callbacks for success, exit, and other events. Use
the public_token and account id from the onSuccess callback to create the external
bank account.

ℹ️

Plaid Link integration methods

Review the official Plaid Link documentation for
up-to-date platform-specific details (Web, iOS, Android, or React Native).

const handler = Plaid.create({
  token: 'GENERATED_LINK_TOKEN', // The plaid_link_token from the workflow
  onSuccess: (public_token, metadata) => {
    // A Plaid public_token and account metadata are returned
    // Use these values to create the external bank account
    // https://plaid.com/docs/link/web/#onsuccess
  },
  onLoad: () => {},
  onExit: (err, metadata) => {
    // Handle the case where the user exits the flow
    // https://plaid.com/docs/link/web/#onexit
  },
  onEvent: (eventName, metadata) => {
    // Optionally subscribe to events during the Plaid flow
    // https://plaid.com/docs/link/web/#onevent
  },
});
ℹ️

Raw routing details fallback

If a domestic customer cannot link their bank account through Plaid (for example, their
institution is not supported by Plaid), contact Cybrid to create an external bank account using
raw routing details. The customer provides their bank's routing number, account number, and
account type to Cybrid, who provisions the account with account_kind raw_routing_details.

Create the external bank account

Endpoint: POST /api/external_bank_accounts
Reference: Create External Bank Account

POST /api/external_bank_accounts
Content-Type: application/json
Authorization: Bearer YOUR_TOKEN

{
  "name": "US Checking",
  "account_kind": "plaid",
  "customer_guid": "customer_guid",
  "asset": "USD",
  "plaid_public_token": "plaid_public_token",
  "plaid_account_id": "plaid_account_id"
}
{
  "guid": "external_bank_account_guid",
  "name": "US Checking",
  "asset": "USD",
  "account_kind": "plaid",
  "customer_guid": "customer_guid",
  "state": "storing",
  "created_at": "2026-07-19T12:00:00.000Z",
  "updated_at": "2026-07-19T12:00:00.000Z"
}

External bank account states

StateDescription
storingPlatform is storing account details
completedAccount linked and ready for use
failedAccount linking failed
refresh_requiredPlaid connection needs refresh
unverifiedAccount awaiting verification
deletingAccount being removed (auto-managed; for example, account no longer valid)
deletedAccount removed and must be re-added

Poll GET /api/external_bank_accounts/{external_bank_account_guid} until state transitions to
unverified, then verify bank account ownership. The account transitions to completed once the
verification passes, at which point it is ready for funding.

Verify the external bank account

Endpoint: POST /api/identity_verifications
Reference: Create Identity Verification

Individual customers must verify ownership of the linked bank account before funding transfers can be
created. Because the account was linked with Plaid (account_kind: plaid), use type: bank_account and
method: account_ownership. The check confirms the Plaid-returned account holder name matches the
customer's KYC name.

⚠️

Wait for the unverified state

Create the bank account identity verification only after the external bank account reaches
unverified. Attempting verification while the account is storing returns a
422 Invalid bank account to verify error.

POST /api/identity_verifications
Content-Type: application/json
Authorization: Bearer YOUR_TOKEN

{
  "type": "bank_account",
  "method": "account_ownership",
  "customer_guid": "customer_guid",
  "external_bank_account_guid": "external_bank_account_guid"
}
{
  "guid": "identity_verification_guid",
  "type": "bank_account",
  "method": "account_ownership",
  "customer_guid": "customer_guid",
  "external_bank_account_guid": "external_bank_account_guid",
  "state": "storing",
  "outcome": null,
  "failure_codes": [],
  "created_at": "2026-07-19T12:00:00.000Z",
  "updated_at": "2026-07-19T12:00:00.000Z"
}

Bank account verification states

StateDescription
storingPlatform is storing verification details
waitingWaiting for a supplementary document (a Persona inquiry is created)
pendingVerification submitted, awaiting processing
reviewingVerification under manual review
expiredVerification session expired
completedVerification completed (check the outcome field)

Poll GET /api/identity_verifications/{identity_verification_guid} until state transitions to
completed, then confirm outcome is passed. When the verification passes, the external bank account
transitions to completed and is ready for funding. On failed, inspect failure_codes (for example
name_check_failure) and create a new verification to retry.

ℹ️

Sandbox testing

To auto-approve in sandbox, either match the customer's Persona-verified name to Plaid's default
sandbox identity (Alberta Charleson), or include expected_behaviours: ["passed_immediately"] on
the verification request to bypass the check. The expected_behaviours field is not permitted in
production.

Related: Verify External Bank Accounts · Plaid Integration · Add Customer External Bank Accounts · Refresh External Bank Accounts


6. Fund the USD account

Pull funds from the linked US bank account into the USD fiat account over ACH. Create a funding quote, then
create the funding transfer against it.

Create the funding quote

Endpoint: POST /api/quotes
Reference: Create Quote

POST /api/quotes
Content-Type: application/json
Authorization: Bearer YOUR_TOKEN

{
  "product_type": "funding",
  "customer_guid": "customer_guid",
  "asset": "USD",
  "side": "deposit",
  "deliver_amount": 10000
}
{
  "guid": "quote_guid",
  "product_type": "funding",
  "customer_guid": "customer_guid",
  "asset": "USD",
  "side": "deposit",
  "deliver_amount": 10000,
  "receive_amount": 10000,
  "fee": 0,
  "expires_at": "2026-07-19T12:00:30.000Z",
  "created_at": "2026-07-19T12:00:00.000Z",
  "updated_at": "2026-07-19T12:00:00.000Z"
}
ℹ️

Amounts are in base units

Amounts are in base units. For USD, 10000 = $100.00 (cents). For USDC_SOL, 10000000 = 10.00
USDC_SOL (micro-dollars).

ℹ️

Deliver vs receive amount

Use deliver_amount by default — this is the amount the customer inputs (what they are sending).
The response returns receive_amount with fees deducted (in production; no fees in sandbox).
Alternatively, specify receive_amount to lock in the amount the recipient receives after fees,
meaning the customer pays receive_amount + fees.

⚠️

Quotes expire in 30 seconds

Quotes are valid for 30 seconds. Use the expires_at field on the quote response to determine
when a quote expires. Trades or transfers against expired quotes fail.

Create the funding transfer

Endpoint: POST /api/transfers
Reference: Create Transfer

The ACH pull sources from the linked external bank account and deposits into the customer's USD fiat account.
Both source_participants and destination_participants are the customer.

POST /api/transfers
Content-Type: application/json
Authorization: Bearer YOUR_TOKEN

{
  "quote_guid": "quote_guid",
  "transfer_type": "funding",
  "external_bank_account_guid": "external_bank_account_guid",
  "payment_rail": "ach",
  "source_participants": [
    { "type": "customer", "guid": "customer_guid", "amount": 10000 }
  ],
  "destination_participants": [
    { "type": "customer", "guid": "customer_guid", "amount": 10000 }
  ]
}
{
  "guid": "transfer_guid",
  "transfer_type": "funding",
  "customer_guid": "customer_guid",
  "quote_guid": "quote_guid",
  "external_bank_account_guid": "external_bank_account_guid",
  "asset": "USD",
  "side": "deposit",
  "state": "storing",
  "estimated_amount": 10000,
  "fee": 0,
  "payment_rail": "ach",
  "created_at": "2026-07-19T12:00:00.000Z",
  "updated_at": "2026-07-19T12:00:00.000Z"
}

Transfer states

StateDescription
storingPlatform is storing transfer details
pendingTransfer pending
holdingTransfer on hold (compliance review)
reviewingTransfer under manual review
completedTransfer completed
failedTransfer failed
cancellingCancellation requested (transient); resolves to failed (failure_code cancelled) or completed

Webhook Events: transfer.pending, transfer.completed, transfer.failed, transfer.cancelling

Poll GET /api/transfers/{transfer_guid} or subscribe to the transfer.completed webhook until state
transitions to completed.

⚠️

Both participants are required

Both source_participants and destination_participants are required on funding transfers.
source_participants identifies who is sending funds; destination_participants identifies who
is receiving funds. Each participant requires type, guid, and amount.

Related: Transfer Process ·
Transfer Participants

Checkpoint

The funding transfer is completed and USD is available in the customer's fiat account. You are ready to
convert it to USDC_SOL.


7. Convert USD to USDC_SOL

Convert the funded USD to USDC_SOL in the trading account. This settlement funds the source account for the
remittance plan. Create a trading quote, then create the trade against it.

Create the trading quote

Endpoint: POST /api/quotes
Reference: Create Quote

POST /api/quotes
Content-Type: application/json
Authorization: Bearer YOUR_TOKEN

{
  "product_type": "trading",
  "customer_guid": "customer_guid",
  "symbol": "USDC_SOL-USD",
  "side": "buy",
  "deliver_amount": 10000
}
{
  "guid": "quote_guid",
  "product_type": "trading",
  "customer_guid": "customer_guid",
  "symbol": "USDC_SOL-USD",
  "side": "buy",
  "receive_amount": 10000000,
  "deliver_amount": 10000,
  "fee": 0,
  "issued_at": "2026-07-19T12:00:00.000Z",
  "expires_at": "2026-07-19T12:00:30.000Z",
  "created_at": "2026-07-19T12:00:00.000Z",
  "updated_at": "2026-07-19T12:00:00.000Z"
}
⚠️

Quotes expire in 30 seconds

Quotes are valid for 30 seconds. Use the expires_at field on the quote response to determine
when a quote expires. Trades or transfers against expired quotes fail.

Execute the trade

Endpoint: POST /api/trades
Reference: Create Trade

POST /api/trades
Content-Type: application/json
Authorization: Bearer YOUR_TOKEN

{
  "quote_guid": "quote_guid"
}
{
  "guid": "trade_guid",
  "trade_type": "platform",
  "customer_guid": "customer_guid",
  "quote_guid": "quote_guid",
  "symbol": "USDC_SOL-USD",
  "side": "buy",
  "state": "storing",
  "receive_amount": 10000000,
  "deliver_amount": 10000,
  "fee": 0,
  "created_at": "2026-07-19T12:00:00.000Z",
  "updated_at": "2026-07-19T12:00:00.000Z"
}

Trade states

StateDescription
storingPlatform is storing trade details
pendingTrade pending execution
executedTrade executed on exchange
settlingTrade settling
completedTrade completed
cancelledTrade cancelled
failedTrade failed

Webhook Events: trade.pending, trade.completed, trade.failed

Poll GET /api/trades/{trade_guid} or subscribe to the trade.completed webhook until state
transitions to completed.

Related: Execute Trades

Checkpoint

The trade is completed and USDC_SOL is available in the trading account. This balance is the source for the
remittance plan.


8. Create the recipient counterparty

Create the recipient as a customer-level counterparty, then verify it via watchlist screening. The
counterparty must reach the verified state before you can create its external bank account or the
remittance plan.

Create the counterparty

Endpoint: POST /api/counterparties
Reference: Create Counterparty

POST /api/counterparties
Content-Type: application/json
Authorization: Bearer YOUR_TOKEN

{
  "type": "individual",
  "customer_guid": "customer_guid",
  "name": {
    "first": "John",
    "last": "Doe"
  },
  "address": {
    "city": "Mumbai",
    "country_code": "IN"
  }
}
{
  "guid": "recipient_counterparty_guid",
  "type": "individual",
  "customer_guid": "customer_guid",
  "state": "storing",
  "created_at": "2026-07-19T12:00:00.000Z",
  "updated_at": "2026-07-19T12:00:00.000Z"
}
ℹ️

Required fields only

For individual counterparties the required fields are type, name.first, name.last,
address.city, and address.country_code. Add customer_guid for customer-scoped counterparties
created with a bank token. The API spec marks only country_code as required on address, but the
Cybrid platform enforces city + country_code together — see
Create a Counterparty.

ℹ️

Corridor-specific fields

For Cybrid-managed remittance corridors, the destination country may require additional fields
on the recipient counterparty (date_of_birth, identification_numbers, address street +
subdivision, etc.). Refer to the relevant payout guide for the destination country before
omitting fields in production. See Supported Corridors.

Verify the counterparty

Endpoint: POST /api/identity_verifications
Reference: Create Identity Verification

Counterparty verification screens the recipient's PII through watchlist checks against government sanctions
lists, PEP databases, and adverse media sources. It is an API call that does not require an SDK.

POST /api/identity_verifications
Content-Type: application/json
Authorization: Bearer YOUR_TOKEN

{
  "type": "counterparty",
  "method": "watchlists",
  "counterparty_guid": "recipient_counterparty_guid"
}
{
  "guid": "identity_verification_guid",
  "type": "counterparty",
  "method": "watchlists",
  "counterparty_guid": "recipient_counterparty_guid",
  "customer_guid": "customer_guid",
  "state": "storing",
  "outcome": null,
  "failure_codes": [],
  "created_at": "2026-07-19T12:00:00.000Z",
  "updated_at": "2026-07-19T12:00:00.000Z"
}

Counterparty states

StateDescription
storingPlatform is storing counterparty details
unverifiedCounterparty created, awaiting verification
verifiedCounterparty verified via watchlist check
rejectedCounterparty rejected

Poll GET /api/identity_verifications/{identity_verification_guid} until state transitions to
completed. When outcome is passed, the counterparty transitions to verified; when failed, inspect
failure_codes (for example watchlist_check_failure, pep_check_failure, media_check_failure) and the
counterparty transitions to rejected. The platform checks counterparty verification results at
one-minute intervals, so completion may take up to one minute.

Webhook Events: identity_verification.waiting, identity_verification.completed

Related: Verify a Counterparty · Creating Counterparties · Sending Cross-Border Payments


9. Create the recipient external bank account

Create the recipient's foreign external bank account tied to the verified counterparty. The example below
uses India (INR over IFSC); required fields vary by destination corridor.

Endpoint: POST /api/external_bank_accounts
Reference: Create External Bank Account

ℹ️

Use raw routing details for foreign payouts

Use account_kind raw_routing_details for the recipient's foreign bank account and tie it to
the verified counterparty_guid. The payment_rail and bank_code_type depend on the
destination corridor — IFSC for India, and other rails (SPEI, PIX, SEPA, and so on) for
other corridors.

ℹ️

Accept-Version for foreign raw-routing accounts

Creating foreign raw-routing external bank accounts requires Accept-Version: 2025-10-01 or
later. Corridor-specific bank-account fields vary by destination; refer to the relevant payout
guide before omitting fields in production.

POST /api/external_bank_accounts
Content-Type: application/json
Accept-Version: 2025-10-01
Authorization: Bearer YOUR_TOKEN

{
  "name": "Recipient INR Account",
  "account_kind": "raw_routing_details",
  "customer_guid": "customer_guid",
  "counterparty_guid": "recipient_counterparty_guid",
  "asset": "INR",
  "counterparty_bank_account_details": [
    {
      "account_identifier": "account_number",
      "payment_rail": "IFSC",
      "bank_code": "bank_ifsc_code",
      "bank_code_type": "IFSC"
    }
  ]
}
{
  "guid": "recipient_external_bank_account_guid",
  "name": "Recipient INR Account",
  "asset": "INR",
  "account_kind": "raw_routing_details",
  "environment": "sandbox",
  "bank_guid": "bank_guid",
  "customer_guid": "customer_guid",
  "counterparty_guid": "recipient_counterparty_guid",
  "state": "storing",
  "created_at": "2026-07-19T12:00:00.000Z",
  "updated_at": "2026-07-19T12:00:00.000Z"
}
ℹ️

Counterparty must be verified

The referenced counterparty must be in the verified state. Creating an external bank account
against an unverified counterparty fails with the unverified_counterparty failure code.

External bank account states

StateDescription
storingPlatform is storing external bank account details
completedExternal bank account created
unverifiedExternal bank account created but not yet verified
failedExternal bank account creation failed
refresh_requiredExternal bank account needs to be refreshed
deletingPlatform is deleting the external bank account
deletedExternal bank account deleted

Poll GET /api/external_bank_accounts/{external_bank_account_guid} until state transitions to completed.

Related: Foreign Fiat External Bank Accounts · Add External Bank Accounts

Checkpoint

The recipient counterparty is verified and their foreign external bank account is completed. You have
everything needed to create and execute the remittance plan.


10. Create and execute the remittance plan

Create a remittance plan with the USDC_SOL trading account as source and the recipient's foreign external bank
account as destination, then execute it. Cybrid converts USDC_SOL to INR and pays out over the local rail.

Create the remittance plan

Endpoint: POST /api/plans
Reference: Create Plan

POST /api/plans
Content-Type: application/json
Authorization: Bearer YOUR_TOKEN

{
  "type": "remittance",
  "customer_guid": "customer_guid",
  "source_account": {
    "guid": "trading_account_usdc_sol_guid",
    "amount": 10000000
  },
  "destination_account": {
    "guid": "recipient_external_bank_account_guid"
  },
  "purpose_of_transaction": "family_support"
}
{
  "guid": "plan_guid",
  "type": "remittance",
  "customer_guid": "customer_guid",
  "state": "storing",
  "expires_at": "2026-07-19T12:00:30.000Z",
  "source_account": {
    "guid": "trading_account_usdc_sol_guid",
    "type": "trading",
    "asset": "USDC_SOL",
    "requested_amount": 10000000
  },
  "destination_account": {
    "guid": "recipient_external_bank_account_guid",
    "type": "external_bank_account",
    "asset": "INR"
  },
  "stages": [],
  "fees": [],
  "created_at": "2026-07-19T12:00:00.000Z",
  "updated_at": "2026-07-19T12:00:00.000Z"
}

Plan states

StateDescription
storingPlatform is storing plan details
planningPlatform is building the plan
completedPlan is ready for execution
failedPlan creation failed

Poll GET /api/plans/{plan_guid} until state transitions to completed, then execute it before it expires.

Webhook Events: plan.planning, plan.completed, plan.failed

⚠️

Enable the remittance feature

Contact Cybrid support to enable the remittance feature on your bank before using this operation.

ℹ️

Specify the amount on the source account

You must specify an amount on either the source or destination account — the amount drives the
plan's quote. If source amount is specified, the plan ensures that amount is debited. If
destination amount is specified, the plan ensures that exact amount is deposited. Include amount
under source_account by default, expressed in the source account asset's base units (USDC_SOL).

ℹ️

Purpose of transaction

purpose_of_transaction is corridor-dependent. Some destinations reject a plan without an
accepted purpose (invalid_purpose_of_transaction). Refer to the destination payout guide for
accepted values.

ℹ️

Supporting documents (corridor-dependent)

Some corridors require supporting documents attached to the plan's supporting_documents (for
example, an invoice carrying a document_reference_number). This is most common for B2B payouts
— see the "Upload a supporting document" step in the CA B2B and US B2B integration guides, and
File Upload, before going live.

ℹ️

Travel rule information

Travel rule information is required only when the originator is not the holder of the trading
account or when the receiver is not the holder of the external bank account.

⚠️

Reversals may incur fees

If the final withdrawal to the destination bank account fails after currency conversions have
occurred, any reversals may incur fees.

Execute the plan

Endpoint: POST /api/executions
Reference: Create Execution

POST /api/executions
Content-Type: application/json
Authorization: Bearer YOUR_TOKEN

{
  "plan_guid": "plan_guid"
}
{
  "guid": "execution_guid",
  "type": "remittance",
  "plan_guid": "plan_guid",
  "customer_guid": "customer_guid",
  "state": "storing",
  "source_account": {
    "guid": "trading_account_usdc_sol_guid",
    "type": "trading",
    "asset": "USDC_SOL"
  },
  "destination_account": {
    "guid": "recipient_external_bank_account_guid",
    "type": "external_bank_account",
    "asset": "INR"
  },
  "stages": [],
  "fees": [],
  "created_at": "2026-07-19T12:00:00.000Z",
  "updated_at": "2026-07-19T12:00:00.000Z"
}

Execution states

StateDescription
storingPlatform is storing execution details
executingPlatform is executing the plan
completedPlan execution completed successfully
failedPlan execution failed

Webhook Events: execution.executing, execution.completed, execution.failed

Poll GET /api/executions/{execution_guid} or subscribe to the execution.completed webhook until state
transitions to completed. On failure, inspect the failure_code (for example compliance_rejection,
invalid_destination, party_name_invalid, external_vendor_error).

ℹ️

Creating an execution initiates the transaction

Creating an execution initiates the actual transaction. Ensure the plan has been reviewed and is
ready before executing.

⚠️

Plans expire quickly

Remittance plans expire (default ~60 seconds, plan_expired error). Use the expires_at field
on the plan response to confirm when the plan expires, and execute it before then.

ℹ️

Executed amounts

The executed amounts in the response represent the actual amounts transferred, which should match
the quoted amounts from the plan.

Related: Sending Cross-Border Payments · Payout Prices


Webhooks

Subscribe to webhooks for real-time notifications instead of polling. Configure webhook endpoints in your bank
settings. Accounts, deposit bank accounts, workflows, and external bank accounts have no webhook events in this
flow — use polling for those resources.

ℹ️

Webhook Events

This integration uses the following webhook events:

EventTrigger
identity_verification.waitingIdentity verification is waiting for the customer or screening to proceed
identity_verification.completedIdentity verification completed (check the outcome)
transfer.pendingFunding transfer is pending
transfer.completedFunding transfer completed
transfer.failedFunding transfer failed
transfer.cancellingFunding transfer cancellation requested
trade.pendingTrade is pending execution
trade.completedTrade completed
trade.failedTrade failed
plan.planningRemittance plan is being built
plan.completedRemittance plan is ready for execution
plan.failedRemittance plan creation failed
execution.executingPlan execution is in progress
execution.completedPlan execution completed
execution.failedPlan execution failed

See: Webhook Events


Additional resources



Did this page help you?