This guide covers how to test your Dakota Platform API integration using our sandbox environment.
Sandbox Environment
The Dakota Platform sandbox provides a fully-featured testing environment that mirrors production behavior with mocked external services:
- Base URL:
https://api.platform.sandbox.dakota.xyz
- Dashboard:
https://platform.sandbox.dakota.xyz
- API Keys: Base64-encoded strings (e.g.,
AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=)
Getting Started
1. Create Sandbox Account
- Ask Dakota Support for a Sandbox account login
- Go to the Dashboard:
https://platform.sandbox.dakota.xyz
- Generate test API keys
- Note the different behavior of sandbox vs production
2. Sandbox Limitations
- No real money or crypto involved
- Simulated payment processing
- Limited external provider integrations
- Faster processing times for testing
3. Key Differences: Sandbox vs Production
| Feature | Production | Sandbox |
|---|
| External API calls | Real third-party providers | Mocked responses |
| Money movement | Real funds | Simulated |
| KYB verification | Real verification | Auto-approved after 5 seconds |
| Compliance checks | Real TRM screening | Configurable mock responses |
| Webhooks | From external providers | Simulated via /sandbox/* endpoints |
| Bank accounts | Real routing/account numbers | Mock data (routing: 123456789) |
| Crypto addresses | Real wallet addresses | Random Ethereum-style addresses |
| Processing times | Minutes to hours | Seconds (configurable) |
Sandbox Behavior Control Headers
The sandbox provides HTTP headers to control behavior, inject errors, and simulate specific scenarios.
| Header | Type | Description |
|---|
X-Sandbox-Instant-Completion | boolean | Complete async operations immediately instead of requiring manual simulation |
X-Sandbox-Skip-Auto-Approval | boolean | Prevent automatic KYB approval (default: KYB auto-approves after 5 seconds) |
X-Sandbox-Scenario | string | Use a predefined test scenario |
X-Sandbox-Error-Status | integer | Inject a specific HTTP error status code (100-599) |
X-Sandbox-Error-Message | string | Custom error message (max 500 characters) |
X-Sandbox-Error-Step | string | Specify at which step the error should occur |
Using Instant Completion
By default, sandbox transactions require manual progression through states via the simulation endpoints. Use this header to complete operations immediately:
curl -X POST https://api.platform.sandbox.dakota.xyz/transactions/one-off \
-H "X-API-Key: YOUR_API_KEY" \
-H "X-Idempotency-Key: $(uuidgen)" \
-H "X-Sandbox-Instant-Completion: true" \
-H "Content-Type: application/json" \
-d '{...}'
Predefined Scenarios
The following scenarios are validated and accepted by the sandbox middleware:
| Scenario | Description | Effect |
|---|
compliance_block | Transaction blocked by compliance | Returns 403 error at compliance_check step |
insufficient_funds | Source account has insufficient funds | Returns 400 error at transaction_processing step |
slow_processing | Delayed processing | Simulates slower processing times |
document_required | Additional documentation needed | Triggers document requirement flow |
# Example: Test compliance block
curl -X POST https://api.platform.sandbox.dakota.xyz/transactions/one-off \
-H "X-API-Key: YOUR_API_KEY" \
-H "X-Idempotency-Key: $(uuidgen)" \
-H "X-Sandbox-Scenario: compliance_block" \
-H "Content-Type: application/json" \
-d '{...}'
Custom Error Injection
For scenarios not in the predefined list, use direct error injection headers:
| Use Case | Headers to Use |
|---|
| KYB rejection | X-Sandbox-Error-Status: 403 + X-Sandbox-Error-Step: kyb_submission |
| Provider maintenance | X-Sandbox-Error-Status: 503 + X-Sandbox-Error-Step: provider_call |
| Network timeout | X-Sandbox-Error-Status: 504 + X-Sandbox-Error-Step: network_call |
| Rate limiting | X-Sandbox-Error-Status: 429 |
| Invalid account | X-Sandbox-Error-Status: 400 + X-Sandbox-Error-Step: account_validation |
Valid Error Steps
| Step | Description |
|---|
account_validation | During account validation |
compliance_check | During compliance screening |
kyb_submission | During KYB submission |
transaction_processing | During transaction processing |
provider_call | When calling external providers |
network_call | For network operations |
# Example: Inject custom error at specific step
curl -X POST https://api.platform.sandbox.dakota.xyz/transactions/one-off \
-H "X-API-Key: YOUR_API_KEY" \
-H "X-Idempotency-Key: $(uuidgen)" \
-H "X-Sandbox-Error-Status: 503" \
-H "X-Sandbox-Error-Message: Provider temporarily unavailable" \
-H "X-Sandbox-Error-Step: provider_call" \
-H "Content-Type: application/json" \
-d '{...}'
Sandbox Simulation Endpoints
The sandbox provides dedicated endpoints to simulate external events that would normally come from provider webhooks.
Simulate Account Activity
Endpoint: POST /sandbox/accounts/{account_id}/simulate
Simulate deposits (onramp) or drains (offramp) on accounts. This endpoint creates webhooks that are processed by the orchestrator to create transactions.
Request Body
| Field | Type | Required | Description |
|---|
amount | string | Yes | Amount to simulate (e.g., "100.00") |
currency | string | Yes | Currency code - must match the account’s input asset |
reference | string | No | Optional reference for the transaction |
target_state | string | No | Target state for simulation |
Currency Validation
The currency must match the account’s input_asset:
- Onramp accounts (USD → USDC): use
currency: "USD"
- Offramp accounts (USDC → USD): use
currency: "USDC"
Target States
For Onramp (Fiat → Crypto):
| Target State | Transaction Status | Description |
|---|
funds_received | pending | Funds received, awaiting processing |
payment_submitted | processing | Payment submitted to provider |
payment_processed | completed | Payment fully processed |
For Offramp (Crypto → Fiat):
| Target State | Transaction Status | Description |
|---|
created | pending | Drain initiated |
pending | processing | Drain in progress |
completed | completed | Drain completed |
# Simulate $100 USD deposit to onramp account
curl -X POST https://api.platform.sandbox.dakota.xyz/sandbox/accounts/{account_id}/simulate \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": "100.00",
"currency": "USD",
"target_state": "payment_processed"
}'
const response = await fetch(
`https://api.platform.sandbox.dakota.xyz/sandbox/accounts/${accountId}/simulate`,
{
method: 'POST',
headers: {
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
amount: '100.00',
currency: 'USD',
target_state: 'payment_processed'
})
}
);
Simulate KYB Status
Endpoint: POST /sandbox/customers/{customer_id}/kyb/simulate
Manually update KYB verification status.
Request Body
| Field | Type | Required | Description |
|---|
status | string | Yes | New status: approved, rejected, in_review, or expired |
reason | string | No | Optional reason for the status change |
# Approve KYB
curl -X POST https://api.platform.sandbox.dakota.xyz/sandbox/customers/{customer_id}/kyb/simulate \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"status": "approved"}'
# Reject KYB with reason
curl -X POST https://api.platform.sandbox.dakota.xyz/sandbox/customers/{customer_id}/kyb/simulate \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"status": "rejected",
"reason": "Unable to verify business documentation"
}'
By default, KYB auto-approves after 5 seconds in sandbox mode. Use X-Sandbox-Skip-Auto-Approval: true header when creating KYB to prevent this and test pending states.
Simulate Transaction State
Endpoint: POST /sandbox/transactions/{transaction_id}/simulate
Manually progress a transaction to a new state.
Request Body
| Field | Type | Required | Description |
|---|
status | string | Yes | New status: pending, processing, completed, or failed |
error_message | string | Conditional | Error message (required if status is failed) |
# Complete a transaction
curl -X POST https://api.platform.sandbox.dakota.xyz/sandbox/transactions/{transaction_id}/simulate \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"status": "completed"}'
# Fail a transaction
curl -X POST https://api.platform.sandbox.dakota.xyz/sandbox/transactions/{transaction_id}/simulate \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"status": "failed",
"error_message": "Insufficient funds"
}'
Simulate Blockchain Event
Endpoint: POST /sandbox/blockchain/simulate
Simulate blockchain events like transfers or confirmations.
curl -X POST https://api.platform.sandbox.dakota.xyz/sandbox/blockchain/simulate \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"event_type": "transfer",
"from": "0x1234...",
"to": "0xabcd...",
"amount": "100.00",
"asset": "USDC",
"network": "ethereum-mainnet"
}'
Compliance Testing
The sandbox includes a mock TRM compliance client. Use specific address patterns to trigger different risk levels:
| Pattern in Address | Risk Level | Risk Score |
|---|
Contains BAD | High | 75 |
Contains SCAM | High | 75 |
Contains hack | High | 75 |
Contains review | Medium (Manual Review) | 50 |
Contains manual | Medium (Manual Review) | 50 |
| Normal addresses | Low | 10 |
Using X-Sandbox-Scenario: compliance_block triggers a critical risk level (score: 95) that blocks the transaction.
Mock Data in Sandbox
The sandbox generates realistic mock data:
| Entity | Mock Values |
|---|
| Routing Number | 123456789 (constant) |
| Account Number | Random 10-digit number |
| Bank Name | Dakota Ridge Bank |
| IBAN | Random German IBAN (DE format) |
| IBAN Bank | Dakota Europa Bank |
| Crypto Address | Random Ethereum-style (0x + 40 hex chars) |
| TX Hash | Random (0x + 64 hex chars) |
Testing KYB Onboarding
Sandbox KYB Behavior
In sandbox, KYB verification is simulated:
// Create test customer - application is automatically created
const customer = await dakota.customers.create({
customer_type: "business",
name: "Test Business"
});
// The response includes application_url for onboarding
console.log('Onboarding URL:', customer.application_url);
// In sandbox, KYB auto-approves after 5 seconds
// Or use the simulate endpoint to manually control status
KYB Test Scenarios
Control KYB outcomes using specific email patterns:
| Email Pattern | KYB Result |
|---|
approved@test.com | Auto-approved |
rejected@test.com | Auto-rejected |
pending@test.com | Stays pending |
review@test.com | Requires manual review |
Testing Transactions
Transaction Test Cases
Successful Onramp
# Successful onramp transaction
curl -X POST https://api.platform.sandbox.dakota.xyz/transactions/one-off \
-H "X-API-Key: AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=" \
-H "X-Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"customer_id": "31TgvwFzi3rstV0DEDzQtuBfwFR",
"destination_id": "31TgvySz1ARnqMZUdbuxykqqxGV",
"source_asset": "USD",
"source_network_id": "fiat",
"destination_asset": "USDC",
"destination_network_id": "ethereum-mainnet",
"amount": {
"value": "100.00",
"asset": "USD"
}
}'
// Successful onramp transaction
const transaction = await fetch('https://api.platform.sandbox.dakota.xyz/transactions/one-off', {
method: 'POST',
headers: {
'X-API-Key': 'AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=',
'X-Idempotency-Key': crypto.randomUUID(),
'Content-Type': 'application/json'
},
body: JSON.stringify({
customer_id: '31TgvwFzi3rstV0DEDzQtuBfwFR',
destination_id: '31TgvySz1ARnqMZUdbuxykqqxGV',
source_asset: 'USD',
source_network_id: 'fiat',
destination_asset: 'USDC',
destination_network_id: 'ethereum-mainnet',
amount: {
value: '100.00',
asset: 'USD'
}
})
});
# Successful onramp transaction
transaction_response = requests.post(
'https://api.platform.sandbox.dakota.xyz/transactions/one-off',
headers={
'X-API-Key': 'AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=',
'X-Idempotency-Key': str(uuid.uuid4()),
'Content-Type': 'application/json'
},
json={
'customer_id': '31TgvwFzi3rstV0DEDzQtuBfwFR',
'destination_id': '31TgvySz1ARnqMZUdbuxykqqxGV',
'source_asset': 'USD',
'source_network_id': 'fiat',
'destination_asset': 'USDC',
'destination_network_id': 'ethereum-mainnet',
'amount': {
'value': '100.00',
'asset': 'USD'
}
}
)
Failed Transaction (Insufficient Funds)
# Transaction that will fail due to large amount
curl -X POST https://api.platform.sandbox.dakota.xyz/transactions/one-off \
-H "X-API-Key: AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=" \
-H "X-Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"customer_id": "31TgvwFzi3rstV0DEDzQtuBfwFR",
"destination_id": "31TgvySz1ARnqMZUdbuxykqqxGV",
"source_asset": "USD",
"source_network_id": "fiat",
"destination_asset": "USDC",
"destination_network_id": "ethereum-mainnet",
"amount": {
"value": "999999.00",
"asset": "USD"
}
}'
// Transaction that will fail due to large amount
const transaction = await fetch('https://api.platform.sandbox.dakota.xyz/transactions/one-off', {
method: 'POST',
headers: {
'X-API-Key': 'AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=',
'X-Idempotency-Key': crypto.randomUUID(),
'Content-Type': 'application/json'
},
body: JSON.stringify({
customer_id: '31TgvwFzi3rstV0DEDzQtuBfwFR',
destination_id: '31TgvySz1ARnqMZUdbuxykqqxGV',
source_asset: 'USD',
source_network_id: 'fiat',
destination_asset: 'USDC',
destination_network_id: 'ethereum-mainnet',
amount: {
value: '999999.00', // Will fail
asset: 'USD'
}
})
});
# Transaction that will fail due to large amount
transaction_response = requests.post(
'https://api.platform.sandbox.dakota.xyz/transactions/one-off',
headers={
'X-API-Key': 'AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=',
'X-Idempotency-Key': str(uuid.uuid4()),
'Content-Type': 'application/json'
},
json={
'customer_id': '31TgvwFzi3rstV0DEDzQtuBfwFR',
'destination_id': '31TgvySz1ARnqMZUdbuxykqqxGV',
'source_asset': 'USD',
'source_network_id': 'fiat',
'destination_asset': 'USDC',
'destination_network_id': 'ethereum-mainnet',
'amount': {
'value': '999999.00', # Will fail
'asset': 'USD'
}
}
)
package main
import (
"bytes"
"net/http"
"github.com/google/uuid"
)
func main() {
client := &http.Client{}
body := bytes.NewBufferString(`{
"customer_id": "31TgvwFzi3rstV0DEDzQtuBfwFR",
"destination_id": "31TgvySz1ARnqMZUdbuxykqqxGV",
"source_asset": "USD",
"source_network_id": "fiat",
"destination_asset": "USDC",
"destination_network_id": "ethereum-mainnet",
"amount": {
"value": "999999.00",
"asset": "USD"
}
}`)
req, _ := http.NewRequest("POST", "https://api.platform.sandbox.dakota.xyz/transactions/one-off", body)
req.Header.Add("X-API-Key", "AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=")
req.Header.Add("X-Idempotency-Key", uuid.New().String())
req.Header.Add("Content-Type", "application/json")
resp, _ := client.Do(req)
defer resp.Body.Close()
}
use reqwest::header::{HeaderMap, HeaderValue};
use uuid::Uuid;
use serde_json::json;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = reqwest::Client::new();
let mut headers = HeaderMap::new();
headers.insert("X-API-Key", HeaderValue::from_static("AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc="));
headers.insert("X-Idempotency-Key", HeaderValue::from_str(&Uuid::new_v4().to_string())?);
headers.insert("Content-Type", HeaderValue::from_static("application/json"));
let body = json!({
"customer_id": "31TgvwFzi3rstV0DEDzQtuBfwFR",
"destination_id": "31TgvySz1ARnqMZUdbuxykqqxGV",
"source_asset": "USD",
"source_network_id": "fiat",
"destination_asset": "USDC",
"destination_network_id": "ethereum-mainnet",
"amount": {
"value": "999999.00",
"asset": "USD"
}
});
let response = client
.post("https://api.platform.sandbox.dakota.xyz/transactions/one-off")
.headers(headers)
.json(&body)
.send()
.await?;
Ok(())
}
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
import java.util.UUID;
public class DakotaFailedTransactionExample {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newHttpClient();
String body = """
{
"customer_id": "31TgvwFzi3rstV0DEDzQtuBfwFR",
"destination_id": "31TgvySz1ARnqMZUdbuxykqqxGV",
"source_asset": "USD",
"source_network_id": "fiat",
"destination_asset": "USDC",
"destination_network_id": "ethereum-mainnet",
"amount": {
"value": "999999.00",
"asset": "USD"
}
}
""";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.platform.sandbox.dakota.xyz/transactions/one-off"))
.header("X-API-Key", "AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=")
.header("X-Idempotency-Key", UUID.randomUUID().toString())
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());
}
}
Simulated Processing Times
In sandbox, transactions complete faster:
- Onramp: 30 seconds to 2 minutes
- Offramp: 1-3 minutes
- Swap: 10-30 seconds
- Failed: Immediate
Webhook Testing
Test Webhook Events
Trigger test webhooks manually:
POST /v1/test/webhooks/trigger
{
"event": "transaction.status.updated",
"webhook_target_id": "wh_test_123",
"data": {
"transaction_id": "31TgvtxUdXi95dUN4M8X1rhSCNS",
"status": "completed"
}
}
ngrok for Local Development
# Expose local server
ngrok http 3000
# Use HTTPS URL for webhook registration
curl -X POST https://api.platform.sandbox.dakota.xyz/webhooks/targets \
-H "X-API-Key: your-api-key" \
-H "X-Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"url": "https://abc123.ngrok.io/webhooks", "active": true}'
Webhook.site for Quick Testing
- Visit webhook.site
- Copy the unique URL
- Register it as your webhook endpoint
- View real-time webhook deliveries
Integration Testing Checklist
Authentication
Customer Management
Transaction Processing
Webhook Handling
Error Simulation
Force Specific Errors
Use special values to trigger errors in sandbox:
Transaction Errors
// Force insufficient funds error
{ source_amount: { value: "999999.00", currency: "USD" }}
// Force invalid address error
{ destination: { address: "invalid_address" }}
// Force rate limit error
// Make 150+ requests in 1 minute with same key
KYB Errors
// Force KYB rejection
{ email: "rejected@test.com" }
// Force timeout
{ email: "timeout@test.com" }
Load Testing
Rate Limit Testing
Test your rate limit handling:
# Simple rate limit test - run this script
for i in {1..150}; do
curl -X GET https://api.platform.sandbox.dakota.xyz/customers \
-H "X-API-Key: AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=" &
done
wait
async function testRateLimit() {
const promises = [];
// Make 150 concurrent requests
for (let i = 0; i < 150; i++) {
promises.push(
fetch('https://api.platform.sandbox.dakota.xyz/customers', {
headers: {
'X-API-Key': 'AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc='
}
}).catch(err => err)
);
}
const results = await Promise.all(promises);
const rateLimited = results.filter(r => r.status === 429);
console.log(`${rateLimited.length} requests were rate limited`);
}
import asyncio
import aiohttp
async def test_rate_limit():
async with aiohttp.ClientSession() as session:
tasks = []
# Make 150 concurrent requests
for i in range(150):
task = session.get(
'https://api.platform.sandbox.dakota.xyz/customers',
headers={'X-API-Key': 'AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc='}
)
tasks.append(task)
responses = await asyncio.gather(*tasks, return_exceptions=True)
rate_limited = [r for r in responses if hasattr(r, 'status') and r.status == 429]
print(f'{len(rate_limited)} requests were rate limited')
# Run the test
asyncio.run(test_rate_limit())
Transaction Volume Testing
# Transaction volume test - run multiple transactions
for i in {1..10}; do
curl -X POST https://api.platform.sandbox.dakota.xyz/transactions/one-off \
-H "X-API-Key: AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=" \
-H "X-Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"customer_id": "31TgvwFzi3rstV0DEDzQtuBfwFR",
"destination_id": "31TgvySz1ARnqMZUdbuxykqqxGV",
"source_asset": "USD",
"source_network_id": "fiat",
"destination_asset": "USDC",
"destination_network_id": "ethereum-mainnet",
"amount": {
"value": "10.00",
"asset": "USD"
}
}' &
done
wait
async function testTransactionVolume() {
const transactions = [];
for (let i = 0; i < 10; i++) {
transactions.push(
fetch('https://api.platform.sandbox.dakota.xyz/transactions/one-off', {
method: 'POST',
headers: {
'X-API-Key': 'AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=',
'X-Idempotency-Key': `test_${i}_${Date.now()}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
customer_id: '31TgvwFzi3rstV0DEDzQtuBfwFR',
destination_id: '31TgvySz1ARnqMZUdbuxykqqxGV',
source_asset: 'USD',
source_network_id: 'fiat',
destination_asset: 'USDC',
destination_network_id: 'ethereum-mainnet',
amount: {
value: '10.00',
asset: 'USD'
}
})
})
);
}
const results = await Promise.allSettled(transactions);
console.log(`${results.filter(r => r.status === 'fulfilled').length} succeeded`);
}
import asyncio
import aiohttp
import uuid
async def test_transaction_volume():
async with aiohttp.ClientSession() as session:
tasks = []
for i in range(10):
task = session.post(
'https://api.platform.sandbox.dakota.xyz/transactions/one-off',
headers={
'X-API-Key': 'AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=',
'X-Idempotency-Key': str(uuid.uuid4()),
'Content-Type': 'application/json'
},
json={
'customer_id': '31TgvwFzi3rstV0DEDzQtuBfwFR',
'destination_id': '31TgvySz1ARnqMZUdbuxykqqxGV',
'source_asset': 'USD',
'source_network_id': 'fiat',
'destination_asset': 'USDC',
'destination_network_id': 'ethereum-mainnet',
'amount': {
'value': '10.00',
'asset': 'USD'
}
}
)
tasks.append(task)
results = await asyncio.gather(*tasks, return_exceptions=True)
succeeded = [r for r in results if not isinstance(r, Exception)]
print(f'{len(succeeded)} transactions succeeded')
# Run the test
asyncio.run(test_transaction_volume())
package main
import (
"bytes"
"fmt"
"net/http"
"sync"
"github.com/google/uuid"
)
func main() {
client := &http.Client{}
var wg sync.WaitGroup
successCount := 0
mu := sync.Mutex{}
for i := 0; i < 10; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
body := bytes.NewBufferString(`{
"customer_id": "31TgvwFzi3rstV0DEDzQtuBfwFR",
"destination_id": "31TgvySz1ARnqMZUdbuxykqqxGV",
"source_asset": "USD",
"source_network_id": "fiat",
"destination_asset": "USDC",
"destination_network_id": "ethereum-mainnet",
"amount": {
"value": "10.00",
"asset": "USD"
}
}`)
req, _ := http.NewRequest("POST", "https://api.platform.sandbox.dakota.xyz/transactions/one-off", body)
req.Header.Add("X-API-Key", "AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=")
req.Header.Add("X-Idempotency-Key", uuid.New().String())
req.Header.Add("Content-Type", "application/json")
resp, err := client.Do(req)
if err == nil && resp.StatusCode < 400 {
mu.Lock()
successCount++
mu.Unlock()
}
if resp != nil {
resp.Body.Close()
}
}(i)
}
wg.Wait()
fmt.Printf("%d transactions succeeded\n", successCount)
}
use reqwest::header::{HeaderMap, HeaderValue};
use uuid::Uuid;
use serde_json::json;
use tokio::task::JoinSet;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = reqwest::Client::new();
let mut tasks = JoinSet::new();
for i in 0..10 {
let client_clone = client.clone();
tasks.spawn(async move {
let mut headers = HeaderMap::new();
headers.insert("X-API-Key", HeaderValue::from_static("AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc="));
headers.insert("X-Idempotency-Key", HeaderValue::from_str(&Uuid::new_v4().to_string()).unwrap());
headers.insert("Content-Type", HeaderValue::from_static("application/json"));
let body = json!({
"customer_id": "31TgvwFzi3rstV0DEDzQtuBfwFR",
"destination_id": "31TgvySz1ARnqMZUdbuxykqqxGV",
"source_asset": "USD",
"source_network_id": "fiat",
"destination_asset": "USDC",
"destination_network_id": "ethereum-mainnet",
"amount": {
"value": "10.00",
"asset": "USD"
}
});
client_clone
.post("https://api.platform.sandbox.dakota.xyz/transactions/one-off")
.headers(headers)
.json(&body)
.send()
.await
.is_ok()
});
}
let mut success_count = 0;
while let Some(result) = tasks.join_next().await {
if result.unwrap_or(false) {
success_count += 1;
}
}
println!("{} transactions succeeded", success_count);
Ok(())
}
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.IntStream;
import java.time.Duration;
public class DakotaTransactionVolumeTest {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(30))
.build();
ExecutorService executor = Executors.newFixedThreadPool(10);
CompletableFuture<Integer>[] futures = IntStream.range(0, 10)
.mapToObj(i -> CompletableFuture.supplyAsync(() -> {
try {
String body = """
{
"customer_id": "31TgvwFzi3rstV0DEDzQtuBfwFR",
"destination_id": "31TgvySz1ARnqMZUdbuxykqqxGV",
"source_asset": "USD",
"source_network_id": "fiat",
"destination_asset": "USDC",
"destination_network_id": "ethereum-mainnet",
"amount": {
"value": "10.00",
"asset": "USD"
}
}
""";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.platform.sandbox.dakota.xyz/transactions/one-off"))
.header("X-API-Key", "AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=")
.header("X-Idempotency-Key", UUID.randomUUID().toString())
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());
return response.statusCode() < 400 ? 1 : 0;
} catch (Exception e) {
return 0;
}
}, executor))
.toArray(CompletableFuture[]::new);
int successCount = CompletableFuture.allOf(futures)
.thenApply(v -> IntStream.range(0, futures.length)
.map(i -> futures[i].join())
.sum())
.get();
System.out.println(successCount + " transactions succeeded");
executor.shutdown();
}
}
Production Readiness
Pre-Launch Checklist
Before switching to production:
Migration to Production
-
Update Configuration
- Change base URL to
https://api.platform.dakota.xyz
- Replace development API keys with production keys
- Update webhook URLs if needed
-
Environment Variables
DAKOTA_API_KEY=your_production_api_key
DAKOTA_BASE_URL=https://api.platform.dakota.xyz
DAKOTA_PUBLIC_KEY=your_production_webhook_public_key
-
Gradual Rollout
- Start with small transaction amounts
- Monitor error rates closely
- Have rollback plan ready
Monitoring and Debugging
Logging Best Practices
// Log all API calls
logger.info('Dakota Platform API call', {
method: 'POST',
endpoint: '/accounts',
account_type: 'onramp',
destination_asset: 'USDC'
});
// Log webhook events
logger.info('Webhook received', {
event: event.event,
webhook_id: event.id,
data: event.data
});
Debug Mode
Enable detailed request/response logging:
const dakota = new DakotaSDK({
baseURL: 'https://api.platform.sandbox.dakota.xyz',
apiKey: 'your-api-key',
debug: true // Logs all HTTP requests/responses
});
X-Idempotency-Key is Required for ALL POST EndpointsPer the OpenAPI specification, the X-Idempotency-Key header is required for all POST endpoints to ensure request idempotency. This includes every create operation in the API.Requests without this header will fail.# Always include X-Idempotency-Key on ALL POST requests
curl -X POST https://api.platform.sandbox.dakota.xyz/customers \
-H "X-API-Key: YOUR_API_KEY" \
-H "X-Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{...}'
The idempotency key should be a unique UUID. If the same key is used within a certain time window, the original response will be returned instead of executing the request again.
Common Issues and Solutions
Issue: HTTP 400 on POST Requests
Cause: The X-Idempotency-Key header is required for ALL POST endpoints but is missing.
Solution: Add X-Idempotency-Key: <unique-uuid> header to every POST request. Generate a new UUID for each unique request.
Issue: Webhooks Not Received
Solution: Check webhook endpoint accessibility and response codes. Use ngrok or webhook.site for local testing.
Issue: Transactions Stuck in Processing
Solution: Use the simulation endpoints to progress transaction state, or add X-Sandbox-Instant-Completion: true header when creating transactions.
Issue: KYB Never Completes
Solution: KYB auto-approves after 5 seconds in sandbox. If using X-Sandbox-Skip-Auto-Approval: true, manually approve via POST /sandbox/customers/{id}/kyb/simulate.
Issue: Rate Limits Hit Quickly
Solution: Implement exponential backoff and request queuing.
Issue: Currency Mismatch Error on Account Simulation
Cause: The currency field in simulate request doesn’t match the account’s input_asset.
Solution: For onramp accounts (USD→USDC), use currency: "USD". For offramp accounts (USDC→USD), use currency: "USDC".
Next Steps
After thorough testing:
- Authentication - Review production security practices
- Customer Onboarding - Set up production KYB flows
- Transactions - Process real transactions
- Webhooks - Configure production webhook endpoints
API Reference
For testing-specific documentation, see: