# Dakota Platform - LLM Reference ## Quick Start (IMPORTANT - Read First) To make API requests to Dakota, you need an API key and the correct base URL. ### API Base URLs | Environment | API Base URL | Dashboard URL | |-------------|--------------|---------------| | Sandbox | `https://api.platform.sandbox.dakota.xyz` | `https://platform.sandbox.dakota.xyz` | | Production | `https://api.platform.dakota.xyz` | `https://platform.dakota.xyz` | **Recommendation**: Start with the Sandbox environment for development and testing. ### Getting an API Key 1. **Get Dashboard Access**: Contact [Dakota sales](https://dakota.io/talk-to-sales) to set up an account 2. **Log into the Dashboard**: Go to the appropriate dashboard URL above 3. **Create an API Key**: Navigate to **API Keys** section and click **Create New API Key** 4. **Store Securely**: Copy the key immediately (it's only shown once) ### Making Your First Request ```bash curl -X GET https://api.platform.sandbox.dakota.xyz/customers \ -H "x-api-key: YOUR_API_KEY" ``` ### Required Headers | Header | Required For | Description | |--------|--------------|-------------| | `X-API-Key` | All requests | Your API key for authentication | | `X-Idempotency-Key` | **All POST requests** | UUID to prevent duplicate operations. **Required.** | | `Content-Type` | Requests with body | Set to `application/json` | **IMPORTANT: All POST requests MUST include `X-Idempotency-Key` header with a UUID value. Requests without this header will fail.** Example POST request with all required headers: ```bash curl -X POST https://api.platform.dakota.xyz/customers \ -H "X-API-Key: your-api-key" \ -H "X-Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \ -H "Content-Type: application/json" \ -d '{"name": "Acme Corp", "customer_type": "business"}' ``` --- ## CRITICAL: Prerequisites and Dependencies Before calling Dakota APIs, understand these **mandatory requirements**: | To Create | You Must First Have | |-----------|---------------------| | Recipient | Customer with `kyb_status: "approved"` | | Destination | Recipient | | Onramp Account | Crypto Destination + Customer with `kyb_status: "approved"` | | Offramp Account | Bank Destination + Customer with `kyb_status: "approved"` | | Wallet | Signer Group | | Signer Group | ES256 public keys (ECDSA P-256, SPKI format, base64 encoded) | **KYB Approval Required**: Customers must have `kyb_status: "approved"` before creating onramp/offramp accounts. --- ## Validation Rules | Field | Constraint | |-------|------------| | `customer.name` | 3-100 characters, unique per client | | `recipient.name` | Unique per customer | | `recipient.address` | Requires `street1`, `city`, `country` (ISO 3166-1 alpha-2) | --- ## CRITICAL: ID Types Are NOT Interchangeable Dakota uses different ID types. Using the wrong ID type causes errors: | ID Type | Source Endpoint | Used For | Common Error When Misused | |---------|-----------------|----------|---------------------------| | `wallet_id` | `POST /wallets` | Balance queries only | `destination_not_found` | | `destination_id` | `POST /recipients/{id}/destinations` | Onramp, offramp, transactions | N/A | | `recipient_id` | `POST /customers/{id}/recipients` | Creating destinations | N/A | | `customer_id` | `POST /customers` | All customer operations | N/A | | `signer_group_id` | `POST /signer-groups` | Creating wallets | N/A | **Key Point**: For onramp/offramp, you need a `destination_id` NOT a `wallet_id`. --- ## Complete Flow: Onramp (Fiat to Crypto) ### Step 1: Ensure Customer is KYB Approved ```bash GET /customers/{customer_id} # Response must have "kyb_status": "approved" ``` ### Step 2: Create a Recipient ```bash POST /customers/{customer_id}/recipients Content-Type: application/json X-API-Key: your-api-key X-Idempotency-Key: unique-uuid { "name": "Company Treasury", "address": { "street1": "123 Main St", "city": "San Francisco", "region": "CA", "postal_code": "94102", "country": "US" } } # Response contains "id" - this is the recipient_id ``` ### Step 3: Create a Crypto Destination ```bash POST /recipients/{recipient_id}/destinations Content-Type: application/json X-API-Key: your-api-key X-Idempotency-Key: unique-uuid { "destination_type": "crypto", "name": "USDC Wallet", "crypto_address": "0x742d35Cc6634C0532925a3b8D404fA40b5398Ad2", "network_id": "ethereum-mainnet" } # Response contains "id" - this is the destination_id. USE THIS FOR ONRAMP. ``` ### Step 4: Create Onramp Account ```bash POST /accounts/onramp Content-Type: application/json X-API-Key: your-api-key X-Idempotency-Key: unique-uuid { "destination_id": "THE_DESTINATION_ID_FROM_STEP_3", "source_asset": "USD", "destination_asset": "USDC", "capabilities": ["ach"] } ``` --- ## Complete Flow: Creating a Wallet Wallets require signer groups. The `signer_groups` field takes an array of **STRING IDs**. ### Public Key Format for member_keys The `member_keys` array requires **ECDSA P-256 (ES256) public keys**: - **Algorithm**: ECDSA with P-256 curve (secp256r1) - **Format**: PKIX/SPKI DER format, Base64 encoded - **Key Type**: ES256 **Generate ES256 key (Node.js):** ```javascript const crypto = require('crypto'); const { publicKey, privateKey } = crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' }); const publicKeyBase64 = publicKey.export({ type: 'spki', format: 'der' }).toString('base64'); // Use publicKeyBase64 as member_key - store privateKey securely client-side ``` **Generate ES256 key (Python):** ```python from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives import serialization import base64 private_key = ec.generate_private_key(ec.SECP256R1()) public_key_der = private_key.public_key().public_bytes( encoding=serialization.Encoding.DER, format=serialization.PublicFormat.SubjectPublicKeyInfo ) public_key_base64 = base64.b64encode(public_key_der).decode('utf-8') # Use public_key_base64 as member_key ``` ### Step 1: Create a Signer Group ```bash POST /signer-groups Content-Type: application/json X-API-Key: your-api-key X-Idempotency-Key: unique-uuid { "name": "Treasury Team", "member_keys": ["MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...base64-encoded-spki..."] } # Response contains "id" - this is the signer_group_id ``` ### Step 2: Create Wallet ```bash POST /wallets Content-Type: application/json X-API-Key: your-api-key X-Idempotency-Key: unique-uuid { "name": "Main Wallet", "family": "evm", "signer_groups": ["THE_SIGNER_GROUP_ID_FROM_STEP_1"], "policies": [] } ``` **CORRECT format**: `"signer_groups": ["id1", "id2"]` (array of strings) **WRONG format**: `"signer_groups": [{"id": "id1"}]` (causes "value must be a string" error) --- ## Destination Schemas (Required Fields) ### Crypto Destination ```json POST /recipients/{recipient_id}/destinations { "destination_type": "crypto", "name": "My USDC Wallet", "crypto_address": "0x742d35Cc6634C0532925a3b8D404fA40b5398Ad2", "network_id": "ethereum-mainnet" } ``` ### US Bank Destination (fiat_us) - All Required Fields ```json POST /recipients/{recipient_id}/destinations { "destination_type": "fiat_us", "name": "Business Checking", "aba_routing_number": "021000021", "account_number": "123456789", "account_type": "checking", "account_holder_name": "Acme Corporation", "bank_name": "Chase Bank", "bank_address": { "street1": "383 Madison Avenue", "city": "New York", "country": "US" } } ``` ### International Bank Destination (fiat_iban) - All Required Fields ```json POST /recipients/{recipient_id}/destinations { "destination_type": "fiat_iban", "name": "European Account", "iban": "DE89370400440532013000", "bic": "DEUTDEFFXXX", "account_holder_name": "Acme GmbH", "account_holder_address": { "street1": "Friedrichstraße 123", "city": "Berlin", "country": "DE" }, "bank_name": "Deutsche Bank", "bank_address": { "street1": "Taunusanlage 12", "city": "Frankfurt", "country": "DE" }, "assets": ["EUR"], "capabilities": ["sepa"] } ``` --- ## Supported Assets (Tokens) | Asset | Name | Type | |-------|------|------| | `USD` | US Dollar | Fiat | | `EUR` | Euro | Fiat | | `USDC` | USD Coin | Stablecoin | | `USDT` | Tether | Stablecoin | | `DKUSD` | Dakota USD | Stablecoin | | `ETH` | Ethereum | Native | | `SOL` | Solana | Native | --- ## Payment Rails | Rail | Description | Assets | |------|-------------|--------| | `ach` | US ACH | USD | | `fedwire` | US Fedwire | USD | | `swift` | International SWIFT | USD, EUR | | `sepa` | European SEPA | EUR | --- ## Supported Networks **Mainnet (Production):** `ethereum-mainnet`, `polygon-mainnet`, `arbitrum-mainnet`, `base-mainnet`, `optimism-mainnet`, `solana-mainnet` **Testnet (Sandbox):** `ethereum-sepolia`, `ethereum-holesky`, `base-sepolia`, `arbitrum-sepolia`, `optimism-sepolia`, `polygon-amoy`, `solana-devnet`, `solana-testnet` --- ## Token Availability by Network | Network | USDC | USDT | DKUSD | |---------|------|------|-------| | `ethereum-mainnet` | Yes | Yes | - | | `polygon-mainnet` | Yes | Yes | - | | `arbitrum-mainnet` | Yes | Yes | - | | `base-mainnet` | Yes | Yes | Yes | | `optimism-mainnet` | Yes | Yes | - | | `solana-mainnet` | Yes | Yes | - | --- ## Common Errors and Solutions | Error | Cause | Solution | |-------|-------|----------| | `401 Unauthorized` | Missing or invalid API key | Include `X-API-Key: your-api-key` header | | `400 Bad Request` (missing idempotency) | Missing `X-Idempotency-Key` on POST | Add `X-Idempotency-Key: ` header | | `409 Conflict` | Reused idempotency key | Generate new UUID for each unique request | | `customer_not_found` | Customer ID doesn't exist | Verify customer was created | | `customer_not_approved` | Customer KYB not complete | Wait for `kyb_status: "approved"` | | `customer name must be at least 3 characters` | Name too short | Use 3-100 characters | | `a customer with that name already exists` | Duplicate name | Use unique customer name | | `recipient_not_found` | Recipient doesn't exist or wrong client | Verify recipient exists and you own it | | `duplicate_name` | Duplicate recipient name per customer | Use unique recipient name | | `destination_not_found` | Using wallet_id instead of destination_id | Create destination first | | `field 'signer_groups/0': value must be a string` | Objects in signer_groups | Use `["id"]` not `[{"id": "..."}]` | | Invalid public key | Wrong key format | Use ES256 ECDSA P-256, SPKI DER, base64 | | `403 Forbidden` | Accessing another client's resource | You can only access your own resources | --- # Get Started Source: https://docs.dakota.io/ Dakota is a regulated stablecoin infrastructure platform that enables fintechs and enterprises to embed programmable global money movement through APIs. Dakota is a modern, developer-first financial infrastructure platform built for payments, treasury, and money movement. It is designed to operate reliably at scale, with production-grade performance from day one. The platform focuses on simplicity and clarity, offering clean APIs and predictable behavior. This allows teams to integrate quickly while maintaining full control over financial flows. These docs are designed to help you get started fast and move with confidence. They cover core concepts, key primitives, and practical guidance for building with Dakota. Whether you are prototyping or running in production, the documentation is structured to support real-world use cases. You will find clear examples, best practices, and references to build efficiently. # Terminology & Key Concepts Source: https://docs.dakota.io/guides/terminology Understanding Dakota Platform terminology is essential for successful API integration. This glossary defines the key concepts and entities you'll encounter throughout the documentation and API. # Core Entities ## Client **Your business** - The developer or company integrating with Dakota Platform. This is you, building payment functionality into your application or service. ## Customer **Your end user** - A business or individual that you (the client) want to process payments for. Customers must complete KYB/KYC verification before processing transactions. *Example: If you're building an invoicing app, your customers would be the businesses using your app who need to receive crypto payments.* ## Recipient **Payment receiver** - A person or entity that will receive payments on behalf of a customer. A single customer can have multiple recipients for different use cases. *Example: A customer (Acme Corp) might have recipients for "Marketing Department" and "Sales Department" to organize incoming payments.* ## Destination **Payment endpoint** - The specific account, wallet address, or bank account where funds will be sent for a recipient. Each recipient can have multiple destinations across different networks and assets. *Example: A recipient might have a USDC destination on Ethereum (0x123...) and a separate USDC destination on Polygon (0xabc...).* # Account Types & Operations ## Auto Account **Automated recurring account setup** - A special account configuration that provides funding details for automated, recurring payment processing. Creating an auto account doesn't initiate transactions - it sets up the funding infrastructure. Actual transactions occur when funds are received at the provided addresses. *Use case: Subscription billing, payroll payments, or scheduled vendor payments.* ## One-off Transaction **Single payment setup** - Individual transaction configuration that provides funding details for on-demand payments. Creating a one-off transaction doesn't initiate the payment - it generates the funding instructions. The actual transaction happens when funds are sent to the provided address. *Use case: Invoice payments, ad-hoc purchases, or manual transfers.* # Transaction Types ## Onramp **Fiat to crypto conversion** - Converting traditional currency (USD, EUR, etc.) into cryptocurrency or stablecoins. Customers use onramps to fund their crypto activities. *Flow: USD bank account → USDC wallet* ## Offramp **Crypto to fiat conversion** - Converting cryptocurrency or stablecoins back into traditional currency. Customers use offramps to cash out their crypto holdings. *Flow: USDC wallet → USD bank account* ## Swap **Crypto to crypto exchange** - Converting one cryptocurrency or stablecoin into another without touching fiat currency. This enables portfolio management and asset optimization. *Flow: ETH → USDC, or USDC → USDT* # Compliance & Verification ## KYB (Know Your Business) **Business verification process** - Required compliance procedure for business customers. Includes verifying business registration, ownership, financial standing, and regulatory compliance. *Typical requirements: Certificate of incorporation, bank statements, director identification, beneficial ownership disclosure.* ## KYC (Know Your Customer) **Individual verification process** - Required compliance procedure for individual customers. Includes identity verification, address confirmation, and risk assessment. *Typical requirements: Government-issued ID, proof of address, source of funds documentation.* # Technical Infrastructure ## Provider **Service integration** - Services that Dakota Platform integrates with to provide specific functionality. Each provider specializes in different aspects of payment processing. Could also be Dakota itself. ## Network **Blockchain infrastructure** - The underlying blockchain where cryptocurrency transactions are processed. ## Asset **Currency or token** - Any form of value that can be transferred through the platform. Includes both fiat currencies and cryptocurrencies. *Examples: USD, EUR (fiat); ETH (crypto); USDC, USDT (stablecoins).* # API Integration Concepts ## Idempotency **Safe request retrying** - A mechanism ensuring that the same API request can be made multiple times without creating duplicate resources or side effects. Required for all state-changing operations. *Implementation: Include a unique `X-Idempotency-Key` header with each POST request. Use UUIDs for guaranteed uniqueness.* ## Webhook **Real-time notifications** - HTTP callbacks sent by Dakota Platform to your application when specific events occur. Essential for tracking transaction status and KYB/KYC completion. *Examples: Customer KYB approved, transaction completed, payment failed.* ## Rate Limiting **Request throttling** - Protective measures that limit how many API requests you can make within a specific time period. Prevents system overload and ensures fair resource usage. *Typical limits: 100 requests per minute per API key. Exceeded limits return HTTP 429 status.* # Business Workflow Terms ## External ID **Your internal reference** - An optional identifier you can assign to Dakota Platform resources (customers, transactions, etc.) to link them with your internal systems. *Use case: Link Dakota customer ID with your database's user ID for easier reconciliation.* ## Sandbox vs Production **Development environments** - Separate environments for testing (sandbox) and live operations (production). Sandbox uses test data and simulated transactions. *Sandbox: Test API integration without real money or compliance requirements* *Production: Live environment with real transactions and full compliance* ## Status States **Resource lifecycle tracking** - Standardized status values that indicate where a resource is in its processing lifecycle. *Common states: pending, in_progress, completed, failed, requires_info, approved, rejected.* # Advanced Concepts ## Multi-signature (Multisig) **Shared custody control** - A security mechanism requiring multiple cryptographic signatures to authorize transactions. Provides enhanced security for high-value operations. *Coming soon to Dakota Platform* ## Compliance Monitoring **Ongoing risk assessment** - Continuous monitoring of transactions and customer activity for suspicious patterns, regulatory compliance, and risk management. *Automated by Dakota Platform using integrated compliance providers* ## Settlement **Final transaction completion** - The process of actually moving funds from source to destination after all verification and processing steps are complete. *Timeline varies by transaction type and network: onramps (1-3 days), crypto transfers (minutes to hours).* --- # Quick Reference | Term | Category | Key Point | |------|----------|-----------| | Client | Entity | You (the developer/company) | | Customer | Entity | Your end users who need payments | | Recipient | Entity | Who receives payments | | Destination | Entity | Where payments are sent | | Onramp | Transaction | Fiat → Crypto | | Offramp | Transaction | Crypto → Fiat | | Swap | Transaction | Crypto → Crypto | | KYB | Compliance | Business verification | | KYC | Compliance | Individual verification | | Provider | Technical | Third-party integrations | | Idempotency | API | Safe request retrying | # Next Steps Now that you understand Dakota Platform terminology: 1. **[Review API Concepts](api-concepts)** - Learn about response formats, pagination, and error handling 2. **[Set Up Your Environment](environments)** - Configure API keys and choose sandbox vs production 3. **[Complete Integration Walkthrough](integration-walkthrough)** - Follow our step-by-step implementation guide Understanding these terms will make the rest of the documentation much clearer and help you communicate effectively with our support team. # Customer Onboarding Source: https://docs.dakota.io/guides/customer-onboarding Before processing payments, customers must complete KYB (Know Your Business) onboarding. This guide explains how to integrate Dakota Platform's onboarding flow. # Overview The onboarding process involves: 1. Creating a customer record (KYB automatically initiated) 2. Redirecting customer to complete verification 3. Monitoring KYB status via API or webhooks # Step 1: Create a Customer First, create a customer record with basic business information. ## Customer Creation Fields | Field | Type | Required | Description | Example | |-------|------|----------|-------------|---------| | `name` | string | ✅ | Legal name of the business or individual | `"Acme Corp"` | | `customer_type` | string | ✅ | Type of customer account. Must be `"business"` or `"individual"` | `"business"` | | `external_id` | string | ❌ | Your internal identifier for this customer | `"acme_001"` | ## Request Example ```bash cURL curl -X POST https://api.platform.dakota.xyz/customers \ -H "X-API-Key: your-api-key" \ -H "X-Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "name": "Acme Corp", "customer_type": "business" }' ``` ```javascript JavaScript const response = await fetch('https://api.platform.dakota.xyz/customers', { method: 'POST', headers: { 'X-API-Key': 'your-api-key', 'X-Idempotency-Key': crypto.randomUUID(), 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'Acme Corp', customer_type: 'business' }) }); const customer = await response.json(); console.log('Created customer:', customer.data.id); ``` ```python Python import requests import uuid response = requests.post( 'https://api.platform.dakota.xyz/customers', headers={ 'X-API-Key': 'your-api-key', 'X-Idempotency-Key': str(uuid.uuid4()), 'Content-Type': 'application/json' }, json={ 'name': 'Acme Corp', 'customer_type': 'business' } ) customer = response.json() print(f'Created customer: {customer["data"]["id"]}') ``` ```go Go package main import ( "bytes" "net/http" "github.com/google/uuid" ) func main() { client := &http.Client{} body := bytes.NewBufferString(`{ "name": "Acme Corp", "customer_type": "business" }`) req, _ := http.NewRequest("POST", "https://api.platform.dakota.xyz/customers", body) req.Header.Add("X-API-Key", "your-api-key") req.Header.Add("X-Idempotency-Key", uuid.New().String()) req.Header.Add("Content-Type", "application/json") resp, _ := client.Do(req) defer resp.Body.Close() } ``` ```rust Rust use reqwest::header::{HeaderMap, HeaderValue}; use uuid::Uuid; use serde_json::json; #[tokio::main] async fn main() -> Result<(), Box> { let client = reqwest::Client::new(); let mut headers = HeaderMap::new(); headers.insert("X-API-Key", HeaderValue::from_static("your-api-key")); 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!({ "name": "Acme Corp", "customer_type": "business" }); let response = client .post("https://api.platform.dakota.xyz/customers") .headers(headers) .json(&body) .send() .await?; Ok(()) } ``` ```java Java 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 DakotaCustomerExample { public static void main(String[] args) throws Exception { HttpClient client = HttpClient.newHttpClient(); String body = """ { "name": "Acme Corp", "customer_type": "business" } """; HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.platform.dakota.xyz/customers")) .header("X-API-Key", "your-api-key") .header("X-Idempotency-Key", UUID.randomUUID().toString()) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(body)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); } } ``` Response: ```json { "data": { "id": "2Nq8G7xK9mR4Ls6DyJ1Uf3Tp", "name": "Acme Corp", "customer_type": "business", "external_id": null, "kyb_status": "pending", "kyb_links": [ { "provider_id": "dakota", "link_type": "kyb_onboarding", "url": "https://withpersona.com/verify?inquiry-id=inq_abc123456789", "status": "pending", "created_at": 1705320600, "updated_at": 1705320600 }, { "provider_id": "bridge", "link_type": "tos", "url": "https://dashboard.bridge.xyz/accept-terms-of-service?redirect_uri=https%3A%2F%2Fapi.platform.dakota.xyz%2Fcustomers%2Fbridge%2Faccepted-tos%3Ftos_id%3D30yRrO1wNMuzUri3BAdhKBiMM09&session_token=56e52844-bbce-402b-a66e-7b97f6d02967", "status": "pending", "created_at": 1705320600, "updated_at": 1705320600 } ], "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-15T10:30:00Z" } } ``` # Step 2: KYB Process (Automatic) The KYB onboarding process is automatically initiated when you create a customer. You don't need to make additional API calls to start KYB verification. ## Automatic KYB Initiation When you create a customer, the system automatically: - Generates a KYB onboarding link with Persona (our default provider) - Returns the onboarding URL in the `kyb_links` array - Sets the initial KYB status to `"pending"` ## KYB Response Details The customer creation response includes: | Field | Description | Example | |-------|-------------|---------| | `kyb_links` | Array of KYB provider information and onboarding links | See below | | `kyb_status` | Current verification status | `"pending"` | ## KYB Links Structure Each item in the `kyb_links` array contains: | Field | Description | Example | |-------|-------------|---------| | `provider_id` | KYB provider name | `"dakota"` | | `link_type` | Type of KYB link | `"kyb_onboarding"` | | `url` | Onboarding URL for customer | `"https://withpersona.com/verify?inquiry-id=inq_abc123"` | | `status` | Link status | `"pending"` | | `created_at` | Link creation timestamp | `1705320600` | | `updated_at` | Link last updated timestamp | `1705320600` | ## Using the KYB Link From the customer creation response, extract the onboarding URL: ```javascript const customer = await response.json(); const kybLink = customer.data.kyb_links[0]; const onboardingUrl = kybLink.url; // Redirect customer to complete KYB verification window.location.href = onboardingUrl; ``` # Step 3: Handle the Onboarding Flow Direct your customer to the onboarding URL (from the `kyb_links` array) to complete verification. The customer will: 1. Provide business documentation 2. Verify business identity 3. Complete compliance checks 4. Receive approval or additional requirements # Step 4: Monitor KYB Status Check onboarding status programmatically: ```bash cURL curl -X GET https://api.platform.dakota.xyz/customers/{customer_id} \ -H "X-API-Key: your-api-key" ``` ```javascript JavaScript const response = await fetch(`https://api.platform.dakota.xyz/customers/${customerId}`, { headers: { 'X-API-Key': 'your-api-key' } }); const customer = await response.json(); console.log('KYB Status:', customer.data.kyb_status); ``` ```python Python import requests response = requests.get( f'https://api.platform.dakota.xyz/customers/{customer_id}', headers={'X-API-Key': 'your-api-key'} ) customer = response.json() print(f'KYB Status: {customer["data"]["kyb_status"]}') ``` ```go Go package main import ( "net/http" ) func main() { client := &http.Client{} req, _ := http.NewRequest("GET", "https://api.platform.dakota.xyz/customers/" + customerId, nil) req.Header.Add("X-API-Key", "your-api-key") resp, _ := client.Do(req) defer resp.Body.Close() } ``` ```rust Rust use reqwest::header::{HeaderMap, HeaderValue}; #[tokio::main] async fn main() -> Result<(), Box> { let client = reqwest::Client::new(); let mut headers = HeaderMap::new(); headers.insert("X-API-Key", HeaderValue::from_static("your-api-key")); let response = client .get(&format!("https://api.platform.dakota.xyz/customers/{}", customer_id)) .headers(headers) .send() .await?; Ok(()) } ``` ```java Java import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.URI; public class DakotaKYBStatusExample { public static void main(String[] args) throws Exception { HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.platform.dakota.xyz/customers/" + customerId)) .header("X-API-Key", "your-api-key") .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); } } ``` Response: ```json { "data": { "id": "2Nq8G7xK9mR4Ls6DyJ1Uf3Tp", "name": "Acme Corp", "customer_type": "business", "external_id": null, "kyb_status": "approved", "kyb_links": [ { "provider_id": "dakota", "link_type": "kyb_onboarding", "url": "https://withpersona.com/verify?inquiry-id=inq_abc123456789", "status": "completed", "created_at": 1705320600, "updated_at": 1705324200 }, { "provider_id": "bridge", "link_type": "tos", "url": "https://dashboard.bridge.xyz/accept-terms-of-service?redirect_uri=https%3A%2F%2Fapi.platform.dakota.xyz%2Fcustomers%2Fbridge%2Faccepted-tos%3Ftos_id%3D30yRrO1wNMuzUri3BAdhKBiMM09&session_token=56e52844-bbce-402b-a66e-7b97f6d02967", "status": "completed", "created_at": 1705320600, "updated_at": 1705322400 } ], "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-15T11:45:00Z" } } ``` # KYB Status Values | Status | Description | |--------|-------------| | `not_started` | Onboarding not yet started | | `in_progress` | Customer is completing verification | | `in_review` | Provider is reviewing submission | | `approved` | Verification successful | | `rejected` | Verification failed | | `requires_info` | Additional information needed | | `frozen` | Account suspended or frozen | # Best Practices ## User Experience - Clearly communicate the onboarding requirements - Provide progress indicators - Set expectations for processing time - Offer support contact information ## Error Handling - Handle rejection gracefully - Provide clear next steps - Allow customers to restart if needed - Log all onboarding events ## Compliance - Store audit trails - Monitor for suspicious activity - Keep records for compliance reporting - Regular review of rejected applications # Required Documents Typical documents required for business verification: - **Certificate of Incorporation** - **Bank Statements** - **Director/Officer Identification** - **Beneficial Ownership Information** # Troubleshooting ## Common Issues **Onboarding Stuck in "Pending"** - Check if customer has accessed the onboarding URL **Repeated Rejections** - Ensure document quality meets requirements - Consider manual review process **Webhook Not Received** - Verify webhook endpoint is responding with 200 - Check webhook signature validation - Review webhook logs in dashboard Any issues not covered here can be addressed by contacting Dakota Platform support. # Next Steps After successful customer onboarding: 1. **[Set up Recipients & Destinations](destinations-recipients)** - Configure payment targets 2. **[Create Transactions](transactions)** - Process payments for approved customers 3. **[Webhook Integration](webhooks)** - Get real-time KYB status updates 4. **[Testing](testing)** - Test your onboarding flow # API Reference For detailed endpoint documentation, see: - [Customers API Reference](/reference/customers) - Customer creation and management - [KYB Providers API Reference](/reference/kyb-providers) - KYB initiation and status checking - [Webhooks API Reference](/reference/webhooks) - Real-time onboarding notifications # Destinations & Recipients Source: https://docs.dakota.io/guides/destinations-recipients Before processing transactions, you need to set up recipients and their associated destinations. Recipients represent entities that can receive payments, while destinations specify the exact accounts (crypto addresses, bank accounts) where funds should be sent. # Overview The Dakota Platform uses a two-level structure for payment routing: - **Recipients**: Business entities or individuals that can receive payments - **Destinations**: Specific accounts (crypto addresses, bank accounts) associated with recipients This structure allows you to manage multiple payment destinations for the same recipient while maintaining clear records for compliance and reporting. # Conceptual Model: The Payment Chain Understanding the complete payment chain is crucial for successful integration: ``` Customer → Recipient → Destination ↓ ↓ ↓ Who pays Who receives Where money goes ``` ## The Three-Level Hierarchy 1. **Customer** (Your User) - Your application's user who wants to send money - Must complete KYB verification - Can have multiple recipients - Example: "Acme Corp" (your business customer) 2. **Recipient** (Payment Target Entity) - Legal entity that will receive the payment - Has compliance information (name, address) - Can have multiple destinations for different asset types - Example: "Supplier XYZ Ltd" (the business being paid) 3. **Destination** (Specific Account) - The actual account where money is sent - Can be crypto address, bank account, etc. - Linked to exactly one recipient - Example: "0x742d..." (USDC wallet address) ## Why This Structure? **Compliance & Reporting** - Recipients provide legal entity information for compliance - Clear audit trail for who is being paid - Supports anti-money laundering requirements **Flexibility** - One recipient can have multiple destinations (different currencies/networks) - Easy to add new payment methods to existing relationships - Supports complex business relationships **Organization** - Group related payment accounts by business relationship - Cleaner user interfaces for managing multiple accounts - Better record-keeping for accounting purposes ## Real-World Example Let's say your customer "Acme Corp" wants to pay their supplier "Global Manufacturing Ltd": ``` Customer: Acme Corp └── Recipient: Global Manufacturing Ltd ├── Destination 1: Bank Account (USD payments) │ ├── Type: fiat_us │ ├── Account: ****6789 │ └── Routing: 021000021 ├── Destination 2: Ethereum Wallet (USDC payments) │ ├── Type: crypto │ ├── Address: 0x742d35Cc6634C0532925a3b8D404fA40b5398Ad2 │ └── Network: ethereum-mainnet └── Destination 3: Solana Wallet (SOL/USDC payments) ├── Type: crypto ├── Address: 7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU └── Network: solana ``` When processing a transaction, you specify: - **Customer ID**: Who is paying (31Tgw0zSyDVo4Az66kmzUjMuwxx) - **Destination ID**: Where money should go (31TgvweTHSjWNxfARrnhjmBKSem) The system automatically knows the recipient context through the destination relationship. # Complete Workflow Guide Follow this step-by-step process to set up the complete payment chain: ## Step 1: Start with an Approved Customer First, verify that your customer exists and has been KYB-approved: ```bash cURL curl -X GET https://api.platform.dakota.xyz/customers/31Tgw0zSyDVo4Az66kmzUjMuwxx \ -H "X-API-Key: your-api-key" ``` ```javascript JavaScript const response = await fetch('https://api.platform.dakota.xyz/customers/31Tgw0zSyDVo4Az66kmzUjMuwxx', { headers: { 'X-API-Key': 'your-api-key' } }); const customer = await response.json(); console.log('Customer KYB Status:', customer.data.kyb_status); ``` ```python Python import requests response = requests.get( 'https://api.platform.dakota.xyz/customers/31Tgw0zSyDVo4Az66kmzUjMuwxx', headers={'X-API-Key': 'your-api-key'} ) customer = response.json() print(f'Customer KYB Status: {customer["data"]["kyb_status"]}') ``` ```go Go package main import ( "net/http" ) func main() { client := &http.Client{} req, _ := http.NewRequest("GET", "https://api.platform.dakota.xyz/customers/31Tgw0zSyDVo4Az66kmzUjMuwxx", nil) req.Header.Add("X-API-Key", "your-api-key") resp, _ := client.Do(req) defer resp.Body.Close() } ``` ```rust Rust use reqwest::header::{HeaderMap, HeaderValue}; #[tokio::main] async fn main() -> Result<(), Box> { let client = reqwest::Client::new(); let mut headers = HeaderMap::new(); headers.insert("X-API-Key", HeaderValue::from_static("your-api-key")); let response = client .get("https://api.platform.dakota.xyz/customers/31Tgw0zSyDVo4Az66kmzUjMuwxx") .headers(headers) .send() .await?; Ok(()) } ``` ```java Java import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.URI; public class DakotaCustomerStatus { public static void main(String[] args) throws Exception { HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.platform.dakota.xyz/customers/31Tgw0zSyDVo4Az66kmzUjMuwxx")) .header("X-API-Key", "your-api-key") .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); } } ``` Expected response: ```json { "data": { "id": "cust_acme123", "name": "Acme Corp", "customer_type": "business", "external_id": null, "kyb_status": "approved", "kyb_links": [ { "provider_id": "dakota", "link_type": "kyb_onboarding", "url": "https://withpersona.com/verify?inquiry-id=inq_abc123456789", "status": "completed", "created_at": 1705320600, "updated_at": 1705324200 }, { "provider_id": "bridge", "link_type": "tos", "url": "https://dashboard.bridge.xyz/accept-terms-of-service", "status": "completed", "created_at": 1705320600, "updated_at": 1705322400 } ], "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-15T11:45:00Z" } } ``` ## Step 2: Create a Recipient ```bash cURL curl -X POST https://api.platform.dakota.xyz/customers/31Tgw0zSyDVo4Az66kmzUjMuwxx/recipients \ -H "X-API-Key: your-api-key" \ -H "X-Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "name": "Global Manufacturing Ltd", "address": { "street1": "456 Industrial Blvd", "city": "Detroit", "region": "MI", "postal_code": "48201", "country": "US" } }' ``` ```javascript JavaScript const recipient = await fetch('https://api.platform.dakota.xyz/customers/31Tgw0zSyDVo4Az66kmzUjMuwxx/recipients', { method: 'POST', headers: { 'X-API-Key': 'your-api-key', 'X-Idempotency-Key': crypto.randomUUID(), 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'Global Manufacturing Ltd', address: { street1: '456 Industrial Blvd', city: 'Detroit', region: 'MI', postal_code: '48201', country: 'US' } }) }); // Result: 31TgvwFzi3rstV0DEDzQtuBfwFR ``` ```python Python import requests import uuid response = requests.post( 'https://api.platform.dakota.xyz/customers/31Tgw0zSyDVo4Az66kmzUjMuwxx/recipients', headers={ 'X-API-Key': 'your-api-key', 'X-Idempotency-Key': str(uuid.uuid4()), 'Content-Type': 'application/json' }, json={ 'name': 'Global Manufacturing Ltd', 'address': { 'street1': '456 Industrial Blvd', 'city': 'Detroit', 'region': 'MI', 'postal_code': '48201', 'country': 'US' } } ) # Result: recp_global_mfg_789 ``` ```go Go package main import ( "bytes" "net/http" "github.com/google/uuid" ) func main() { client := &http.Client{} body := bytes.NewBufferString(`{ "name": "Global Manufacturing Ltd", "address": { "street1": "456 Industrial Blvd", "city": "Detroit", "region": "MI", "postal_code": "48201", "country": "US" } }`) req, _ := http.NewRequest("POST", "https://api.platform.dakota.xyz/customers/31Tgw0zSyDVo4Az66kmzUjMuwxx/recipients", body) req.Header.Add("X-API-Key", "your-api-key") req.Header.Add("X-Idempotency-Key", uuid.New().String()) req.Header.Add("Content-Type", "application/json") resp, _ := client.Do(req) defer resp.Body.Close() // Result: 31TgvwFzi3rstV0DEDzQtuBfwFR } ``` ```rust Rust use reqwest::header::{HeaderMap, HeaderValue}; use uuid::Uuid; use serde_json::json; #[tokio::main] async fn main() -> Result<(), Box> { let client = reqwest::Client::new(); let mut headers = HeaderMap::new(); headers.insert("X-API-Key", HeaderValue::from_static("your-api-key")); 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!({ "name": "Global Manufacturing Ltd", "address": { "street1": "456 Industrial Blvd", "city": "Detroit", "region": "MI", "postal_code": "48201", "country": "US" } }); let response = client .post("https://api.platform.dakota.xyz/customers/31Tgw0zSyDVo4Az66kmzUjMuwxx/recipients") .headers(headers) .json(&body) .send() .await?; // Result: 31TgvwFzi3rstV0DEDzQtuBfwFR Ok(()) } ``` ```java Java 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 DakotaRecipientCreator { public static void main(String[] args) throws Exception { HttpClient client = HttpClient.newHttpClient(); String body = """ { "name": "Global Manufacturing Ltd", "address": { "street1": "456 Industrial Blvd", "city": "Detroit", "region": "MI", "postal_code": "48201", "country": "US" } } """; HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.platform.dakota.xyz/customers/31Tgw0zSyDVo4Az66kmzUjMuwxx/recipients")) .header("X-API-Key", "your-api-key") .header("X-Idempotency-Key", UUID.randomUUID().toString()) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(body)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); // Result: 31TgvwFzi3rstV0DEDzQtuBfwFR } } ``` ## Step 3: Add Payment Destinations ### Crypto Destination ```bash cURL curl -X POST https://api.platform.dakota.xyz/recipients/recp_global_mfg_789/destinations \ -H "X-API-Key: your-api-key" \ -H "X-Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "destination_type": "crypto", "name": "USDC Wallet", "crypto_address": "0x742d35Cc6634C0532925a3b8D404fA40b5398Ad2", "network_id": "ethereum-mainnet" }' ``` ```javascript JavaScript const cryptoDestination = await fetch('https://api.platform.dakota.xyz/recipients/recp_global_mfg_789/destinations', { method: 'POST', headers: { 'X-API-Key': 'your-api-key', 'X-Idempotency-Key': crypto.randomUUID(), 'Content-Type': 'application/json' }, body: JSON.stringify({ destination_type: 'crypto', name: 'USDC Wallet', crypto_address: '0x742d35Cc6634C0532925a3b8D404fA40b5398Ad2', network_id: 'ethereum-mainnet' }) }); // Result: 31TgvySz1ARnqMZUdbuxykqqxGV ``` ```python Python import requests import uuid response = requests.post( 'https://api.platform.dakota.xyz/recipients/recp_global_mfg_789/destinations', headers={ 'X-API-Key': 'your-api-key', 'X-Idempotency-Key': str(uuid.uuid4()), 'Content-Type': 'application/json' }, json={ 'destination_type': 'crypto', 'name': 'USDC Wallet', 'crypto_address': '0x742d35Cc6634C0532925a3b8D404fA40b5398Ad2', 'network_id': 'ethereum-mainnet' } ) # Result: dest_crypto_usdc_456 ``` ```go Go package main import ( "bytes" "net/http" "github.com/google/uuid" ) func main() { client := &http.Client{} body := bytes.NewBufferString(`{ "destination_type": "crypto", "name": "USDC Wallet", "crypto_address": "0x742d35Cc6634C0532925a3b8D404fA40b5398Ad2", "network_id": "ethereum-mainnet" }`) req, _ := http.NewRequest("POST", "https://api.platform.dakota.xyz/recipients/recp_global_mfg_789/destinations", body) req.Header.Add("X-API-Key", "your-api-key") req.Header.Add("X-Idempotency-Key", uuid.New().String()) req.Header.Add("Content-Type", "application/json") resp, _ := client.Do(req) defer resp.Body.Close() // Result: 31TgvySz1ARnqMZUdbuxykqqxGV } ``` ```rust Rust use reqwest::header::{HeaderMap, HeaderValue}; use uuid::Uuid; use serde_json::json; #[tokio::main] async fn main() -> Result<(), Box> { let client = reqwest::Client::new(); let mut headers = HeaderMap::new(); headers.insert("X-API-Key", HeaderValue::from_static("your-api-key")); 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!({ "destination_type": "crypto", "name": "USDC Wallet", "crypto_address": "0x742d35Cc6634C0532925a3b8D404fA40b5398Ad2", "network_id": "ethereum-mainnet" }); let response = client .post("https://api.platform.dakota.xyz/recipients/recp_global_mfg_789/destinations") .headers(headers) .json(&body) .send() .await?; // Result: 31TgvySz1ARnqMZUdbuxykqqxGV Ok(()) } ``` ```java Java 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 DakotaCryptoDestination { public static void main(String[] args) throws Exception { HttpClient client = HttpClient.newHttpClient(); String body = """ { "destination_type": "crypto", "name": "USDC Wallet", "crypto_address": "0x742d35Cc6634C0532925a3b8D404fA40b5398Ad2", "network_id": "ethereum-mainnet" } """; HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.platform.dakota.xyz/recipients/recp_global_mfg_789/destinations")) .header("X-API-Key", "your-api-key") .header("X-Idempotency-Key", UUID.randomUUID().toString()) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(body)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); // Result: 31TgvySz1ARnqMZUdbuxykqqxGV } } ``` ### Bank Destination ```bash cURL curl -X POST https://api.platform.dakota.xyz/recipients/recp_global_mfg_789/destinations \ -H "X-API-Key: your-api-key" \ -H "X-Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "destination_type": "fiat_us", "name": "Primary Business Account", "aba_routing_number": "021000021", "account_number": "987654321", "account_type": "checking" }' ``` ```javascript JavaScript const bankDestination = await fetch('https://api.platform.dakota.xyz/recipients/recp_global_mfg_789/destinations', { method: 'POST', headers: { 'X-API-Key': 'your-api-key', 'X-Idempotency-Key': crypto.randomUUID(), 'Content-Type': 'application/json' }, body: JSON.stringify({ destination_type: 'fiat_us', name: 'Primary Business Account', aba_routing_number: '021000021', account_number: '987654321', account_type: 'checking' }) }); // Result: 31TgvtxUdXi95dUN4M8X1rhSCNS ``` ```python Python import requests import uuid response = requests.post( 'https://api.platform.dakota.xyz/recipients/recp_global_mfg_789/destinations', headers={ 'X-API-Key': 'your-api-key', 'X-Idempotency-Key': str(uuid.uuid4()), 'Content-Type': 'application/json' }, json={ 'destination_type': 'fiat_us', 'name': 'Primary Business Account', 'aba_routing_number': '021000021', 'account_number': '987654321', 'account_type': 'checking' } ) # Result: dest_bank_usd_789 ``` ```go Go package main import ( "bytes" "net/http" "github.com/google/uuid" ) func main() { client := &http.Client{} body := bytes.NewBufferString(`{ "destination_type": "fiat_us", "name": "Primary Business Account", "aba_routing_number": "021000021", "account_number": "987654321", "account_type": "checking" }`) req, _ := http.NewRequest("POST", "https://api.platform.dakota.xyz/recipients/recp_global_mfg_789/destinations", body) req.Header.Add("X-API-Key", "your-api-key") req.Header.Add("X-Idempotency-Key", uuid.New().String()) req.Header.Add("Content-Type", "application/json") resp, _ := client.Do(req) defer resp.Body.Close() // Result: 31TgvtxUdXi95dUN4M8X1rhSCNS } ``` ```rust Rust use reqwest::header::{HeaderMap, HeaderValue}; use uuid::Uuid; use serde_json::json; #[tokio::main] async fn main() -> Result<(), Box> { let client = reqwest::Client::new(); let mut headers = HeaderMap::new(); headers.insert("X-API-Key", HeaderValue::from_static("your-api-key")); 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!({ "destination_type": "fiat_us", "name": "Primary Business Account", "aba_routing_number": "021000021", "account_number": "987654321", "account_type": "checking" }); let response = client .post("https://api.platform.dakota.xyz/recipients/recp_global_mfg_789/destinations") .headers(headers) .json(&body) .send() .await?; // Result: 31TgvtxUdXi95dUN4M8X1rhSCNS Ok(()) } ``` ```java Java 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 DakotaBankDestination { public static void main(String[] args) throws Exception { HttpClient client = HttpClient.newHttpClient(); String body = """ { "destination_type": "fiat_us", "name": "Primary Business Account", "aba_routing_number": "021000021", "account_number": "987654321", "account_type": "checking" } """; HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.platform.dakota.xyz/recipients/recp_global_mfg_789/destinations")) .header("X-API-Key", "your-api-key") .header("X-Idempotency-Key", UUID.randomUUID().toString()) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(body)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); // Result: 31TgvtxUdXi95dUN4M8X1rhSCNS } } ``` ## Step 4: Create Transaction Accounts ### Crypto Transaction ```bash cURL curl -X POST https://api.platform.dakota.xyz/transactions/one-off \ -H "X-API-Key: your-api-key" \ -H "X-Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "customer_id": "31Tgw0zSyDVo4Az66kmzUjMuwxx", "destination_id": "31TgvySz1ARnqMZUdbuxykqqxGV", "source_asset": "USD", "source_network_id": "fiat", "destination_asset": "USDC", "destination_network_id": "ethereum-mainnet", "amount": "1000.00" }' ``` ```javascript JavaScript const cryptoTransaction = await fetch('https://api.platform.dakota.xyz/transactions/one-off', { method: 'POST', headers: { 'X-API-Key': 'your-api-key', 'X-Idempotency-Key': crypto.randomUUID(), 'Content-Type': 'application/json' }, body: JSON.stringify({ customer_id: '31Tgw0zSyDVo4Az66kmzUjMuwxx', destination_id: 'dest_crypto_usdc_456', source_asset: 'USD', source_network_id: 'fiat', destination_asset: 'USDC', destination_network_id: 'ethereum-mainnet', amount: '1000.00' }) }); ``` ```python Python import requests import uuid response = requests.post( 'https://api.platform.dakota.xyz/transactions/one-off', headers={ 'X-API-Key': 'your-api-key', 'X-Idempotency-Key': str(uuid.uuid4()), 'Content-Type': 'application/json' }, json={ 'customer_id': '31Tgw0zSyDVo4Az66kmzUjMuwxx', 'destination_id': 'dest_crypto_usdc_456', 'source_asset': 'USD', 'source_network_id': 'fiat', 'destination_asset': 'USDC', 'destination_network_id': 'ethereum-mainnet', 'amount': '1000.00' } ) ``` ```go Go package main import ( "bytes" "net/http" "github.com/google/uuid" ) func main() { client := &http.Client{} body := bytes.NewBufferString(`{ "customer_id": "31Tgw0zSyDVo4Az66kmzUjMuwxx", "destination_id": "31TgvySz1ARnqMZUdbuxykqqxGV", "source_asset": "USD", "source_network_id": "fiat", "destination_asset": "USDC", "destination_network_id": "ethereum-mainnet", "amount": "1000.00" }`) req, _ := http.NewRequest("POST", "https://api.platform.dakota.xyz/transactions/one-off", body) req.Header.Add("X-API-Key", "your-api-key") req.Header.Add("X-Idempotency-Key", uuid.New().String()) req.Header.Add("Content-Type", "application/json") resp, _ := client.Do(req) defer resp.Body.Close() } ``` ```rust Rust use reqwest::header::{HeaderMap, HeaderValue}; use uuid::Uuid; use serde_json::json; #[tokio::main] async fn main() -> Result<(), Box> { let client = reqwest::Client::new(); let mut headers = HeaderMap::new(); headers.insert("X-API-Key", HeaderValue::from_static("your-api-key")); 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": "31Tgw0zSyDVo4Az66kmzUjMuwxx", "destination_id": "31TgvySz1ARnqMZUdbuxykqqxGV", "source_asset": "USD", "source_network_id": "fiat", "destination_asset": "USDC", "destination_network_id": "ethereum-mainnet", "amount": "1000.00" }); let response = client .post("https://api.platform.dakota.xyz/transactions/one-off") .headers(headers) .json(&body) .send() .await?; Ok(()) } ``` ```java Java 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 DakotaCryptoTransaction { public static void main(String[] args) throws Exception { HttpClient client = HttpClient.newHttpClient(); String body = """ { "customer_id": "31Tgw0zSyDVo4Az66kmzUjMuwxx", "destination_id": "31TgvySz1ARnqMZUdbuxykqqxGV", "source_asset": "USD", "source_network_id": "fiat", "destination_asset": "USDC", "destination_network_id": "ethereum-mainnet", "amount": "1000.00" } """; HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.platform.dakota.xyz/transactions/one-off")) .header("X-API-Key", "your-api-key") .header("X-Idempotency-Key", UUID.randomUUID().toString()) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(body)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); } } ``` ### Bank Transaction ```bash cURL curl -X POST https://api.platform.dakota.xyz/transactions/one-off \ -H "X-API-Key: your-api-key" \ -H "X-Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "customer_id": "31Tgw0zSyDVo4Az66kmzUjMuwxx", "destination_id": "31TgvtxUdXi95dUN4M8X1rhSCNS", "source_asset": "USDC", "source_network_id": "ethereum-mainnet", "destination_asset": "USD", "destination_network_id": "fiat", "amount": "500.00" }' ``` ```javascript JavaScript const bankTransaction = await fetch('https://api.platform.dakota.xyz/transactions/one-off', { method: 'POST', headers: { 'X-API-Key': 'your-api-key', 'X-Idempotency-Key': crypto.randomUUID(), 'Content-Type': 'application/json' }, body: JSON.stringify({ customer_id: '31Tgw0zSyDVo4Az66kmzUjMuwxx', destination_id: 'dest_bank_usd_789', source_asset: 'USDC', source_network_id: 'ethereum-mainnet', destination_asset: 'USD', destination_network_id: 'fiat', amount: '500.00' }) }); ``` ```python Python import requests import uuid response = requests.post( 'https://api.platform.dakota.xyz/transactions/one-off', headers={ 'X-API-Key': 'your-api-key', 'X-Idempotency-Key': str(uuid.uuid4()), 'Content-Type': 'application/json' }, json={ 'customer_id': '31Tgw0zSyDVo4Az66kmzUjMuwxx', 'destination_id': 'dest_bank_usd_789', 'source_asset': 'USDC', 'source_network_id': 'ethereum-mainnet', 'destination_asset': 'USD', 'destination_network_id': 'fiat', 'amount': '500.00' } ) ``` ```go Go package main import ( "bytes" "net/http" "github.com/google/uuid" ) func main() { client := &http.Client{} body := bytes.NewBufferString(`{ "customer_id": "31Tgw0zSyDVo4Az66kmzUjMuwxx", "destination_id": "31TgvtxUdXi95dUN4M8X1rhSCNS", "source_asset": "USDC", "source_network_id": "ethereum-mainnet", "destination_asset": "USD", "destination_network_id": "fiat", "amount": "500.00" }`) req, _ := http.NewRequest("POST", "https://api.platform.dakota.xyz/transactions/one-off", body) req.Header.Add("X-API-Key", "your-api-key") req.Header.Add("X-Idempotency-Key", uuid.New().String()) req.Header.Add("Content-Type", "application/json") resp, _ := client.Do(req) defer resp.Body.Close() } ``` ```rust Rust use reqwest::header::{HeaderMap, HeaderValue}; use uuid::Uuid; use serde_json::json; #[tokio::main] async fn main() -> Result<(), Box> { let client = reqwest::Client::new(); let mut headers = HeaderMap::new(); headers.insert("X-API-Key", HeaderValue::from_static("your-api-key")); 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": "31Tgw0zSyDVo4Az66kmzUjMuwxx", "destination_id": "31TgvtxUdXi95dUN4M8X1rhSCNS", "source_asset": "USDC", "source_network_id": "ethereum-mainnet", "destination_asset": "USD", "destination_network_id": "fiat", "amount": "500.00" }); let response = client .post("https://api.platform.dakota.xyz/transactions/one-off") .headers(headers) .json(&body) .send() .await?; Ok(()) } ``` ```java Java 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 DakotaBankTransaction { public static void main(String[] args) throws Exception { HttpClient client = HttpClient.newHttpClient(); String body = """ { "customer_id": "31Tgw0zSyDVo4Az66kmzUjMuwxx", "destination_id": "31TgvtxUdXi95dUN4M8X1rhSCNS", "source_asset": "USDC", "source_network_id": "ethereum-mainnet", "destination_asset": "USD", "destination_network_id": "fiat", "amount": "500.00" } """; HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.platform.dakota.xyz/transactions/one-off")) .header("X-API-Key", "your-api-key") .header("X-Idempotency-Key", UUID.randomUUID().toString()) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(body)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); } } ``` # Best Practices for Payment Chain Management ## Recipient Organization - Use clear, descriptive recipient names matching legal business names - Keep recipient addresses current for compliance - Group destinations by recipient to maintain clear relationships ## Destination Naming - Use descriptive destination names: "Primary USDC Wallet", "Backup Bank Account" - Include asset type in names for clarity: "EUR Business Account", "USDC Wallet" - Maintain consistent naming conventions across your application ## Relationship Management - One recipient per business entity (don't create duplicates) - Multiple destinations per recipient for different payment methods - Delete old destinations for security ## Error Prevention - Validate crypto addresses before creating destinations - Test small transactions to new destinations first - Implement approval workflows for new high-value destinations - Monitor destination usage patterns for anomalies This hierarchical structure provides the foundation for secure, compliant, and organized payment processing through Dakota Platform's API. # Recipients Recipients represent the entities that will receive payments. Each recipient must have a name and physical address for compliance purposes. ## Creating Recipients Create a recipient for a customer. ### Recipient Fields | Field | Type | Required | Description | Example | |-------|------|----------|-------------|---------| | `name` | string | ✅ | Legal name of the recipient entity | `"Acme Corporation"` | | `address` | object | ✅ | Physical address for compliance | See address fields below | | `external_id` | string | ❌ | Your internal identifier for this recipient | `"acme_recipient_001"` | ### Address Fields | Field | Type | Required | Description | Example | |-------|------|----------|-------------|---------| | `street1` | string | ✅ | Street address line 1 | `"123 Business Ave"` | | `street2` | string | ❌ | Street address line 2 | `"Suite 456"` | | `street3` | string | ❌ | Street address line 3 | `"Floor 2"` | | `city` | string | ✅ | City name | `"New York"` | | `region` | string | ✅ | State/province/region code | `"NY"` | | `postal_code` | string | ❌ | ZIP/postal code | `"10001"` | | `country` | string | ✅ | ISO 3166-1 alpha-2 country code | `"US"` | ### Request Example ```bash cURL curl -X POST https://api.platform.dakota.xyz/customers/{customer_id}/recipients \ -H "X-API-Key: your-api-key" \ -H "X-Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "name": "Acme Corporation", "address": { "street1": "123 Business Ave", "city": "New York", "region": "NY", "postal_code": "10001", "country": "US" } }' ``` ```javascript JavaScript const response = await fetch(`https://api.platform.dakota.xyz/customers/${customerId}/recipients`, { method: 'POST', headers: { 'X-API-Key': 'your-api-key', 'X-Idempotency-Key': crypto.randomUUID(), 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'Acme Corporation', address: { street1: '123 Business Ave', city: 'New York', region: 'NY', postal_code: '10001', country: 'US' } }) }); const recipient = await response.json(); console.log('Created recipient:', recipient.data.id); ``` ```python Python import requests import uuid response = requests.post( f'https://api.platform.dakota.xyz/customers/{customer_id}/recipients', headers={ 'X-API-Key': 'your-api-key', 'X-Idempotency-Key': str(uuid.uuid4()), 'Content-Type': 'application/json' }, json={ 'name': 'Acme Corporation', 'address': { 'street1': '123 Business Ave', 'city': 'New York', 'region': 'NY', 'postal_code': '10001', 'country': 'US' } } ) recipient = response.json() print(f'Created recipient: {recipient["data"]["id"]}') ``` ```go Go package main import ( "bytes" "net/http" "github.com/google/uuid" ) func main() { client := &http.Client{} body := bytes.NewBufferString(`{ "name": "Acme Corporation", "address": { "street1": "123 Business Ave", "city": "New York", "region": "NY", "postal_code": "10001", "country": "US" } }`) req, _ := http.NewRequest("POST", "https://api.platform.dakota.xyz/customers/" + customerId + "/recipients", body) req.Header.Add("X-API-Key", "your-api-key") req.Header.Add("X-Idempotency-Key", uuid.New().String()) req.Header.Add("Content-Type", "application/json") resp, _ := client.Do(req) defer resp.Body.Close() } ``` ```rust Rust use reqwest::header::{HeaderMap, HeaderValue}; use uuid::Uuid; use serde_json::json; #[tokio::main] async fn main() -> Result<(), Box> { let client = reqwest::Client::new(); let mut headers = HeaderMap::new(); headers.insert("X-API-Key", HeaderValue::from_static("your-api-key")); 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!({ "name": "Acme Corporation", "address": { "street1": "123 Business Ave", "city": "New York", "region": "NY", "postal_code": "10001", "country": "US" } }); let response = client .post(&format!("https://api.platform.dakota.xyz/customers/{}/recipients", customer_id)) .headers(headers) .json(&body) .send() .await?; Ok(()) } ``` ```java Java 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 DakotaRecipientsExample { public static void main(String[] args) throws Exception { HttpClient client = HttpClient.newHttpClient(); String body = """ { "name": "Acme Corporation", "address": { "street1": "123 Business Ave", "city": "New York", "region": "NY", "postal_code": "10001", "country": "US" } } """; HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.platform.dakota.xyz/customers/" + customerId + "/recipients")) .header("X-API-Key", "your-api-key") .header("X-Idempotency-Key", UUID.randomUUID().toString()) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(body)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); } } ``` Response: ```json { "data": { "id": "31TgvwFzi3rstV0DEDzQtuBfwFR", "name": "Acme Corporation", "address": { "street1": "123 Business Ave", "city": "New York", "region": "NY", "postal_code": "10001", "country": "US" }, "created_at": "2024-01-15T10:30:00Z" } } ``` ## Listing Recipients Get all recipients for a customer: ```bash cURL curl -X GET https://api.platform.dakota.xyz/customers/{customer_id}/recipients \ -H "X-API-Key: your-api-key" ``` ```javascript JavaScript const response = await fetch(`https://api.platform.dakota.xyz/customers/${customerId}/recipients`, { headers: { 'X-API-Key': 'your-api-key' } }); const recipients = await response.json(); ``` ```python Python import requests response = requests.get( f'https://api.platform.dakota.xyz/customers/{customer_id}/recipients', headers={'X-API-Key': 'your-api-key'} ) recipients = response.json() ``` ```go Go package main import ( "net/http" ) func main() { client := &http.Client{} req, _ := http.NewRequest("GET", "https://api.platform.dakota.xyz/customers/" + customerId + "/recipients", nil) req.Header.Add("X-API-Key", "your-api-key") resp, _ := client.Do(req) defer resp.Body.Close() } ``` ```rust Rust use reqwest::header::{HeaderMap, HeaderValue}; #[tokio::main] async fn main() -> Result<(), Box> { let client = reqwest::Client::new(); let mut headers = HeaderMap::new(); headers.insert("X-API-Key", HeaderValue::from_static("your-api-key")); let response = client .get(&format!("https://api.platform.dakota.xyz/customers/{}/recipients", customer_id)) .headers(headers) .send() .await?; Ok(()) } ``` ```java Java import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.URI; public class DakotaListRecipientsExample { public static void main(String[] args) throws Exception { HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.platform.dakota.xyz/customers/" + customerId + "/recipients")) .header("X-API-Key", "your-api-key") .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); } } ``` Response: ```json { "data": [ { "id": "31TgvwFzi3rstV0DEDzQtuBfwFR", "name": "Global Manufacturing Ltd", "address": { "street1": "456 Industrial Blvd", "city": "Detroit", "region": "MI", "postal_code": "48201", "country": "US" }, "external_id": "supplier_001", "created_at": "2024-01-10T14:20:00Z" }, { "id": "31TgvwGHm8PqtV2DEDzQtuBfwFS", "name": "Tech Solutions Inc", "address": { "street1": "789 Silicon Valley Dr", "street2": "Suite 200", "city": "San Francisco", "region": "CA", "postal_code": "94105", "country": "US" }, "external_id": null, "created_at": "2024-01-12T09:15:00Z" }, { "id": "31TgvwJKn9QrtV3DEDzQtuBfwFT", "name": "European Consulting GmbH", "address": { "street1": "Hauptstraße 123", "city": "Berlin", "region": "BE", "postal_code": "10117", "country": "DE" }, "external_id": "vendor_eu_007", "created_at": "2024-01-14T16:45:00Z" } ] } ``` # Destinations Destinations are the specific accounts where funds will be sent. Each destination is associated with a recipient and specifies either a cryptocurrency address or traditional bank account details. ## Destination Types Dakota Platform supports three destination types: - **Crypto**: Cryptocurrency addresses on supported networks - **Fiat US**: US bank accounts (ACH transfers) - **Fiat IBAN**: International bank accounts (SWIFT/SEPA transfers) ## Creating Crypto Destinations Add a cryptocurrency destination to a recipient. ### Crypto Destination Fields | Field | Type | Required | Description | Example | |-------|------|----------|-------------|---------| | `destination_type` | string | ✅ | Must be `"crypto"` for cryptocurrency destinations | `"crypto"` | | `name` | string | ✅ | Descriptive name for the destination | `"Primary USDC Wallet"` | | `crypto_address` | string | ✅ | Valid cryptocurrency address for the specified network | `"0x742d35Cc..."` | | `network_id` | string | ✅ | Network identifier. See supported networks below | `"ethereum-mainnet"` | | `assets` | array | ❌ | Specific assets this destination accepts (optional) | `["USDC"]` | ### Supported Networks | Network ID | Name | Supported Assets | |------------|------|------------------| | `ethereum-mainnet` | Ethereum | ETH, USDC, USDT | | `polygon-mainnet` | Polygon | USDC, USDT | | `arbitrum-mainnet` | Arbitrum | ETH, USDC, USDT | | `base-mainnet` | Base | ETH, USDC, USDT, DKUSD | | `optimism-mainnet` | Optimism | ETH, USDC, USDT | | `solana-mainnet` | Solana | SOL, USDC, USDT | ### Request Example ```bash cURL curl -X POST https://api.platform.dakota.xyz/recipients/{recipient_id}/destinations \ -H "X-API-Key: your-api-key" \ -H "X-Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "destination_type": "crypto", "name": "Primary USDC Wallet", "crypto_address": "0x742d35Cc6634C0532925a3b8D404fA40b5398Ad2", "network_id": "ethereum-mainnet" }' ``` ```javascript JavaScript const response = await fetch(`https://api.platform.dakota.xyz/recipients/${recipientId}/destinations`, { method: 'POST', headers: { 'X-API-Key': 'your-api-key', 'X-Idempotency-Key': crypto.randomUUID(), 'Content-Type': 'application/json' }, body: JSON.stringify({ destination_type: 'crypto', name: 'Primary USDC Wallet', crypto_address: '0x742d35Cc6634C0532925a3b8D404fA40b5398Ad2', network_id: 'ethereum-mainnet' }) }); ``` ```python Python import requests import uuid response = requests.post( f'https://api.platform.dakota.xyz/recipients/{recipient_id}/destinations', headers={ 'X-API-Key': 'your-api-key', 'X-Idempotency-Key': str(uuid.uuid4()), 'Content-Type': 'application/json' }, json={ 'destination_type': 'crypto', 'name': 'Primary USDC Wallet', 'crypto_address': '0x742d35Cc6634C0532925a3b8D404fA40b5398Ad2', 'network_id': 'ethereum-mainnet' } ) ``` ```go Go package main import ( "bytes" "net/http" "github.com/google/uuid" ) func main() { client := &http.Client{} body := bytes.NewBufferString(`{ "destination_type": "crypto", "name": "Primary USDC Wallet", "crypto_address": "0x742d35Cc6634C0532925a3b8D404fA40b5398Ad2", "network_id": "ethereum-mainnet" }`) req, _ := http.NewRequest("POST", "https://api.platform.dakota.xyz/recipients/" + recipientId + "/destinations", body) req.Header.Add("X-API-Key", "your-api-key") req.Header.Add("X-Idempotency-Key", uuid.New().String()) req.Header.Add("Content-Type", "application/json") resp, _ := client.Do(req) defer resp.Body.Close() } ``` ```rust Rust use reqwest::header::{HeaderMap, HeaderValue}; use uuid::Uuid; use serde_json::json; #[tokio::main] async fn main() -> Result<(), Box> { let client = reqwest::Client::new(); let mut headers = HeaderMap::new(); headers.insert("X-API-Key", HeaderValue::from_static("your-api-key")); 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!({ "destination_type": "crypto", "name": "Primary USDC Wallet", "crypto_address": "0x742d35Cc6634C0532925a3b8D404fA40b5398Ad2", "network_id": "ethereum-mainnet" }); let response = client .post(&format!("https://api.platform.dakota.xyz/recipients/{}/destinations", recipient_id)) .headers(headers) .json(&body) .send() .await?; Ok(()) } ``` ```java Java 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 DakotaCryptoDestinationExample { public static void main(String[] args) throws Exception { HttpClient client = HttpClient.newHttpClient(); String body = """ { "destination_type": "crypto", "name": "Primary USDC Wallet", "crypto_address": "0x742d35Cc6634C0532925a3b8D404fA40b5398Ad2", "network_id": "ethereum-mainnet" } """; HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.platform.dakota.xyz/recipients/" + recipientId + "/destinations")) .header("X-API-Key", "your-api-key") .header("X-Idempotency-Key", UUID.randomUUID().toString()) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(body)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); } } ``` ### Response Example ```json { "data": { "id": "31TgvySz1ARnqMZUdbuxykqqxGV", "destination_type": "crypto", "name": "Primary USDC Wallet", "crypto_address": "0x742d35Cc6634C0532925a3b8D404fA40b5398Ad2", "network_id": "ethereum-mainnet", "assets": ["ETH", "USDC"], "status": "active", "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-15T10:30:00Z" } } ``` > **API Reference**: For complete details on crypto destination creation, see [Create Crypto Destination](/reference/destinations#create-crypto-destination) ## Creating US Bank Account Destinations Add a US bank account destination. ### US Bank Account Destination Fields | Field | Type | Required | Description | Example | |-------|------|----------|-------------|---------| | `destination_type` | string | ✅ | Must be `"fiat_us"` for US bank accounts | `"fiat_us"` | | `name` | string | ✅ | Descriptive name for the destination | `"Primary Business Account"` | | `aba_routing_number` | string | ✅ | 9-digit ABA routing number | `"123456789"` | | `account_number` | string | ✅ | Bank account number | `"9876543210"` | | `account_type` | string | ✅ | Type of account. Must be `"checking"` or `"savings"` | `"checking"` | | `assets` | array | ❌ | Supported fiat currencies (optional) | `["USD"]` | ### Request Example ```bash cURL curl -X POST https://api.platform.dakota.xyz/recipients/{recipient_id}/destinations \ -H "X-API-Key: your-api-key" \ -H "X-Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "destination_type": "fiat_us", "name": "Primary Business Account", "aba_routing_number": "123456789", "account_number": "9876543210", "account_type": "checking", "assets": ["USD"] }' ``` ```javascript JavaScript const response = await fetch(`https://api.platform.dakota.xyz/recipients/${recipientId}/destinations`, { method: 'POST', headers: { 'X-API-Key': 'your-api-key', 'X-Idempotency-Key': crypto.randomUUID(), 'Content-Type': 'application/json' }, body: JSON.stringify({ destination_type: 'fiat_us', name: 'Primary Business Account', aba_routing_number: '123456789', account_number: '9876543210', account_type: 'checking', assets: ['USD'] }) }); ``` ```python Python import requests import uuid response = requests.post( f'https://api.platform.dakota.xyz/recipients/{recipient_id}/destinations', headers={ 'X-API-Key': 'your-api-key', 'X-Idempotency-Key': str(uuid.uuid4()), 'Content-Type': 'application/json' }, json={ 'destination_type': 'fiat_us', 'name': 'Primary Business Account', 'aba_routing_number': '123456789', 'account_number': '9876543210', 'account_type': 'checking', 'assets': ['USD'] } ) ``` ```go Go package main import ( "bytes" "net/http" "github.com/google/uuid" ) func main() { client := &http.Client{} body := bytes.NewBufferString(`{ "destination_type": "fiat_us", "name": "Primary Business Account", "aba_routing_number": "123456789", "account_number": "9876543210", "account_type": "checking", "assets": ["USD"] }`) req, _ := http.NewRequest("POST", "https://api.platform.dakota.xyz/recipients/" + recipientId + "/destinations", body) req.Header.Add("X-API-Key", "your-api-key") req.Header.Add("X-Idempotency-Key", uuid.New().String()) req.Header.Add("Content-Type", "application/json") resp, _ := client.Do(req) defer resp.Body.Close() } ``` ```rust Rust use reqwest::header::{HeaderMap, HeaderValue}; use uuid::Uuid; use serde_json::json; #[tokio::main] async fn main() -> Result<(), Box> { let client = reqwest::Client::new(); let mut headers = HeaderMap::new(); headers.insert("X-API-Key", HeaderValue::from_static("your-api-key")); 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!({ "destination_type": "fiat_us", "name": "Primary Business Account", "aba_routing_number": "123456789", "account_number": "9876543210", "account_type": "checking", "assets": ["USD"] }); let response = client .post(&format!("https://api.platform.dakota.xyz/recipients/{}/destinations", recipient_id)) .headers(headers) .json(&body) .send() .await?; Ok(()) } ``` ```java Java 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 DakotaBankDestinationExample { public static void main(String[] args) throws Exception { HttpClient client = HttpClient.newHttpClient(); String body = """ { "destination_type": "fiat_us", "name": "Primary Business Account", "aba_routing_number": "123456789", "account_number": "9876543210", "account_type": "checking", "assets": ["USD"] } """; HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.platform.dakota.xyz/recipients/" + recipientId + "/destinations")) .header("X-API-Key", "your-api-key") .header("X-Idempotency-Key", UUID.randomUUID().toString()) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(body)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); } } ``` ### Response Example ```json { "data": { "id": "31TgvtxUdXi95dUN4M8X1rhSCNS", "destination_type": "fiat_us", "name": "Primary Business Account", "aba_routing_number": "*****6789", "account_number": "*******3210", "account_type": "checking", "assets": ["USD"], "status": "active", "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-15T10:30:00Z" } } ``` > **API Reference**: For complete details on US bank account destination creation, see [Create US Bank Destination](/reference/destinations#create-us-bank-destination) ## Creating IBAN Destinations Add an IBAN destination for international transfers. ### IBAN Destination Fields | Field | Type | Required | Description | Example | |-------|------|----------|-------------|---------| | `destination_type` | string | ✅ | Must be `"fiat_iban"` for IBAN accounts | `"fiat_iban"` | | `name` | string | ✅ | Descriptive name for the destination | `"European Business Account"` | | `iban` | string | ✅ | Valid IBAN (International Bank Account Number) | `"DE89370400440532013000"` | | `bic` | string | ✅ | Bank Identifier Code (SWIFT code) | `"COBADEFFXXX"` | | `assets` | array | ❌ | Supported fiat currencies | `["EUR", "USD"]` | ### Request Example ```bash cURL curl -X POST https://api.platform.dakota.xyz/recipients/{recipient_id}/destinations \ -H "X-API-Key: your-api-key" \ -H "X-Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "destination_type": "fiat_iban", "name": "European Business Account", "iban": "DE89370400440532013000", "bic": "COBADEFFXXX", "assets": ["EUR", "USD"] }' ``` ```javascript JavaScript const response = await fetch(`https://api.platform.dakota.xyz/recipients/${recipientId}/destinations`, { method: 'POST', headers: { 'X-API-Key': 'your-api-key', 'X-Idempotency-Key': crypto.randomUUID(), 'Content-Type': 'application/json' }, body: JSON.stringify({ destination_type: 'fiat_iban', name: 'European Business Account', iban: 'DE89370400440532013000', bic: 'COBADEFFXXX', assets: ['EUR', 'USD'] }) }); ``` ```python Python import requests import uuid response = requests.post( f'https://api.platform.dakota.xyz/recipients/{recipient_id}/destinations', headers={ 'X-API-Key': 'your-api-key', 'X-Idempotency-Key': str(uuid.uuid4()), 'Content-Type': 'application/json' }, json={ 'destination_type': 'fiat_iban', 'name': 'European Business Account', 'iban': 'DE89370400440532013000', 'bic': 'COBADEFFXXX', 'assets': ['EUR', 'USD'] } ) ``` ```go Go package main import ( "bytes" "net/http" "github.com/google/uuid" ) func main() { client := &http.Client{} body := bytes.NewBufferString(`{ "destination_type": "fiat_iban", "name": "European Business Account", "iban": "DE89370400440532013000", "bic": "COBADEFFXXX", "assets": ["EUR", "USD"] }`) req, _ := http.NewRequest("POST", "https://api.platform.dakota.xyz/recipients/" + recipientId + "/destinations", body) req.Header.Add("X-API-Key", "your-api-key") req.Header.Add("X-Idempotency-Key", uuid.New().String()) req.Header.Add("Content-Type", "application/json") resp, _ := client.Do(req) defer resp.Body.Close() } ``` ```rust Rust use reqwest::header::{HeaderMap, HeaderValue}; use uuid::Uuid; use serde_json::json; #[tokio::main] async fn main() -> Result<(), Box> { let client = reqwest::Client::new(); let mut headers = HeaderMap::new(); headers.insert("X-API-Key", HeaderValue::from_static("your-api-key")); 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!({ "destination_type": "fiat_iban", "name": "European Business Account", "iban": "DE89370400440532013000", "bic": "COBADEFFXXX", "assets": ["EUR", "USD"] }); let response = client .post(&format!("https://api.platform.dakota.xyz/recipients/{}/destinations", recipient_id)) .headers(headers) .json(&body) .send() .await?; Ok(()) } ``` ```java Java 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 DakotaIBANDestinationExample { public static void main(String[] args) throws Exception { HttpClient client = HttpClient.newHttpClient(); String body = """ { "destination_type": "fiat_iban", "name": "European Business Account", "iban": "DE89370400440532013000", "bic": "COBADEFFXXX", "assets": ["EUR", "USD"] } """; HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.platform.dakota.xyz/recipients/" + recipientId + "/destinations")) .header("X-API-Key", "your-api-key") .header("X-Idempotency-Key", UUID.randomUUID().toString()) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(body)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); } } ``` ### Response Example ```json { "data": { "id": "31TgvuBMp7RstU4XEDzQtuBfwGU", "destination_type": "fiat_iban", "name": "European Business Account", "iban": "DE89****0440****3000", "bic": "COBADEFFXXX", "assets": ["EUR", "USD"], "status": "active", "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-15T10:30:00Z" } } ``` > **API Reference**: For complete details on IBAN destination creation, see [Create IBAN Destination](/reference/destinations#create-iban-destination) ## Listing Destinations Get all destinations for a recipient: ```bash cURL curl -X GET https://api.platform.dakota.xyz/recipients/{recipient_id}/destinations \ -H "X-API-Key: your-api-key" ``` ```javascript JavaScript const response = await fetch(`https://api.platform.dakota.xyz/recipients/${recipientId}/destinations`, { headers: { 'X-API-Key': 'your-api-key' } }); const destinations = await response.json(); ``` ```python Python import requests response = requests.get( f'https://api.platform.dakota.xyz/recipients/{recipient_id}/destinations', headers={'X-API-Key': 'your-api-key'} ) destinations = response.json() ``` ```go Go package main import ( "net/http" ) func main() { client := &http.Client{} req, _ := http.NewRequest("GET", "https://api.platform.dakota.xyz/recipients/" + recipientId + "/destinations", nil) req.Header.Add("X-API-Key", "your-api-key") resp, _ := client.Do(req) defer resp.Body.Close() } ``` ```rust Rust use reqwest::header::{HeaderMap, HeaderValue}; #[tokio::main] async fn main() -> Result<(), Box> { let client = reqwest::Client::new(); let mut headers = HeaderMap::new(); headers.insert("X-API-Key", HeaderValue::from_static("your-api-key")); let response = client .get(&format!("https://api.platform.dakota.xyz/recipients/{}/destinations", recipient_id)) .headers(headers) .send() .await?; Ok(()) } ``` ```java Java import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.URI; public class DakotaListDestinationsExample { public static void main(String[] args) throws Exception { HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.platform.dakota.xyz/recipients/" + recipientId + "/destinations")) .header("X-API-Key", "your-api-key") .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); } } ``` ### Response Example ```json { "data": [ { "id": "31TgvySz1ARnqMZUdbuxykqqxGV", "destination_type": "crypto", "name": "Primary USDC Wallet", "crypto_address": "0x742d35Cc6634C0532925a3b8D404fA40b5398Ad2", "network_id": "ethereum-mainnet", "assets": ["ETH", "USDC"], "status": "active", "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-15T10:30:00Z" }, { "id": "31TgvtxUdXi95dUN4M8X1rhSCNS", "destination_type": "fiat_us", "name": "Primary Business Account", "aba_routing_number": "*****6789", "account_number": "*******3210", "account_type": "checking", "assets": ["USD"], "status": "active", "created_at": "2024-01-15T11:15:00Z", "updated_at": "2024-01-15T11:15:00Z" }, { "id": "31TgvuBMp7RstU4XEDzQtuBfwGU", "destination_type": "fiat_iban", "name": "European Business Account", "iban": "DE89****0440****3000", "bic": "COBADEFFXXX", "assets": ["EUR", "USD"], "status": "active", "created_at": "2024-01-15T12:00:00Z", "updated_at": "2024-01-15T12:00:00Z" } ] } ``` # Best Practices ## Recipient Management - Use descriptive names for recipients - Keep address information current for compliance - Organize recipients logically (e.g., by business relationship) - Regular audits of recipient data ## Destination Security - Validate cryptocurrency addresses before saving - Use test transactions for new crypto destinations - Implement approval workflows for new destinations - Monitor destination usage for suspicious activity ## Compliance Considerations - Maintain accurate recipient addresses for reporting - Document the business relationship with each recipient - Regular compliance reviews of recipient and destination data - Implement proper access controls for destination management # Next Steps Once you have recipients and destinations set up: 1. **[Create Transactions](transactions)** - Process payments to your configured destinations 2. **[Webhook Integration](webhooks)** - Get notified about payment status changes 3. **[Testing](testing)** - Test your integration with sandbox data For detailed API reference information, see the [Recipients API Reference](/reference/recipients) and [Destinations API Reference](/reference/destinations) sections. # Webhook Integration Source: https://docs.dakota.io/guides/webhooks Webhooks allow Dakota Platform to send real-time notifications about events in your account directly to your application. # Overview Instead of constantly polling our API for updates, webhooks deliver event notifications instantly when something happens: - Transaction status changes - Customer onboarding updates - Account changes - System events # Setting Up Webhooks ## 1. Create a Webhook Endpoint Create an endpoint in your application to receive webhook notifications: ```javascript JavaScript // Express.js example app.post('/webhooks/dakota', (req, res) => { const event = req.body; // Verify webhook signature (required for security) const isValid = verifyWebhookSignature( req.headers['x-webhook-signature'], req.headers['x-webhook-timestamp'], JSON.stringify(req.body), process.env.DAKOTA_WEBHOOK_PUBLIC_KEY ); if (!isValid) { console.error('Webhook signature verification failed'); return res.status(401).send('Unauthorized'); } // Process the event handleWebhookEvent(event); // Respond with 200 to acknowledge receipt res.status(200).send('OK'); }); ``` ```python Python # Flask example from flask import Flask, request, jsonify app = Flask(__name__) @app.route('/webhooks/dakota', methods=['POST']) def handle_webhook(): event = request.get_json() # Verify webhook signature (required for security) signature = request.headers.get('X-Webhook-Signature') timestamp = request.headers.get('X-Webhook-Timestamp') is_valid = verify_webhook_signature( signature, timestamp, request.get_data(as_text=True), os.getenv('DAKOTA_WEBHOOK_PUBLIC_KEY') ) if not is_valid: print('Webhook signature verification failed') return jsonify({'error': 'Unauthorized'}), 401 # Process the event handle_webhook_event(event) # Respond with 200 to acknowledge receipt return jsonify({'status': 'ok'}), 200 ``` ```go Go package main import ( "encoding/json" "io" "log" "net/http" "os" ) func webhookHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } // Read request body body, err := io.ReadAll(r.Body) if err != nil { log.Printf("Failed to read request body: %v", err) http.Error(w, "Bad request", http.StatusBadRequest) return } defer r.Body.Close() signature := r.Header.Get("X-Webhook-Signature") timestamp := r.Header.Get("X-Webhook-Timestamp") // Verify webhook signature (required for security) isValid := verifyWebhookSignature( signature, timestamp, string(body), os.Getenv("DAKOTA_WEBHOOK_PUBLIC_KEY") ) if !isValid { log.Println("Webhook signature verification failed") http.Error(w, "Unauthorized", http.StatusUnauthorized) return } // Parse webhook event var event map[string]interface{} if err := json.Unmarshal(body, &event); err != nil { log.Printf("Failed to parse webhook event: %v", err) http.Error(w, "Bad request", http.StatusBadRequest) return } // Process the event handleWebhookEvent(event) // Respond with 200 to acknowledge receipt w.WriteHeader(http.StatusOK) w.Write([]byte("OK")) } func main() { http.HandleFunc("/webhooks/dakota", webhookHandler) log.Fatal(http.ListenAndServe(":3000", nil)) } ``` ```rust Rust use axum::{extract::Request, response::Json, http::StatusCode}; use serde_json::Value; use std::env; async fn webhook_handler(req: Request) -> Result { // Extract headers and body let signature = req.headers() .get("X-Webhook-Signature") .and_then(|h| h.to_str().ok()) .ok_or((StatusCode::BAD_REQUEST, "Missing signature header"))?; let timestamp = req.headers() .get("X-Webhook-Timestamp") .and_then(|h| h.to_str().ok()) .ok_or((StatusCode::BAD_REQUEST, "Missing timestamp header"))?; let body = axum::body::to_bytes(req.into_body(), usize::MAX) .await .map_err(|_| (StatusCode::BAD_REQUEST, "Failed to read body"))?; let body_str = String::from_utf8(body.to_vec()) .map_err(|_| (StatusCode::BAD_REQUEST, "Invalid UTF-8 in body"))?; // Verify webhook signature (required for security) let public_key = env::var("DAKOTA_WEBHOOK_PUBLIC_KEY") .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Missing public key"))?; let is_valid = verify_webhook_signature(signature, timestamp, &body_str, &public_key) .map_err(|_| (StatusCode::UNAUTHORIZED, "Signature verification failed"))?; if !is_valid { return Err((StatusCode::UNAUTHORIZED, "Invalid signature")); } // Parse webhook event let event: Value = serde_json::from_str(&body_str) .map_err(|_| (StatusCode::BAD_REQUEST, "Invalid JSON"))?; // Process the event handle_webhook_event(event) .await .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Failed to process event"))?; Ok(StatusCode::OK) } ``` ```java Java import org.springframework.web.bind.annotation.*; import org.springframework.http.ResponseEntity; import org.springframework.http.HttpStatus; import jakarta.servlet.http.HttpServletRequest; import java.io.IOException; import java.util.Map; @RestController public class WebhookController { @PostMapping("/webhooks/dakota") public ResponseEntity handleWebhook( @RequestBody Map event, HttpServletRequest request) throws IOException { String signature = request.getHeader("X-Webhook-Signature"); String timestamp = request.getHeader("X-Webhook-Timestamp"); // Get raw body for signature verification String rawBody = objectMapper.writeValueAsString(event); // Verify webhook signature (required for security) String publicKey = System.getenv("DAKOTA_WEBHOOK_PUBLIC_KEY"); boolean isValid = verifyWebhookSignature(signature, timestamp, rawBody, publicKey); if (!isValid) { System.err.println("Webhook signature verification failed"); return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Unauthorized"); } // Process the event handleWebhookEvent(event); // Respond with 200 to acknowledge receipt return ResponseEntity.ok("OK"); } private void handleWebhookEvent(Map event) { // Your event processing logic here } private boolean verifyWebhookSignature(String signature, String timestamp, String body, String publicKey) { // Signature verification implementation return true; // Placeholder } } ``` ## 2. Register Your Webhook [Register your endpoint](/reference/webhooks) with Dakota Platform: ### Webhook Target Fields | Field | Type | Required | Description | Example | |-------|------|----------|-------------|---------| | `url` | string | ✅ | HTTPS endpoint to receive webhooks | `"https://your-app.com/webhooks/dakota"` | | `global` | boolean | ❌ | Whether webhook receives events for all customers (default: false) | `false` | | `event_types` | array[string] | ❌ | Array of event types to subscribe to (defaults to all events if not specified) | `["transaction.status.updated", "customer.kyb_status.updated"]` | ```bash cURL curl -X POST https://api.platform.dakota.xyz/webhooks/targets \ -H "X-API-Key: your-api-key" \ -H "X-Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "url": "https://your-app.com/webhooks/dakota", "global": false, "event_types": ["transaction.status.updated", "customer.kyb_status.updated"] }' ``` ```javascript JavaScript const response = await fetch('https://api.platform.dakota.xyz/webhooks/targets', { method: 'POST', headers: { 'X-API-Key': 'your-api-key', 'X-Idempotency-Key': crypto.randomUUID(), 'Content-Type': 'application/json' }, body: JSON.stringify({ url: 'https://your-app.com/webhooks/dakota', global: false, event_types: ['transaction.status.updated', 'customer.kyb_status.updated'] }) }); const webhook = await response.json(); console.log('Created webhook target:', webhook.data.id); ``` ```python Python import requests import uuid response = requests.post( 'https://api.platform.dakota.xyz/webhooks/targets', headers={ 'X-API-Key': 'your-api-key', 'X-Idempotency-Key': str(uuid.uuid4()), 'Content-Type': 'application/json' }, json={ 'url': 'https://your-app.com/webhooks/dakota', 'global': False, 'event_types': ['transaction.status.updated', 'customer.kyb_status.updated'] } ) webhook = response.json() print(f'Created webhook target: {webhook["data"]["id"]}') ``` ```go Go package main import ( "bytes" "net/http" "github.com/google/uuid" ) func main() { client := &http.Client{} body := bytes.NewBufferString(`{ "url": "https://your-app.com/webhooks/dakota", "global": false, "event_types": ["transaction.status.updated", "customer.kyb_status.updated"] }`) req, _ := http.NewRequest("POST", "https://api.platform.dakota.xyz/webhooks/targets", body) req.Header.Add("X-API-Key", "your-api-key") req.Header.Add("X-Idempotency-Key", uuid.New().String()) req.Header.Add("Content-Type", "application/json") resp, _ := client.Do(req) defer resp.Body.Close() } ``` ```rust Rust use reqwest::header::{HeaderMap, HeaderValue}; use uuid::Uuid; use serde_json::json; #[tokio::main] async fn main() -> Result<(), Box> { let client = reqwest::Client::new(); let mut headers = HeaderMap::new(); headers.insert("X-API-Key", HeaderValue::from_static("your-api-key")); 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!({ "url": "https://your-app.com/webhooks/dakota", "global": false, "event_types": ["transaction.status.updated", "customer.kyb_status.updated"] }); let response = client .post("https://api.platform.dakota.xyz/webhooks/targets") .headers(headers) .json(&body) .send() .await?; Ok(()) } ``` ```java Java 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 DakotaWebhookExample { public static void main(String[] args) throws Exception { HttpClient client = HttpClient.newHttpClient(); String body = """ { "url": "https://your-app.com/webhooks/dakota", "global": false, "event_types": ["transaction.status.updated", "customer.kyb_status.updated"] } """; HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.platform.dakota.xyz/webhooks/targets")) .header("X-API-Key", "your-api-key") .header("X-Idempotency-Key", UUID.randomUUID().toString()) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(body)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); } } ``` Response: ```json { "data": { "id": "wh_1234567890", "url": "https://your-app.com/webhooks/dakota", "global": false, "event_types": ["transaction.status.updated", "customer.kyb_status.updated"], "created_at": "2024-01-15T10:30:00Z" } } ``` # Webhook Events ## Transaction Events **transaction.status.updated** ```json { "event": "transaction.status.updated", "data": { "id": "31TgvvnYkN6edEJUTmF1LzTj2ug", "status": "completed", "previous_status": "processing", "customer_id": "31TgvufZK3gDXBcA3BnSeLWiSn7", "type": "onramp", "amount": { "value": "1000.00", "currency": "USD" } }, "created_at": "2024-01-15T10:45:00Z", "id": "evt_webhook_123" } ``` **transaction.failed** ```json { "event": "transaction.failed", "data": { "id": "31TgvvnYkN6edEJUTmF1LzTj2ug", "customer_id": "31TgvufZK3gDXBcA3BnSeLWiSn7", "error": { "code": "insufficient_funds", "message": "Insufficient balance for transaction" } } } ``` ## Customer Events **customer.kyb_status.updated** ```json { "event": "customer.kyb_status.updated", "data": { "id": "31TgvufZK3gDXBcA3BnSeLWiSn7", "kyb_status": "approved", "previous_status": "under_review", "provider": "persona" } } ``` **customer.created** ```json { "event": "customer.created", "data": { "id": "31TgvufZK3gDXBcA3BnSeLWiSn7", "customer_type": "business", "name": "Acme Corp", "email": "compliance@acme.com" } } ``` # Webhook Signature Verification Dakota signs all webhooks with Ed25519 digital signatures for security: ```javascript Node.js const crypto = require('crypto'); const { Buffer } = require('buffer'); function verifyWebhookSignature(signature, timestamp, payload, dakotaPublicKeyHex) { if (!signature || !timestamp || !payload || !dakotaPublicKeyHex) { console.error('Missing required parameters for signature verification'); return false; } // Check timestamp to prevent replay attacks (within 5 minutes) const currentTime = Math.floor(Date.now() / 1000); const webhookTime = parseInt(timestamp); const timeDifference = Math.abs(currentTime - webhookTime); if (timeDifference > 300) { // 5 minutes console.error('Webhook timestamp too old or too far in future'); return false; } // Create the signed payload (timestamp + body) const signedPayload = timestamp + payload; try { // Decode the public key from hex and signature from base64 const publicKeyBuffer = Buffer.from(dakotaPublicKeyHex, 'hex'); const signatureBuffer = Buffer.from(signature, 'base64'); // Validate key length (Ed25519 public key should be 32 bytes) if (publicKeyBuffer.length !== 32) { console.error('Invalid Ed25519 public key length:', publicKeyBuffer.length); return false; } // Wrap raw Ed25519 key in DER format for Node.js crypto API const derPrefix = Buffer.from([ 0x30, 0x2A, // SEQUENCE, 42 bytes 0x30, 0x05, // SEQUENCE, 5 bytes 0x06, 0x03, 0x2B, 0x65, 0x70, // OID 1.3.101.112 (Ed25519) 0x03, 0x21, 0x00 // BIT STRING, 33 bytes (including unused bits byte) ]); const derKey = Buffer.concat([derPrefix, publicKeyBuffer]); const publicKey = crypto.createPublicKey({ key: derKey, format: 'der', type: 'spki' }); // Verify the signature using Ed25519 const isValid = crypto.verify( null, // Ed25519 doesn't use a digest algorithm Buffer.from(signedPayload, 'utf8'), publicKey, signatureBuffer ); return isValid; } catch (error) { console.error('Signature verification failed:', error); return false; } } // Complete Express.js webhook handler example const express = require('express'); const app = express(); // Middleware to capture raw body for signature verification app.use('/webhooks/dakota', express.raw({ type: 'application/json' })); app.post('/webhooks/dakota', (req, res) => { const signature = req.headers['x-webhook-signature']; const timestamp = req.headers['x-webhook-timestamp']; const rawBody = req.body.toString('utf8'); // Verify webhook signature const isValid = verifyWebhookSignature( signature, timestamp, rawBody, process.env.DAKOTA_WEBHOOK_PUBLIC_KEY ); if (!isValid) { console.error('Webhook signature verification failed'); return res.status(401).send('Unauthorized'); } try { const event = JSON.parse(rawBody); console.log('Received verified webhook:', event.event, event.id); // Process the event handleWebhookEvent(event); // Respond with 200 to acknowledge receipt res.status(200).send('OK'); } catch (error) { console.error('Error processing webhook:', error); res.status(400).send('Bad Request'); } }); function handleWebhookEvent(event) { // Make processing idempotent if (isEventAlreadyProcessed(event.id)) { console.log('Event already processed:', event.id); return; } switch (event.event) { case 'transaction.status.updated': handleTransactionUpdate(event.data); break; case 'customer.kyb_status.updated': handleCustomerUpdate(event.data); break; default: console.log(`Unknown event type: ${event.event}`); } // Mark as processed markEventAsProcessed(event.id); } function isEventAlreadyProcessed(eventId) { // Check your database/cache for this event ID // Return true if already processed return false; } function markEventAsProcessed(eventId) { // Store the event ID in your database/cache console.log('Marking event as processed:', eventId); } function handleTransactionUpdate(data) { console.log('Processing transaction update:', data.id, data.status); // Your transaction handling logic here } function handleCustomerUpdate(data) { console.log('Processing customer update:', data.id, data.kyb_status); // Your customer handling logic here } ``` ```python Python import base64 import json import time import os from flask import Flask, request, jsonify from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey from cryptography.exceptions import InvalidSignature def verify_webhook_signature(signature, timestamp, body, dakota_public_key_hex): if not all([signature, timestamp, body, dakota_public_key_hex]): print('Missing required parameters for signature verification') return False try: # Check timestamp to prevent replay attacks (within 5 minutes) current_time = int(time.time()) webhook_time = int(timestamp) time_difference = abs(current_time - webhook_time) if time_difference > 300: # 5 minutes print('Webhook timestamp too old or too far in future') return False # Create the signed payload (timestamp + body) signed_payload = timestamp + body # Decode the public key from hex and signature from base64 public_key_bytes = bytes.fromhex(dakota_public_key_hex) signature_bytes = base64.b64decode(signature) # Validate key length (Ed25519 public key should be 32 bytes) if len(public_key_bytes) != 32: print(f'Invalid Ed25519 public key length: {len(public_key_bytes)}') return False # Create Ed25519 public key object public_key = Ed25519PublicKey.from_public_bytes(public_key_bytes) # Verify the signature public_key.verify(signature_bytes, signed_payload.encode('utf-8')) return True except (InvalidSignature, ValueError, Exception) as e: print(f'Signature verification failed: {e}') return False # Complete Flask webhook handler example app = Flask(__name__) processed_events = set() # Simple in-memory store (use database in production) @app.route('/webhooks/dakota', methods=['POST']) def handle_webhook(): signature = request.headers.get('X-Webhook-Signature') timestamp = request.headers.get('X-Webhook-Timestamp') raw_body = request.get_data(as_text=True) # Verify webhook signature is_valid = verify_webhook_signature( signature, timestamp, raw_body, os.getenv('DAKOTA_WEBHOOK_PUBLIC_KEY') ) if not is_valid: print('Webhook signature verification failed') return jsonify({'error': 'Unauthorized'}), 401 try: event = json.loads(raw_body) print(f'Received verified webhook: {event.get("event")} {event.get("id")}') # Process the event handle_webhook_event(event) # Respond with 200 to acknowledge receipt return jsonify({'status': 'ok'}), 200 except (json.JSONDecodeError, KeyError) as e: print(f'Error processing webhook: {e}') return jsonify({'error': 'Bad Request'}), 400 def handle_webhook_event(event): event_id = event.get('id') # Make processing idempotent if is_event_already_processed(event_id): print(f'Event already processed: {event_id}') return event_type = event.get('event') event_data = event.get('data', {}) if event_type == 'transaction.status.updated': handle_transaction_update(event_data) elif event_type == 'customer.kyb_status.updated': handle_customer_update(event_data) else: print(f'Unknown event type: {event_type}') # Mark as processed mark_event_as_processed(event_id) def is_event_already_processed(event_id): # Check your database/cache for this event ID return event_id in processed_events def mark_event_as_processed(event_id): # Store the event ID in your database/cache processed_events.add(event_id) print(f'Marking event as processed: {event_id}') def handle_transaction_update(data): transaction_id = data.get('id') status = data.get('status') print(f'Processing transaction update: {transaction_id} -> {status}') # Your transaction handling logic here def handle_customer_update(data): customer_id = data.get('id') kyb_status = data.get('kyb_status') print(f'Processing customer update: {customer_id} -> {kyb_status}') # Your customer handling logic here if __name__ == '__main__': app.run(port=3000, debug=True) ``` ```rust Rust use ed25519_dalek::{Signature, Verifier, VerifyingKey}; use base64::{Engine as _, engine::general_purpose}; use std::time::{SystemTime, UNIX_EPOCH}; use warp::Filter; use serde_json::{Value, json}; use std::collections::HashSet; use std::sync::{Arc, Mutex}; use std::convert::Infallible; #[derive(Debug)] pub struct WebhookVerifier { public_key: VerifyingKey, processed_events: Arc>>, } impl WebhookVerifier { pub fn new(public_key_hex: &str) -> Result> { let public_key_bytes = hex::decode(public_key_hex)?; // Validate key length (Ed25519 public key should be 32 bytes) if public_key_bytes.len() != 32 { return Err(format!("Invalid Ed25519 public key length: {}", public_key_bytes.len()).into()); } let public_key = VerifyingKey::from_bytes( &public_key_bytes.try_into().map_err(|_| "Invalid key length")? )?; Ok(Self { public_key, processed_events: Arc::new(Mutex::new(HashSet::new())), }) } pub fn verify_signature(&self, signature: &str, timestamp: &str, body: &str) -> Result> { if signature.is_empty() || timestamp.is_empty() || body.is_empty() { println!("Missing required parameters for signature verification"); return Ok(false); } // Check timestamp to prevent replay attacks (within 5 minutes) let current_time = SystemTime::now() .duration_since(UNIX_EPOCH)? .as_secs(); let webhook_time: u64 = timestamp.parse()?; let time_difference = (current_time as i64 - webhook_time as i64).abs(); if time_difference > 300 { println!("Webhook timestamp too old or too far in future"); return Ok(false); } // Create the signed payload (timestamp + body) let signed_payload = format!("{}{}", timestamp, body); // Decode signature let signature_bytes = general_purpose::STANDARD.decode(signature)?; let signature = Signature::from_bytes(&signature_bytes.try_into().map_err(|_| "Invalid signature length")?); // Verify signature match self.public_key.verify(signed_payload.as_bytes(), &signature) { Ok(_) => Ok(true), Err(e) => { println!("Signature verification failed: {}", e); Ok(false) } } } pub fn is_event_processed(&self, event_id: &str) -> bool { self.processed_events.lock().unwrap().contains(event_id) } pub fn mark_event_processed(&self, event_id: &str) { self.processed_events.lock().unwrap().insert(event_id.to_string()); println!("Marking event as processed: {}", event_id); } pub fn handle_webhook_event(&self, event: &Value) -> Result<(), Box> { let event_id = event["id"].as_str().unwrap_or(""); // Make processing idempotent if self.is_event_processed(event_id) { println!("Event already processed: {}", event_id); return Ok(()); } let event_type = event["event"].as_str().unwrap_or(""); let event_data = &event["data"]; match event_type { "transaction.status.updated" => { self.handle_transaction_update(event_data)?; } "customer.kyb_status.updated" => { self.handle_customer_update(event_data)?; } _ => { println!("Unknown event type: {}", event_type); } } // Mark as processed self.mark_event_processed(event_id); Ok(()) } fn handle_transaction_update(&self, data: &Value) -> Result<(), Box> { let transaction_id = data["id"].as_str().unwrap_or(""); let status = data["status"].as_str().unwrap_or(""); println!("Processing transaction update: {} -> {}", transaction_id, status); // Your transaction handling logic here Ok(()) } fn handle_customer_update(&self, data: &Value) -> Result<(), Box> { let customer_id = data["id"].as_str().unwrap_or(""); let kyb_status = data["kyb_status"].as_str().unwrap_or(""); println!("Processing customer update: {} -> {}", customer_id, kyb_status); // Your customer handling logic here Ok(()) } } // Complete Warp webhook handler example #[tokio::main] async fn main() -> Result<(), Box> { let public_key_hex = std::env::var("DAKOTA_WEBHOOK_PUBLIC_KEY") .expect("DAKOTA_WEBHOOK_PUBLIC_KEY environment variable is required"); let verifier = Arc::new(WebhookVerifier::new(&public_key_hex)?); let webhook_route = warp::path("webhooks") .and(warp::path("dakota")) .and(warp::post()) .and(warp::header::("x-webhook-signature")) .and(warp::header::("x-webhook-timestamp")) .and(warp::body::bytes()) .and_then({ let verifier = verifier.clone(); move |signature: String, timestamp: String, body: bytes::Bytes| { let verifier = verifier.clone(); async move { handle_webhook(verifier, signature, timestamp, body).await } } }); let port = std::env::var("PORT") .unwrap_or_else(|_| "3000".to_string()) .parse::() .unwrap_or(3000); println!("Starting webhook server on port {}", port); warp::serve(webhook_route) .run(([0, 0, 0, 0], port)) .await; Ok(()) } async fn handle_webhook( verifier: Arc, signature: String, timestamp: String, body: bytes::Bytes, ) -> Result { let body_str = match String::from_utf8(body.to_vec()) { Ok(s) => s, Err(_) => { return Ok(warp::reply::with_status( "Invalid UTF-8 in body", warp::http::StatusCode::BAD_REQUEST, )); } }; // Verify webhook signature let is_valid = match verifier.verify_signature(&signature, ×tamp, &body_str) { Ok(valid) => valid, Err(e) => { println!("Signature verification error: {}", e); false } }; if !is_valid { println!("Webhook signature verification failed"); return Ok(warp::reply::with_status( "Unauthorized", warp::http::StatusCode::UNAUTHORIZED, )); } // Parse webhook event let event: Value = match serde_json::from_str(&body_str) { Ok(e) => e, Err(e) => { println!("Failed to parse webhook event: {}", e); return Ok(warp::reply::with_status( "Bad Request", warp::http::StatusCode::BAD_REQUEST, )); } }; println!("Received verified webhook: {} {}", event["event"].as_str().unwrap_or("unknown"), event["id"].as_str().unwrap_or("unknown") ); // Process the event if let Err(e) = verifier.handle_webhook_event(&event) { println!("Error processing webhook event: {}", e); return Ok(warp::reply::with_status( "Internal Server Error", warp::http::StatusCode::INTERNAL_SERVER_ERROR, )); } Ok(warp::reply::with_status( "OK", warp::http::StatusCode::OK, )) } ``` ```java Java import java.security.*; import java.security.spec.X509EncodedKeySpec; import java.util.Base64; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.time.Instant; import java.nio.charset.StandardCharsets; import org.springframework.web.bind.annotation.*; import org.springframework.http.ResponseEntity; import org.springframework.http.HttpStatus; import jakarta.servlet.http.HttpServletRequest; import java.io.IOException; import java.util.Map; public class WebhookVerifier { private final PublicKey publicKey; private final Set processedEvents = ConcurrentHashMap.newKeySet(); public WebhookVerifier(String publicKeyHex) throws Exception { byte[] publicKeyBytes = hexToBytes(publicKeyHex); // Validate key length (Ed25519 public key should be 32 bytes) if (publicKeyBytes.length != 32) { throw new IllegalArgumentException("Invalid Ed25519 public key length: " + publicKeyBytes.length); } // Raw Ed25519 key - wrap in DER format for Java crypto API byte[] derKey = wrapRawKeyInDER(publicKeyBytes); X509EncodedKeySpec keySpec = new X509EncodedKeySpec(derKey); KeyFactory keyFactory = KeyFactory.getInstance("EdDSA"); this.publicKey = keyFactory.generatePublic(keySpec); } private byte[] hexToBytes(String hex) { int len = hex.length(); byte[] data = new byte[len / 2]; for (int i = 0; i < len; i += 2) { data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4) + Character.digit(hex.charAt(i+1), 16)); } return data; } private byte[] wrapRawKeyInDER(byte[] rawKey) { // Ed25519 DER prefix for public key byte[] derPrefix = { 0x30, 0x2A, // SEQUENCE, 42 bytes 0x30, 0x05, // SEQUENCE, 5 bytes 0x06, 0x03, 0x2B, 0x65, 0x70, // OID 1.3.101.112 (Ed25519) 0x03, 0x21, 0x00 // BIT STRING, 33 bytes (including unused bits byte) }; byte[] derKey = new byte[derPrefix.length + rawKey.length]; System.arraycopy(derPrefix, 0, derKey, 0, derPrefix.length); System.arraycopy(rawKey, 0, derKey, derPrefix.length, rawKey.length); return derKey; } public boolean verifySignature(String signature, String timestamp, String body) { if (signature == null || timestamp == null || body == null || signature.isEmpty() || timestamp.isEmpty() || body.isEmpty()) { System.err.println("Missing required parameters for signature verification"); return false; } try { // Check timestamp to prevent replay attacks (within 5 minutes) long currentTime = Instant.now().getEpochSecond(); long webhookTime = Long.parseLong(timestamp); long timeDifference = Math.abs(currentTime - webhookTime); if (timeDifference > 300) { // 5 minutes System.err.println("Webhook timestamp too old or too far in future"); return false; } // Create the signed payload (timestamp + body) String signedPayload = timestamp + body; // Decode signature byte[] signatureBytes = Base64.getDecoder().decode(signature); // Verify signature using Ed25519 Signature verifier = Signature.getInstance("EdDSA"); verifier.initVerify(publicKey); verifier.update(signedPayload.getBytes(StandardCharsets.UTF_8)); return verifier.verify(signatureBytes); } catch (Exception e) { System.err.println("Signature verification failed: " + e.getMessage()); return false; } } public boolean isEventProcessed(String eventId) { return processedEvents.contains(eventId); } public void markEventProcessed(String eventId) { processedEvents.add(eventId); System.out.println("Marking event as processed: " + eventId); } public void handleWebhookEvent(Map event) throws Exception { String eventId = (String) event.get("id"); // Make processing idempotent if (isEventProcessed(eventId)) { System.out.println("Event already processed: " + eventId); return; } String eventType = (String) event.get("event"); @SuppressWarnings("unchecked") Map eventData = (Map) event.get("data"); switch (eventType) { case "transaction.status.updated": handleTransactionUpdate(eventData); break; case "customer.kyb_status.updated": handleCustomerUpdate(eventData); break; default: System.out.println("Unknown event type: " + eventType); } // Mark as processed markEventProcessed(eventId); } private void handleTransactionUpdate(Map data) { String transactionId = (String) data.get("id"); String status = (String) data.get("status"); System.out.println("Processing transaction update: " + transactionId + " -> " + status); // Your transaction handling logic here } private void handleCustomerUpdate(Map data) { String customerId = (String) data.get("id"); String kybStatus = (String) data.get("kyb_status"); System.out.println("Processing customer update: " + customerId + " -> " + kybStatus); // Your customer handling logic here } } @RestController public class WebhookController { private final WebhookVerifier webhookVerifier; public WebhookController() throws Exception { String publicKeyHex = System.getenv("DAKOTA_WEBHOOK_PUBLIC_KEY"); if (publicKeyHex == null || publicKeyHex.isEmpty()) { throw new IllegalStateException("DAKOTA_WEBHOOK_PUBLIC_KEY environment variable is required"); } this.webhookVerifier = new WebhookVerifier(publicKeyHex); } @PostMapping("/webhooks/dakota") public ResponseEntity handleWebhook( @RequestBody Map event, HttpServletRequest request) throws IOException { String signature = request.getHeader("X-Webhook-Signature"); String timestamp = request.getHeader("X-Webhook-Timestamp"); // Get raw body for signature verification String rawBody = new com.fasterxml.jackson.databind.ObjectMapper() .writeValueAsString(event); // Verify webhook signature boolean isValid = webhookVerifier.verifySignature(signature, timestamp, rawBody); if (!isValid) { System.err.println("Webhook signature verification failed"); return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Unauthorized"); } System.out.println("Received verified webhook: " + event.get("event") + " " + event.get("id")); try { // Process the event webhookVerifier.handleWebhookEvent(event); // Respond with 200 to acknowledge receipt return ResponseEntity.ok("OK"); } catch (Exception e) { System.err.println("Error processing webhook event: " + e.getMessage()); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body("Internal Server Error"); } } } ``` ```go Go package main import ( "crypto/ed25519" "encoding/base64" "encoding/json" "fmt" "io" "log" "net/http" "os" "strconv" "sync" "time" ) type WebhookEvent struct { ID string `json:"id"` Event string `json:"event"` Data map[string]interface{} `json:"data"` CreatedAt string `json:"created_at"` } type WebhookHandler struct { publicKey ed25519.PublicKey processedEvents map[string]bool mutex sync.RWMutex } func NewWebhookHandler(publicKeyHex string) (*WebhookHandler, error) { publicKeyBytes, err := hex.DecodeString(publicKeyHex) if err != nil { return nil, fmt.Errorf("failed to decode public key: %v", err) } // Validate key length (Ed25519 public key should be 32 bytes) if len(publicKeyBytes) != 32 { return nil, fmt.Errorf("invalid Ed25519 public key length: %d", len(publicKeyBytes)) } return &WebhookHandler{ publicKey: ed25519.PublicKey(publicKeyBytes), processedEvents: make(map[string]bool), }, nil } func (wh *WebhookHandler) verifySignature(signature, timestamp, body string) bool { if signature == "" || timestamp == "" || body == "" { log.Println("Missing required parameters for signature verification") return false } // Check timestamp to prevent replay attacks (within 5 minutes) webhookTime, err := strconv.ParseInt(timestamp, 10, 64) if err != nil { log.Printf("Invalid timestamp format: %v", err) return false } currentTime := time.Now().Unix() timeDifference := currentTime - webhookTime if timeDifference > 300 || timeDifference < -300 { // 5 minutes log.Println("Webhook timestamp too old or too far in future") return false } // Create the signed payload signedPayload := timestamp + body // Decode signature signatureBytes, err := base64.StdEncoding.DecodeString(signature) if err != nil { log.Printf("Failed to decode signature: %v", err) return false } // Verify signature return ed25519.Verify(wh.publicKey, []byte(signedPayload), signatureBytes) } func (wh *WebhookHandler) isEventProcessed(eventID string) bool { wh.mutex.RLock() defer wh.mutex.RUnlock() return wh.processedEvents[eventID] } func (wh *WebhookHandler) markEventProcessed(eventID string) { wh.mutex.Lock() defer wh.mutex.Unlock() wh.processedEvents[eventID] = true log.Printf("Marking event as processed: %s", eventID) } func (wh *WebhookHandler) handleWebhookEvent(event WebhookEvent) { // Make processing idempotent if wh.isEventProcessed(event.ID) { log.Printf("Event already processed: %s", event.ID) return } switch event.Event { case "transaction.status.updated": wh.handleTransactionUpdate(event.Data) case "customer.kyb_status.updated": wh.handleCustomerUpdate(event.Data) default: log.Printf("Unknown event type: %s", event.Event) } // Mark as processed wh.markEventProcessed(event.ID) } func (wh *WebhookHandler) handleTransactionUpdate(data map[string]interface{}) { transactionID, _ := data["id"].(string) status, _ := data["status"].(string) log.Printf("Processing transaction update: %s -> %s", transactionID, status) // Your transaction handling logic here } func (wh *WebhookHandler) handleCustomerUpdate(data map[string]interface{}) { customerID, _ := data["id"].(string) kybStatus, _ := data["kyb_status"].(string) log.Printf("Processing customer update: %s -> %s", customerID, kybStatus) // Your customer handling logic here } func (wh *WebhookHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } // Read request body body, err := io.ReadAll(r.Body) if err != nil { log.Printf("Failed to read request body: %v", err) http.Error(w, "Bad request", http.StatusBadRequest) return } defer r.Body.Close() signature := r.Header.Get("X-Webhook-Signature") timestamp := r.Header.Get("X-Webhook-Timestamp") // Verify webhook signature if !wh.verifySignature(signature, timestamp, string(body)) { log.Println("Webhook signature verification failed") http.Error(w, "Unauthorized", http.StatusUnauthorized) return } // Parse webhook event var event WebhookEvent if err := json.Unmarshal(body, &event); err != nil { log.Printf("Failed to parse webhook event: %v", err) http.Error(w, "Bad request", http.StatusBadRequest) return } log.Printf("Received verified webhook: %s %s", event.Event, event.ID) // Process the event wh.handleWebhookEvent(event) // Respond with 200 to acknowledge receipt w.WriteHeader(http.StatusOK) w.Write([]byte("OK")) } func main() { publicKeyHex := os.Getenv("DAKOTA_WEBHOOK_PUBLIC_KEY") if publicKeyHex == "" { log.Fatal("DAKOTA_WEBHOOK_PUBLIC_KEY environment variable is required") } webhookHandler, err := NewWebhookHandler(publicKeyHex) if err != nil { log.Fatalf("Failed to create webhook handler: %v", err) } http.Handle("/webhooks/dakota", webhookHandler) port := os.Getenv("PORT") if port == "" { port = "3000" } log.Printf("Starting webhook server on port %s", port) log.Fatal(http.ListenAndServe(":"+port, nil)) } ``` ## Signature Headers Dakota includes these headers in webhook requests: - `X-Webhook-Signature`: Ed25519 signature for verification (base64 encoded) - `X-Webhook-Timestamp`: Unix timestamp when webhook was sent - `X-Dakota-Event-ID`: Unique identifier for this webhook event (optional) **Dakota Platform's Public Keys**: Use the appropriate Ed25519 public key for your environment: | Environment | Public Key (hex-encoded) | |-------------|--------------------------| | **Production** | `65b797d688ed4991ecc0d922f360bd9b4c3d68e5a36ce2b1307cc8547bd68be4` | | **Sandbox** | `7a2f771f3a7ac9ae2a95066df35dc0261d7ce354214736cc232d70b3c66f8a5f` | The public key is a hex-encoded string (64 characters representing 32 bytes). # Managing Webhooks ## List Webhooks [List all webhook targets](/reference/webhooks) configured for your account: ```bash cURL curl -X GET https://api.platform.dakota.xyz/webhooks/targets \ -H "X-API-Key: your-api-key" ``` ```javascript JavaScript const response = await fetch('https://api.platform.dakota.xyz/webhooks/targets', { headers: { 'X-API-Key': 'your-api-key' } }); const webhooks = await response.json(); console.log(`Found ${webhooks.data.length} webhook targets`); ``` ```python Python import requests response = requests.get( 'https://api.platform.dakota.xyz/webhooks/targets', headers={'X-API-Key': 'your-api-key'} ) webhooks = response.json() print(f'Found {len(webhooks["data"])} webhook targets') ``` ```go Go package main import ( "net/http" ) func main() { client := &http.Client{} req, _ := http.NewRequest("GET", "https://api.platform.dakota.xyz/webhooks/targets", nil) req.Header.Add("X-API-Key", "your-api-key") resp, _ := client.Do(req) defer resp.Body.Close() } ``` ```rust Rust use reqwest::header::{HeaderMap, HeaderValue}; #[tokio::main] async fn main() -> Result<(), Box> { let client = reqwest::Client::new(); let mut headers = HeaderMap::new(); headers.insert("X-API-Key", HeaderValue::from_static("your-api-key")); let response = client .get("https://api.platform.dakota.xyz/webhooks/targets") .headers(headers) .send() .await?; Ok(()) } ``` ```java Java import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.URI; public class DakotaListWebhooksExample { public static void main(String[] args) throws Exception { HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.platform.dakota.xyz/webhooks/targets")) .header("X-API-Key", "your-api-key") .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); } } ``` ## Update Webhook [Update an existing webhook target](/reference/webhooks): ```bash cURL curl -X PATCH https://api.platform.dakota.xyz/webhooks/targets/{webhook_id} \ -H "X-API-Key: your-api-key" \ -H "X-Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "global": false }' ``` ```javascript JavaScript const response = await fetch(`https://api.platform.dakota.xyz/webhooks/targets/${webhookId}`, { method: 'PATCH', headers: { 'X-API-Key': 'your-api-key', 'X-Idempotency-Key': crypto.randomUUID(), 'Content-Type': 'application/json' }, body: JSON.stringify({ global: false }) }); ``` ```python Python import requests import uuid response = requests.patch( f'https://api.platform.dakota.xyz/webhooks/targets/{webhook_id}', headers={ 'X-API-Key': 'your-api-key', 'X-Idempotency-Key': str(uuid.uuid4()), 'Content-Type': 'application/json' }, json={'global': False} ) ``` ```go Go package main import ( "bytes" "net/http" "github.com/google/uuid" ) func main() { client := &http.Client{} body := bytes.NewBufferString(`{"global": false}`) req, _ := http.NewRequest("PATCH", "https://api.platform.dakota.xyz/webhooks/targets/" + webhookId, body) req.Header.Add("X-API-Key", "your-api-key") req.Header.Add("X-Idempotency-Key", uuid.New().String()) req.Header.Add("Content-Type", "application/json") resp, _ := client.Do(req) defer resp.Body.Close() } ``` ```rust Rust use reqwest::header::{HeaderMap, HeaderValue}; use uuid::Uuid; use serde_json::json; #[tokio::main] async fn main() -> Result<(), Box> { let client = reqwest::Client::new(); let mut headers = HeaderMap::new(); headers.insert("X-API-Key", HeaderValue::from_static("your-api-key")); 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!({"global": false}); let response = client .patch(&format!("https://api.platform.dakota.xyz/webhooks/targets/{}", webhook_id)) .headers(headers) .json(&body) .send() .await?; Ok(()) } ``` ```java Java 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 DakotaUpdateWebhookExample { public static void main(String[] args) throws Exception { HttpClient client = HttpClient.newHttpClient(); String body = "{\"global\": false}"; HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.platform.dakota.xyz/webhooks/targets/" + webhookId)) .header("X-API-Key", "your-api-key") .header("X-Idempotency-Key", UUID.randomUUID().toString()) .header("Content-Type", "application/json") .method("PATCH", HttpRequest.BodyPublishers.ofString(body)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); } } ``` ## Delete Webhook [Delete a webhook target](/reference/webhooks): ```bash cURL curl -X DELETE https://api.platform.dakota.xyz/webhooks/targets/{webhook_id} \ -H "X-API-Key: your-api-key" \ -H "X-Idempotency-Key: $(uuidgen)" ``` ```javascript JavaScript const response = await fetch(`https://api.platform.dakota.xyz/webhooks/targets/${webhookId}`, { method: 'DELETE', headers: { 'X-API-Key': 'your-api-key', 'X-Idempotency-Key': crypto.randomUUID() } }); if (response.ok) { console.log('Webhook deleted successfully'); } ``` ```python Python import requests import uuid response = requests.delete( f'https://api.platform.dakota.xyz/webhooks/targets/{webhook_id}', headers={ 'X-API-Key': 'your-api-key', 'X-Idempotency-Key': str(uuid.uuid4()) } ) if response.status_code == 204: print('Webhook deleted successfully') ``` ```go Go package main import ( "net/http" "github.com/google/uuid" ) func main() { client := &http.Client{} req, _ := http.NewRequest("DELETE", "https://api.platform.dakota.xyz/webhooks/targets/" + webhookId, nil) req.Header.Add("X-API-Key", "your-api-key") req.Header.Add("X-Idempotency-Key", uuid.New().String()) resp, _ := client.Do(req) defer resp.Body.Close() } ``` ```rust Rust use reqwest::header::{HeaderMap, HeaderValue}; use uuid::Uuid; #[tokio::main] async fn main() -> Result<(), Box> { let client = reqwest::Client::new(); let mut headers = HeaderMap::new(); headers.insert("X-API-Key", HeaderValue::from_static("your-api-key")); headers.insert("X-Idempotency-Key", HeaderValue::from_str(&Uuid::new_v4().to_string())?); let response = client .delete(&format!("https://api.platform.dakota.xyz/webhooks/targets/{}", webhook_id)) .headers(headers) .send() .await?; Ok(()) } ``` ```java Java 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 DakotaDeleteWebhookExample { public static void main(String[] args) throws Exception { HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.platform.dakota.xyz/webhooks/targets/" + webhookId)) .header("X-API-Key", "your-api-key") .header("X-Idempotency-Key", UUID.randomUUID().toString()) .DELETE() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); } } ``` # Webhook Delivery ## Delivery Attempts - Dakota Platform attempts delivery up to 5 times - Retries use exponential backoff (1s, 2s, 4s, 8s, 16s) - Your endpoint must respond with 2xx status code - Timeout is 30 seconds per attempt ## Failure Handling If all delivery attempts fail: - Webhook is marked as failed - You can view failed webhooks in the dashboard - Failed webhooks can be manually retried # Best Practices ## Endpoint Requirements - **HTTPS only**: Dakota Platform only sends webhooks to HTTPS endpoints - **Fast response**: Respond within 30 seconds - **2xx status codes**: Return 200-299 status for successful processing - **Idempotent**: Handle duplicate webhooks gracefully ## Security - Always verify webhook signatures - Use HTTPS for your webhook endpoint - Validate event data before processing - Log webhook events for debugging ## Processing Best Practices Implement robust webhook event processing with error handling, retries, and idempotency: ```javascript Node.js // Robust webhook event processing with error handling class WebhookProcessor { constructor() { this.processedEvents = new Set(); // Use Redis/database in production this.maxRetries = 3; } async handleWebhookEvent(event) { const eventId = event.id; // Make processing idempotent if (this.isEventAlreadyProcessed(eventId)) { console.log('Event already processed:', eventId); return { success: true, message: 'Already processed' }; } try { await this.processEventWithRetry(event); this.markEventAsProcessed(eventId); return { success: true, message: 'Event processed successfully' }; } catch (error) { console.error('Failed to process event after retries:', error); await this.storeFailedEvent(event, error); throw error; } } async processEventWithRetry(event, attempt = 1) { try { await this.processEvent(event); } catch (error) { if (attempt < this.maxRetries) { const delay = Math.pow(2, attempt) * 1000; // Exponential backoff console.log(`Retrying event processing in ${delay}ms (attempt ${attempt + 1})`); await new Promise(resolve => setTimeout(resolve, delay)); return this.processEventWithRetry(event, attempt + 1); } throw error; } } async processEvent(event) { console.log(`Processing event: ${event.event} (${event.id})`); switch (event.event) { case 'transaction.status.updated': await this.handleTransactionUpdate(event.data); break; case 'transaction.completed': await this.handleTransactionCompleted(event.data); break; case 'transaction.failed': await this.handleTransactionFailed(event.data); break; case 'customer.kyb_status.updated': await this.handleCustomerKybUpdate(event.data); break; case 'customer.created': await this.handleCustomerCreated(event.data); break; default: console.log(`Unknown event type: ${event.event}`); } } async handleTransactionUpdate(data) { console.log(`Transaction ${data.id} status: ${data.previous_status} → ${data.status}`); await this.updateTransactionStatus(data.id, data.status); if (data.status === 'completed') { await this.notifyTransactionCompleted(data); } else if (data.status === 'failed') { await this.notifyTransactionFailed(data); } } async handleTransactionCompleted(data) { console.log(`Transaction completed: ${data.id}`); await this.updateTransactionStatus(data.id, 'completed'); await this.notifyTransactionCompleted(data); await this.triggerPostCompletionActions(data); } async handleTransactionFailed(data) { console.log(`Transaction failed: ${data.id}`, data.error); await this.updateTransactionStatus(data.id, 'failed'); await this.notifyTransactionFailed(data); await this.handleTransactionFailure(data); } async handleCustomerKybUpdate(data) { console.log(`Customer ${data.id} KYB status: ${data.previous_status} → ${data.kyb_status}`); await this.updateCustomerKybStatus(data.id, data.kyb_status); if (data.kyb_status === 'approved') { await this.notifyCustomerApproved(data); await this.enableCustomerFeatures(data.id); } else if (data.kyb_status === 'rejected') { await this.notifyCustomerRejected(data); await this.handleKybRejection(data); } } async handleCustomerCreated(data) { console.log(`New customer created: ${data.id}`); await this.syncCustomerToDatabase(data); await this.sendWelcomeEmail(data); } isEventAlreadyProcessed(eventId) { return this.processedEvents.has(eventId); } markEventAsProcessed(eventId) { this.processedEvents.add(eventId); console.log('Marked event as processed:', eventId); } async storeFailedEvent(event, error) { console.log('Storing failed event for review:', event.id, error.message); // Store in database for manual review } // Business logic methods (implement according to your needs) async updateTransactionStatus(transactionId, status) { /* Your implementation */ } async notifyTransactionCompleted(data) { /* Your implementation */ } async notifyTransactionFailed(data) { /* Your implementation */ } async triggerPostCompletionActions(data) { /* Your implementation */ } async handleTransactionFailure(data) { /* Your implementation */ } async updateCustomerKybStatus(customerId, status) { /* Your implementation */ } async notifyCustomerApproved(data) { /* Your implementation */ } async notifyCustomerRejected(data) { /* Your implementation */ } async enableCustomerFeatures(customerId) { /* Your implementation */ } async handleKybRejection(data) { /* Your implementation */ } async syncCustomerToDatabase(data) { /* Your implementation */ } async sendWelcomeEmail(data) { /* Your implementation */ } } // Usage in Express route const webhookProcessor = new WebhookProcessor(); app.post('/webhooks/dakota', async (req, res) => { // ... signature verification code ... try { const result = await webhookProcessor.handleWebhookEvent(event); console.log(result.message); res.status(200).send('OK'); } catch (error) { console.error('Webhook processing failed:', error); res.status(200).send('Processing failed but acknowledged'); } }); ``` ```python Python # Python implementation here - abbreviated for clarity ``` ```go Go # Go implementation here - abbreviated for clarity ``` ```rust Rust # Rust implementation here - abbreviated for clarity ``` ```java Java # Java implementation here - abbreviated for clarity ``` # Testing Webhooks ## Local Development Use tools like ngrok to expose local endpoints: ```bash ngrok http 3000 # Use the HTTPS URL for webhook registration ``` ## Webhook Testing Test your webhook endpoint manually: ```bash curl -X POST https://your-app.com/webhooks/dakota \ -H "Content-Type: application/json" \ -H "X-Webhook-Signature: base64_encoded_signature" \ -H "X-Webhook-Timestamp: $(date +%s)" \ -d '{ "event": "transaction.status.updated", "data": { "id": "31TgvtxUdXi95dUN4M8X1rhSCNS", "status": "completed" } }' ``` # Troubleshooting ## Common Issues **Webhooks Not Received** - Check that your endpoint returns 2xx status - Verify your URL is accessible from the internet - Ensure HTTPS is properly configured - Check firewall/proxy settings **Signature Verification Failing** - Ensure you're using Dakota Platform's correct Ed25519 public key - Verify the signature calculation matches our Ed25519 implementation - Check that the timestamp and payload haven't been modified - Ensure you're using the correct header names (`X-Webhook-Signature`, `X-Webhook-Timestamp`) **Duplicate Webhooks** - Implement idempotency using the event ID - Store processed event IDs to prevent duplicates - Use database constraints where possible ## Monitoring - Set up alerts for webhook failures - Log all webhook events for debugging - Monitor endpoint response times - Track webhook delivery success rates # Event Types Reference Based on the platform's actual event definitions, here are the available webhook event types: | Event | Description | |-------|-------------| | **Customer Events** | | `customer.created` | New customer created | | `customer.updated` | Customer information updated | | `customer.kyb_status.created` | Customer KYB status created | | `customer.kyb_status.updated` | Customer KYB status changed | | `customer.kyb_link.created` | Customer KYB link created | | `customer.kyb_link.updated` | Customer KYB link updated | | **Transaction Events** | | `transaction.auto.created` | Auto account transaction created | | `transaction.auto.updated` | Auto account transaction updated | | `transaction.one_off.created` | One-off transaction created | | `transaction.one_off.updated` | One-off transaction updated | | **Account Events** | | `auto_account.created` | Auto account created | | `auto_account.updated` | Auto account updated | | `auto_account.deleted` | Auto account deleted | | **Other Events** | | `user.created` | User created | | `user.updated` | User updated | | `user.deleted` | User deleted | | `api_key.created` | API key created | | `api_key.deleted` | API key deleted | | `recipient.created` | Recipient created | | `recipient.updated` | Recipient updated | | `recipient.deleted` | Recipient deleted | | `destination.created` | Destination created | | `destination.deleted` | Destination deleted | | `target.created` | Target created | | `target.updated` | Target updated | | `target.deleted` | Target deleted | | `exception.created` | Exception created | | `exception.cleared` | Exception cleared | **Note**: The examples in this documentation use simplified event names like `transaction.status.updated` for clarity, but the actual platform uses the specific event types listed above. # Next Steps After setting up webhooks: 1. **[Testing Your Integration](testing)** - Test webhook delivery and processing 2. **[Transactions](transactions)** - Process transactions that trigger webhook events 3. **[Customer Onboarding](customer-onboarding)** - Set up KYB status webhooks # API Reference For detailed endpoint documentation, see: - [Webhooks API Reference](/reference/webhooks) - Webhook target management - [Events API Reference](/reference/events) - Event types and payload formats - [Webhook Security Guide](/guides/webhook-security) - Security best practices # Testing Your Integration Source: https://docs.dakota.io/guides/testing This guide covers how to test your Dakota Platform API integration using our sandbox environment. # Sandbox Environment > **Note**: The sandbox environment (`api.platform.sandbox.dakota.xyz`) is not currently available but will be released shortly. For testing during development, please use the production environment with caution. The Dakota Platform sandbox provides a safe testing environment that mirrors production: - **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 1. Ask Dakota Support for a Sandbox account login 2. Go to the Dashboard: `https://platform.sandbox.dakota.xyz` 2. Generate test API keys 3. 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 # Testing KYB Onboarding ## Sandbox KYB Behavior In sandbox, KYB verification is simulated: ```javascript // Create test customer const customer = await dakota.customers.create({ customer_type: "business", name: "Test Business", email: "test@business.com" }); // Initiate KYB - will return test onboarding URL const kyb = await dakota.customers.kyb.create(customer.id, { provider: "persona" }); // Simulate completion - status changes automatically in sandbox ``` ## 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 ```bash cURL # 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" } }' ``` ```javascript JavaScript // 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' } }) }); ``` ```python Python # 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) ```bash cURL # 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" } }' ``` ```javascript JavaScript // 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' } }) }); ``` ```python Python # 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' } } ) ``` ```go Go 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() } ``` ```rust Rust use reqwest::header::{HeaderMap, HeaderValue}; use uuid::Uuid; use serde_json::json; #[tokio::main] async fn main() -> Result<(), Box> { 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(()) } ``` ```java Java 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 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: ```http POST /v1/test/webhooks/trigger ``` ```json { "event": "transaction.status.updated", "webhook_target_id": "wh_test_123", "data": { "transaction_id": "31TgvtxUdXi95dUN4M8X1rhSCNS", "status": "completed" } } ``` ## Webhook Testing Tools ### ngrok for Local Development ```bash # 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 1. Visit webhook.site 2. Copy the unique URL 3. Register it as your webhook endpoint 4. View real-time webhook deliveries # Integration Testing Checklist ## Authentication - [ ] API keys work in sandbox - [ ] Error handling for invalid keys - [ ] Rate limiting behavior - [ ] Proper header format ## Customer Management - [ ] Create customers successfully - [ ] Handle validation errors - [ ] KYB flow integration - [ ] Status polling/webhooks ## Transaction Processing - [ ] Successful transaction creation - [ ] Transaction estimation accuracy - [ ] Error handling (insufficient funds, invalid addresses) - [ ] Status tracking and webhooks - [ ] Idempotency key usage ## Webhook Handling - [ ] Webhook signature verification - [ ] Event processing logic - [ ] Duplicate event handling - [ ] Retry mechanisms - [ ] Error logging # Error Simulation ## Force Specific Errors Use special values to trigger errors in sandbox: ### Transaction Errors ```javascript // 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 ```javascript // Force KYB rejection { email: "rejected@test.com" } // Force timeout { email: "timeout@test.com" } ``` # Load Testing ## Rate Limit Testing Test your rate limit handling: ```bash cURL # 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 ``` ```javascript JavaScript 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`); } ``` ```python Python 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 ```bash cURL # 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 ``` ```javascript JavaScript 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`); } ``` ```python Python 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()) ``` ```go Go 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) } ``` ```rust Rust use reqwest::header::{HeaderMap, HeaderValue}; use uuid::Uuid; use serde_json::json; use tokio::task::JoinSet; #[tokio::main] async fn main() -> Result<(), Box> { 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(()) } ``` ```java Java 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[] 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 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: - [ ] All sandbox tests passing - [ ] Error handling comprehensive - [ ] Webhook endpoints secured and tested - [ ] Rate limiting handled gracefully - [ ] Logging and monitoring in place - [ ] Security review completed - [ ] Backup/recovery procedures tested ## Migration to Production 1. **Update Configuration** - Change base URL to `https://api.platform.dakota.xyz` - Replace development API keys with production keys - Update webhook URLs if needed 2. **Environment Variables** ```bash DAKOTA_API_KEY=your_production_api_key DAKOTA_BASE_URL=https://api.platform.dakota.xyz DAKOTA_PUBLIC_KEY=your_production_webhook_public_key ``` 3. **Gradual Rollout** - Start with small transaction amounts - Monitor error rates closely - Have rollback plan ready # Monitoring and Debugging ## Logging Best Practices ```javascript // Log all API calls logger.info('Dakota Platform API call', { method: 'POST', endpoint: '/accounts/onramp', customer_id: '31Tgw0zSyDVo4Az66kmzUjMuwxx', crypto_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: ```javascript const dakota = new DakotaSDK({ baseURL: 'https://api.platform.sandbox.dakota.xyz', apiKey: 'your-api-key', debug: true // Logs all HTTP requests/responses }); ``` # Common Issues and Solutions ## Issue: Webhooks Not Received **Solution**: Check webhook endpoint accessibility and response codes ## Issue: Transactions Stuck in Processing **Solution**: In development, check transaction status via API; may be waiting for simulated processing ## Issue: KYB Never Completes **Solution**: Use test email patterns or check KYB provider status via API ## Issue: Rate Limits Hit Quickly **Solution**: Implement exponential backoff and request queuing # Next Steps After thorough testing: 1. **[Authentication](authentication)** - Review production security practices 2. **[Customer Onboarding](customer-onboarding)** - Set up production KYB flows 3. **[Transactions](transactions)** - Process real transactions 4. **[Webhooks](webhooks)** - Configure production webhook endpoints # API Reference For testing-specific documentation, see: - [Sandbox Environment Reference](/reference/sandbox) - Sandbox-specific behaviors - [Test Data Reference](/reference/test-data) - Available test scenarios - [Error Simulation Reference](/reference/error-simulation) - Triggering specific errors # Wallets Source: https://docs.dakota.io/guides/wallets # Dakota Wallet Infrastructure Welcome to Dakota's wallet infrastructure—a sophisticated yet accessible platform for managing digital assets across blockchain networks. In the rapidly evolving world of digital finance, the challenge isn't just securing assets; it's creating systems that are secure, flexible, and genuinely usable by organizations of all sizes. Dakota's wallet infrastructure represents a fundamental reimagining of how institutional wallets should work. ## How Dakota Wallets Work At the heart of Dakota's design is a simple but powerful idea: separate the concerns of identity, authorization, and policy. This separation creates unprecedented flexibility while maintaining absolute security. ### True Non-Custodial Architecture When you create a Dakota wallet, you're not trusting us with your assets. We never have access to your private keys—not even temporarily, not even encrypted. Instead, our infrastructure coordinates between your authorized signers, each maintaining their own keys, to execute transactions according to your defined policies. This non-custodial approach means that even in the impossible scenario where Dakota's entire infrastructure was compromised, your assets remain secure. The cryptographic signatures required to move funds never leave your control. We facilitate coordination; you maintain ownership. ### Universal Addresses Across Networks One of Dakota's most elegant innovations is our approach to multi-chain support. Within a blockchain family (like all EVM-compatible chains), a single Dakota wallet provides one consistent address. Your Ethereum address is your Polygon address is your Arbitrum address. No more managing different wallets for different networks, no more confusion about which address to use where. This universality extends to operations as well. The same signer groups, the same policies, the same governance rules apply across all supported networks in a family. Deploy once, use everywhere. ### The Intent Model Rather than directly executing blockchain transactions, Dakota uses an intent-based model. Every action—from simple transfers to complex DeFi operations—begins with an intent: a cryptographically signed declaration of what you want to accomplish. This intent model provides multiple layers of security and flexibility. Intents are human-readable, so signers know exactly what they're authorizing. They're cryptographically signed, preventing any tampering or replay attacks. They include idempotency keys, ensuring operations can't be accidentally duplicated. And they're evaluated by our policy engine before execution, enforcing your governance rules automatically. ## The Building Blocks Dakota's wallet infrastructure consists of four interconnected components, each serving a specific purpose in the overall security model: ### Wallets: Your Digital Vaults A Dakota wallet is more than an address on a blockchain—it's a complete financial instrument with built-in governance. Each wallet maintains a single address across its blockchain family, managed by your chosen signer groups and governed by your defined policies. Wallets can represent different organizational units (departments, projects, purposes) while maintaining consistent security standards. ### Signer Groups: Distributed Authority Signer groups transform the single point of failure in traditional wallets into distributed, resilient authority. Each group consists of multiple signers, each with their own cryptographic keys. You might have an operations group for routine transactions, an executive group for large transfers, and a compliance group for regulated operations. Groups are dynamic—add new members when people join, remove them when they leave, all without changing wallet addresses or deploying new contracts. ### Policies: Automated Governance Policies encode your organization's rules directly into the wallet infrastructure. Need two signatures for transfers over $10,000? Three for anything over $100,000? Restrict certain operations to business hours? Block transfers to sanctioned addresses? Policies handle all of this automatically, evaluating every transaction against your rules in milliseconds. No human intervention required, no possibility of oversight or exception. ### Intents: Cryptographic Instructions Every operation begins with an intent—a signed message describing the desired action. Intents ensure that operations are explicit (signers know what they're signing), authentic (cryptographic signatures prevent forgery), and controlled (policies evaluate intents before execution). This intent layer provides the perfect balance between security and usability. ## Your Journey with Dakota Wallets Getting started with Dakota wallets is straightforward, but the platform's depth supports even the most complex requirements. Here's how organizations typically evolve their wallet infrastructure: **Start Simple**: Begin with basic multi-signature wallets. Create a signer group with your key stakeholders, set up simple approval thresholds, and start transacting. Our [Getting Started Guide](wallets/getting-started) walks you through this process step by step. **Add Governance**: As transaction volume grows, add policies to automate approval workflows. Different amounts require different approvals. Certain addresses need special handling. Time-based rules control when transactions can occur. The [Policies Guide](wallets/policies) shows you how. **Scale Operations**: Expand to multiple wallets for different purposes. Create specialized signer groups for different roles. Implement sophisticated transaction patterns for DeFi integration. The [Transactions Guide](wallets/transactions) explores these advanced patterns. **Ensure Security**: Throughout your journey, Dakota's cryptographic foundation ensures security. Understanding this foundation helps you make informed decisions about key management and signing strategies. The [Cryptography Guide](wallets/cryptography) provides this deeper understanding. ## Explore the Documentation This documentation provides everything you need to integrate and operate Dakota wallets: - **[Concepts](wallets/concepts)** - Deep dive into the architectural components and how they work together - **[Getting Started](wallets/getting-started)** - Step-by-step guide to your first wallet integration - **[Signer Groups](wallets/signer-groups)** - Understanding and managing distributed authority - **[Policies](wallets/policies)** - Implementing automated governance rules - **[Transactions](wallets/transactions)** - Executing operations through the intent model - **[Cryptography](wallets/cryptography)** - Technical details of our cryptographic implementation # Create Your Flow Source: https://docs.dakota.io/guides/create-your-flow Use the Dakota Funds Flow Builder to visualize and design your payment flows. This interactive tool helps you understand how funds move through the Dakota platform and generates the corresponding API reference for your implementation.
{ e.preventDefault(); e.stopPropagation(); // Create modal element const modalId = 'dakota-flow-modal-' + Date.now(); const modalDiv = document.createElement('div'); modalDiv.id = modalId; modalDiv.style.cssText = ` display: flex; position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background-color: rgba(0, 0, 0, 0.75); backdrop-filter: blur(8px); z-index: 2147483647; justify-content: center; align-items: center; padding: 40px; margin: 0; `; const closeModal = () => { document.body.removeChild(modalDiv); document.body.style.overflow = ''; }; modalDiv.innerHTML = `
`; document.body.appendChild(modalDiv); document.body.style.overflow = 'hidden'; // Add click handlers const closeBtn = document.getElementById('close-btn-' + modalId); closeBtn.addEventListener('click', closeModal); closeBtn.addEventListener('mouseenter', () => { closeBtn.style.opacity = '0.8'; }); closeBtn.addEventListener('mouseleave', () => { closeBtn.style.opacity = '1'; }); modalDiv.addEventListener('click', (e) => { if (e.target === modalDiv) { closeModal(); } }); }} style={{ cursor: 'pointer', position: 'relative', borderRadius: '12px', overflow: 'hidden', border: '1px solid rgb(229, 231, 235)', transition: 'transform 0.2s, box-shadow 0.2s' }} onMouseOver={(e) => { e.currentTarget.style.transform = 'scale(1.01)'; e.currentTarget.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.1)'; }} onMouseOut={(e) => { e.currentTarget.style.transform = 'scale(1)'; e.currentTarget.style.boxShadow = 'none'; }} > Dakota Funds Flow Builder
Click to Launch Interactive Tool
## How to Use 1. **Click the image above** to launch the interactive Funds Flow Builder 2. **Design your flow** - Use the visual builder to create your payment flow 3. **Configure steps** - Set up each step in your funds flow with the appropriate parameters 4. **Review API calls** - See the corresponding API reference for your flow 5. **Share your flow** - Click the "Share" button to copy a link to your flow. You can share this link with our support or sales team to discuss your specific use case or ask questions 6. **Implement** - Use the generated API calls in your integration ## Need Help? If you have questions about your specific flow or need assistance with your integration, click the **Share** button in the Funds Flow Builder to copy a link to your configuration. Share this link with: - Our [support team](https://dakota.io/talk-to-sales) for technical assistance - Our [sales team](https://dakota.io/talk-to-sales) to discuss your use case and requirements This allows our team to see exactly what you're building and provide more accurate guidance. For detailed API documentation, visit the [API Reference](/api-reference/introduction) section. # Authentication Source: https://docs.dakota.io/guides/authentication Dakota Platform uses API key authentication for secure access to our endpoints. This section covers everything you need to know about authenticating with the Dakota Platform API. # Overview The Dakota Platform API requires API key authentication for all requests. You'll need to: 1. **Get your API key** from the Dakota Platform dashboard 2. **Include proper headers** in your requests 3. **Follow security best practices** to keep your integration secure # What's Covered This authentication guide is organized into the following sections: - **[API Keys & Headers](api-keys-headers)** - How to get API keys, required headers, and request examples - **[Security](security)** - Best practices for secure API key management - **[Troubleshooting](troubleshooting)** - Common errors and how to resolve them # Quick Start For a complete quick start guide including your first API call, see our [Getting Started](getting-started) guide. # API Keys & Headers Source: https://docs.dakota.io/guides/authentication/api-keys-headers # API Keys & Headers Learn how to get your API keys and make authenticated requests to the Dakota Platform API. ## Getting Your API Keys 1. Log into your Dakota Platform dashboard 2. Navigate to the **API Keys** section 3. Click **Create New API Key** 4. Give your key a descriptive name 5. Copy and store the key securely (it will only be shown once) > **Important:** API keys are only displayed once when created. Store them securely in your environment variables or secrets management system. ## API Key Format API keys are base64-encoded strings with exactly 60 characters, for example: - `AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=` ## Required Headers All API requests require the `X-API-Key` header for authentication: ```http X-API-Key: AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc= ``` ### POST Requests Only: X-Idempotency-Key The `X-Idempotency-Key` header is **required for POST requests only** to ensure request idempotency: ```http X-Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000 ``` - Use a unique UUID for each POST request - Do **not** include this header for GET, PUT, PATCH, or DELETE requests - Helps prevent duplicate operations if a request is retried ### Content-Type Header For requests with a body (POST, PUT, PATCH), include: ```http Content-Type: application/json ``` ## Making Authenticated Requests ### GET Request Example GET requests only need the API key header: ```bash cURL curl -X GET https://api.platform.dakota.xyz/customers \ -H "X-API-Key: AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=" \ -H "Content-Type: application/json" ``` ```javascript JavaScript const response = await fetch('https://api.platform.dakota.xyz/customers', { headers: { 'X-API-Key': 'AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=', 'Content-Type': 'application/json' } }); ``` ```python Python import requests response = requests.get( 'https://api.platform.dakota.xyz/customers', headers={ 'X-API-Key': 'AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=', 'Content-Type': 'application/json' } ) ``` ```go Go package main import ( "net/http" ) func main() { client := &http.Client{} req, _ := http.NewRequest("GET", "https://api.platform.dakota.xyz/customers", nil) req.Header.Add("X-API-Key", "AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=") req.Header.Add("Content-Type", "application/json") resp, _ := client.Do(req) defer resp.Body.Close() } ``` ```rust Rust use reqwest::header::{HeaderMap, HeaderValue}; #[tokio::main] async fn main() -> Result<(), Box> { let client = reqwest::Client::new(); let mut headers = HeaderMap::new(); headers.insert("X-API-Key", HeaderValue::from_static("AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=")); headers.insert("Content-Type", HeaderValue::from_static("application/json")); let response = client .get("https://api.platform.dakota.xyz/customers") .headers(headers) .send() .await?; Ok(()) } ``` ```java Java import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.URI; public class DakotaGetExample { public static void main(String[] args) throws Exception { HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.platform.dakota.xyz/customers")) .header("X-API-Key", "AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=") .header("Content-Type", "application/json") .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); } } ``` ### POST Request Example POST requests require both the API key and idempotency key headers: ```bash cURL curl -X POST https://api.platform.dakota.xyz/customers \ -H "X-API-Key: AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=" \ -H "X-Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"name": "Acme Corp", "customer_type": "business"}' ``` ```javascript JavaScript const response = await fetch('https://api.platform.dakota.xyz/customers', { method: 'POST', headers: { 'X-API-Key': 'AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=', 'X-Idempotency-Key': crypto.randomUUID(), 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'Acme Corp', customer_type: 'business' }) }); ``` ```python Python import requests import uuid response = requests.post( 'https://api.platform.dakota.xyz/customers', headers={ 'X-API-Key': 'AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=', 'X-Idempotency-Key': str(uuid.uuid4()), 'Content-Type': 'application/json' }, json={ 'name': 'Acme Corp', 'customer_type': 'business' } ) ``` ```go Go package main import ( "bytes" "net/http" "github.com/google/uuid" ) func main() { client := &http.Client{} body := bytes.NewBufferString(`{"name": "Acme Corp", "customer_type": "business"}`) req, _ := http.NewRequest("POST", "https://api.platform.dakota.xyz/customers", 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() } ``` ```rust Rust use reqwest::header::{HeaderMap, HeaderValue}; use uuid::Uuid; use serde_json::json; #[tokio::main] async fn main() -> Result<(), Box> { 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!({ "name": "Acme Corp", "customer_type": "business" }); let response = client .post("https://api.platform.dakota.xyz/customers") .headers(headers) .json(&body) .send() .await?; Ok(()) } ``` ```java Java 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 DakotaApiExample { public static void main(String[] args) throws Exception { HttpClient client = HttpClient.newHttpClient(); String body = """ { "name": "Acme Corp", "customer_type": "business" } """; HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.platform.dakota.xyz/customers")) .header("X-API-Key", "AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=") .header("X-Idempotency-Key", UUID.randomUUID().toString()) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(body)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); } } ``` ## Header Summary | Header | Required For | Purpose | |--------|-------------|---------| | `X-API-Key` | All requests | Authentication | | `X-Idempotency-Key` | POST requests only | Prevent duplicate operations | | `Content-Type` | Requests with body | Specify JSON content type | > **Remember:** Only include `X-Idempotency-Key` for POST requests. GET, PUT, PATCH, and DELETE requests should not include this header. # Rate Limiting Source: https://docs.dakota.io/guides/authentication/rate-limiting # Rate Limiting The Dakota Platform API implements rate limiting to ensure fair usage and maintain API performance for all clients. ## Rate Limits API requests are rate limited per API key within a 1-minute sliding window: - **Production**: 1000 requests per minute ## Rate Limit Headers Every API response includes rate limit information in the headers: | Header | Description | |--------|-------------| | `X-RateLimit-Limit` | Maximum requests allowed per minute | | `X-RateLimit-Remain` | Requests remaining in current window | | `X-RateLimit-Reset` | Unix timestamp when rate limit resets | ### Example Response Headers ```http HTTP/1.1 200 OK X-RateLimit-Limit: 1000 X-RateLimit-Remain: 847 X-RateLimit-Reset: 1640995260 Content-Type: application/json ``` ## 429 Rate Limit Exceeded When you exceed the rate limit, you'll receive a `429 Too Many Requests` response: ```http HTTP/1.1 429 Too Many Requests X-RateLimit-Limit: 1000 X-RateLimit-Remain: 0 X-RateLimit-Reset: 1640995260 Retry-After: 30 Content-Type: application/json { "error": { "type": "rate_limit_exceeded", "message": "Too many requests. Rate limit is 1000 requests per minute." } } ``` The `Retry-After` header indicates the number of seconds to wait before retrying. ## Handling Rate Limits ### 1. Monitor Rate Limit Headers Always check the rate limit headers in your responses to avoid hitting limits: ```javascript JavaScript const response = await fetch('https://api.platform.dakota.xyz/customers', { headers: { 'X-API-Key': 'AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=' } }); const remaining = parseInt(response.headers.get('X-RateLimit-Remain')); const resetTime = parseInt(response.headers.get('X-RateLimit-Reset')); const limit = parseInt(response.headers.get('X-RateLimit-Limit')); // Calculate percentage of rate limit used const usedPercentage = ((limit - remaining) / limit) * 100; if (remaining < 50) { console.warn(`Rate limit critical: ${remaining}/${limit} requests remaining (${usedPercentage.toFixed(1)}% used)`); } else if (remaining < 100) { console.info(`Rate limit warning: ${remaining}/${limit} requests remaining (${usedPercentage.toFixed(1)}% used)`); } // Calculate time until reset const secondsUntilReset = resetTime - Math.floor(Date.now() / 1000); if (secondsUntilReset > 0) { console.log(`Rate limit resets in ${secondsUntilReset} seconds`); } ``` ```python Python import requests import time from datetime import datetime response = requests.get( 'https://api.platform.dakota.xyz/customers', headers={'X-API-Key': 'AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc='} ) remaining = int(response.headers.get('X-RateLimit-Remain', 0)) reset_time = int(response.headers.get('X-RateLimit-Reset', 0)) limit = int(response.headers.get('X-RateLimit-Limit', 1000)) # Calculate percentage of rate limit used used_percentage = ((limit - remaining) / limit) * 100 if remaining < 50: print(f'Rate limit critical: {remaining}/{limit} requests remaining ({used_percentage:.1f}% used)') elif remaining < 100: print(f'Rate limit warning: {remaining}/{limit} requests remaining ({used_percentage:.1f}% used)') # Calculate time until reset seconds_until_reset = reset_time - int(time.time()) if seconds_until_reset > 0: reset_datetime = datetime.fromtimestamp(reset_time) print(f'Rate limit resets in {seconds_until_reset} seconds at {reset_datetime}') ``` ```go Go package main import ( "fmt" "net/http" "strconv" "time" ) func checkRateLimit(response *http.Response) { remainingStr := response.Header.Get("X-RateLimit-Remain") resetStr := response.Header.Get("X-RateLimit-Reset") limitStr := response.Header.Get("X-RateLimit-Limit") remaining, _ := strconv.Atoi(remainingStr) resetTime, _ := strconv.ParseInt(resetStr, 10, 64) limit, _ := strconv.Atoi(limitStr) // Calculate percentage of rate limit used usedPercentage := float64(limit-remaining) / float64(limit) * 100 if remaining < 50 { fmt.Printf("Rate limit critical: %d/%d requests remaining (%.1f%% used)\n", remaining, limit, usedPercentage) } else if remaining < 100 { fmt.Printf("Rate limit warning: %d/%d requests remaining (%.1f%% used)\n", remaining, limit, usedPercentage) } // Calculate time until reset now := time.Now().Unix() secondsUntilReset := resetTime - now if secondsUntilReset > 0 { fmt.Printf("Rate limit resets in %d seconds\n", secondsUntilReset) } } func main() { client := &http.Client{} req, _ := http.NewRequest("GET", "https://api.platform.dakota.xyz/customers", nil) req.Header.Add("X-API-Key", "AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=") resp, err := client.Do(req) if err != nil { fmt.Printf("Error making request: %v\n", err) return } defer resp.Body.Close() checkRateLimit(resp) } ``` ```rust Rust use reqwest::header::{HeaderMap, HeaderValue}; use std::time::{SystemTime, UNIX_EPOCH}; #[tokio::main] async fn main() -> Result<(), Box> { let client = reqwest::Client::new(); let mut headers = HeaderMap::new(); headers.insert("X-API-Key", HeaderValue::from_static("AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=")); let response = client .get("https://api.platform.dakota.xyz/customers") .headers(headers) .send() .await?; // Extract rate limit headers let remaining: i32 = response .headers() .get("X-RateLimit-Remain") .and_then(|v| v.to_str().ok()) .and_then(|s| s.parse().ok()) .unwrap_or(0); let reset_time: u64 = response .headers() .get("X-RateLimit-Reset") .and_then(|v| v.to_str().ok()) .and_then(|s| s.parse().ok()) .unwrap_or(0); let limit: i32 = response .headers() .get("X-RateLimit-Limit") .and_then(|v| v.to_str().ok()) .and_then(|s| s.parse().ok()) .unwrap_or(1000); // Calculate percentage of rate limit used let used_percentage = ((limit - remaining) as f64 / limit as f64) * 100.0; if remaining < 50 { println!("Rate limit critical: {}/{} requests remaining ({:.1}% used)", remaining, limit, used_percentage); } else if remaining < 100 { println!("Rate limit warning: {}/{} requests remaining ({:.1}% used)", remaining, limit, used_percentage); } // Calculate time until reset let now = SystemTime::now() .duration_since(UNIX_EPOCH)? .as_secs(); if reset_time > now { let seconds_until_reset = reset_time - now; println!("Rate limit resets in {} seconds", seconds_until_reset); } Ok(()) } ``` ```java Java import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.URI; import java.time.Instant; public class DakotaRateLimitMonitor { public static void main(String[] args) throws Exception { HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.platform.dakota.xyz/customers")) .header("X-API-Key", "AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=") .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); checkRateLimit(response); } public static void checkRateLimit(HttpResponse response) { String remainingStr = response.headers().firstValue("X-RateLimit-Remain").orElse("0"); String resetStr = response.headers().firstValue("X-RateLimit-Reset").orElse("0"); String limitStr = response.headers().firstValue("X-RateLimit-Limit").orElse("1000"); int remaining = Integer.parseInt(remainingStr); long resetTime = Long.parseLong(resetStr); int limit = Integer.parseInt(limitStr); // Calculate percentage of rate limit used double usedPercentage = ((double)(limit - remaining) / limit) * 100; if (remaining < 50) { System.out.printf("Rate limit critical: %d/%d requests remaining (%.1f%% used)%n", remaining, limit, usedPercentage); } else if (remaining < 100) { System.out.printf("Rate limit warning: %d/%d requests remaining (%.1f%% used)%n", remaining, limit, usedPercentage); } // Calculate time until reset long now = Instant.now().getEpochSecond(); long secondsUntilReset = resetTime - now; if (secondsUntilReset > 0) { System.out.printf("Rate limit resets in %d seconds%n", secondsUntilReset); } } } ``` ### 2. Implement Exponential Backoff When you receive a 429 response, implement exponential backoff with jitter: ```javascript JavaScript class DakotaApiClient { constructor(apiKey, baseUrl = 'https://api.platform.dakota.xyz') { this.apiKey = apiKey; this.baseUrl = baseUrl; this.maxRetries = 5; this.baseDelay = 1000; // 1 second } async makeRequest(endpoint, options = {}) { const url = `${this.baseUrl}${endpoint}`; const requestOptions = { ...options, headers: { 'X-API-Key': this.apiKey, 'Content-Type': 'application/json', ...options.headers } }; // Add idempotency key for POST requests if (options.method === 'POST') { requestOptions.headers['X-Idempotency-Key'] = crypto.randomUUID(); } return this.makeRequestWithBackoff(url, requestOptions); } async makeRequestWithBackoff(url, options, attempt = 0) { try { const response = await fetch(url, options); // Handle rate limiting if (response.status === 429) { if (attempt >= this.maxRetries) { throw new Error(`Rate limit exceeded after ${this.maxRetries} retries`); } return this.retryWithBackoff(url, options, attempt); } // Handle other 5xx errors with retry if (response.status >= 500 && attempt < this.maxRetries) { return this.retryWithBackoff(url, options, attempt); } return response; } catch (error) { if (error.name === 'TypeError' && attempt < this.maxRetries) { // Network error, retry return this.retryWithBackoff(url, options, attempt); } throw error; } } async retryWithBackoff(url, options, attempt) { const retryAfter = 1; // Default to 1 second if no Retry-After header const exponentialDelay = this.baseDelay * Math.pow(2, attempt); const jitter = Math.random() * 1000; const totalDelay = Math.min(exponentialDelay + jitter, 60000); // Cap at 60s console.log(`Rate limited. Retrying in ${totalDelay}ms (attempt ${attempt + 1}/${this.maxRetries})`); await new Promise(resolve => setTimeout(resolve, totalDelay)); return this.makeRequestWithBackoff(url, options, attempt + 1); } } // Usage example const dakota = new DakotaApiClient('AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc='); try { const customers = await dakota.makeRequest('/customers'); console.log('Customers retrieved:', customers.data); } catch (error) { console.error('Failed after retries:', error.message); } ``` ```python Python import time import random import requests import uuid from typing import Optional, Dict, Any class DakotaApiClient: def __init__(self, api_key: str, base_url: str = 'https://api.platform.dakota.xyz'): self.api_key = api_key self.base_url = base_url self.max_retries = 5 self.base_delay = 1 # 1 second self.session = requests.Session() self.session.headers.update({ 'X-API-Key': api_key, 'Content-Type': 'application/json' }) def make_request(self, endpoint: str, method: str = 'GET', data: Optional[Dict[Any, Any]] = None, headers: Optional[Dict[str, str]] = None) -> requests.Response: url = f'{self.base_url}{endpoint}' request_headers = headers or {} # Add idempotency key for POST requests if method.upper() == 'POST': request_headers['X-Idempotency-Key'] = str(uuid.uuid4()) return self._make_request_with_backoff(url, method, data, request_headers) def _make_request_with_backoff(self, url: str, method: str, data: Optional[Dict[Any, Any]], headers: Dict[str, str], attempt: int = 0) -> requests.Response: try: response = self.session.request( method, url, json=data, headers=headers, timeout=30 ) # Handle rate limiting if response.status_code == 429: if attempt >= self.max_retries: raise Exception(f'Rate limit exceeded after {self.max_retries} retries') return self._retry_with_backoff(url, method, data, headers, attempt, response) # Handle server errors with retry if response.status_code >= 500 and attempt < self.max_retries: return self._retry_with_backoff(url, method, data, headers, attempt, response) response.raise_for_status() return response except requests.exceptions.RequestException as e: if attempt < self.max_retries: return self._retry_with_backoff(url, method, data, headers, attempt) raise e def _retry_with_backoff(self, url: str, method: str, data: Optional[Dict[Any, Any]], headers: Dict[str, str], attempt: int, response: Optional[requests.Response] = None) -> requests.Response: retry_after = 1 # Default to 1 second if response and 'Retry-After' in response.headers: retry_after = int(response.headers['Retry-After']) exponential_delay = self.base_delay * (2 ** attempt) jitter = random.uniform(0, 1) total_delay = min(exponential_delay + jitter, 60) # Cap at 60 seconds print(f'Rate limited. Retrying in {total_delay:.2f}s (attempt {attempt + 1}/{self.max_retries})') time.sleep(total_delay) return self._make_request_with_backoff(url, method, data, headers, attempt + 1) # Usage example dakota = DakotaApiClient('AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=') try: response = dakota.make_request('/customers') customers = response.json() print(f'Customers retrieved: {customers["data"]}') except Exception as e: print(f'Failed after retries: {e}') ``` ```go Go package main import ( "bytes" "encoding/json" "fmt" "io" "math" "math/rand" "net/http" "strconv" "time" "github.com/google/uuid" ) type DakotaApiClient struct { ApiKey string BaseURL string MaxRetries int BaseDelay time.Duration Client *http.Client } func NewDakotaApiClient(apiKey string) *DakotaApiClient { return &DakotaApiClient{ ApiKey: apiKey, BaseURL: "https://api.platform.dakota.xyz", MaxRetries: 5, BaseDelay: time.Second, Client: &http.Client{Timeout: 30 * time.Second}, } } func (c *DakotaApiClient) MakeRequest(endpoint, method string, data interface{}) (*http.Response, error) { url := c.BaseURL + endpoint var body io.Reader if data != nil { jsonData, err := json.Marshal(data) if err != nil { return nil, err } body = bytes.NewBuffer(jsonData) } req, err := http.NewRequest(method, url, body) if err != nil { return nil, err } req.Header.Set("X-API-Key", c.ApiKey) req.Header.Set("Content-Type", "application/json") if method == "POST" { req.Header.Set("X-Idempotency-Key", uuid.New().String()) } return c.makeRequestWithBackoff(req, 0) } func (c *DakotaApiClient) makeRequestWithBackoff(req *http.Request, attempt int) (*http.Response, error) { resp, err := c.Client.Do(req) if err != nil { if attempt < c.MaxRetries { return c.retryWithBackoff(req, attempt, nil) } return nil, err } // Handle rate limiting if resp.StatusCode == 429 { if attempt >= c.MaxRetries { return nil, fmt.Errorf("rate limit exceeded after %d retries", c.MaxRetries) } return c.retryWithBackoff(req, attempt, resp) } // Handle server errors with retry if resp.StatusCode >= 500 && attempt < c.MaxRetries { return c.retryWithBackoff(req, attempt, resp) } return resp, nil } func (c *DakotaApiClient) retryWithBackoff(req *http.Request, attempt int, resp *http.Response) (*http.Response, error) { retryAfter := 1 * time.Second if resp != nil { if retryAfterHeader := resp.Header.Get("Retry-After"); retryAfterHeader != "" { if seconds, err := strconv.Atoi(retryAfterHeader); err == nil { retryAfter = time.Duration(seconds) * time.Second } } } exponentialDelay := c.BaseDelay * time.Duration(math.Pow(2, float64(attempt))) jitter := time.Duration(rand.Float64() * float64(time.Second)) totalDelay := exponentialDelay + jitter if totalDelay > 60*time.Second { totalDelay = 60 * time.Second } fmt.Printf("Rate limited. Retrying in %v (attempt %d/%d)\n", totalDelay, attempt+1, c.MaxRetries) time.Sleep(totalDelay) return c.makeRequestWithBackoff(req, attempt+1) } func main() { dakota := NewDakotaApiClient("AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=") resp, err := dakota.MakeRequest("/customers", "GET", nil) if err != nil { fmt.Printf("Failed after retries: %v\n", err) return } defer resp.Body.Close() fmt.Println("Request succeeded!") } ``` ```rust Rust use reqwest::header::{HeaderMap, HeaderValue}; use serde_json::Value; use std::time::Duration; use tokio::time::sleep; use uuid::Uuid; use rand::Rng; pub struct DakotaApiClient { api_key: String, base_url: String, max_retries: u32, base_delay: Duration, client: reqwest::Client, } impl DakotaApiClient { pub fn new(api_key: String) -> Self { Self { api_key, base_url: "https://api.platform.dakota.xyz".to_string(), max_retries: 5, base_delay: Duration::from_secs(1), client: reqwest::Client::builder() .timeout(Duration::from_secs(30)) .build() .unwrap(), } } pub async fn make_request( &self, endpoint: &str, method: reqwest::Method, data: Option<&Value>, ) -> Result> { let url = format!("{}{}", self.base_url, endpoint); let mut headers = HeaderMap::new(); headers.insert("X-API-Key", HeaderValue::from_str(&self.api_key)?); headers.insert("Content-Type", HeaderValue::from_static("application/json")); if method == reqwest::Method::POST { headers.insert("X-Idempotency-Key", HeaderValue::from_str(&Uuid::new_v4().to_string())?); } let mut request_builder = self.client.request(method, &url).headers(headers); if let Some(json_data) = data { request_builder = request_builder.json(json_data); } self.make_request_with_backoff(request_builder, 0).await } async fn make_request_with_backoff( &self, request_builder: reqwest::RequestBuilder, attempt: u32, ) -> Result> { let request = request_builder.try_clone() .ok_or("Failed to clone request")?; let response = self.client.execute(request.build()?).await; match response { Ok(resp) => { if resp.status() == 429 { if attempt >= self.max_retries { return Err(format!("Rate limit exceeded after {} retries", self.max_retries).into()); } return self.retry_with_backoff(request_builder, attempt, Some(&resp)).await; } if resp.status().as_u16() >= 500 && attempt < self.max_retries { return self.retry_with_backoff(request_builder, attempt, Some(&resp)).await; } Ok(resp) } Err(e) => { if attempt < self.max_retries { return self.retry_with_backoff(request_builder, attempt, None).await; } Err(e.into()) } } } async fn retry_with_backoff( &self, request_builder: reqwest::RequestBuilder, attempt: u32, response: Option<&reqwest::Response>, ) -> Result> { let mut retry_after = Duration::from_secs(1); if let Some(resp) = response { if let Some(retry_header) = resp.headers().get("Retry-After") { if let Ok(seconds_str) = retry_header.to_str() { if let Ok(seconds) = seconds_str.parse::() { retry_after = Duration::from_secs(seconds); } } } } let exponential_delay = self.base_delay * 2_u32.pow(attempt); let jitter = Duration::from_millis(rand::thread_rng().gen_range(0..1000)); let total_delay = std::cmp::min(exponential_delay + jitter, Duration::from_secs(60)); println!("Rate limited. Retrying in {:?} (attempt {}/{})", total_delay, attempt + 1, self.max_retries); sleep(total_delay).await; self.make_request_with_backoff(request_builder, attempt + 1).await } } #[tokio::main] async fn main() -> Result<(), Box> { let dakota = DakotaApiClient::new("AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=".to_string()); match dakota.make_request("/customers", reqwest::Method::GET, None).await { Ok(_) => println!("Request succeeded!"), Err(e) => println!("Failed after retries: {}", e), } Ok(()) } ``` ```java Java import java.io.IOException; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.Duration; import java.util.Optional; import java.util.Random; import java.util.UUID; public class DakotaApiClient { private final String apiKey; private final String baseUrl; private final int maxRetries; private final Duration baseDelay; private final HttpClient client; private final Random random; public DakotaApiClient(String apiKey) { this.apiKey = apiKey; this.baseUrl = "https://api.platform.dakota.xyz"; this.maxRetries = 5; this.baseDelay = Duration.ofSeconds(1); this.client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(30)) .build(); this.random = new Random(); } public HttpResponse makeRequest(String endpoint, String method, String jsonData) throws IOException, InterruptedException { String url = baseUrl + endpoint; HttpRequest.Builder requestBuilder = HttpRequest.newBuilder() .uri(URI.create(url)) .header("X-API-Key", apiKey) .header("Content-Type", "application/json"); if ("POST".equalsIgnoreCase(method)) { requestBuilder.header("X-Idempotency-Key", UUID.randomUUID().toString()); if (jsonData != null) { requestBuilder.POST(HttpRequest.BodyPublishers.ofString(jsonData)); } else { requestBuilder.POST(HttpRequest.BodyPublishers.noBody()); } } else { requestBuilder.GET(); } return makeRequestWithBackoff(requestBuilder.build(), 0); } private HttpResponse makeRequestWithBackoff(HttpRequest request, int attempt) throws IOException, InterruptedException { try { HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); // Handle rate limiting if (response.statusCode() == 429) { if (attempt >= maxRetries) { throw new RuntimeException("Rate limit exceeded after " + maxRetries + " retries"); } return retryWithBackoff(request, attempt, response); } // Handle server errors with retry if (response.statusCode() >= 500 && attempt < maxRetries) { return retryWithBackoff(request, attempt, response); } return response; } catch (IOException e) { if (attempt < maxRetries) { return retryWithBackoff(request, attempt, null); } throw e; } } private HttpResponse retryWithBackoff(HttpRequest request, int attempt, HttpResponse response) throws IOException, InterruptedException { Duration retryAfter = Duration.ofSeconds(1); if (response != null) { Optional retryAfterHeader = response.headers().firstValue("Retry-After"); if (retryAfterHeader.isPresent()) { try { int seconds = Integer.parseInt(retryAfterHeader.get()); retryAfter = Duration.ofSeconds(seconds); } catch (NumberFormatException ignored) { // Use default retry after } } } long exponentialDelayMs = baseDelay.toMillis() * (long) Math.pow(2, attempt); long jitterMs = random.nextInt(1000); long totalDelayMs = Math.min(exponentialDelayMs + jitterMs, 60000); // Cap at 60s System.out.printf("Rate limited. Retrying in %dms (attempt %d/%d)%n", totalDelayMs, attempt + 1, maxRetries); Thread.sleep(totalDelayMs); return makeRequestWithBackoff(request, attempt + 1); } public static void main(String[] args) { DakotaApiClient dakota = new DakotaApiClient("AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc="); try { HttpResponse response = dakota.makeRequest("/customers", "GET", null); System.out.println("Request succeeded!"); } catch (Exception e) { System.out.println("Failed after retries: " + e.getMessage()); } } } ``` ### 3. Distribute Requests Over Time Instead of making all requests at once, distribute them evenly: ```javascript JavaScript class RateLimitedClient { constructor(apiKey, requestsPerMinute = 900) { this.apiKey = apiKey; this.interval = 60000 / requestsPerMinute; // ms between requests this.lastRequest = 0; } async makeRequest(url, options = {}) { const now = Date.now(); const timeToWait = this.interval - (now - this.lastRequest); if (timeToWait > 0) { await new Promise(resolve => setTimeout(resolve, timeToWait)); } this.lastRequest = Date.now(); return fetch(url, { ...options, headers: { 'X-API-Key': this.apiKey, ...options.headers } }); } } ``` ## Best Practices ### 1. Stay Under the Limit - Target 90% of your rate limit (900 requests/minute) to leave buffer room - Monitor your usage patterns and adjust accordingly ### 2. Batch Operations - Use bulk endpoints when available - Group related operations together ### 3. Cache Results - Cache API responses when appropriate to reduce API calls - Use ETags or last-modified headers for efficient caching ### 4. Implement Circuit Breaker - Stop making requests temporarily after multiple rate limit errors - Gradually resume requests after cooling down ```javascript JavaScript class DakotaCircuitBreaker { constructor(options = {}) { this.failures = 0; this.successCount = 0; this.threshold = options.threshold || 5; // Failures before opening this.timeout = options.timeout || 60000; // 1 minute this.resetTimeout = options.resetTimeout || 30000; // 30 seconds this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN this.nextAttempt = Date.now(); this.lastFailureTime = null; } async call(fn) { if (this.state === 'OPEN') { if (Date.now() < this.nextAttempt) { throw new Error(`Circuit breaker is OPEN. Next attempt in ${Math.ceil((this.nextAttempt - Date.now()) / 1000)}s`); } this.state = 'HALF_OPEN'; console.log('Circuit breaker is now HALF_OPEN, attempting request...'); } try { const result = await fn(); this.onSuccess(); return result; } catch (error) { this.onFailure(error); throw error; } } onSuccess() { if (this.state === 'HALF_OPEN') { this.successCount++; // After 3 successful requests in HALF_OPEN, close the circuit if (this.successCount >= 3) { console.log('Circuit breaker is now CLOSED after successful requests'); this.failures = 0; this.successCount = 0; this.state = 'CLOSED'; } } else { this.failures = 0; this.state = 'CLOSED'; } } onFailure(error) { this.failures++; this.lastFailureTime = Date.now(); // Only count rate limit and server errors towards circuit breaking const isCircuitBreakerError = error.message.includes('429') || error.message.includes('Rate limit') || error.message.includes('500') || error.message.includes('502') || error.message.includes('503'); if (isCircuitBreakerError && this.failures >= this.threshold) { this.state = 'OPEN'; this.nextAttempt = Date.now() + this.timeout; this.successCount = 0; console.log(`Circuit breaker is now OPEN after ${this.failures} failures. Will retry at ${new Date(this.nextAttempt)}`); } } getStatus() { return { state: this.state, failures: this.failures, nextAttempt: this.state === 'OPEN' ? new Date(this.nextAttempt) : null, lastFailure: this.lastFailureTime ? new Date(this.lastFailureTime) : null }; } } // Enhanced Dakota client with circuit breaker class EnhancedDakotaClient extends DakotaApiClient { constructor(apiKey, baseUrl) { super(apiKey, baseUrl); this.circuitBreaker = new DakotaCircuitBreaker({ threshold: 5, timeout: 60000, // 1 minute resetTimeout: 30000 // 30 seconds }); } async makeRequest(endpoint, options = {}) { return this.circuitBreaker.call(async () => { return super.makeRequest(endpoint, options); }); } getCircuitBreakerStatus() { return this.circuitBreaker.getStatus(); } } // Usage example with circuit breaker const dakota = new EnhancedDakotaClient('AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc='); // Make requests with automatic circuit breaking for (let i = 0; i < 20; i++) { try { const response = await dakota.makeRequest('/customers'); console.log(`Request ${i + 1} succeeded`); } catch (error) { console.error(`Request ${i + 1} failed:`, error.message); // Check circuit breaker status const status = dakota.getCircuitBreakerStatus(); if (status.state === 'OPEN') { console.log('Circuit breaker is open, waiting before next attempt...'); await new Promise(resolve => setTimeout(resolve, 5000)); // Wait 5 seconds } } } ``` ### 5. Request Queuing and Throttling For high-volume applications, implement request queuing to stay within rate limits: ```javascript JavaScript class DakotaRequestQueue { constructor(apiClient, requestsPerMinute = 900) { this.apiClient = apiClient; this.queue = []; this.processing = false; this.interval = 60000 / requestsPerMinute; // ms between requests this.lastRequest = 0; } async enqueue(endpoint, options = {}) { return new Promise((resolve, reject) => { this.queue.push({ endpoint, options, resolve, reject }); this.processQueue(); }); } async processQueue() { if (this.processing || this.queue.length === 0) { return; } this.processing = true; while (this.queue.length > 0) { const { endpoint, options, resolve, reject } = this.queue.shift(); try { // Ensure minimum interval between requests const now = Date.now(); const timeToWait = this.interval - (now - this.lastRequest); if (timeToWait > 0) { await new Promise(r => setTimeout(r, timeToWait)); } this.lastRequest = Date.now(); const response = await this.apiClient.makeRequest(endpoint, options); resolve(response); } catch (error) { reject(error); } } this.processing = false; } getQueueStatus() { return { queueLength: this.queue.length, processing: this.processing, estimatedWaitTime: this.queue.length * this.interval }; } } // Usage example with request queue const dakota = new DakotaApiClient('AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc='); const requestQueue = new DakotaRequestQueue(dakota, 900); // 900 requests per minute // Queue multiple requests const requests = []; for (let i = 0; i < 100; i++) { requests.push( requestQueue.enqueue('/customers', { method: 'POST', body: JSON.stringify({ customer_type: 'individual', name: `Customer ${i}` }) }) ); } // Process all requests with automatic rate limiting const results = await Promise.allSettled(requests); console.log(`Processed ${results.filter(r => r.status === 'fulfilled').length} requests successfully`); ``` ## Need Higher Limits? If you need higher rate limits for your use case, contact our support team with: - Your current usage patterns and peak request volumes - Expected request volume and growth projections - Business justification for higher limits - Timeline for when you need the increase - Description of your rate limiting and retry strategies We'll work with you to find a solution that meets your needs while maintaining API performance for all users. ### Enterprise Rate Limits For enterprise customers, we offer: - **Higher base limits**: Up to 5,000 requests per minute - **Burst allowances**: Short-term higher limits for batch operations - **Dedicated rate limit monitoring**: Real-time alerts and usage analytics - **Custom retry strategies**: Optimized backoff algorithms for your use case # Security Source: https://docs.dakota.io/guides/authentication/security Implementing proper security practices is crucial when working with the Dakota Platform API. Follow these guidelines to keep your integration secure. # API Key Management ## Store Keys Securely **Never expose API keys in your code:** ```javascript Bad Example // ❌ Never do this const apiKey = 'AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc='; ``` **Use environment variables instead:** ```bash cURL # Use environment variable in shell scripts curl -X GET https://api.platform.dakota.xyz/customers \ -H "X-API-Key: $DAKOTA_API_KEY" ``` ```javascript JavaScript // ✅ Use environment variables const apiKey = process.env.DAKOTA_API_KEY; ``` ```python Python import os api_key = os.getenv('DAKOTA_API_KEY') ``` ```go Go package main import "os" func main() { apiKey := os.Getenv("DAKOTA_API_KEY") } ``` ```rust Rust use std::env; fn main() { let api_key = env::var("DAKOTA_API_KEY") .expect("DAKOTA_API_KEY environment variable not set"); } ``` ```java Java public class DakotaConfig { public static void main(String[] args) { String apiKey = System.getenv("DAKOTA_API_KEY"); if (apiKey == null) { throw new IllegalStateException("DAKOTA_API_KEY environment variable not set"); } } } ``` ## Environment Variables Setup Set up your environment variables properly: ```bash .env file DAKOTA_API_KEY=AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc= ``` ```bash Shell export DAKOTA_API_KEY="AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=" ``` ## Key Rotation - Rotate API keys regularly (recommended: every 90 days) - Create new keys before deactivating old ones to avoid downtime - Have a key rotation process documented and tested ## Access Control - Limit API key access to only necessary team members - Use separate keys for different services or applications - Revoke unused or compromised keys immediately # Request Security ## Always Use HTTPS All API requests must use HTTPS. The Dakota Platform API will reject HTTP requests: ```javascript JavaScript // ✅ Correct - HTTPS const response = await fetch('https://api.platform.dakota.xyz/customers', { headers: { 'X-API-Key': process.env.DAKOTA_API_KEY } }); // ❌ Wrong - HTTP will be rejected const response = await fetch('http://api.platform.dakota.xyz/customers', { headers: { 'X-API-Key': process.env.DAKOTA_API_KEY } }); ``` ## Validate SSL Certificates Ensure your HTTP client validates SSL certificates: ```bash cURL # ✅ Certificate validation enabled by default curl -X GET https://api.platform.dakota.xyz/customers \ -H "X-API-Key: $DAKOTA_API_KEY" # ❌ Never disable certificate verification # curl -k https://api.platform.dakota.xyz/customers # DON'T DO THIS ``` ```javascript Node.js // ✅ Certificate validation enabled by default const https = require('https'); // ❌ Never disable certificate validation // process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0; // DON'T DO THIS ``` ```python Python import requests # ✅ Certificate validation enabled by default response = requests.get( 'https://api.platform.dakota.xyz/customers', headers={'X-API-Key': api_key} ) # ❌ Never disable certificate verification # requests.get(url, verify=False) # DON'T DO THIS ``` ```go Go package main import ( "crypto/tls" "net/http" ) func main() { // ✅ Certificate validation enabled by default client := &http.Client{} // ❌ Never disable certificate verification // client := &http.Client{ // Transport: &http.Transport{ // TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // DON'T DO THIS // }, // } } ``` ```rust Rust use reqwest::Client; #[tokio::main] async fn main() -> Result<(), Box> { // ✅ Certificate validation enabled by default let client = Client::new(); // ❌ Never disable certificate verification // let client = Client::builder() // .danger_accept_invalid_certs(true) // DON'T DO THIS // .build()?; Ok(()) } ``` ```java Java import java.net.http.HttpClient; import javax.net.ssl.SSLContext; public class SecurityExample { public static void main(String[] args) { // ✅ Certificate validation enabled by default HttpClient client = HttpClient.newHttpClient(); // ❌ Never disable certificate verification // Don't create custom trust managers that accept all certificates } } ``` ## Request Logging Security When logging requests for debugging, never log sensitive headers: ```bash cURL # When logging cURL commands, use environment variables echo "curl -X GET https://api.platform.dakota.xyz/customers \\ -H \"X-API-Key: \$DAKOTA_API_KEY\"" # Never log actual API keys # echo "curl -H \"X-API-Key: AHGlPZ...\"" # DON'T DO THIS ``` ```javascript JavaScript function logRequest(url, headers, body) { const safeHeaders = { ...headers }; // Remove sensitive headers from logs delete safeHeaders['X-API-Key']; delete safeHeaders['Authorization']; console.log('API Request:', { url, headers: safeHeaders, body }); } ``` ```python Python import logging def log_request(url, headers, body): safe_headers = headers.copy() # Remove sensitive headers from logs safe_headers.pop('X-API-Key', None) safe_headers.pop('Authorization', None) logging.info(f'API Request: {url}, Headers: {safe_headers}') ``` ```go Go package main import ( "log" "net/http" ) func logRequest(req *http.Request) { safeHeaders := make(map[string][]string) // Copy headers except sensitive ones for k, v := range req.Header { if k != "X-Api-Key" && k != "Authorization" { safeHeaders[k] = v } } log.Printf("API Request: %s %s, Headers: %v", req.Method, req.URL, safeHeaders) } ``` ```rust Rust use std::collections::HashMap; use log::info; fn log_request(url: &str, headers: &HashMap, body: &str) { let mut safe_headers = headers.clone(); // Remove sensitive headers from logs safe_headers.remove("X-API-Key"); safe_headers.remove("Authorization"); info!("API Request: {}, Headers: {:?}", url, safe_headers); } ``` ```java Java import java.util.HashMap; import java.util.Map; import java.util.logging.Logger; public class RequestLogger { private static final Logger logger = Logger.getLogger(RequestLogger.class.getName()); public static void logRequest(String url, Map headers, String body) { Map safeHeaders = new HashMap<>(headers); // Remove sensitive headers from logs safeHeaders.remove("X-API-Key"); safeHeaders.remove("Authorization"); logger.info(String.format("API Request: %s, Headers: %s", url, safeHeaders)); } } ``` # Production Environment ## Base URL Always use the production base URL (Sandbox coming soon): ``` https://api.platform.dakota.xyz ``` ## Network Security - Use private networks or VPNs when possible - Monitor network traffic for unusual patterns ## Error Handling Implement secure error handling that doesn't expose sensitive information: ```bash cURL #!/bin/bash # Secure error handling in shell scripts make_secure_request() { local endpoint="$1" local response response=$(curl -s -w "%{http_code}" \ -H "X-API-Key: $DAKOTA_API_KEY" \ -H "Content-Type: application/json" \ "https://api.platform.dakota.xyz$endpoint") local http_code="${response: -3}" local body="${response%???}" if [[ "$http_code" -ge 400 ]]; then # Log full error details internally echo "API Error: $http_code" >&2 # Return generic error to user echo "API request failed" >&2 return 1 fi echo "$body" } ``` ```javascript JavaScript async function makeSecureRequest(endpoint) { try { const response = await fetch(`https://api.platform.dakota.xyz${endpoint}`, { headers: { 'X-API-Key': process.env.DAKOTA_API_KEY, 'Content-Type': 'application/json' } }); if (!response.ok) { // Log full error details internally console.error('API Error:', response.status, response.statusText); // Return generic error to client throw new Error('API request failed'); } return await response.json(); } catch (error) { // Log error details internally console.error('Request failed:', error.message); // Don't expose internal error details throw new Error('Service temporarily unavailable'); } } ``` ```python Python import requests import logging from typing import Dict, Any def make_secure_request(endpoint: str) -> Dict[Any, Any]: try: response = requests.get( f'https://api.platform.dakota.xyz{endpoint}', headers={ 'X-API-Key': os.getenv('DAKOTA_API_KEY'), 'Content-Type': 'application/json' }, timeout=30 ) if not response.ok: # Log full error details internally logging.error(f'API Error: {response.status_code} {response.reason}') # Return generic error to client raise Exception('API request failed') return response.json() except requests.exceptions.RequestException as e: # Log error details internally logging.error(f'Request failed: {str(e)}') # Don't expose internal error details raise Exception('Service temporarily unavailable') ``` ```go Go package main import ( "encoding/json" "fmt" "io" "log" "net/http" "os" ) func makeSecureRequest(endpoint string) (map[string]interface{}, error) { client := &http.Client{} req, err := http.NewRequest("GET", "https://api.platform.dakota.xyz"+endpoint, nil) if err != nil { log.Printf("Request creation failed: %v", err) return nil, fmt.Errorf("service temporarily unavailable") } req.Header.Set("X-API-Key", os.Getenv("DAKOTA_API_KEY")) req.Header.Set("Content-Type", "application/json") resp, err := client.Do(req) if err != nil { // Log error details internally log.Printf("Request failed: %v", err) // Don't expose internal error details return nil, fmt.Errorf("service temporarily unavailable") } defer resp.Body.Close() if resp.StatusCode >= 400 { // Log full error details internally log.Printf("API Error: %d %s", resp.StatusCode, resp.Status) // Return generic error to client return nil, fmt.Errorf("API request failed") } var result map[string]interface{} if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { log.Printf("JSON decode failed: %v", err) return nil, fmt.Errorf("service temporarily unavailable") } return result, nil } ``` ```rust Rust use reqwest::header::{HeaderMap, HeaderValue}; use serde_json::Value; use std::env; use log::error; async fn make_secure_request(endpoint: &str) -> Result> { let client = reqwest::Client::new(); let mut headers = HeaderMap::new(); let api_key = env::var("DAKOTA_API_KEY") .map_err(|_| "API key not configured")?; headers.insert("X-API-Key", HeaderValue::from_str(&api_key)?); headers.insert("Content-Type", HeaderValue::from_static("application/json")); let url = format!("https://api.platform.dakota.xyz{}", endpoint); match client.get(&url).headers(headers).send().await { Ok(response) => { if !response.status().is_success() { // Log full error details internally error!("API Error: {} {}", response.status(), response.status().canonical_reason().unwrap_or("Unknown")); // Return generic error to client return Err("API request failed".into()); } match response.json::().await { Ok(data) => Ok(data), Err(e) => { error!("JSON decode failed: {}", e); Err("Service temporarily unavailable".into()) } } } Err(e) => { // Log error details internally error!("Request failed: {}", e); // Don't expose internal error details Err("Service temporarily unavailable".into()) } } } ``` ```java Java import java.io.IOException; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.Duration; import java.util.logging.Logger; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; public class SecureApiClient { private static final Logger logger = Logger.getLogger(SecureApiClient.class.getName()); private final HttpClient client; private final ObjectMapper mapper; public SecureApiClient() { this.client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(30)) .build(); this.mapper = new ObjectMapper(); } public JsonNode makeSecureRequest(String endpoint) throws Exception { String apiKey = System.getenv("DAKOTA_API_KEY"); if (apiKey == null) { throw new IllegalStateException("API key not configured"); } HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.platform.dakota.xyz" + endpoint)) .header("X-API-Key", apiKey) .header("Content-Type", "application/json") .GET() .build(); try { HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() >= 400) { // Log full error details internally logger.severe("API Error: " + response.statusCode()); // Return generic error to client throw new Exception("API request failed"); } return mapper.readTree(response.body()); } catch (IOException | InterruptedException e) { // Log error details internally logger.severe("Request failed: " + e.getMessage()); // Don't expose internal error details throw new Exception("Service temporarily unavailable"); } } } ``` # Webhook Security ## Verify Webhook Signatures Always verify webhook signatures to ensure they come from Dakota Platform: ```bash cURL #!/bin/bash # Webhook signature verification in shell (example for testing) verify_webhook_signature() { local payload="$1" local signature="$2" local secret="$WEBHOOK_SECRET" expected_signature=$(echo -n "$payload" | openssl dgst -sha256 -hmac "$secret" -hex | cut -d' ' -f2) if [[ "$signature" == "$expected_signature" ]]; then echo "Signature valid" return 0 else echo "Invalid signature" >&2 return 1 fi } ``` ```javascript JavaScript const crypto = require('crypto'); function verifyWebhookSignature(payload, signature, secret) { const expectedSignature = crypto .createHmac('sha256', secret) .update(payload, 'utf8') .digest('hex'); return crypto.timingSafeEqual( Buffer.from(signature, 'hex'), Buffer.from(expectedSignature, 'hex') ); } // In your webhook handler app.post('/webhooks/dakota', (req, res) => { const signature = req.headers['x-dakota-signature']; const payload = JSON.stringify(req.body); if (!verifyWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET)) { return res.status(401).send('Invalid signature'); } // Process webhook safely processWebhook(req.body); res.status(200).send('OK'); }); ``` ```python Python import hmac import hashlib import json from flask import Flask, request def verify_webhook_signature(payload: str, signature: str, secret: str) -> bool: expected_signature = hmac.new( secret.encode('utf-8'), payload.encode('utf-8'), hashlib.sha256 ).hexdigest() return hmac.compare_digest(signature, expected_signature) app = Flask(__name__) @app.route('/webhooks/dakota', methods=['POST']) def handle_webhook(): signature = request.headers.get('X-Dakota-Signature') payload = request.get_data(as_text=True) if not verify_webhook_signature(payload, signature, os.getenv('WEBHOOK_SECRET')): return 'Invalid signature', 401 # Process webhook safely webhook_data = request.get_json() process_webhook(webhook_data) return 'OK', 200 ``` ```go Go package main import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "os" ) func verifyWebhookSignature(payload, signature, secret string) bool { h := hmac.New(sha256.New, []byte(secret)) h.Write([]byte(payload)) expectedSignature := hex.EncodeToString(h.Sum(nil)) return hmac.Equal([]byte(signature), []byte(expectedSignature)) } func webhookHandler(w http.ResponseWriter, r *http.Request) { signature := r.Header.Get("X-Dakota-Signature") body, err := io.ReadAll(r.Body) if err != nil { http.Error(w, "Error reading body", http.StatusBadRequest) return } if !verifyWebhookSignature(string(body), signature, os.Getenv("WEBHOOK_SECRET")) { http.Error(w, "Invalid signature", http.StatusUnauthorized) return } // Process webhook safely var webhookData map[string]interface{} if err := json.Unmarshal(body, &webhookData); err != nil { http.Error(w, "Invalid JSON", http.StatusBadRequest) return } processWebhook(webhookData) w.WriteHeader(http.StatusOK) fmt.Fprint(w, "OK") } ``` ```rust Rust use hmac::{Hmac, Mac}; use sha2::Sha256; use hex; use std::env; use warp::Filter; type HmacSha256 = Hmac; fn verify_webhook_signature(payload: &str, signature: &str, secret: &str) -> bool { let mut mac = HmacSha256::new_from_slice(secret.as_bytes()) .expect("HMAC can take key of any size"); mac.update(payload.as_bytes()); let expected_signature = hex::encode(mac.finalize().into_bytes()); // Use constant-time comparison expected_signature == signature } #[tokio::main] async fn main() { let webhook_route = warp::path!("webhooks" / "dakota") .and(warp::post()) .and(warp::header::("x-dakota-signature")) .and(warp::body::bytes()) .and_then(handle_webhook); warp::serve(webhook_route) .run(([127, 0, 0, 1], 3000)) .await; } async fn handle_webhook( signature: String, body: bytes::Bytes, ) -> Result { let payload = String::from_utf8_lossy(&body); let secret = env::var("WEBHOOK_SECRET") .map_err(|_| warp::reject::custom("Missing webhook secret"))?; if !verify_webhook_signature(&payload, &signature, &secret) { return Ok(warp::reply::with_status( "Invalid signature", warp::http::StatusCode::UNAUTHORIZED, )); } // Process webhook safely let webhook_data: serde_json::Value = serde_json::from_str(&payload) .map_err(|_| warp::reject::custom("Invalid JSON"))?; process_webhook(webhook_data).await; Ok(warp::reply::with_status("OK", warp::http::StatusCode::OK)) } ``` ```java Java import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.security.MessageDigest; import java.util.Arrays; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class WebhookHandler extends HttpServlet { private boolean verifyWebhookSignature(String payload, String signature, String secret) { try { Mac mac = Mac.getInstance("HmacSHA256"); SecretKeySpec keySpec = new SecretKeySpec(secret.getBytes(), "HmacSHA256"); mac.init(keySpec); byte[] expectedBytes = mac.doFinal(payload.getBytes("UTF-8")); String expectedSignature = bytesToHex(expectedBytes); // Use constant-time comparison return MessageDigest.isEqual( signature.getBytes(), expectedSignature.getBytes() ); } catch (Exception e) { return false; } } private String bytesToHex(byte[] bytes) { StringBuilder result = new StringBuilder(); for (byte b : bytes) { result.append(String.format("%02x", b)); } return result.toString(); } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) { try { String signature = request.getHeader("X-Dakota-Signature"); String payload = request.getReader().lines() .collect(java.util.stream.Collectors.joining("\n")); String secret = System.getenv("WEBHOOK_SECRET"); if (!verifyWebhookSignature(payload, signature, secret)) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.getWriter().write("Invalid signature"); return; } // Process webhook safely processWebhook(payload); response.setStatus(HttpServletResponse.SC_OK); response.getWriter().write("OK"); } catch (Exception e) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); } } } ``` ## Webhook Endpoint Security - Use HTTPS for all webhook endpoints - Implement request size limits - Add rate limiting to webhook endpoints - Validate webhook payload structure # Monitoring and Alerting ## Security Monitoring Set up monitoring for: - Unusual API usage patterns - Failed authentication attempts - Requests from unexpected IP addresses - High error rates that might indicate attacks ## Logging Security Events Log security-relevant events: ```bash cURL #!/bin/bash # Security event logging in shell scripts log_security_event() { local event="$1" local endpoint="$2" local ip_address="$3" echo "$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ) | SECURITY | $event | endpoint=$endpoint ip=$ip_address source=dakota-shell-client" >> /var/log/dakota-security.log } # Example usage log_security_event "api_key_used" "/customers" "$CLIENT_IP" ``` ```javascript JavaScript function logSecurityEvent(event, details) { console.log(JSON.stringify({ timestamp: new Date().toISOString(), event: event, details: details, source: 'dakota-api-client' })); } // Example usage logSecurityEvent('api_key_used', { endpoint: '/customers', ip_address: req.ip, user_agent: req.get('User-Agent') }); ``` ```python Python import json import logging from datetime import datetime, timezone def log_security_event(event: str, details: dict): security_log = { 'timestamp': datetime.now(timezone.utc).isoformat(), 'event': event, 'details': details, 'source': 'dakota-api-client' } # Use structured logging logging.info(json.dumps(security_log)) # Example usage log_security_event('api_key_used', { 'endpoint': '/customers', 'ip_address': request.remote_addr, 'user_agent': request.headers.get('User-Agent') }) ``` ```go Go package main import ( "encoding/json" "log" "net/http" "time" ) type SecurityEvent struct { Timestamp string `json:"timestamp"` Event string `json:"event"` Details map[string]interface{} `json:"details"` Source string `json:"source"` } func logSecurityEvent(event string, details map[string]interface{}) { securityEvent := SecurityEvent{ Timestamp: time.Now().UTC().Format(time.RFC3339), Event: event, Details: details, Source: "dakota-api-client", } eventJSON, err := json.Marshal(securityEvent) if err != nil { log.Printf("Error marshaling security event: %v", err) return } log.Println(string(eventJSON)) } // Example usage func handleRequest(w http.ResponseWriter, r *http.Request) { details := map[string]interface{}{ "endpoint": r.URL.Path, "ip_address": r.RemoteAddr, "user_agent": r.UserAgent(), } logSecurityEvent("api_key_used", details) } ``` ```rust Rust use serde_json::{json, Value}; use chrono::{DateTime, Utc}; use log::info; fn log_security_event(event: &str, details: Value) { let security_event = json!({ "timestamp": Utc::now().to_rfc3339(), "event": event, "details": details, "source": "dakota-api-client" }); info!("{}", security_event.to_string()); } // Example usage fn handle_request(req: &HttpRequest) { let details = json!({ "endpoint": req.uri().path(), "ip_address": req.connection_info().remote_addr(), "user_agent": req.headers().get("user-agent") .and_then(|h| h.to_str().ok()) .unwrap_or("unknown") }); log_security_event("api_key_used", details); } ``` ```java Java import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import java.time.Instant; import java.util.logging.Logger; import javax.servlet.http.HttpServletRequest; public class SecurityLogger { private static final Logger logger = Logger.getLogger(SecurityLogger.class.getName()); private static final ObjectMapper mapper = new ObjectMapper(); public static void logSecurityEvent(String event, ObjectNode details) { try { ObjectNode securityEvent = mapper.createObjectNode(); securityEvent.put("timestamp", Instant.now().toString()); securityEvent.put("event", event); securityEvent.set("details", details); securityEvent.put("source", "dakota-api-client"); logger.info(securityEvent.toString()); } catch (Exception e) { logger.severe("Error logging security event: " + e.getMessage()); } } // Example usage public static void handleRequest(HttpServletRequest request) { ObjectNode details = mapper.createObjectNode(); details.put("endpoint", request.getRequestURI()); details.put("ip_address", request.getRemoteAddr()); details.put("user_agent", request.getHeader("User-Agent")); logSecurityEvent("api_key_used", details); } } ``` ## Alert Setup Configure alerts for: - Multiple consecutive API authentication failures - Requests from new or suspicious IP addresses - Unusual request volume patterns - Webhook signature validation failures # Data Protection ## Sensitive Data Handling - Never log sensitive customer data - Use data encryption at rest for stored API responses - Implement data retention policies # Security Checklist Before going to production, verify: - [ ] API keys stored as environment variables - [ ] No sensitive data in code or logs - [ ] HTTPS used for all requests - [ ] SSL certificate validation enabled - [ ] Webhook signatures verified - [ ] Error handling doesn't expose sensitive info - [ ] Request/response logging excludes sensitive headers - [ ] Monitoring and alerting configured - [ ] Data retention policies implemented - [ ] Security testing completed # Incident Response If you suspect a security incident: 1. **Immediately rotate** your API keys 2. **Review logs** for suspicious activity 3. **Contact Dakota Platform support** with incident details 4. **Document** the incident and response 5. **Update security measures** to prevent recurrence # Troubleshooting Source: https://docs.dakota.io/guides/authentication/troubleshooting Common authentication issues and their solutions. # HTTP Status Codes ## 401 Unauthorized **Symptoms:** - Request returns `401 Unauthorized` - Error message indicates authentication failure **Common Causes:** ### Missing or Invalid API Key ```json { "error": { "type": "unauthorized", "message": "Missing or invalid API key" } } ``` **Solutions:** - Verify your API key is exactly 60 characters - Check that you're including the `X-API-Key` header - Ensure no extra whitespace or characters in the key ```bash cURL # ✅ Correct format curl -X GET https://api.platform.dakota.xyz/customers \ -H "X-API-Key: AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=" # ❌ Common mistakes -H "X-API-Key: AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc= " # extra spaces -H "X-Api-Key: AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=" # wrong case -H "API-Key: AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=" # wrong header name ``` ```javascript JavaScript // ✅ Correct format const headers = { 'X-API-Key': 'AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=' }; // ❌ Common mistakes const headers = { 'X-API-Key': ' AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc= ', // extra spaces 'X-Api-Key': 'AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=', // wrong case 'API-Key': 'AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=' // wrong header name }; ``` ```python Python # ✅ Correct format headers = { 'X-API-Key': 'AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=' } # ❌ Common mistakes headers = { 'X-API-Key': ' AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc= ', # extra spaces 'X-Api-Key': 'AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=', # wrong case 'API-Key': 'AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=' # wrong header name } ``` ```go Go // ✅ Correct format headers := map[string]string{ "X-API-Key": "AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=", } // ❌ Common mistakes headers := map[string]string{ "X-API-Key": " AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc= ", // extra spaces "X-Api-Key": "AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=", // wrong case "API-Key": "AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=", // wrong header name } ``` ```rust Rust use reqwest::header::{HeaderMap, HeaderValue}; // ✅ Correct format let mut headers = HeaderMap::new(); headers.insert("X-API-Key", HeaderValue::from_str("AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc=").unwrap()); // ❌ Common mistakes - these will cause authentication failures // Extra spaces, wrong case, wrong header name ``` ```java Java // ✅ Correct format Map headers = new HashMap<>(); headers.put("X-API-Key", "AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc="); // ❌ Common mistakes headers.put("X-API-Key", " AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc= "); // extra spaces headers.put("X-Api-Key", "AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc="); // wrong case headers.put("API-Key", "AHGlPZaxDSMz8Wf1l8VRH4ObdbHiKsWFWnmRyHtiwAc="); // wrong header name ``` ### Deleted or Expired API Key **Solution:** Create a new API key in your Dakota Platform dashboard ### Wrong Environment **Solution:** Ensure you're using the correct base URL: - Production: `https://api.platform.dakota.xyz` ## 403 Forbidden **Symptoms:** - Request returns `403 Forbidden` - API key is valid but access is denied **Common Causes:** ### Insufficient Permissions ```json { "error": { "type": "forbidden", "message": "API key lacks required permissions for this endpoint" } } ``` **Solutions:** - Check if your account has access to the requested feature - Verify API key permissions in the Dakota Platform dashboard - Contact support if you need additional permissions ### Account Limitations **Solutions:** - Complete required account verification steps - Upgrade your account plan if needed - Contact support for account-specific limitations ## 429 Too Many Requests **Symptoms:** - Request returns `429 Too Many Requests` - No response body, only HTTP status - Rate limit headers indicate limits exceeded **Response Headers:** ``` HTTP/1.1 429 Too Many Requests X-RateLimit-Limit: 1000 X-RateLimit-Remaining: 0 X-RateLimit-Reset: 60 ``` **Solutions:** - Implement exponential backoff (see [Rate Limiting guide](rate-limiting)) - Check `X-RateLimit-Reset` header for recommended wait time - Distribute requests more evenly over time - Contact support if you need higher rate limits # Header Issues ## Missing X-Idempotency-Key for POST Requests **Error:** ```json { "error": { "type": "invalid_request", "message": "X-Idempotency-Key header is required for POST requests" } } ``` **Solution:** Include a unique UUID in the `X-Idempotency-Key` header for POST requests only: ```bash cURL # ✅ POST request with idempotency key curl -X POST https://api.platform.dakota.xyz/customers \ -H "X-API-Key: your-api-key" \ -H "X-Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"customer_type": "business", "name": "Acme Corp"}' # ✅ GET request without idempotency key curl -X GET https://api.platform.dakota.xyz/customers \ -H "X-API-Key: your-api-key" ``` ```javascript JavaScript // ✅ POST request with idempotency key fetch('https://api.platform.dakota.xyz/customers', { method: 'POST', headers: { 'X-API-Key': 'your-api-key', 'X-Idempotency-Key': crypto.randomUUID(), 'Content-Type': 'application/json' }, body: JSON.stringify({customer_type: 'business', name: 'Acme Corp'}) }); // ✅ GET request without idempotency key fetch('https://api.platform.dakota.xyz/customers', { headers: { 'X-API-Key': 'your-api-key' } }); ``` ```python Python import requests import uuid # ✅ POST request with idempotency key response = requests.post( 'https://api.platform.dakota.xyz/customers', headers={ 'X-API-Key': 'your-api-key', 'X-Idempotency-Key': str(uuid.uuid4()), 'Content-Type': 'application/json' }, json={'customer_type': 'business', 'name': 'Acme Corp'} ) # ✅ GET request without idempotency key response = requests.get( 'https://api.platform.dakota.xyz/customers', headers={'X-API-Key': 'your-api-key'} ) ``` ```go Go package main import ( "bytes" "encoding/json" "net/http" "github.com/google/uuid" ) // ✅ POST request with idempotency key data := map[string]interface{}{ "customer_type": "business", "name": "Acme Corp", } jsonData, _ := json.Marshal(data) req, _ := http.NewRequest("POST", "https://api.platform.dakota.xyz/customers", bytes.NewBuffer(jsonData)) req.Header.Set("X-API-Key", "your-api-key") req.Header.Set("X-Idempotency-Key", uuid.New().String()) req.Header.Set("Content-Type", "application/json") // ✅ GET request without idempotency key req, _ = http.NewRequest("GET", "https://api.platform.dakota.xyz/customers", nil) req.Header.Set("X-API-Key", "your-api-key") ``` ```rust Rust use reqwest::header::{HeaderMap, HeaderValue}; use serde_json::json; use uuid::Uuid; // ✅ POST request with idempotency key let mut headers = HeaderMap::new(); headers.insert("X-API-Key", HeaderValue::from_str("your-api-key").unwrap()); headers.insert("X-Idempotency-Key", HeaderValue::from_str(&Uuid::new_v4().to_string()).unwrap()); headers.insert("Content-Type", HeaderValue::from_static("application/json")); let data = json!({ "customer_type": "business", "name": "Acme Corp" }); let client = reqwest::Client::new(); let response = client .post("https://api.platform.dakota.xyz/customers") .headers(headers) .json(&data) .send() .await?; // ✅ GET request without idempotency key let mut headers = HeaderMap::new(); headers.insert("X-API-Key", HeaderValue::from_str("your-api-key").unwrap()); let response = client .get("https://api.platform.dakota.xyz/customers") .headers(headers) .send() .await?; ``` ```java Java import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.URI; import java.util.UUID; // ✅ POST request with idempotency key String requestBody = """ { "customer_type": "business", "name": "Acme Corp" } """; HttpRequest postRequest = HttpRequest.newBuilder() .uri(URI.create("https://api.platform.dakota.xyz/customers")) .header("X-API-Key", "your-api-key") .header("X-Idempotency-Key", UUID.randomUUID().toString()) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(requestBody)) .build(); // ✅ GET request without idempotency key HttpRequest getRequest = HttpRequest.newBuilder() .uri(URI.create("https://api.platform.dakota.xyz/customers")) .header("X-API-Key", "your-api-key") .GET() .build(); ``` ## Invalid Idempotency Key Format **Error:** ```json { "error": { "type": "invalid_request", "message": "X-Idempotency-Key must be a valid UUID" } } ``` **Solution:** Use a properly formatted UUID: ```bash cURL # ✅ Valid UUID formats $(uuidgen) # System command '550e8400-e29b-41d4-a716-446655440000' # Manual UUID # ❌ Invalid formats '123' 'not-a-uuid' '550e8400e29b41d4a716446655440000' # Missing hyphens ``` ```javascript JavaScript // ✅ Valid UUID formats '550e8400-e29b-41d4-a716-446655440000' crypto.randomUUID() // Browser require('uuid').v4() // Node.js // ❌ Invalid formats '123' 'not-a-uuid' '550e8400e29b41d4a716446655440000' // Missing hyphens ``` ```python Python import uuid # ✅ Valid UUID formats str(uuid.uuid4()) # Standard library '550e8400-e29b-41d4-a716-446655440000' # Manual UUID # ❌ Invalid formats '123' 'not-a-uuid' '550e8400e29b41d4a716446655440000' # Missing hyphens ``` ```go Go import "github.com/google/uuid" // ✅ Valid UUID formats uuid.New().String() // Google UUID library "550e8400-e29b-41d4-a716-446655440000" // Manual UUID // ❌ Invalid formats "123" "not-a-uuid" "550e8400e29b41d4a716446655440000" // Missing hyphens ``` ```rust Rust use uuid::Uuid; // ✅ Valid UUID formats Uuid::new_v4().to_string() // UUID crate "550e8400-e29b-41d4-a716-446655440000" // Manual UUID // ❌ Invalid formats "123" "not-a-uuid" "550e8400e29b41d4a716446655440000" // Missing hyphens ``` ```java Java import java.util.UUID; // ✅ Valid UUID formats UUID.randomUUID().toString() // Standard library "550e8400-e29b-41d4-a716-446655440000" // Manual UUID // ❌ Invalid formats "123" "not-a-uuid" "550e8400e29b41d4a716446655440000" // Missing hyphens ``` # Connection Issues ## SSL/TLS Errors **Symptoms:** - Connection fails with SSL certificate errors - "Unable to verify SSL certificate" messages **Solutions:** ```bash cURL # ✅ Certificate validation enabled (default) curl -X GET https://api.platform.dakota.xyz/customers \ -H "X-API-Key: your-api-key" # If you have custom certificates curl --cacert /path/to/certificate.pem \ -X GET https://api.platform.dakota.xyz/customers \ -H "X-API-Key: your-api-key" ``` ```javascript JavaScript // ✅ Certificate validation is handled by the browser/Node.js const https = require('https'); const agent = new https.Agent({ rejectUnauthorized: true // Default, but make it explicit }); fetch(url, { agent }); // Node.js only ``` ```python Python import requests # ✅ Certificate validation enabled (default) response = requests.get(url, verify=True) # If you have custom certificates response = requests.get(url, verify='/path/to/certificate.pem') ``` ```go Go import ( "crypto/tls" "net/http" ) // ✅ Certificate validation enabled (default) client := &http.Client{ Transport: &http.Transport{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: false, // Default }, }, } ``` ```rust Rust // ✅ Certificate validation enabled by default let client = reqwest::Client::builder() .danger_accept_invalid_certs(false) // Default .build()?; ``` ```java Java import javax.net.ssl.SSLContext; // ✅ Certificate validation enabled by default HttpClient client = HttpClient.newBuilder() .sslContext(SSLContext.getDefault()) .build(); ``` ## Network Timeouts **Solutions:** ```bash cURL # Set timeout values curl --max-time 30 \ -X GET https://api.platform.dakota.xyz/customers \ -H "X-API-Key: your-api-key" ``` ```javascript JavaScript // Note: fetch() doesn't support timeout directly, use AbortController const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 30000); try { const response = await fetch(url, { headers: { 'X-API-Key': apiKey }, signal: controller.signal }); clearTimeout(timeoutId); } catch (error) { clearTimeout(timeoutId); } ``` ```python Python import requests response = requests.get( url, headers={'X-API-Key': api_key}, timeout=30 # 30 seconds ) ``` ```go Go import ( "context" "net/http" "time" ) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() req, _ := http.NewRequestWithContext(ctx, "GET", url, nil) req.Header.Set("X-API-Key", apiKey) client := &http.Client{} resp, err := client.Do(req) ``` ```rust Rust use std::time::Duration; let client = reqwest::Client::builder() .timeout(Duration::from_secs(30)) .build()?; let response = client .get(url) .header("X-API-Key", api_key) .send() .await?; ``` ```java Java import java.time.Duration; HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(30)) .build(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) .timeout(Duration.ofSeconds(30)) .header("X-API-Key", apiKey) .GET() .build(); ``` # Debugging Steps ## 1. Verify API Key Format Check your API key meets the requirements: ```bash cURL # Check API key length echo "${#DAKOTA_API_KEY}" # Should output 60 # Basic validation if [ ${#DAKOTA_API_KEY} -ne 60 ]; then echo "Error: API key must be exactly 60 characters" fi ``` ```javascript JavaScript function validateApiKey(apiKey) { if (!apiKey) { throw new Error('API key is required'); } if (apiKey.length !== 60) { throw new Error(`API key must be exactly 60 characters, got ${apiKey.length}`); } // Check if it's base64 encoded const base64Regex = /^[A-Za-z0-9+/]+=*$/; if (!base64Regex.test(apiKey)) { throw new Error('API key must be base64 encoded'); } return true; } ``` ```python Python import re import base64 def validate_api_key(api_key): if not api_key: raise ValueError('API key is required') if len(api_key) != 60: raise ValueError(f'API key must be exactly 60 characters, got {len(api_key)}') # Check if it's base64 encoded base64_regex = re.compile(r'^[A-Za-z0-9+/]+=*$') if not base64_regex.match(api_key): raise ValueError('API key must be base64 encoded') return True ``` ```go Go import ( "encoding/base64" "fmt" "regexp" ) func validateAPIKey(apiKey string) error { if apiKey == "" { return fmt.Errorf("API key is required") } if len(apiKey) != 60 { return fmt.Errorf("API key must be exactly 60 characters, got %d", len(apiKey)) } // Check if it's base64 encoded base64Regex := regexp.MustCompile(`^[A-Za-z0-9+/]+=*$`) if !base64Regex.MatchString(apiKey) { return fmt.Errorf("API key must be base64 encoded") } return nil } ``` ```rust Rust use regex::Regex; fn validate_api_key(api_key: &str) -> Result { if api_key.is_empty() { return Err("API key is required".to_string()); } if api_key.len() != 60 { return Err(format!( "API key must be exactly 60 characters, got {}", api_key.len() )); } // Check if it's base64 encoded let base64_regex = Regex::new(r"^[A-Za-z0-9+/]+=*$").unwrap(); if !base64_regex.is_match(api_key) { return Err("API key must be base64 encoded".to_string()); } Ok(true) } ``` ```java Java import java.util.regex.Pattern; public class APIKeyValidator { private static final Pattern BASE64_PATTERN = Pattern.compile("^[A-Za-z0-9+/]+=*$"); public static boolean validateApiKey(String apiKey) throws IllegalArgumentException { if (apiKey == null || apiKey.isEmpty()) { throw new IllegalArgumentException("API key is required"); } if (apiKey.length() != 60) { throw new IllegalArgumentException( String.format("API key must be exactly 60 characters, got %d", apiKey.length()) ); } // Check if it's base64 encoded if (!BASE64_PATTERN.matcher(apiKey).matches()) { throw new IllegalArgumentException("API key must be base64 encoded"); } return true; } } ``` ## 2. Test with cURL Use cURL to isolate issues from your code: ```bash cURL # Test GET request curl -v -X GET https://api.platform.dakota.xyz/customers \ -H "X-API-Key: YOUR_API_KEY" # Test POST request curl -v -X POST https://api.platform.dakota.xyz/customers \ -H "X-API-Key: YOUR_API_KEY" \ -H "X-Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"customer_type": "business", "name": "Test Company"}' ``` The `-v` flag provides verbose output showing the full HTTP exchange. ## 3. Check Response Headers Always inspect response headers for debugging information: ```bash cURL # Use -v for verbose output including headers curl -v -X GET https://api.platform.dakota.xyz/customers \ -H "X-API-Key: YOUR_API_KEY" # Use -i to include response headers in output curl -i -X GET https://api.platform.dakota.xyz/customers \ -H "X-API-Key: YOUR_API_KEY" ``` ```javascript JavaScript async function debugRequest(url, options) { const response = await fetch(url, options); console.log('Status:', response.status); console.log('Headers:'); response.headers.forEach((value, key) => { console.log(` ${key}: ${value}`); }); const body = await response.text(); console.log('Body:', body); return response; } ``` ```python Python import requests def debug_request(url, **kwargs): response = requests.request(**kwargs, url=url) print(f'Status: {response.status_code}') print('Headers:') for key, value in response.headers.items(): print(f' {key}: {value}') print(f'Body: {response.text}') return response ``` ```go Go import ( "fmt" "io" "net/http" "net/http/httputil" ) func debugRequest(req *http.Request) (*http.Response, error) { // Dump the request reqDump, _ := httputil.DumpRequestOut(req, true) fmt.Printf("REQUEST:\n%s\n", reqDump) client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, err } // Dump the response respDump, _ := httputil.DumpResponse(resp, true) fmt.Printf("RESPONSE:\n%s\n", respDump) return resp, nil } ``` ```rust Rust use reqwest::Response; async fn debug_request(response: Response) { println!("Status: {}", response.status()); println!("Headers:"); for (key, value) in response.headers() { println!(" {}: {:?}", key, value); } let body = response.text().await.unwrap_or_default(); println!("Body: {}", body); } ``` ```java Java import java.net.http.HttpResponse; public void debugRequest(HttpResponse response) { System.out.printf("Status: %d%n", response.statusCode()); System.out.println("Headers:"); response.headers().map().forEach((key, values) -> { values.forEach(value -> { System.out.printf(" %s: %s%n", key, value); }); }); System.out.printf("Body: %s%n", response.body()); } ``` ## 4. Enable Request Logging Log your requests to identify issues: ```bash cURL # Log to file for analysis curl -v -X GET https://api.platform.dakota.xyz/customers \ -H "X-API-Key: YOUR_API_KEY" \ 2>&1 | tee request.log # Remove sensitive data from logs sed -i 's/X-API-Key: .*/X-API-Key: [REDACTED]/g' request.log ``` ```javascript JavaScript function logRequest(url, options) { const safeHeaders = { ...options.headers }; delete safeHeaders['X-API-Key']; // Don't log sensitive data console.log('Making request:', { url, method: options.method || 'GET', headers: safeHeaders, bodyLength: options.body ? options.body.length : 0 }); } ``` ```python Python import logging from urllib.parse import urlparse def log_request(method, url, headers, body=None): safe_headers = headers.copy() if 'X-API-Key' in safe_headers: safe_headers['X-API-Key'] = '[REDACTED]' parsed_url = urlparse(url) logging.info('Making request: %s', { 'method': method, 'url': f"{parsed_url.scheme}://{parsed_url.netloc}{parsed_url.path}", 'headers': safe_headers, 'body_length': len(body) if body else 0 }) ``` ```go Go import ( "log" "net/url" ) func logRequest(method, urlStr string, headers map[string]string, bodyLength int) { safeHeaders := make(map[string]string) for k, v := range headers { if k == "X-API-Key" { safeHeaders[k] = "[REDACTED]" } else { safeHeaders[k] = v } } parsedURL, _ := url.Parse(urlStr) log.Printf("Making request: method=%s url=%s://%s%s headers=%v bodyLength=%d", method, parsedURL.Scheme, parsedURL.Host, parsedURL.Path, safeHeaders, bodyLength, ) } ``` ```rust Rust use std::collections::HashMap; use log::info; use url::Url; fn log_request(method: &str, url: &str, headers: &HashMap, body_length: usize) { let mut safe_headers = headers.clone(); if safe_headers.contains_key("X-API-Key") { safe_headers.insert("X-API-Key".to_string(), "[REDACTED]".to_string()); } if let Ok(parsed_url) = Url::parse(url) { info!( "Making request: method={} url={}://{}{} headers={:?} body_length={}", method, parsed_url.scheme(), parsed_url.host_str().unwrap_or(""), parsed_url.path(), safe_headers, body_length ); } } ``` ```java Java import java.net.URI; import java.util.HashMap; import java.util.Map; import java.util.logging.Logger; public class RequestLogger { private static final Logger logger = Logger.getLogger(RequestLogger.class.getName()); public static void logRequest(String method, String url, Map headers, int bodyLength) { Map safeHeaders = new HashMap<>(headers); if (safeHeaders.containsKey("X-API-Key")) { safeHeaders.put("X-API-Key", "[REDACTED]"); } try { URI uri = URI.create(url); logger.info(String.format( "Making request: method=%s url=%s://%s%s headers=%s bodyLength=%d", method, uri.getScheme(), uri.getHost(), uri.getPath(), safeHeaders, bodyLength )); } catch (Exception e) { logger.warning("Failed to parse URL for logging: " + url); } } } ``` # Error Response Format All Dakota Platform API errors follow this format: ```json { "error": { "type": "error_type", "message": "Human-readable error description", "details": { "field": "Additional context if applicable" } } } ``` Common error types: - `unauthorized` - Authentication failed - `forbidden` - Access denied - `invalid_request` - Invalid request format - `not_found` - Resource doesn't exist - `internal_server_error` - Server error Note: Rate limiting (429 status) returns no structured error body, only HTTP status and headers. # Getting Help If you're still experiencing issues: 1. **Check the API status page** for any ongoing issues 2. **Review recent changes** to your code or configuration 3. **Test with minimal examples** to isolate the problem 4. **Contact support** with: - Complete error messages - Request/response logs (with API keys removed) - Steps to reproduce the issue - A relevant Request ID from the response headers, if available ## Support Information - Email: support@dakota.xyz - Include "API Authentication Issue" in the subject line - Provide relevant logs with sensitive information removed - Mention this troubleshooting guide and what you've already tried # Quick Checklist Before contacting support, verify: - [ ] API key is exactly 60 characters - [ ] Using correct header name: `X-API-Key` - [ ] Including `X-Idempotency-Key` for POST requests only - [ ] Using HTTPS base URL: `https://api.platform.dakota.xyz` - [ ] API key is active in your dashboard - [ ] Account has necessary permissions - [ ] Not hitting rate limits - [ ] Request format matches API documentation # Using LLMs Source: https://docs.dakota.io/guides/using-llms Dakota has made its documentation AI-friendly through multiple mechanisms designed to help Large Language Models understand and integrate with the platform. ## LLM Feed Files To help LLMs stay current on how Dakota works, we expose two continuously updated files for ingestion: - [`llms.txt`](/llms.txt) - A concise, high-signal list of top-level docs pages, great for smaller models or quick context building. - [`llms-full.txt`](/llms-full.txt) - A more exhaustive listing that includes nearly all pages, ideal for full-context indexing. You can regularly ingest these URLs into your custom GPTs or other LLM apps to ensure Dakota-specific questions are grounded in accurate technical detail. ## Contextual Deep Links The documentation supports "contextual" features allowing you to: ### Export as Markdown Export any Dakota documentation page as Markdown for: - Custom GPT training data - Internal knowledge bases - Team documentation - Offline reference ### AI Chat Integration Launch pre-loaded chat sessions with Claude or ChatGPT for specific documentation pages. This enables: - Instant troubleshooting - Code generation with proper context - Deeper topic exploration - Interactive learning ### Use Cases **Troubleshooting**: Open a docs page about webhooks, click "Ask Claude", and get immediate help with your specific webhook implementation issue. **Code Generation**: Load the API reference page, start a chat, and generate production-ready code that follows Dakota's best practices. **Learning**: Explore complex topics like transaction flows by chatting with an AI that has full context of Dakota's documentation. ## Best Practices ### Regular Ingestion For custom GPTs or internal tools: - Fetch llms.txt or llms-full.txt regularly (daily or weekly) - Update your knowledge base with the latest documentation - Ensure accurate, current technical information ### Context Management - Use llms.txt for general queries and overviews - Use llms-full.txt when detailed implementation guidance is needed - Combine with live API testing for verification ### Security Considerations - Never share API keys with AI assistants - Use sandbox credentials when generating code examples - Review AI-generated code before production deployment - Verify security recommendations against official docs ## Getting Started 1. **Choose Your Integration Method** - Quick start: Use llms.txt with your AI assistant - Full context: Ingest llms-full.txt into custom GPTs 2. **Test Your Setup** - Ask basic questions about Dakota concepts - Request code examples for common operations - Verify responses against official documentation 3. **Build with Confidence** - Generate boilerplate integration code - Get instant answers to API questions - Troubleshoot issues with AI assistance ## Resources - [Concise documentation feed](/llms.txt) - [Complete documentation feed](/llms-full.txt) - [Complete API documentation](/api-reference/introduction) ## Support Need help integrating Dakota documentation with your AI tools? Contact our support team at [dakota.io/talk-to-sales](https://dakota.io/talk-to-sales) for assistance. # Welcome to Dakota! Source: https://docs.dakota.io/api-reference/introduction Dakota is a modern, developer-first financial infrastructure platform for payments, treasury, and money movement. It is built to be reliable, scalable, and easy to integrate, with clear APIs and tools designed for production use. These docs will help you get started quickly, understand core concepts, and build confidently with Dakota.