Overview

OpenFX provides two fully isolated environments. Each has its own base URL, credentials, and data. Resources created in sandbox never exist in production and vice versa.
SandboxProduction
Base URLhttps://sandbox.api.openfx.com/v1https://api.openfx.com/v1
PurposeIntegration development and testingLive money movement
Real moneyNoYes
Simulation endpointsAvailable (5 endpoints)Return 404 Not Found
KYB reviewSimulated via APIPerformed by compliance team
Payment settlementSimulatedReal settlement on payment rails
WebhooksDelivered to your test URLDelivered to your production URL
Rate limitsRelaxed for testingStandard production limits
Data isolationCompletely separate from productionCompletely separate from sandbox

Base URLs

All API requests are made to the versioned base URL for the target environment:
https://sandbox.api.openfx.com/v1
Never use production credentials in sandbox or sandbox credentials in production. The environments are fully isolated — credentials from one environment will return 401 authentication_error in the other.

Credentials

Each environment requires its own set of credentials. You manage credentials through the OpenFX Dashboard.
CredentialSandboxProduction
API key (Bearer token)Prefixed with sk_sandbox_Prefixed with sk_live_
Ed25519 public keyRegistered in sandbox settingsRegistered in production settings
Webhook signing secretUnique per sandbox subscriptionUnique per production subscription
All three authentication headers are required in both environments:
HeaderPurpose
AuthorizationBearer token carrying your API key. Establishes caller identity.
X-SignatureEd25519 signature over the request. Provides integrity and non-repudiation.
X-TimestampUnix timestamp. Server rejects requests outside a tolerance window for replay protection.
See the Authentication guide for complete details on generating Ed25519 key pairs, computing request signatures, and registering public keys.

Sandbox environment

The sandbox environment is a full replica of the production API surface with the addition of simulation endpoints. All 138 API operations are available. The only functional difference is that no real money moves and certain asynchronous processes (KYB review, payment settlement, inbound deposits) are controllable via simulation.

What works the same as production

  • Entity, customer, account, counterparty, and payment method CRUD
  • FX quotes and conversions (using sandbox rates)
  • Payment creation and lifecycle state transitions
  • Webhook delivery to your configured endpoints
  • Idempotency enforcement
  • Request signing validation
  • Error responses and validation rules
  • Pagination and filtering
  • Rate limiting (relaxed thresholds)

What is different from production

  • No real money movement. Payments are created and transition through statuses but no actual bank transfers or blockchain transactions occur.
  • Simulation endpoints are available. Five endpoints let you trigger events that would normally require external action.
  • KYB is instant. Use the simulation endpoint to approve or reject customers immediately instead of waiting for manual review.
  • Sandbox FX rates may differ slightly from live market rates but follow the same pricing structure.

Simulation endpoints

Simulation endpoints are available only in sandbox at paths under /simulation/. They allow you to trigger events that depend on external systems — inbound payments from banks, KYB decisions from compliance, and payment status transitions from rail providers.
Simulation endpoints return 404 Not Found in production. Never depend on simulation behavior in production integrations.

Simulate KYB decision

Approve or reject a customer’s KYB review. In production, this happens asynchronously via the compliance team and is communicated through the customer.kyb_status_changed webhook.
POST /simulation/customers/{customerId}/kyb-decision
Request body
{
  "decision": "approved",
  "reason": "Sandbox auto-approval for testing"
}
The decision field accepts approved or rejected.

Simulate account deposit

Add funds to an account for testing. In production, funds arrive via real bank transfers or crypto deposits.
POST /simulation/accounts/{accountId}/deposit
Request body
{
  "amount": {
    "currency": "USD", "value": "5000000"
  },
  "reference": "Test deposit for integration testing"
}

Simulate inbound payment

Create an inbound payment record as if funds had arrived via a specific payment rail. In production, these are created automatically by the system when external deposits are detected.
POST /simulation/payments/{rail}/inbound
The {rail} parameter accepts any supported rail: ach, fedwire, swift, fednow, crypto, sepa, fps.

Simulate payment status change

Force a payment into a specific status for testing lifecycle transitions, error handling, and webhook processing. In production, status changes are driven by rail-level settlement events.
POST /simulation/payments/{paymentId}/status-change
Request body
{
  "status": "completed",
  "reason": "Sandbox simulation for testing"
}
The status field accepts any valid payment status: processing, completed, returned, reversed, failed, canceled.

Simulate collection status change

Force a collection into a specific status for testing ACH debit return scenarios.
POST /simulation/collections/{collectionId}/status-change
Request body
{
  "status": "returned",
  "returnCode": "R01",
  "reason": "Insufficient funds"
}

Testing best practices

Build your integration against sandbox first, then switch to production credentials when you are ready to go live. The API surface is identical.

Use unique idempotency keys

Every state-changing request requires an Idempotency-Key header. Use UUID v4 values and generate a new one for each distinct operation. Reusing a key with a different request body returns a 409 idempotency_error.

Test the full lifecycle

Do not test just the happy path. Use simulation endpoints to exercise:
  • KYB rejection — Call simulate KYB with "decision": "rejected" and verify your integration handles the customer.kyb_status_changed webhook correctly.
  • Payment failures — Simulate failed and returned statuses to test your error handling and reconciliation flows.
  • Compliance gates — Create payments that enter requires_action status and test submitting compliance actions.

Set up webhooks early

Register a webhook subscription in sandbox pointing to a test URL (tools like webhook.site or ngrok work well). This lets you observe the full event stream as you test:
POST /webhooks/subscriptions
{
  "url": "https://your-test-url.example.com/webhooks",
  "events": ["*"]
}
The wildcard ["*"] subscribes to all event types. In production, subscribe only to the event types you need.

Test with realistic data

Use realistic but fictional data in sandbox. This helps catch validation errors early and ensures your integration produces sensible output.

Verify webhook signatures

Even in sandbox, verify the X-OpenFX-Signature header on every webhook delivery. This ensures your signature verification code works before you go to production. See Signature Verification for implementation details.

Going to production

When your sandbox integration is working correctly:
  1. Create production credentials in the OpenFX Dashboard.
  2. Register your production Ed25519 public key.
  3. Update your base URL from sandbox.api.openfx.com/v1 to api.openfx.com/v1.
  4. Update your API key from sk_sandbox_... to sk_live_....
  5. Register production webhook subscriptions with your production endpoint URL.
  6. Remove any simulation endpoint calls from your production code path.
Simulation endpoints return 404 in production. Ensure your code does not call any /simulation/ paths in production.

Environment-specific headers

All standard response headers are present in both environments:
HeaderDescription
X-Request-IDUnique request identifier. Include in support tickets.
X-RateLimit-LimitMaximum requests allowed in the current window.
X-RateLimit-RemainingRequests remaining in the current window.
X-RateLimit-ResetSeconds until the rate-limit window resets.
Retry-AfterSeconds to wait before retrying (on 429 and 409 responses).
Idempotency-Replayedtrue when the response is served from the idempotency cache.

Next steps