This guide walks you through the complete flow of sending a payment on the OpenFX sandbox. By the end, you will have onboarded a customer, created an account, and originated a USD ACH payment.
Sandbox environment. This guide uses the sandbox at sandbox.api.openfx.com. In sandbox, KYB review is auto-approved and test balances are available for new accounts. See Authentication for details on setting up your credentials.

Prerequisites

Before you begin, you need:
  1. An OpenFX sandbox API key — Obtain from the OpenFX Dashboard. This is your Bearer token for the Authorization header.
  2. An Ed25519 key pair — Used for request signing. See Authentication for how to generate one and register the public key.
  3. The sandbox base URLhttps://sandbox.api.openfx.com/v1
All requests require three authentication headers: Authorization (Bearer token), X-Signature (Ed25519 signature), and X-Timestamp (Unix timestamp). For readability, the examples below show simplified headers. See the Request Signing guide for the full signing implementation.

Step 1: Onboard a customer

The orchestrated onboarding endpoint creates both an entity (identity record) and a customer (program enrollment) in a single API call. With autoSubmit: true, it also submits the customer for KYB review automatically.
curl -X POST https://sandbox.api.openfx.com/v1/onboardings \
  -H "Authorization: Bearer sk_sandbox_your_api_key" \
  -H "X-Signature: <computed-signature>" \
  -H "X-Timestamp: 1740500000" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440001" \
  -d '{
    "type": "business",
    "legalName": "Horizon Payments Ltd",
    "jurisdiction": "US",
    "email": "ops@horizonpayments.example.com",
    "sourceOfFunds": "business_revenue",
    "address": {
      "line1": "100 Financial Drive",
      "city": "San Francisco",
      "state": "CA",
      "postalCode": "94105",
      "country": "US"
    },
    "autoSubmit": true
  }'
The response returns the onboarding resource with references to the newly created entity and customer.

Step 2: Wait for KYB approval

In sandbox, KYB review is auto-approved. Poll the customer endpoint to confirm the customer is active before proceeding.
import time

# Poll until KYB is approved (sandbox auto-approves quickly)
while True:
    customer = requests.get(
        f"{base_url}/customers/{customer_id}",
        headers={
            "Authorization": headers["Authorization"],
            "X-Signature": headers["X-Signature"],
            "X-Timestamp": headers["X-Timestamp"],
        },
    ).json()

    if customer["status"] == "active" and customer["kybStatus"] == "approved":
        print(f"Customer {customer_id} is active and approved!")
        break

    print(f"Status: {customer['status']}, KYB: {customer['kybStatus']}. Waiting...")
    time.sleep(2)
In production, use webhooks instead of polling. Subscribe to customer.kyb_status_changed to receive a notification when KYB completes. See Webhooks for setup instructions.
The customer is now active with kybStatus: approved. You can create accounts and send payments.

Step 3: Create an account

Create a USD fiat account for the customer. This is a demand_deposit account — a standard bank account that can hold a USD balance and receive deposits.
curl -X POST https://sandbox.api.openfx.com/v1/accounts \
  -H "Authorization: Bearer sk_sandbox_your_api_key" \
  -H "X-Signature: <computed-signature>" \
  -H "X-Timestamp: 1740500000" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440005" \
  -d '{
    "customerId": "cus_01953e1a5f4b7000",
    "type": "demand_deposit",
    "currency": "USD",
    "label": "Primary USD Operating",
    "purpose": "operating"
  }'

Step 4: Fund the account

In production, funds arrive via real bank transfers or crypto deposits to the account’s virtual account number. In sandbox, new accounts are provisioned with a test balance that you can use immediately.
You can verify the balance by calling GET /accounts/{accountId}/balances. The available balance should reflect the sandbox test funds.

Step 5: Create a counterparty and payment method

A counterparty is an external party you send money to. A payment method holds the rail-specific delivery details (bank account, crypto address, etc.). First, create the counterparty:
curl -X POST https://sandbox.api.openfx.com/v1/counterparties \
  -H "Authorization: Bearer sk_sandbox_your_api_key" \
  -H "X-Signature: <computed-signature>" \
  -H "X-Timestamp: 1740500000" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440007" \
  -d '{
    "customerId": "cus_01953e1a5f4b7000",
    "entityType": "business",
    "name": "Acme Supplies Inc",
    "email": "billing@acmesupplies.example.com",
    "address": {
      "line1": "200 Commerce Blvd",
      "city": "New York",
      "state": "NY",
      "postalCode": "10001",
      "country": "US"
    }
  }'
Now add a US bank payment method for ACH delivery:
curl -X POST https://sandbox.api.openfx.com/v1/counterparties/cpt_01953e1a5f4b7004/payment-methods \
  -H "Authorization: Bearer sk_sandbox_your_api_key" \
  -H "X-Signature: <computed-signature>" \
  -H "X-Timestamp: 1740500000" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440008" \
  -d '{
    "type": "us_bank",
    "currency": "USD",
    "routingNumber": "021000021",
    "accountNumber": "123456789012",
    "bankAccountType": "checking",
    "label": "Acme primary checking"
  }'
The accountNumber field is write-only. Subsequent GET responses will return accountNumberLast4 (e.g., "9012") instead of the full account number.

Step 6: Create a payment

Everything is in place. Create a USD ACH payment to the counterparty. This example uses the unified POST /payments endpoint. You could also use the rail-specific POST /payments/ach endpoint for a simpler request schema.
curl -X POST https://sandbox.api.openfx.com/v1/payments \
  -H "Authorization: Bearer sk_sandbox_your_api_key" \
  -H "X-Signature: <computed-signature>" \
  -H "X-Timestamp: 1740500000" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440009" \
  -d '{
    "sourceAccountId": "acc_01953e1a5f4b7002",
    "counterpartyId": "cpt_01953e1a5f4b7004",
    "paymentMethodId": "pm_01953e1a5f4b7300",
    "rail": "ach",
    "sendAmount": {
      "currency": "USD", "value": "150000"
    },
    "reference": "Invoice #INV-2026-0042"
  }'
Your first payment is created. The initial status is created. In sandbox, the payment will progress through the lifecycle automatically.

What you just built

Here is a summary of the resources created in this quickstart:
StepResourceIDWhat it represents
1Onboardingonb_01953e1a5f4b7800Orchestrated onboarding for Horizon Payments Ltd
1Entityent_01953e1a5f4b7100Business identity for Horizon Payments Ltd
1Customercus_01953e1a5f4b7000Program enrollment with KYB tracking
2KYBCustomer approved for platform operations
3Accountacc_01953e1a5f4b7002USD demand deposit account
5Counterpartycpt_01953e1a5f4b7004Acme Supplies Inc (payment recipient)
5Payment Methodpm_01953e1a5f4b7300US bank checking account for ACH
6Paymentpmt_01953e1a5f4b7005$1,500.00 ACH payment to Acme

Next steps