# Daya API Documentation - Full Context > Expanded machine-readable context generated from the docs navigation. Use this when an AI assistant needs more detail than `/llms.txt` provides. Important agent guidance: - Prefer current endpoint docs and concept docs over memory or inferred behavior. - Keep sandbox and production environments separate. - Verify webhooks before processing event payloads. - Do not create duplicate transfers, payouts, or settlements after timeouts; reconcile state first. - For temporary NGN funding accounts and onramps, use `amount` from the create response as the exact transfer instruction. It may differ from the request `amount` because the payment provider can add a collection charge. ## Daya API ### Getting Started #### Daya API Path: / Description: Move money between NGN and stablecoins — accept local bank transfers, receive crypto deposits, and send transfers through a single API. ## What is the Daya API? The Daya API lets merchants move customers between Nigerian Naira (NGN) and stablecoins (USDC/USDT), usually in under one minute: - **Onramps** — Collect Naira (NGN) from customers and send them stablecoins - **Offramps** — Collect stablecoins from customers and send them Naira (NGN) Beyond conversion, the API gives you a full money-movement toolkit: - **USD Virtual Accounts** — Provision USD bank accounts for your customers to receive dollar deposits - **USD Payouts** — Send USD to your customers by ACH, wire, or RTP Create your first onramp or offramp in minutes ## How it Works 1. **Get a rate** — Fetch a firm FX quote with `GET /v1/rates?side=BUY` 2. **Create an onramp** — Provision an NGN funding account with `POST /v1/funding-accounts` 3. **Customer pays** — Your customer sends Naira to the account details 4. **Daya settles** — Funds are converted and settled on-chain or to your balance 5. **Get notified** — Receive webhook events as the deposit progresses 1. **Create an offramp** — Generate a crypto funding account with `POST /v1/funding-accounts`, including `asset` and `chain` 2. **Customer deposits crypto** — Your customer sends the selected USDC/USDT asset to the address 3. **Daya settles** — Funds are credited to your balance or paid out as NGN 4. **Get notified** — Receive webhook events as the deposit settles ## Key Features Give customers Nigerian bank account details so they can send NGN into Daya. Create USD bank accounts for tier-2-verified customers to receive dollar deposits. Give customers USDC/USDT addresses so they can send crypto into Daya. Send NGN, USD (ACH/wire), and SWIFT transfers to saved or inline recipients. Lock in exchange rates with buy/sell sides and defined validity windows. Settle on-chain, to your Daya balance, or as NGN to a bank account per funding account. Separate collection and withdrawal balances with balance transfer and merchant funding. Receive events for funding accounts, deposits, withdrawals, transfers, USD virtual accounts, and customer verification. Test end-to-end with simulated deposits and testnet assets — same API surface as production. ## Settlement Options Settle USDC/USDT directly to a blockchain address on supported chains. Credit your Daya USD balance for aggregation and later withdrawal. Convert crypto deposits to NGN and pay out to a Nigerian bank account. ## Environments **Base URL:** `https://api.sandbox.daya.co` Test environment with simulated deposits and testnet assets. - Separate API keys from production - No real money movement - Full feature parity with production **Base URL:** `https://api.daya.co` Live environment with real NGN deposits and mainnet stablecoins. - Production API keys required - Real money movement - Full audit trail ## Next Steps Create your first onramp or offramp in minutes Learn how to authenticate your API requests Create onramps and offramps Collect NGN from customers Collect stablecoins from customers Understand funding accounts, onramps, offramps, deposits, and rates Set up real-time event notifications View supported blockchain networks and tokens Explore all API endpoints ## Support Need help? Reach out to us at [support@daya.co](mailto:support@daya.co). #### Quick Start Path: /quickstart Description: Create your first onramp or offramp and track the first deposit ## Prerequisites Sign up at [dashboard.daya.co](https://dashboard.daya.co) and generate sandbox API keys. Funding accounts are created for existing customers. Create one with [`POST /v1/customers`](/api-reference/customers/create-customer) or use a customer you already have. ```bash curl https://api.sandbox.daya.co/health ``` --- Pick the receive flow you want to test: ## Create an onramp Create a temporary onramp to accept NGN and settle to your Daya balance. Daya returns bank account details your customer can pay into. ```bash cURL curl --request POST \ --url https://api.sandbox.daya.co/v1/funding-accounts \ --header 'X-Api-Key: YOUR_SANDBOX_API_KEY' \ --header 'X-Idempotency-Key: ngn-onramp-001' \ --header 'Content-Type: application/json' \ --data '{ "type": "TEMPORARY", "rail": "NGN_VIRTUAL_ACCOUNT", "customer": { "customer_id": "650e8400-e29b-41d4-a716-446655440000" }, "currency": "NGN", "amount": 50000, "settlement_destination": { "type": "INTERNAL_BALANCE" } }' ``` ```javascript JavaScript const fundingAccount = await fetch('https://api.sandbox.daya.co/v1/funding-accounts', { method: 'POST', headers: { 'X-Api-Key': 'YOUR_SANDBOX_API_KEY', 'X-Idempotency-Key': 'ngn-onramp-001', 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'TEMPORARY', rail: 'NGN_VIRTUAL_ACCOUNT', customer: { customer_id: '650e8400-e29b-41d4-a716-446655440000' }, currency: 'NGN', amount: 50000, settlement_destination: { type: 'INTERNAL_BALANCE' } }) }).then((response) => response.json()); ``` Example response: ```json { "object": "funding_account", "id": "750e8400-e29b-41d4-a716-446655440100", "type": "TEMPORARY", "status": "ACTIVE", "rail": "NGN_VIRTUAL_ACCOUNT", "customer_id": "650e8400-e29b-41d4-a716-446655440000", "currency": "NGN", "amount": 50000, "settlement_destination": { "type": "INTERNAL_BALANCE" }, "instructions": [ { "type": "NGN_VIRTUAL_ACCOUNT", "status": "ACTIVE", "bank_name": "Wema Bank", "bank_code": "035", "account_number": "0690000031", "account_name": "Daya - Customer", "currency": "NGN" } ], "expires_at": "2026-01-14T15:25:12Z" } ``` Show the `instructions` bank details to your customer. When the customer pays, reconcile deposits with `/v1/deposits` and match rows by `funding_account_id`: ```bash curl --request GET \ --url 'https://api.sandbox.daya.co/v1/deposits' \ --header 'X-Api-Key: YOUR_SANDBOX_API_KEY' ``` ## Create an offramp First resolve the destination bank account: ```bash curl --request POST \ --url https://api.sandbox.daya.co/v1/banks/resolve \ --header 'X-Api-Key: YOUR_SANDBOX_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "account_number": "0690000031", "bank_code": "044" }' ``` Then create a temporary offramp to accept stablecoins and settle as an NGN payout. ```bash cURL curl --request POST \ --url https://api.sandbox.daya.co/v1/funding-accounts \ --header 'X-Api-Key: YOUR_SANDBOX_API_KEY' \ --header 'X-Idempotency-Key: crypto-offramp-001' \ --header 'Content-Type: application/json' \ --data '{ "type": "TEMPORARY", "rail": "CRYPTO_ADDRESS", "customer": { "customer_id": "650e8400-e29b-41d4-a716-446655440000" }, "asset": "USDC", "chain": "BASE", "settlement_destination": { "type": "NGN_PAYOUT", "rate_id": "550e8400-e29b-41d4-a716-446655440000", "destination_bank": { "account_number": "0690000031", "bank_code": "044" } } }' ``` ```javascript JavaScript const fundingAccount = await fetch('https://api.sandbox.daya.co/v1/funding-accounts', { method: 'POST', headers: { 'X-Api-Key': 'YOUR_SANDBOX_API_KEY', 'X-Idempotency-Key': 'crypto-offramp-001', 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'TEMPORARY', rail: 'CRYPTO_ADDRESS', customer: { customer_id: '650e8400-e29b-41d4-a716-446655440000' }, asset: 'USDC', chain: 'BASE', settlement_destination: { type: 'NGN_PAYOUT', rate_id: '550e8400-e29b-41d4-a716-446655440000', destination_bank: { account_number: '0690000031', bank_code: '044' } } }) }).then((response) => response.json()); ``` Example response: ```json { "object": "funding_account", "id": "750e8400-e29b-41d4-a716-446655440200", "type": "TEMPORARY", "status": "ACTIVE", "rail": "CRYPTO_ADDRESS", "customer_id": "650e8400-e29b-41d4-a716-446655440000", "asset": "USDC", "chain": "BASE", "settlement_destination": { "type": "NGN_PAYOUT", "destination_currency": "NGN", "destination_bank": { "account_number": "0690000031", "bank_code": "044" } }, "instructions": [ { "type": "CRYPTO_ADDRESS", "status": "ACTIVE", "address": "0x742d35cc6634c0532925a3b844bc9e7595f2bd18", "chain": "BASE" } ] } ``` Show the crypto address to your customer. When crypto lands, reconcile deposits with `/v1/deposits` and match rows by `funding_account_id`: ```bash curl --request GET \ --url 'https://api.sandbox.daya.co/v1/deposits' \ --header 'X-Api-Key: YOUR_SANDBOX_API_KEY' ``` ## Track with Webhooks Subscribe to webhook events so your system updates without polling. | Event | Use it for | |-------|------------| | `funding_account.active` | Show payment details to the customer | | `funding_account.failed` | Stop the flow and ask the customer to retry | | `deposit.received` | Mark incoming funds as detected | | `deposit.processing` | Settlement has started | | `deposit.completed` | Settlement reached its destination | | `deposit.requires_review` | The deposit needs review before continuing | | `deposit.failed` | The deposit failed | ## Next Steps Learn the receive-money model. See every request field. Reconcile incoming money. #### Authentication Path: /authentication Description: Secure your API requests with API keys ## Overview All protected Daya API requests require authentication using API keys. Each key is tied to a specific merchant and environment (Sandbox or Production). API keys grant full access to your account. **Never** share them publicly or commit them to version control. ## API Keys ### Generating Keys 1. Sign up at [dashboard.daya.co](https://dashboard.daya.co) 2. Navigate to **API Keys** 3. Generate separate keys for Sandbox and Production ### Key Format | Environment | Prefix | Example | |------------|--------|---------| | Sandbox | `sk_sandbox_` | `sk_sandbox_abc123...` | | Production | `sk_live_` | `sk_live_xyz789...` | ### Environments | Environment | Purpose | Base URL | |------------|---------|----------| | **Sandbox** | Testing with fake funds | `https://api.sandbox.daya.co` | | **Production** | Live transactions with real money | `https://api.daya.co` | Sandbox and Production environments are **completely isolated**. Data and keys do not cross environments. ## Making Authenticated Requests Include your API key in the `X-Api-Key` header on every protected request: ```bash cURL curl --request GET \ --url https://api.daya.co/v1/rates \ --header 'X-Api-Key: YOUR_API_KEY' ``` ```javascript JavaScript const response = await fetch('https://api.daya.co/v1/rates', { headers: { 'X-Api-Key': 'YOUR_API_KEY', 'Content-Type': 'application/json' } }); ``` ```python Python import requests headers = { 'X-Api-Key': 'YOUR_API_KEY', 'Content-Type': 'application/json' } response = requests.get('https://api.daya.co/v1/rates', headers=headers) ``` ```go Go client := &http.Client{} req, _ := http.NewRequest("GET", "https://api.daya.co/v1/rates", nil) req.Header.Add("X-Api-Key", "YOUR_API_KEY") req.Header.Add("Content-Type", "application/json") resp, _ := client.Do(req) ``` ## Idempotent Write Requests Endpoints that create resources, such as `POST /v1/funding-accounts`, `POST /v1/transfers`, and `POST /v1/merchant/withdrawals`, also require an `X-Idempotency-Key` header. Use a new value for each new write attempt, and reuse the same value only when retrying the exact same request. ```bash cURL curl --request POST \ --url https://api.daya.co/v1/funding-accounts \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'X-Idempotency-Key: funding-account-20260320-0001' \ --header 'Content-Type: application/json' ``` ## Environment Isolation **For:** Integration testing, development **Characteristics:** - Separate API keys from production - Simulated NGN deposits - Testnet USDC/USDT (no real value) - Same API surface as production - No KYB required **Use when:** Building and testing your integration **For:** Live transactions with real money **Characteristics:** - Unique production API keys - Real NGN bank transfers - Mainnet USDC/USDT (real value) - KYB verification required - Audit logs and compliance monitoring **Use when:** Serving real users and processing actual funds **Never** use production keys in sandbox or vice versa. The API will reject cross-environment requests. ## Security Best Practices - Use environment variables or secret management systems (AWS Secrets Manager, HashiCorp Vault) - Never hardcode keys in source code - Never commit keys to Git repositories ```bash .env DAYA_SANDBOX_KEY=sk_sandbox_abc123... DAYA_PRODUCTION_KEY=sk_live_xyz789... ``` Rotate API keys every 90 days or immediately if compromised: 1. Generate new key in dashboard 2. Update your application configuration 3. Verify new key works 4. Delete old key Implement client-side rate limiting to avoid hitting API limits: - 100 requests per minute per key - 1,000 funding account creations per day (see [Limits](/limits/overview)) ## Error Responses ### 401 Unauthorized Missing or invalid API key: ```json { "error": { "code": "unauthorized", "message": "Invalid or missing API key", "details": "Ensure the X-Api-Key header is present and contains a valid API key" } } ``` **Common causes:** - Missing `X-Api-Key` header - Empty or malformed API key value - Invalid or revoked API key - Using sandbox key with production URL (or vice versa) ### 403 Forbidden Merchant account frozen or suspended: ```json { "error": { "code": "merchant_frozen", "message": "Merchant account is frozen", "details": "Contact support@daya.co for assistance" } } ``` **Why merchants are frozen:** - Exceeded funding account creation limit (1,000/day) - Risk or compliance review triggered - Manual suspension by operations If your merchant account is frozen, new funding accounts, FX conversions, transfers, and withdrawals are blocked. Contact support for resolution. ## Webhook Authentication Webhooks use **HMAC-SHA256** signatures, not API keys. See [Webhook Verification](/api-reference/webhooks/verification). ## Testing Authentication Verify your API key works: ```bash cURL curl --request GET \ --url https://api.sandbox.daya.co/v1/rates?from=NGN \ --header 'X-Api-Key: YOUR_SANDBOX_KEY' ``` ```javascript JavaScript // Test authentication async function testAuth() { const response = await fetch( 'https://api.sandbox.daya.co/v1/rates?from=NGN', { headers: { 'X-Api-Key': 'YOUR_SANDBOX_KEY' } } ); if (response.ok) { console.log('Authentication successful'); } else { console.error('Authentication failed:', response.status); } } ``` Expected response (if successful): ```json { "rate_id": "rate_abc123", "from": "NGN", "to": "USDC", "rate": 1545.50, ... } ``` ## Next Steps Understand funding accounts, deposits, transfers, and rates Create your first funding account #### Building with AI Path: /building-with-ai Description: Use AI coding assistants like Claude Code, Cursor, Codex, and GitHub Copilot to explore, build, and debug with the Daya API. AI coding assistants can help you move faster with the Daya API when you give them the right docs, workflow context, and safety boundaries. Use this page to give tools like Claude Code, Cursor, Codex, GitHub Copilot, and ChatGPT enough context to explain Daya concepts, generate integration code, write tests, and debug API or webhook issues. Do not paste production API keys, secrets, private credentials, customer data, wallet private keys, or sensitive financial information into AI assistants. Use sandbox credentials and test data while developing. ## Start with these resources Share the most relevant Daya docs with your AI assistant before asking it to generate or edit code. Create your first onramp or offramp and track the first deposit. Learn how to authenticate requests with API keys and idempotency keys. Understand funding accounts, deposits, transfers, rates, and settlement. Test Daya flows safely before moving money in production. Receive real-time event notifications from Daya. Reference the primary endpoint for creating onramp and offramp flows. If you are building with Daya Pro, also share the [Daya Pro overview](/pro/overview), [Daya Pro authentication guide](/pro/authentication), and the relevant Daya Pro endpoint docs. ## Machine-readable docs Use these files when an AI assistant needs a compact docs index, full docs context, or Daya-specific scenario guidance. Compact AI-readable index of Daya docs and endpoint pages. Expanded context generated from the docs navigation. Exhaustive Business API, Pro API, and combined-product scenarios for agents. ## Recommended workflow Be specific about whether you are creating customers, funding accounts, onramps, offramps, transfers, USD virtual accounts, webhooks, or Daya Pro trading flows. Give your assistant links to the quickstart, authentication guide, and the specific endpoint or concept pages for your flow. Have it identify your language, framework, existing API client patterns, environment variable conventions, and test setup before it writes code. Use sandbox API keys, sandbox base URLs, test data, and simulated flows until your integration is ready for production. Ask for request validation, idempotency handling, webhook signature verification, retries, logging, and error handling. ## Use with AI coding assistants Open your project in Codex, share the relevant Daya docs links, then ask Codex to inspect your codebase before making changes. A good starting prompt: ```text I am integrating the Daya API into this project. Read the Daya quickstart, authentication guide, core concepts, and relevant endpoint docs. First inspect the codebase and explain the best integration point. Then propose the smallest implementation plan before editing files. ``` Add the relevant Daya docs and endpoint pages to Cursor context, then ask Cursor to follow your existing API client and test patterns. A good starting prompt: ```text Use the Daya API docs in context to implement this workflow. Follow the existing code style, keep secrets in environment variables, add tests, and use the sandbox base URL while developing. ``` Start Claude Code from your project root, provide the Daya docs links, and ask it to reason through the API flow before making file changes. A good starting prompt: ```text I want to build a Daya integration in this repository. Read the Daya docs I provide, inspect the current architecture, and tell me where the API client, webhook handler, and tests should live before you implement anything. ``` Use Copilot Chat with the relevant files and Daya docs open. Ask it to generate narrow changes, then review the code for secret handling and error handling. A good starting prompt: ```text Using the Daya API docs, help me add this endpoint call to the selected file. Use environment variables for the API key, include an idempotency key for write requests, and return a typed error for failed responses. ``` ## Example prompts ### Explore the API ```text I am building with the Daya API. Read the Daya quickstart, authentication guide, core concepts, and relevant endpoint docs. Explain the main Daya resources, the authentication model, the sandbox environment, and the recommended first integration path. ``` ### Generate an integration ```text Using the Daya API docs, generate a [language/framework] implementation for [specific workflow]. Include authentication, idempotency for write requests, request validation, typed responses, error handling, and a simple test. ``` ### Create a funding account ```text Help me implement a Daya funding account flow for [onramp/offramp]. Use the sandbox base URL, keep the API key in an environment variable, include an X-Idempotency-Key header, and show how to store the returned funding_account id. ``` ### Implement webhooks ```text Help me implement Daya webhook handling in [framework]. Include signature verification, idempotent event processing, structured logging, retry-safe behavior, and tests for valid and invalid signatures. ``` ### Debug an API error ```text I am calling the Daya API and getting this error: [paste sanitized error response]. Based on the Daya docs, explain the likely cause, the smallest fix, and what I should log or test to confirm it. ``` ### Review an integration ```text Review this Daya integration for correctness, security issues, missing idempotency, missing webhook verification, weak error handling, sandbox/production mixups, and production readiness. ``` ## What to include in your prompt The more precise your prompt, the better your assistant's output will be. | Include | Example | | --- | --- | | Workflow | "Create an onramp funding account and reconcile deposits" | | Runtime | "Node.js 20 with Express" | | Environment | "Sandbox only" | | Daya docs | "Quickstart, authentication, webhooks, and endpoint docs" | | Existing code context | "Use the existing API client in `src/lib/api.ts`" | | Safety requirements | "No production keys, no customer data, add tests" | ## Common Daya tasks for AI assistants Ask your assistant to help with focused tasks instead of broad, open-ended requests. Ask the assistant to create a small client that reads `DAYA_API_KEY` and `DAYA_BASE_URL` from environment variables, sends the `X-Api-Key` header, and returns structured errors. Ask the assistant to add an `X-Idempotency-Key` header for create or transfer requests and explain when the same key should be reused. Ask the assistant to use `https://api.sandbox.daya.co`, fake customer data, and sandbox-only keys while implementing and testing the first flow. Ask the assistant to implement Daya webhook signature verification before any event processing logic runs. Ask the assistant to check for missing logs, retries, idempotency, environment separation, test coverage, and secret handling. ## Security checklist Before you paste context into an AI assistant or accept generated code, check that: - Production API keys and webhook secrets are not included in prompts - Customer personal information and sensitive financial data are removed - API keys are loaded from environment variables or a secret manager - Sandbox and production base URLs are clearly separated - Write requests use idempotency keys where required - Webhook handlers verify signatures before trusting event payloads - Logs do not include full secrets, private credentials, or sensitive customer data ## Next steps Start with the Daya quickstart. Learn how API keys and idempotency keys work. Validate your integration before production. Receive and verify Daya events. #### AI Scenario FAQ Path: /llms-faq Description: Exhaustive integration scenarios, common mistakes, troubleshooting, and Business/Pro distinctions for AI agents building with Daya. # Daya AI Scenario FAQ > Scenario-oriented context for AI agents building with the Daya docs. Use this with `/llms.txt`, `/llms-full.txt`, and the linked endpoint docs. This file is intentionally direct and machine-friendly. Global rules for agents: - Treat Daya Business API and Daya Pro API as separate surfaces. Pro has separate API keys, scopes, environments, and product purpose. - Use sandbox for Business API development: `https://api.sandbox.daya.co`. - Use production for Business API only after KYB, production approval, production keys, production webhook setup, and go-live checks. - Never ask a developer to paste production API keys, webhook secrets, private credentials, customer PII, or sensitive financial data into an AI assistant. - For create or money-movement requests, include the idempotency header shown in the endpoint examples. - For webhooks, verify signatures before trusting payloads and process events idempotently. - If a timeout or pending state happens, do not automatically create a replacement transfer, payout, funding account, or settlement. Reconcile state first. - If chain/token support matters, call `/v1/supported-chains` at runtime instead of relying on a static list. ## Scenario: First Business API integration Use when: A partner wants the safest first path for integrating the Daya Business API. Relevant docs: - `/` - `/quickstart` - `/authentication` - `/partner-integration` - `/limits/sandbox-testing` - `/api-reference/webhooks/overview` - `/api-reference/webhooks/verification` Recommended flow: 1. Get sandbox API keys from the dashboard or Daya team. 2. Confirm connectivity with the sandbox base URL. 3. Create or choose a test customer. 4. Pick one receive flow: NGN onramp, crypto offramp, USD virtual account, or bank transfer. 5. Implement the API client with environment variables for base URL and API key. 6. Add idempotency keys to write requests. 7. Implement webhook signature verification and idempotent event processing. 8. Test in sandbox with simulated deposits and fake customer data. 9. Add reconciliation jobs before production. Common mistakes: - Starting in production. - Why: Live rails can move or hold real funds before the integration has proven webhook handling, reconciliation, and retry behavior. - Hardcoding API keys. - Why: Keys in source code are easy to leak through commits, logs, screenshots, or shared AI context. - Building a flow without webhook handling. - Why: Many Daya resources reach final state asynchronously, so polling alone can miss or delay important state changes. - Treating Daya as the source of per-user balances instead of maintaining a partner ledger. - Why: Daya exposes product and merchant resources, while the partner is responsible for mapping those resources to end-user balances. - Assuming Business API rates and Pro orderbook prices are the same surface. - Why: Business API rates and Pro market prices come from different products, liquidity contexts, and execution flows. ## Scenario: Create an NGN onramp and settle to internal balance Use when: A customer pays NGN into a Daya-provided bank account and the merchant wants the funds to settle into Daya balance. Relevant docs: - `/concepts/funding-accounts` - `/concepts/onramps` - `/concepts/deposits` - `/api-reference/funding-accounts/create-funding-account` - `/api-reference/deposits/list-deposits` - `/api-reference/webhooks/events` Recommended flow: 1. Create or select a customer. 2. Create a funding account with `rail: NGN_VIRTUAL_ACCOUNT`, `currency: NGN`, and `settlement_destination.type: INTERNAL_BALANCE`. 3. Use `TEMPORARY` for one-time payment details or `PERMANENT` for reusable payment details. 4. Show the returned bank account details from `instructions`. 5. Store the public `funding_account_id`. 6. Listen for `funding_account.*` provisioning events. 7. Reconcile incoming funds with `deposit.*` events and `/v1/deposits`. 8. Credit the customer in the partner ledger using the deposit state and amounts. Common mistakes: - Showing a recalculated or rounded amount for a temporary onramp. - Why: Temporary flows may require the exact amount returned by the API for matching and settlement logic. - Ignoring amount mismatch behavior. - Why: Overpayments, underpayments, or edited amounts can change the deposit path and require refund, review, or support handling. - Crediting a user before deposit finalization rules are satisfied. - Why: Early crediting can leave the partner ledger ahead of actual settled funds. - Losing the relationship between deposit IDs and funding account IDs. - Why: Support, reconciliation, and user-credit decisions depend on linking each deposit back to its receive resource. ## Scenario: Create an NGN onramp and settle on-chain Use when: A customer pays NGN and the merchant wants stablecoin settlement to an on-chain address. Relevant docs: - `/concepts/onramps` - `/concepts/rates-and-settlement` - `/concepts/supported-chains` - `/api-reference/rates/get-rates` - `/api-reference/supported-chains/list-supported-chains` - `/api-reference/funding-accounts/create-funding-account` Recommended flow: 1. Check supported chains and tokens with `/v1/supported-chains`. 2. Fetch a rate with the correct side for the flow. 3. Create an NGN virtual account funding account with `settlement_destination.type: ONCHAIN`. 4. Include the destination address, asset, chain, and rate fields required by the endpoint docs. 5. Display exact payment details from the API response. 6. Track settlement using `deposit.*` webhooks. Common mistakes: - Hardcoding supported chains. - Why: Chain, token, and direction support can change, so production decisions should come from the live supported-chains endpoint. - Comparing Business API rates to Pro orderbook rates. - Why: The two prices are not guaranteed to use the same amount, side, fees, liquidity, or execution assumptions. - Using an expired rate ID. - Why: Expired rates may no longer be executable, which can cause request failure or mismatched settlement expectations. - Allowing the user to edit the exact amount for a temporary flow. - Why: Edited amounts can break payment matching and trigger amount-mismatch handling. ## Scenario: Create a crypto offramp to NGN payout Use when: A customer sends USDC or USDT to a Daya crypto address and wants NGN sent to a Nigerian bank account. Relevant docs: - `/concepts/offramps` - `/concepts/funding-accounts` - `/api-reference/banks/list-banks` - `/api-reference/banks/resolve-bank-account` - `/api-reference/funding-accounts/create-funding-account` - `/api-reference/deposits/list-deposits` Recommended flow: 1. Check supported chains and tokens before displaying deposit options. 2. List banks and resolve the destination bank account. 3. Create a funding account with `rail: CRYPTO_ADDRESS`, `asset`, `chain`, and `settlement_destination.type: NGN_PAYOUT`. 4. Use `TEMPORARY` for a one-time crypto address or `PERMANENT` for a reusable address. 5. Show the returned address from `instructions`. 6. Track incoming crypto and NGN payout settlement with deposit API state and `deposit.*` webhooks. Common mistakes: - Skipping bank account resolution. - Why: Unresolved bank details can send payouts to invalid or incorrect destinations. - Treating a delayed NGN payout as failed and creating a duplicate payout. - Why: Delayed settlement can still complete, so a replacement payout may double-pay the recipient. - Presenting a below-minimum crypto deposit as an ordinary failure. - Why: Below-minimum deposits may need a different user message and support path than failed or rejected deposits. - Assuming all supported chains are enabled for both deposit and withdrawal directions. - Why: A token can be supported differently by chain, rail, product, or direction. ## Scenario: Reusable crypto address to Daya balance Use when: A merchant wants each customer to have a reusable stablecoin address and settle deposits into the merchant's Daya balance. Relevant docs: - `/concepts/funding-accounts` - `/concepts/offramps` - `/concepts/deposits` - `/api-reference/funding-accounts/create-funding-account` - `/api-reference/funding-accounts/update-settlement-destination` Recommended flow: 1. Create or choose a customer. 2. Create a `PERMANENT` funding account with `rail: CRYPTO_ADDRESS`, `asset`, `chain`, and `settlement_destination.type: INTERNAL_BALANCE`. 3. Store the funding account ID and returned address. 4. Use `deposit.*` events to credit the customer on the partner ledger. 5. Rotate settlement destination with the settlement-destination endpoint only when the docs say the funding account type supports it. Common mistakes: - Assuming Daya maintains a customer balance. - Why: The partner ledger is where customer-level crediting, debiting, and balance display should be controlled. - Crediting the user from wallet-address observation alone instead of deposit records. - Why: Wallet observation can miss final processing state, minimum thresholds, attribution, or settlement outcome. - Reusing one customer funding account for multiple unrelated end users. - Why: Shared receive details make attribution, compliance review, support, and reconciliation harder. ## Scenario: Permanent NGN virtual account Use when: A customer needs reusable Nigerian bank account details for recurring NGN deposits. Relevant docs: - `/concepts/funding-accounts` - `/concepts/onramps` - `/api-reference/customers/create-customer` - `/api-reference/customers/submit-tier1-verification` - `/api-reference/funding-accounts/create-funding-account` Recommended flow: 1. Create the customer. 2. Complete required Tier 1 verification. 3. Create a `PERMANENT` funding account with `rail: NGN_VIRTUAL_ACCOUNT`. 4. Store the returned bank details and funding account ID. 5. Reconcile each deposit independently with `deposit.*` webhooks and `/v1/deposits`. Common mistakes: - Creating a permanent NGN virtual account for an unverified customer. - Why: Permanent account provisioning can depend on completed customer verification requirements. - Treating the virtual account as a bank balance. - Why: A virtual account is a receiving rail, not a user-controlled stored-value account. - Failing to reconcile repeat deposits separately. - Why: Reusable account details can receive many deposits, and each deposit needs its own ledger and support trail. ## Scenario: USD virtual account Use when: A merchant wants to provision a USD receiving account for a verified customer. Relevant docs: - `/concepts/virtual-accounts` - `/api-reference/virtual-accounts/create-virtual-account` - `/api-reference/virtual-account-deposits/list-usd-account-deposits` - `/api-reference/webhooks/events` Recommended flow: 1. Create a customer. 2. Complete required verification before provisioning the account. 3. Create the USD virtual account. 4. Give the account details to the customer or sender. 5. Track inbound USD deposits through virtual account deposit APIs and `deposit.*` webhooks. 6. Credit the customer in the partner ledger; Daya does not maintain per-user balances. Common mistakes: - Confusing USD virtual accounts with ACH or wire sending rails. - Why: These docs describe USD receiving accounts, not a general-purpose outbound payment product. - Assuming the customer can spend directly from the virtual account. - Why: The virtual account receives funds; customer spendability still depends on the partner's ledger and approved product flow. - Treating verification rejections and provider outages as the same class of error. - Why: Verification failures usually require customer or compliance action, while provider outages usually require retry, status, or support handling. ## Scenario: Merchant ledger and reconciliation Use when: A partner needs to map Daya events to its own user balances and financial records. Relevant docs: - `/partner-integration` - `/concepts/deposits` - `/concepts/transfers` - `/api-reference/deposits/list-deposits` - `/api-reference/transfers/list-transfers` - `/api-reference/webhooks/events` Recommended flow: 1. Maintain a partner-side ledger for each end user. 2. Store Daya resource IDs: customer ID, funding account ID, deposit ID, transfer ID, webhook event ID. 3. Credit or debit users based on final or product-approved states, not just request creation. 4. Reconcile webhook state with polling APIs. 5. Handle duplicate webhook deliveries idempotently. 6. Keep collection balance and withdrawal balance separate in ledger logic. Common mistakes: - Assuming Daya maintains per-user balances. - Why: Daya resource states must be translated into the partner's own customer ledger. - Crediting a customer without recording the Daya deposit ID. - Why: Missing IDs make duplicate detection, reconciliation, and support escalation much weaker. - Treating webhook delivery as guaranteed exactly-once. - Why: Webhook systems can deliver duplicates or arrive out of order, so processing must be idempotent. - Funding transfers from collection balance when withdrawal balance is required. - Why: Collection and withdrawal balances represent different accounting surfaces and should not be substituted for each other. ## Scenario: Webhook implementation Use when: A developer needs to receive Daya state changes reliably. Relevant docs: - `/api-reference/webhooks/overview` - `/api-reference/webhooks/events` - `/api-reference/webhooks/verification` - `/partner-integration` Recommended flow: 1. Configure a stable HTTPS webhook URL in the dashboard. 2. Verify the webhook signature using the raw request body. 3. Return HTTP 200 quickly after validation. 4. Process events asynchronously. 5. Store event IDs and resource IDs for idempotency and support. 6. Handle unknown event types gracefully. 7. Reconcile API state if final webhook delivery is delayed. Common mistakes: - Parsing JSON before signature verification when raw body is required. - Why: Any body transformation can make signature verification fail or create a gap before authenticity is checked. - Doing slow business logic before returning 200. - Why: Slow responses can cause retries and duplicate deliveries, even when the event was received. - Failing on duplicate delivery. - Why: Duplicate webhooks are normal enough that handlers must safely ignore already-processed event IDs. - Treating webhook order as the only source of truth. - Why: Events can be delayed or arrive out of order, so final resource state should be reconfirmed when needed. ## Scenario: Sandbox testing Use when: A developer wants to test without moving real money. Relevant docs: - `/limits/sandbox-testing` - `/quickstart` - `/authentication` - `/api-reference/sandbox/create-sandbox-deposit` Recommended flow: 1. Use sandbox base URL and sandbox API keys. 2. Use fake customer and bank data. 3. Create funding accounts and trigger simulated deposits where supported. 4. Confirm webhook delivery and reconciliation behavior. 5. Test happy path, failed provisioning, amount mismatch, delayed state, duplicate webhook, and retry behavior. Common mistakes: - Mixing sandbox keys with production base URL or production keys with sandbox base URL. - Why: Cross-environment credentials will fail and can hide whether the integration is testing the right environment. - Skipping webhook tests because polling appears to work. - Why: Polling may pass happy-path tests while missing duplicate, delayed, or final-state event behavior. - Using real customer data in sandbox prompts or logs. - Why: Sandbox testing should not expose sensitive user data to development logs, shared tools, or AI assistants. ## Scenario: Transfers to bank recipients Use when: A merchant wants to send NGN or USD from Daya withdrawal balance to a bank recipient. Relevant docs: - `/concepts/transfers` - `/api-reference/transfers/create-transfer` - `/api-reference/transfers/get-transfer` - `/api-reference/banks/resolve-bank-account` - `/api-reference/webhooks/events` Recommended flow: 1. Confirm sufficient withdrawal balance. 2. Resolve bank details where required. 3. Create the transfer with the documented idempotency header. 4. Store the transfer ID. 5. Track transfer lifecycle through transfer APIs and `transfer.*` webhooks. 6. If the transfer times out or remains pending, reconcile before retrying. Common mistakes: - Funding a transfer from collection balance. - Why: Transfers should use the balance type required by the endpoint and product flow, not any available merchant balance. - Creating a duplicate transfer after timeout. - Why: A timed-out request or pending transfer may still complete, so retrying blindly can send money twice. - Ignoring verification requirements for USD or SWIFT destinations. - Why: Some destinations require additional checks before funds can be sent reliably or compliantly. ## Scenario: Developer fees Use when: A partner wants to collect a percentage fee on received deposits. Relevant docs: - `/concepts/funding-accounts` - `/partner-integration` - `/api-reference/funding-accounts/create-funding-account` - `/api-reference/virtual-accounts/create-virtual-account` Recommended flow: 1. Add `developer_fee.percentage` when creating the supported receive resource. 2. Use a decimal string from `0` to `50`. 3. Reconcile fee fields from deposit responses and deposit webhooks. 4. Credit the user with `customer_amount` when that field is provided. 5. Explain that the developer fee is deducted before the final customer amount is calculated. For onramps and offramps, it is not added as a separate charge; use `developer_fee.amount` and `developer_fee.currency` for the exact fee. Common mistakes: - Treating developer fees as a custom flat fee. - Why: The documented field is percentage-based, so flat-fee assumptions can create incorrect customer and partner accounting. - Expecting the developer fee to increase the NGN amount the user pays. - Why: The developer fee is deducted from the final customer amount and appears in `developer_fee`; Daya platform fees may still be charged separately. - Forgetting to reconcile partner-side fees separately. - Why: Fees affect gross, net, and customer amounts differently and should be visible in the partner ledger. - Crediting gross deposit amount instead of the customer amount after fees. - Why: Crediting gross amount can overstate the customer's balance after partner fees are deducted. ## Scenario: Business API rates versus Daya Pro market prices Use when: A developer asks why an API rate differs from a Pro app orderbook or market price. Relevant docs: - `/partner-integration` - `/concepts/rates-and-settlement` - `/api-reference/rates/get-rates` - `/pro/overview` - `/pro/api-reference/get-orderbook` - `/pro/api-reference/get-last-price` Recommended answer: The Business API rate and the Pro app orderbook are different pricing surfaces. The Pro orderbook reflects bids and offers from users in the market at that moment. The Business API rate is what Daya can offer and execute within the rate expiry window. They may differ. Compare the same asset, side, amount, direction, timestamp, and gross/net fee basis before treating the difference as a bug. Common mistakes: - Comparing a Business API `BUY` rate to a Pro sell-side market price. - Why: Side, product, and execution context must match before a price comparison is meaningful. - Ignoring fees and destination amount. - Why: Gross and net amounts can differ, so price checks should compare the actual amount the customer receives or pays. - Assuming Pro liquidity and Business API settlement pricing are interchangeable. - Why: Pro exposes market trading, while the Business API offers settlement rates for merchant money movement flows. ## Scenario: Daya Pro trading integration Use when: A developer is building trading bots, market data tools, portfolio dashboards, or order management on Daya Pro. Relevant docs: - `/pro/overview` - `/pro/authentication` - `/pro/quickstart` - `/pro/api-reference/list-markets` - `/pro/api-reference/get-orderbook` - `/pro/api-reference/place-order` - `/pro/api-reference/list-orders` - `/pro/webhooks/overview` Recommended flow: 1. Treat Pro as a separate platform from Business API / Onramp. 2. Request Pro access and required API scopes from Daya support. 3. Use public endpoints for markets and orderbook where no authentication is required. 4. Use authenticated endpoints for account, balances, orders, trades, withdrawals, and webhook management. 5. Respect scopes: Read, Trade, Write. 6. Use Pro webhook docs for Pro events; do not assume Business API webhook event shapes. Common mistakes: - Using Business API sandbox keys with Pro. - Why: Pro has its own access model, scopes, and API surface, so Business credentials should not be expected to authorize Pro requests. - Assuming Pro has sandbox parity. - Why: Pro availability and test behavior may differ from Business API sandbox flows. - Using Business API funding-account flows as Pro deposit flows. - Why: Funding accounts are Business API receive resources, not a substitute for Pro account funding docs or approved Pro operations. - Treating Business API rates as Pro execution prices. - Why: Pro execution depends on market/order behavior, while Business API rates are for Business settlement flows. ## Scenario: Product that uses both Business API and Pro API Use when: A product discussion mentions plumbing Business API flows and Pro API flows together. Relevant docs: - `/partner-integration` - `/concepts/overview` - `/pro/overview` - `/pro/authentication` - `/pro/api-reference/get-balances` - `/api-reference/merchant-balance/get-merchant-balance` Recommended answer: Treat the Business API and Pro API as separate products and accounting surfaces unless Daya has explicitly approved a combined product flow. Business API is for merchant money movement, receive flows, conversion, transfers, virtual accounts, and merchant balance operations. Pro API is for currency trading, market data, orders, trades, balances, and Pro withdrawals. Do not imply that a Business API collection balance automatically becomes Pro trading balance or that Pro balances can be managed through Business API endpoints. If a user wants a combined product, ask for the approved operational flow and reconcile each surface separately. Common mistakes: - Moving user ledger balances between products without a defined Daya operation. - Why: Cross-product balance movement needs an approved operational flow so accounting, compliance, and support records line up. - Comparing Business merchant balance to Pro account balance. - Why: They represent separate products and should not be treated as the same pool of funds. - Reusing API keys across products. - Why: Business and Pro credentials can have different scopes, permissions, and environments. - Reusing webhook handlers without checking event schemas. - Why: Similar event names can carry different payload shapes or lifecycle meanings across products. ## Scenario: Production go-live Use when: A partner is ready to move from sandbox to production. Relevant docs: - `/partner-integration` - `/authentication` - `/api-reference/webhooks/verification` - `/limits/overview` - `/limits/amount-mismatches` - `/limits/sandbox-testing` Recommended flow: 1. Complete KYB and receive production approval. 2. Generate production API keys and store them server-side. 3. Configure production webhook URL and verify signatures. 4. Confirm supported chains and rails at runtime. 5. Test idempotency and duplicate webhook handling. 6. Test amount mismatch, minimum amounts, delayed payout, transfer timeout, and provider incident paths. 7. Confirm support escalation includes environment, endpoint, request ID, resource IDs, timestamps, and sanitized request bodies. Common mistakes: - Launching with sandbox callback URLs. - Why: Production events must reach production infrastructure, or live state changes will not be processed correctly. - Logging full API keys or webhook secrets. - Why: Logs often reach shared observability tools and support workflows where secrets should never appear. - Not separating collection balance and withdrawal balance. - Why: Mixing balances can cause incorrect transfer funding, customer credits, or operational reports. - Not having reconciliation jobs before live money movement. - Why: Reconciliation is the fallback when webhooks are delayed, duplicated, missed, or require support investigation. #### Partner Integration Path: /partner-integration Description: Build reliable Daya API integrations with funding accounts, deposits, transfers, webhooks, balances, and go-live checks ## What Is the Daya API? The Daya API lets businesses move money between Nigerian Naira (NGN), USD bank rails, and stablecoins such as USDC and USDT through one programmable interface. Instead of building exchange logic, wallet infrastructure, KYC flows, banking rails, and payout operations from scratch, partners can integrate Daya and focus on their product experience. Common products partners can build include: - A fintech app that lets Nigerian users buy stablecoins without leaving the product. - A savings, payroll, or marketplace app that lets users cash out stablecoins into Nigerian bank accounts. - A business banking or creator product that gives customers USD virtual accounts and outbound USD transfer options. | Capability | What it does | Primary docs | | --- | --- | --- | | Funding accounts | Create NGN virtual accounts and crypto deposit addresses for receiving funds. | [Funding Accounts](/concepts/funding-accounts) | | Onramps | Collect NGN and settle to stablecoins or Daya balance. | [Onramps](/concepts/onramps) | | Offramps | Collect USDC/USDT and settle to NGN payout or Daya balance. | [Offramps](/concepts/offramps) | | Deposits | Reconcile inbound NGN, crypto, and virtual account deposits. | [Deposits](/concepts/deposits) | | Transfers | Send NGN to Nigerian banks or USD through supported rails. | [Transfers](/concepts/transfers) | | USD virtual accounts | Provision USD receiving accounts for verified customers. | [Virtual Accounts](/concepts/virtual-accounts) | | Webhooks | Receive transaction and verification events in real time. | [Webhooks](/api-reference/webhooks/overview) | | Supported chains | Check live chain and token availability by direction. | [Supported Chains](/api-reference/supported-chains/list-supported-chains) | ## Sandbox vs Production All new partners should start in sandbox. Sandbox testing protects the partner, Daya, and end users from real-money mistakes while implementation details are still being validated. | Area | Sandbox | Production | | --- | --- | --- | | Base URL | `https://api.sandbox.daya.co` | `https://api.daya.co` | | Real money | No. Deposits and money movement are simulated. | Yes. Real NGN, USD, and stablecoins move. | | API keys | Separate sandbox keys. | Issued after KYB approval. | | Access | Request invite from the Daya team. | Complete KYB and receive dashboard access. | | Use | Build, test, and validate flows. | Serve real users after launch checks. | If you receive a dashboard invite but cannot log in, use the exact email address the invite was sent to. Using another email can return a `401 Unauthorized` or `USER_NOT_FOUND` style error. If production calls return `APP_PAUSED` or a `503` app-paused response, Daya needs to activate the app on its side. This is not usually a partner code issue. Contact support or your partner channel. ## Production Access and KYB Production access requires KYB verification. Treat KYB as part of the launch path, not as optional admin work. KYB unlocks production API keys, real money movement, USD virtual accounts, and USD transfer rails where available. 1. Request production access through `support@daya.co` or the dedicated partner channel. 2. Submit the KYB form and business documents, including company registration, director details, and proof of business. 3. Wait for Daya review and approval. 4. Accept the production dashboard invite using the exact invited email address. 5. Generate production API keys and configure webhooks. If the dashboard invite link does not work, ask Daya to resend the invite or verify the email address. ## Dashboard Setup The dashboard is where partners manage API keys, webhooks, team access, balances, and transaction monitoring. - Generate and rotate API keys separately for sandbox and production. - Configure webhook endpoints before launch. - Add team members with appropriate access. - Monitor merchant balances, deposits, transfers, and payout status. ### Webhook Setup 1. Add the webhook endpoint URL in the Daya dashboard. 2. Configure your server to accept Daya webhook requests, including IP allowlisting where required. 3. Verify webhook signatures using the [webhook verification docs](/api-reference/webhooks/verification). 4. Log event IDs and resource IDs so support can investigate issues quickly. For staging and production, use a publicly accessible HTTPS endpoint that remains stable over time. Temporary tunneling services such as ngrok, Cloudflare Tunnel, or localtunnel should only be used during active local development and testing. Verify the webhook signature and return HTTP `200` as quickly as possible. Process the event asynchronously after the response has been sent. Delayed responses may cause delivery retries and duplicate event processing if handlers are not idempotent. ## Balance Model Balance handling is one of the most important concepts to understand before building on Daya. Many issues come from assuming Daya maintains individual user balances or that all funds are immediately withdrawable. | Balance type | What goes in | What comes out | | --- | --- | --- | | Collection balance | Funding account deposits and virtual account deposits. | Funds moved to withdrawal balance through merchant balance transfer. | | Withdrawal balance | Merchant funding deposits and funds moved from collection balance. | Transfers, bank payouts, and supported withdrawals. | Transfers and payouts are funded from withdrawal balance, not collection balance. Ensure sufficient withdrawal balance is available before initiating outbound money movement. Daya does not maintain balances for each end user. Partners must maintain their own ledger using webhooks, deposit records, transfer records, and reconciliation jobs. A typical ledger flow: 1. A user deposits funds or receives money through a Daya-powered flow. 2. The funds settle into the merchant's Daya balance. 3. Daya sends webhook events and exposes transaction state through the API. 4. The partner credits or debits the user on the partner's own ledger. 5. The partner initiates payout, transfer, or withdrawal from merchant balance when needed. ## Rates and Rate IDs Temporary funding account flows that use a quoted conversion require a rate before creation. | Flow | Direction | Why it matters | | --- | --- | --- | | Onramp, or NGN funding account | `BUY` | User sends NGN and receives stablecoin settlement. | | Offramp, or crypto funding account | `SELL` | User sends stablecoin and receives NGN payout. | Use [`GET /v1/rates`](/api-reference/rates/get-rates) to retrieve rates and pass the returned `rate_id` when creating a temporary funding account that performs a quoted conversion. This includes temporary NGN funding accounts settling to `INTERNAL_BALANCE` or `ONCHAIN`, and temporary crypto funding accounts settling through `NGN_PAYOUT`. Respect the rate expiry window. If a rate expires before creation or before payment arrives, the deposit may be flagged for review instead of settling automatically. The Business API rate and the Pro app order book rate are different pricing surfaces. The Pro app order book reflects bids and offers from users in the market at that moment. The Business API rate is what Daya can offer and execute within the rate expiry window. They may differ. When comparing rates, compare the same asset, amount, direction, timestamp, and whether you are looking at a gross FX rate or final net destination amount after fees. ## Onramps: NGN to USDC/USDT An onramp lets a customer pay NGN into a Daya bank account. Daya records the incoming money as a deposit, then settles the value to your Daya balance or to an onchain stablecoin address. New integrations create onramps with [`POST /v1/funding-accounts`](/api-reference/funding-accounts/create-funding-account) using: | Field | Value | | --- | --- | | `rail` | `NGN_VIRTUAL_ACCOUNT` | | `currency` | `NGN` | | `type` | `TEMPORARY` or `PERMANENT` | | Type | Use case | Expiry | Rate | Verification | Settlement | | --- | --- | --- | --- | --- | --- | | `TEMPORARY` | One-off deposit at a locked rate. | Yes. | `rate_id` required for `INTERNAL_BALANCE` and `ONCHAIN` settlement. | Usually not required. | `INTERNAL_BALANCE` or `ONCHAIN`. | | `PERMANENT` | Recurring deposits from the same customer. | No expiry. | Applied at deposit time. | Tier 1 KYC required. | `INTERNAL_BALANCE` or `ONCHAIN`. | ### Onramp Fees | Fee | Amount | When charged | Important note | | --- | --- | --- | --- | | Payment-provider collection charge | Included in the create response `amount` | When a temporary NGN payment account is created. | Added by the payment provider before payment. Display the response `amount`; do not calculate this charge yourself. | | NGN deposit charge | 0.1%, capped at NGN 100 | When Naira lands in the virtual account. | Deducted before conversion. | | Crypto settlement fee | USD 0.10 default; USD 0.20 on Ethereum; USD 1.00 on Tron | When stablecoin is paid out. | Deducted from stablecoin received based on settlement chain. | When presenting onchain settlement amounts to your users, show the crypto settlement fee before they confirm the payment. For example, if the estimated stablecoin amount is `1.07 USDC` and the crypto settlement fee is `0.10 USDC`, the user should see that the final amount delivered onchain is `0.97 USDC`. For temporary onramps, the request `amount` is the principal before any payment-provider collection charge. Always display and transfer the response `amount` exactly. Do not recalculate, round, or let the user edit it. Amount mismatch can trigger an automatic refund or review state. Pass the customer's name when creating an onramp. The name can appear on the virtual account holder details, and Nigerian users often verify the account name before sending a bank transfer. ## Offramps: USDC/USDT to NGN An offramp gives your customer a crypto address for sending USDC or USDT into Daya. Daya records the incoming crypto as a deposit, then settles the value to your Daya balance or to a Nigerian bank account. New integrations create offramps with [`POST /v1/funding-accounts`](/api-reference/funding-accounts/create-funding-account) using: | Field | Value | | --- | --- | | `rail` | `CRYPTO_ADDRESS` | | `asset` | `USDC` or `USDT` | | `chain` | Supported network | | `type` | `TEMPORARY` or `PERMANENT` | | Type | Use case | Address expiry | Settlement modes | | --- | --- | --- | --- | | `TEMPORARY` | One-time crypto deposit. | Yes. | `NGN_PAYOUT` only. | | `PERMANENT` | Recurring deposits from the same customer. | No expiry. | `INTERNAL_BALANCE` or `NGN_PAYOUT`. | ### Offramp Fees | Fee | Amount | Cap | Notes | | --- | --- | --- | --- | | Crypto sent to address | No Daya fee | None | User pays only blockchain gas or network fees. | | NGN bank payout | 0.1% | NGN 100 | Charged when NGN is paid to the bank account. No separate stamp duty or VAT is expected on top of this fee. | ### Bank Account Resolution Before creating an offramp with `NGN_PAYOUT`, resolve the destination account using [`POST /v1/banks/resolve`](/api-reference/banks/resolve-bank-account). This verifies the bank account number and account holder name before funds are committed. ### Minimum Deposit Handling Deposits below the minimum supported threshold can be flagged and may not be eligible for settlement. Do not present a flagged deposit as a failed deposit. Clearly communicate that the deposited amount did not meet the minimum settlement requirement and provide support guidance. ### Payout Timing NGN bank payouts are not guaranteed to be instant. Daya depends on third-party payout providers. Provider incidents can delay completion. After an offramp deposit is received, NGN payout delivery happens in the background as part of deposit settlement. Track the customer-facing state from the deposit `status`, `settlement_status`, and `deposit.*` webhooks. Your product can show states such as `Deposit received`, `Settlement processing`, `Settlement settled`, `Settlement failed`, or `Requires review`. Do not wait for a separate transfer or payout webhook for funding-account offramps, and do not show a generic complete state as final bank receipt until the deposit settlement has completed. If a user confirms bank receipt but the deposit still appears to be processing, treat the API and webhook state as temporarily out of sync. Do not create a duplicate payout. Poll, reconcile bank receipt, and escalate with IDs if the final webhook does not arrive. ## Supported Chains and Tokens Chain and token support is runtime-configured. Partners should not hard-code support from this guide, screenshots, or old conversations. Always call [`GET /v1/supported-chains`](/api-reference/supported-chains/list-supported-chains) before showing a chain/token option to users or before attempting onchain settlement. | Flag | Meaning | | --- | --- | | `deposit_enabled` | Users can deposit that token on that chain into Daya. Commonly relevant to offramps. | | `withdraw_enabled` | Daya can send that token on that chain out to a wallet. Commonly relevant to onramp `ONCHAIN` settlement. | Do not rely on a static chain table for production decisions. The supported-chains endpoint is the authoritative live source. This matters during chain rollouts, where individual tokens and directions can go live at different times. If you receive `VALIDATION_FAILED - unsupported settlement chain/token`, the chain/token is not enabled for the requested direction. Check both the chain and token with `/v1/supported-chains` before retrying. ## USD Virtual Accounts A USD virtual account is a US bank account provisioned for a verified customer so the customer can receive USD payments. The sender does not need to understand crypto. Typical prerequisites: 1. Create a customer. 2. Complete required verification: tier 1 BVN + phone number + selfie and tier 2 KYC for individual people, or business KYB for entities. 3. Wait for verification status to become approved. 4. Create the virtual account. For individual customers, submit [`POST /v1/customers/{id}/tier1-verification`](/api-reference/customers/submit-tier1-verification) first, then submit [`POST /v1/customers/{id}/tier2-verification`](/api-reference/customers/submit-tier2-verification) with the individual KYC fields. For a business or entity customer, use the tier 2 endpoint with `customer_type: "business"` and the business KYB fields. Both flows update the same customer object; after approval, `tier_2_kyc_complete` becomes `true` and the customer can be used with [`POST /v1/virtual-accounts`](/api-reference/virtual-accounts/create-virtual-account). A USD virtual account does not maintain a separate spendable balance for the customer inside Daya. Deposits settle into the merchant collection balance. The partner must credit the customer on its own ledger using deposit webhooks and reconciliation. Verification failures and provider outages should be handled differently. A verification rejection usually requires corrected information from the customer. A provider outage or upstream service error should be retried after a short backoff before asking the customer to resubmit documents. ## Developer Fees Developer fees let you keep a percentage of received deposits while Daya reports the split for reconciliation. Configure a developer fee by sending `developer_fee.percentage` when creating a funding account, legacy onramp, legacy offramp, or USD virtual account. Use a decimal string from `0` to `50`. Omit `developer_fee` to use `0%`. `developer_fee.percentage` is a percentage value, not basis points. For example: | Value | Meaning | | --- | --- | | `0.5` | `0.5%` | | `1` | `1%` | | `2` | `2%` | | `50` | `50%` | This is different from `fee_bps` in rate responses, where `50` basis points means `0.5%`. The percentage applies to deposits received through that account. Deposit responses and deposit webhooks include `developer_fee` and `customer_amount` after Daya calculates the split. Use `customer_amount` when crediting the customer in your own ledger. For onramps and offramps, the developer fee is deducted before the final customer amount is calculated. It is not added as an extra charge on top of the amount the user sends. Use `developer_fee.amount` and `developer_fee.currency` to track the exact fee kept by your merchant account, and use `customer_amount` to know the final amount left for the customer. Flat developer fees, such as `NGN 500` or `$1` per transaction, are not currently supported through `developer_fee`. If you charge users outside Daya, make that fee clear in your own product and reconcile it separately. ## Transfers Transfers are merchant-initiated sends of funds to a bank recipient. They are funded from withdrawal balance, not collection balance. Use [`POST /v1/transfers`](/api-reference/transfers/create-transfer) to create a transfer. ### NGN Transfers NGN transfers send Naira to Nigerian bank accounts. Resolve bank account details before creating the transfer and make sure the relevant withdrawal balance is funded. ### USD Transfers USD transfers send money through supported USD rails. The customer on whose behalf the partner is sending may need additional verification, especially for inline USD bank or SWIFT destinations. ### Idempotency Always include an `Idempotency-Key` header when creating transfers or other money-movement requests that may be retried. Use a unique UUID per logical transfer attempt to avoid duplicates. A timeout or pending state does not necessarily mean a transfer failed. Do not automatically create a replacement transfer. Wait for final status updates, reconciliation results, or webhook notifications before retrying. ## Fee Reference | Transaction type | Fee | Cap | Notes | | --- | --- | --- | --- | | Onramp - NGN deposit | 0.1% | NGN 100 | Charged when Naira lands in the virtual account. | | Onramp - crypto settlement | USD 0.10 default; USD 0.20 on Ethereum; USD 1.00 on Tron | No cap | Deducted from stablecoin payout based on settlement chain. | | Offramp - crypto sent to address | No Daya fee | None | User pays blockchain gas or network fee only. | | Offramp - NGN bank payout | 0.1% | NGN 100 | Covers applicable NGN payout charges; no separate stamp duty or VAT expected. | | USD virtual account deposit | 0.2% | No cap | Charged when a payment into a USD virtual account settles. | | USD transfer - ACH | 0.2% | No cap | Confirm current rail availability and provider behavior before launch. | | USD transfer - Wire | 0.2% | No cap | Confirm current rail availability before launch. | | USD transfer - RTP / FedNow | 0.2% | No cap | Available only where the provider and recipient bank support instant rails. | | USD transfer - SWIFT | 0.2% | No cap | Confirm commercial availability before promising it to users. | | Developer fee on received deposits | Merchant-set percentage | 50% maximum | Configure with `developer_fee.percentage`; reported separately from Daya fees. | If you also charge users outside Daya, make that clear in your product and reconcile it separately from the Daya developer fee fields. ## Going Live Checklist - Complete KYB and receive production approval. - Log in to the production dashboard with the exact invited email. - Generate production API keys and store them securely server-side. - Set up the production webhook endpoint and signature verification. - Configure webhook IP allowlisting if required. - Check `/v1/supported-chains` at runtime and build direction-aware controls. - Test onramp flows with exact amounts and minimum-amount edge cases. - Test offramp flows including bank account resolution, delayed processing states, and webhook finalization. - Test any `developer_fee.percentage` configuration, including the `0` to `50` accepted range and the deposit `customer_amount` your ledger will credit. - Confirm pricing accounts for Daya fees and any partner-side fee. - Add idempotency keys to retryable money-movement requests. - Implement reconciliation jobs for webhooks, API state, and partner ledger state. ## Troubleshooting ### Tier 2 and KYC errors Some Tier 2 errors are request validation failures before Daya sends the customer to the verification provider. Other statuses come from provider review. Treat them differently so customers are not asked to resubmit the wrong information. | Error or status | Meaning | What to do | | --- | --- | --- | | `ResidentialAddress.Subdivision` failed on the `max` tag | The state or subdivision value is too long. | Send a short subdivision/state code, max 10 characters, such as `LA` or `FC`. | | `IdentifyingInformation.ImageFront` failed on `data_uri_max` | The ID front image is too large. | Compress or resize the image before base64 encoding. ID images must be base64 data URLs up to 1 MiB decoded. | | `TIER2_ALREADY_PENDING` | The customer already has a Tier 2 review in progress. | Do not resubmit repeatedly. Wait for the customer status, webhook update, or provider review outcome. | | `TIER2_ALREADY_VERIFIED` | The customer has already passed Tier 2 verification. | No resubmission is needed. Continue with the USD, virtual account, transfer, or banking flow that required Tier 2. | | `INFORMATION_REQUESTED` | The provider needs more information, often tax ID/TIN or extra compliance details. | Collect the requested information securely and resubmit through the API when supported. Do not send tax IDs, documents, or other sensitive KYC data over Slack, WhatsApp, email, or support channels. | | Provider rejection or `information could not be verified` | The provider could not approve the submitted KYC data. | Review the rejection reason if one is available, correct the customer details or documents, then resubmit when the API allows it. | | Symptom or error | Likely cause | What to do | | --- | --- | --- | | `APP_PAUSED` / `503` | Production app not enabled by Daya. | Contact Daya support or partner channel and ask for app activation. | | `401 Unauthorized` / `USER_NOT_FOUND` | Wrong login email or API key context. | Use the exact invited email and correct environment keys. | | Webhook `403` | IP allowlist or signature verification rejection. | Confirm Daya IPs and webhook signature logic. | | `VALIDATION_FAILED - unsupported settlement chain/token` | Chain/token not enabled for requested direction. | Call `/v1/supported-chains` and check `deposit_enabled` or `withdraw_enabled`. | | User was refunded on onramp | The transfer did not match the create response `amount`, or was below the minimum after fees. | Display and transfer the exact response `amount`; do not use the request value or round it. | | Deposit stuck in processing | Provider delay or API/webhook finalization lag. | Do not duplicate payout. Poll, reconcile receipt, and escalate with IDs if delayed. | | API rate differs from Pro app rate | Different pricing surfaces or gross/net comparison. | Confirm side, asset, amount, direction, timestamp, and fee inclusion. | | Failed to provision wallet address | Temporary wallet/address provisioning issue. | Retry only if safe and escalate with request ID if persistent. | | Final webhook not received | Webhook delivery issue or payout state lag. | Poll API status, check webhook logs, and reconcile before manual intervention. | | Webhook returns `400` | Signature validation failure or payload parsing error. | Verify signatures using the raw request body and handle unknown event types gracefully. | | Webhook returns `500` | Internal error in webhook processing. | Review application logs and ensure webhook handlers are fault tolerant. | | Transfer times out or remains pending | Downstream banking infrastructure delay. | Do not create a duplicate transfer. Wait for final status updates and reconciliation. | | Payout fails despite successful user activity | Insufficient withdrawal balance. | Confirm funds are available in withdrawal balance before initiating payouts. | ## FAQ ### Why did the user get refunded? Temporary onramps require the transfer to match `amount` from the create response. The request value can be lower because the payment provider may add a collection charge. Sending the request value instead can trigger an automatic refund or review state. ### Why is the API offramp rate different from the Pro app market or order book rate? The Pro app order book reflects user bids and offers at that moment, while the Business API rate is what Daya can offer and execute within the rate expiry window. They are separate pricing surfaces and may differ. For offramps, confirm the partner is using the `SELL` rate and comparing the same asset, amount, direction, and timestamp. ### Is Aptos supported? Aptos support is available only when the live supported-chains endpoint says the exact token and direction are enabled. Check USDT and USDC separately and check `deposit_enabled` versus `withdraw_enabled`. ### Can partners configure a custom flat fee in Daya? Not currently. Partners should implement any additional fee in their own product flow until Daya exposes native custom-fee support. ### Are virtual accounts and ACH the same thing? No. Virtual accounts receive funds. ACH, Wire, RTP/FedNow, and SWIFT are transfer rails for sending funds. ## Support and Escalation | Channel | Use for | | --- | --- | | Dedicated partner channel | Fastest route for integration questions, launch blockers, production incidents, and unclear API behavior. | | `support@daya.co` | General support, dashboard access, non-urgent issues, and production activation requests. | | [docs.daya.co](https://docs.daya.co) | Concept docs, endpoint references, webhook docs, fee and limits documentation. | | [dashboard.daya.co](https://dashboard.daya.co) | API keys, webhook endpoints, balances, transaction monitoring, and team access. | When escalating, include environment, endpoint, request body with secrets removed, `request_id`, funding account ID, deposit ID, transfer ID, chain/token, bank name where relevant, timestamp, and webhook event IDs or delivery logs. ### Core Concepts #### Core Concepts Path: /concepts/overview Description: Understand the core building blocks of the Daya API ## Overview The **Daya API** is built around a set of core concepts that work together to handle fiat-crypto conversion, cross-border transfers, and merchant balance management. ## Resource Model Public webhooks follow the resource a merchant creates or reconciles, not the internal provider engine Daya uses to execute it. | Merchant-facing concept | Public resource | Rails / implementation detail | Webhook family | |-------------------------|-----------------|-------------------------------|----------------| | Onramp | Funding account | `NGN_VIRTUAL_ACCOUNT`. Created through `/v1/funding-accounts`. | `funding_account.*` for provisioning, `deposit.*` when money lands | | Offramp | Funding account | `CRYPTO_ADDRESS`. Created through `/v1/funding-accounts` with `asset` and `chain`. | `funding_account.*` for provisioning, `deposit.*` when crypto lands | | Inbound NGN or crypto | Deposit | NGN bank transfer or crypto deposit received through a funding account. | `deposit.*` | | Inbound USD to a bank account | USD account deposit | ACH or wire payment received into a USD virtual account. | `deposit.*` | | Internal balance to NGN/USD bank | Transfer | Merchant-created bank send through `/v1/transfers`. | `transfer.*` | | Stablecoin or crypto wallet send | Withdrawal | Merchant withdrawal from withdrawal balance to an on-chain address. | `withdrawal.*` | Use `/v1/transfers` and `transfer.*` webhooks for merchant-initiated bank sends. Use `deposit.*` webhooks to track incoming money and settlement into Daya. How customers receive funds into Daya Inbound NGN and crypto received through funding accounts Collect NGN from customers Collect stablecoins from customers Send NGN or USD to bank recipients (ACH, wire, SWIFT) USD virtual accounts for tier-2-verified customers Dual-balance model with collection and withdrawal balances Firm FX quotes with buy/sell sides and defined validity windows #### Funding Accounts Path: /concepts/funding-accounts Description: How customers receive funds into Daya ## Overview A **funding account** is the API resource that powers onramps and offramps. It gives a customer either an NGN virtual account or a crypto address, and lets you control where the money settles after it arrives. Funding accounts are scoped to one customer and one rail: | Rail | What the customer receives | Required fields | |------|----------------------------|-----------------| | `NGN_VIRTUAL_ACCOUNT` | Bank account details for NGN transfers | `currency: NGN` | | `CRYPTO_ADDRESS` | Stablecoin address on a supported chain | `asset`, `chain` | ## What You Can Build Use funding accounts to create the receive flow you need: | Flow | Funding account shape | Settlement | |------|-----------------------|------------| | Temporary onramp | `NGN_VIRTUAL_ACCOUNT` + `TEMPORARY` | `INTERNAL_BALANCE` or `ONCHAIN` | | Permanent onramp | `NGN_VIRTUAL_ACCOUNT` + `PERMANENT` | `INTERNAL_BALANCE` or `ONCHAIN` | | Temporary offramp | `CRYPTO_ADDRESS` + `TEMPORARY` | `NGN_PAYOUT` | | Permanent offramp | `CRYPTO_ADDRESS` + `PERMANENT` | `INTERNAL_BALANCE` or `NGN_PAYOUT` | For example, if you want to give a customer a reusable wallet address to collect USDC and keep the funds in your Daya balance, create a `PERMANENT` crypto funding account with `settlement_destination.type: INTERNAL_BALANCE`. ```json Reusable crypto address to Daya balance { "type": "PERMANENT", "rail": "CRYPTO_ADDRESS", "customer": { "customer_id": "650e8400-e29b-41d4-a716-446655440000" }, "asset": "USDC", "chain": "BASE", "settlement_destination": { "type": "INTERNAL_BALANCE" } } ``` The account or address the customer should pay into is returned in `instructions`. For NGN, `instructions` contains the bank account details: ```json { "currency": "NGN", "instructions": [ { "type": "NGN_VIRTUAL_ACCOUNT", "provider": "FLUTTERWAVE", "provider_availability": { "status": "OPERATIONAL" }, "status": "ACTIVE", "bank_name": "Wema Bank", "bank_code": "035", "account_number": "1234567890", "account_name": "Daya - Ada Lovelace", "currency": "NGN" }, { "type": "NGN_VIRTUAL_ACCOUNT", "provider": "PAYSTACK", "provider_availability": { "status": "DEGRADED", "message": "This provider is experiencing funding-account delays." }, "status": "ACTIVE", "bank_name": "Wema Bank", "bank_code": "035", "account_number": "9876543210", "account_name": "Ada Lovelace", "currency": "NGN" } ] } ``` `provider` identifies the funding provider. `bank_name` is the bank the customer pays. These values may be different, and two providers may return accounts from the same bank. `bank_code` is included when the provider returns it or Daya can safely resolve it from the assigned `bank_name`. If it is omitted, use the returned `bank_name` and `account_number`; do not substitute a code from the account-creation request. Treat the entries in `instructions` as independent payment options. Use each entry's `provider` and `status`; do not infer special meaning from its array position. Every permanent NGN funding account response includes Flutterwave and Paystack. If a Paystack account has not been requested because the customer's bank details are missing, the Paystack entry is `REQUIRES_INFORMATION` and lists the fields you must submit. This entry does not contain a virtual account number. Show it as a payment option only after its status becomes `ACTIVE` and account details are present. For crypto, `asset` identifies the stablecoin, `chain` identifies the network, and `instructions` contains the wallet address: ```json { "asset": "USDC", "chain": "BASE", "instructions": [ { "type": "CRYPTO_ADDRESS", "status": "ACTIVE", "address": "0x742d35cc6634c0532925a3b844bc9e7595f2bd18", "chain": "BASE" } ] } ``` The product concepts are still onramps and offramps. New integrations create both with `/v1/funding-accounts`. The Legacy API section lists older `/v1/onramps` and `/v1/offramps` routes for existing integrations. ## Account Types | Type | Use case | Amount | Lifetime | |------|----------|--------|----------| | `TEMPORARY` | One-time payment details | Required integer `amount` for NGN virtual accounts | May expire | | `PERMANENT` | Reusable payment details | Not allowed | Reusable until disabled | Permanent NGN virtual accounts require the customer to have completed Tier 1 KYC. ## Permanent NGN Accounts Permanent NGN funding accounts support two banking providers: `FLUTTERWAVE` and `PAYSTACK`. Complete Tier 1 KYC to create a permanent NGN funding account. Add the customer's verified bank details when you submit Tier 1 KYC, or [update them later](/api-reference/customers/update-tier1-verification), so Paystack can verify the customer and provide another instruction. Submitting bank details starts Paystack's asynchronous bank account verification even when the customer does not yet have a permanent NGN funding account. If Paystack is temporarily unavailable, Daya keeps the verification pending and starts it automatically after the provider recovers. Subscribe to `customer.bank_account_verification.succeeded` and `customer.bank_account_verification.failed` for the result. After a successful result, Daya adds or activates the Paystack instruction on an existing permanent NGN funding account. If this makes a pending funding account usable, Daya sends `funding_account.active`; otherwise it sends `funding_account.updated`. If the customer does not have a permanent NGN funding account yet, Daya retains the verified result and uses it when you create one later. A failed Paystack verification does not prevent an available Flutterwave instruction from being created or used. Before submitting bank details, get the supported bank code from [`GET /v1/banks`](/api-reference/banks/list-banks) and verify the account with [`POST /v1/banks/resolve`](/api-reference/banks/resolve-bank-account). The submitted account is not the funding account you give the customer. The create response returns the available instructions. Show the customer any instruction whose `status` is `ACTIVE`. There is no provider field in the create request. | Instruction status | What to do | |--------------------|------------| | `REQUIRES_INFORMATION` | Submit the fields listed in `required_fields`, then create the same permanent funding account again. | | `PENDING` | Wait for `funding_account.active` when no payment option is active yet, or `funding_account.updated` when another instruction is already active. | | `ACTIVE` | The instruction is ready to show to the customer. | | `FAILED` | Read `failure.code` and `failure.message`, correct the customer details if needed, then create the same permanent funding account again. | Calling create again with the same permanent account details does not create another live funding account. Daya returns the existing account with its latest instructions. ## Provider Availability Each provider-backed instruction includes `provider_availability` so your integration can explain provider delays and outages. | Field | Meaning | |-------|---------| | `status` | `OPERATIONAL`, `DEGRADED`, or `UNAVAILABLE`. | | `message` | A user-facing explanation of a disruption. It is omitted for `OPERATIONAL`. | | Availability status | How to handle it | |---------------------|------------------| | `OPERATIONAL` | The provider is working normally. | | `DEGRADED` | The provider accepts new requests but has a known delay or limitation. Show `message` where useful. | | `UNAVAILABLE` | The provider is temporarily unavailable for new requests. Show `message` for the current guidance. | For permanent NGN accounts, Daya evaluates Flutterwave and Paystack independently: - If both providers are available, the response includes both provider instructions. - If Flutterwave is `UNAVAILABLE` and Paystack can accept the request, Daya continues through Paystack when the customer has verified bank details. The response still includes the Flutterwave entry with `provider_availability.status: UNAVAILABLE`. - If Paystack is `UNAVAILABLE`, Daya can continue through Flutterwave. The Paystack entry remains visible with `provider_availability.status: UNAVAILABLE`. - If neither provider can accept a new permanent NGN request, creation returns HTTP `503` with error code `PROVIDER_UNAVAILABLE` and the configured message. Temporary NGN accounts and crypto accounts use a single provider. Their creation returns `503 PROVIDER_UNAVAILABLE` when that provider is unavailable. ## Developer Fees Developer fees let you keep a percentage of each deposit received through a funding account. Add `developer_fee.percentage` when creating the funding account. Use a decimal string from `0` to `50`. Omit `developer_fee` to use `0%`. `developer_fee.percentage` uses percentage values, not basis points. For example, `0.5` means `0.5%`, `2` means `2%`, and `50` means `50%`. This differs from rate `fee_bps`, where `50` means `0.5%`. The percentage is stored on the funding account and applies to every deposit received through that account. Funding account responses include `developer_fee.percentage`. Deposit responses and deposit webhooks include `developer_fee` and `customer_amount` after Daya calculates the split, so you can reconcile your fee separately from Daya fees. The developer fee is deducted before the final customer amount is calculated. It is not added as a separate charge. Use `developer_fee.amount` and `developer_fee.currency` to track the fee your merchant account kept, and use `customer_amount` as the amount left for the customer. Flat developer fees, such as `NGN 500` or `$1` per transaction, are not currently supported through `developer_fee`. ## Settlement Destinations Settlement destination controls where received funds are delivered after the funding account receives money. | Rail | Type | Supported destinations | |------|------|------------------------| | `NGN_VIRTUAL_ACCOUNT` | `TEMPORARY` | `INTERNAL_BALANCE`, `ONCHAIN` | | `NGN_VIRTUAL_ACCOUNT` | `PERMANENT` | `INTERNAL_BALANCE`, `ONCHAIN` | | `CRYPTO_ADDRESS` | `TEMPORARY` | `NGN_PAYOUT` | | `CRYPTO_ADDRESS` | `PERMANENT` | `INTERNAL_BALANCE`, `NGN_PAYOUT` | For `NGN_PAYOUT`, fetch supported banks with [`GET /v1/banks`](/api-reference/banks/list-banks), verify the account with [`POST /v1/banks/resolve`](/api-reference/banks/resolve-bank-account), then send the verified `destination_bank.account_number` and `destination_bank.bank_code`. ## Lifecycle Funding accounts move through these statuses: | Status | Meaning | |--------|---------| | `PENDING` | Funding account setup is in progress | | `ACTIVE` | Payment details are ready and can be shown to the customer | | `FAILED` | Funding account setup failed | | `DISABLED` | Funding account and active payment details are disabled | Permanent active funding accounts can rotate settlement destination with `PATCH /v1/funding-accounts/{id}/settlement-destination`. Rotation does not create new payment details. ## Webhooks Funding account setup emits: | Event | When sent | |-------|-----------| | `funding_account.created` | Funding account record is created in `PENDING` state | | `funding_account.active` | Payment details are ready | | `funding_account.updated` | A funding-account instruction changed status or received updated payment details | | `funding_account.failed` | Setup fails | | `funding_account.disabled` | An active funding account is disabled | The webhook `data` object is the same public funding account response returned by the API. Bank account verification is reported separately: | Event | When sent | |-------|-----------| | `customer.bank_account_verification.succeeded` | Paystack verified the customer's current bank details | | `customer.bank_account_verification.failed` | Paystack rejected the customer's current bank details; read `failure_code` and `failure_message` | See [Webhook Events](/api-reference/webhooks/events#bank-account-verification-events) for the result fields and payload examples. ## Deposits from Funding Accounts When money arrives through a funding account, Daya creates a deposit. Deposit API responses and deposit webhook payloads include `funding_account_id` so you can connect the incoming money back to the funding account the customer paid into. Funding-account instruction IDs stay internal. Your integration should store the public `funding_account_id` and the deposit `id`. | Event | When sent | |-------|-----------| | `deposit.received` | Incoming funds are recorded | | `deposit.processing` | Settlement has started | | `deposit.requires_review` | The deposit needs review before it can continue | | `deposit.completed` | Funds reached the configured settlement destination | | `deposit.failed` | The deposit failed | | `deposit.reversed` | A completed deposit was reversed | If settlement requires background delivery work, track the merchant-facing lifecycle with `deposit.*` events. Funding-account settlement does not emit separate payout webhooks. ## Next Steps Create NGN or crypto payment details. Collect NGN from customers. Collect stablecoins from customers. Track funding account and deposit state changes in real time. #### Onramps Path: /concepts/onramps Description: Collect Naira (NGN) from customers and send them stablecoins ## Overview An **onramp** lets your customer pay Naira (NGN) into a Daya bank account. Daya records the incoming money as a deposit, then sends the value as stablecoins. New integrations create onramps with [Funding Accounts](/concepts/funding-accounts). In the API, an onramp is a funding account with: | Field | Value | |-------|-------| | `rail` | `NGN_VIRTUAL_ACCOUNT` | | `currency` | `NGN` | | `type` | `TEMPORARY` or `PERMANENT` | The older `/v1/onramps` routes still exist for existing integrations. New integrations should create onramps with `/v1/funding-accounts`. ## Customer Every onramp belongs to a customer. Create the customer first with [`POST /v1/customers`](/api-reference/customers/create-customer), then pass `customer.customer_id` when creating the funding account. Customers are scoped to your merchant account. The same customer can have more than one temporary onramp, but permanent onramps are reused when there is already a live one for that customer. ## Temporary Onramps Use a temporary onramp for one payment. | Behavior | Details | |----------|---------| | Purpose | One-off, time-sensitive payment | | Amount | Required | | Reuse | Never reused | | Customer KYC | Not required | | Expiry | Yes. Funds must arrive before the payment window closes | | Settlement | `INTERNAL_BALANCE` or `ONCHAIN` | | Rate | Required only when settlement is `ONCHAIN` | For `ONCHAIN` settlement, get a BUY rate first with [`GET /v1/rates?side=BUY`](/api-reference/rates/get-rates). Pass the returned `rate_id` when creating the onramp. The rate must still be valid when the onramp is created. After creation, display these values to the customer: - Bank name - Account number - Account name - Exact NGN `amount` from the create response - Expiry time, when present The request `amount` is the principal before any payment-provider collection charge. Tell the customer to send the exact response `amount` before the payment window ends. Do not recalculate or round it. Deposits that arrive after the window, or after a bound rate expires, are flagged for review instead of settling automatically. ```json Temporary onramp to onchain settlement { "type": "TEMPORARY", "rail": "NGN_VIRTUAL_ACCOUNT", "customer": { "customer_id": "650e8400-e29b-41d4-a716-446655440000" }, "currency": "NGN", "amount": 50000, "settlement_destination": { "type": "ONCHAIN", "rate_id": "550e8400-e29b-41d4-a716-446655440000", "destination_asset": "USDC", "destination_chain": "BASE", "destination_address": "0x742d35cc6634c0532925a3b844bc9e7595f2bd18" } } ``` ## Permanent Onramps Use a permanent onramp when the same customer should keep the same NGN bank details for repeat payments. | Behavior | Details | |----------|---------| | Purpose | Long-lived customer deposit account | | Amount | Do not send | | Reuse | One live permanent NGN account per merchant and customer | | Customer KYC | Tier 1 required | | Expiry | No expiry while active | | Settlement | `INTERNAL_BALANCE` or `ONCHAIN` | | Rate | Applied when each deposit settles | If a live permanent onramp already exists for that customer, Daya returns the same account. Updating settlement does not change the bank account details shown to the customer. ```json Permanent onramp to Daya balance { "type": "PERMANENT", "rail": "NGN_VIRTUAL_ACCOUNT", "customer": { "customer_id": "650e8400-e29b-41d4-a716-446655440000" }, "currency": "NGN", "settlement_destination": { "type": "INTERNAL_BALANCE" } } ``` ## Rate Behavior | Onramp type | FX behavior | |-------------|-------------| | `TEMPORARY` | Rate is locked at creation when `ONCHAIN` settlement is used | | `PERMANENT` | Rate is selected when each deposit settles | For temporary onramps with a bound `rate_id`, the customer must pay during the valid payment window. Late deposits do not settle automatically. ## Settlement Choose where the NGN value goes after Daya receives the bank transfer. | Settlement destination | What happens | Required fields | |------------------------|--------------|-----------------| | `INTERNAL_BALANCE` | Funds are credited to your Daya balance | None | | `ONCHAIN` | Funds are converted and sent to a stablecoin address | `destination_asset`, `destination_chain`, `destination_address` | For `ONCHAIN`, use [Supported Chains](/concepts/supported-chains) to choose a supported network. ## Developer Fees To keep a percentage of each onramp deposit, send `developer_fee.percentage` when creating the funding account. Use a decimal string from `0` to `50`. Deposit responses and deposit webhooks show the developer fee and the customer amount separately from Daya fees. For onramps, the developer fee is deducted before the final customer amount is calculated. It is not added as an extra charge. Use `developer_fee.amount` and `developer_fee.currency` to track the fee, and use `customer_amount` to know what the customer should receive or be credited with. `developer_fee.percentage` is a percentage value, not basis points. For example, `0.5` means `0.5%`, `2` means `2%`, and `50` means `50%`. ## Track Deposits Use `/v1/deposits` to reconcile money received through onramps. Deposit responses include `funding_account_id`, so you can connect the payment back to the account shown to the customer. Listen for: | Event | Meaning | |-------|---------| | `funding_account.active` | NGN account details are ready | | `deposit.received` | Daya recorded the incoming NGN transfer | | `deposit.processing` | Settlement has started | | `deposit.completed` | Settlement reached its destination | | `deposit.requires_review` | The deposit needs review before continuing | ## Limits Funding account creation limits may apply to high-volume integrations. If you need a higher limit, contact Daya before launch. ## Next Steps Create an onramp with an NGN virtual account. Understand BUY rates and settlement destinations. Reconcile money received through onramps. #### Offramps Path: /concepts/offramps ## Bank Payouts For `NGN_PAYOUT`, resolve the bank account before creating the offramp: 1. List supported banks with [`GET /v1/banks`](/api-reference/banks/list-banks). 2. Resolve the account with [`POST /v1/banks/resolve`](/api-reference/banks/resolve-bank-account). 3. Send the verified `account_number` and `bank_code` in `destination_bank`. Daya resolves and stores the account name. You do not send `account_name` in the funding account request. Invalid bank accounts are rejected. Always resolve the bank account before creating an offramp with `NGN_PAYOUT`. ## Settlement Choose where the crypto value goes after Daya receives the deposit. | Settlement destination | What happens | Allowed types | |------------------------|--------------|---------------| | `INTERNAL_BALANCE` | Funds are credited to your Daya balance | `PERMANENT` | | `NGN_PAYOUT` | Funds are converted to NGN and paid to a Nigerian bank account | `TEMPORARY`, `PERMANENT` | Temporary offramps support `NGN_PAYOUT` only. Permanent offramps support `INTERNAL_BALANCE` and `NGN_PAYOUT`. ## Developer Fees To keep a percentage of each offramp deposit, send `developer_fee.percentage` when creating the funding account. Use a decimal string from `0` to `50`. Deposit responses and deposit webhooks show the developer fee and the customer amount separately from Daya fees. For offramps, the developer fee is deducted before the final customer amount is calculated. The deposit response and webhook show the exact fee in `developer_fee` and the amount left for the customer in `customer_amount`. `developer_fee.percentage` is a percentage value, not basis points. For example, `0.5` means `0.5%`, `2` means `2%`, and `50` means `50%`. ## Supported Chains Offramps support APTOS, BASE, BSC, CELO, ETHEREUM, POLYGON, SOLANA, SUI, TEMPO, and TRON. Sui supports USDC deposits only. Tempo supports USDT deposits only when enabled in production. See [Supported Chains](/concepts/supported-chains) and `GET /v1/supported-chains` for the authoritative live deposit and withdrawal matrix per environment. ## Track Deposits Use `/v1/deposits` to reconcile money received through offramps. Deposit responses include `funding_account_id`, `asset`, `chain`, and `tx_hash`. For funding-account offramps with `NGN_PAYOUT`, NGN payout delivery happens in the background as part of deposit settlement. Track the customer-facing state from the deposit `status`, `settlement_status`, and `deposit.*` webhooks. Show states such as `Deposit received`, `Settlement processing`, `Settlement settled`, `Settlement failed`, or `Requires review`. Do not wait for a separate transfer or payout webhook for this flow. Listen for: | Event | Meaning | |-------|---------| | `funding_account.active` | Crypto address is ready | | `deposit.received` | Daya recorded the incoming crypto deposit | | `deposit.processing` | Settlement has started | | `deposit.completed` | Settlement reached its destination | | `deposit.requires_review` | The deposit needs review before continuing | ## Next Steps Create an offramp with a crypto address. Resolve a bank account before NGN payout. Reconcile crypto deposits. #### Transfers Path: /concepts/transfers Description: How merchant-initiated transfers work for NGN and USD ## What is a Transfer? A **transfer** is a merchant-initiated send of funds to a bank recipient. Transfers support both Nigerian Naira (NGN) and US Dollar (USD) destinations. | Currency | Recipient Types | |----------|----------------| | `NGN` | `BANK_ACCOUNT` | | `USD` | `US_BANK_ACCOUNT` (ACH or wire), `SWIFT_BANK_ACCOUNT` | Transfers are the API resource for sending money from your Daya withdrawal balance to an external bank destination. They emit `transfer.*` webhooks. --- ## Funding Transfers Transfers are funded from your **withdrawal balance**. There are two ways to ensure your balance has funds: 1. **From funding account and USD virtual account deposits** — these land in your **collection balance**. Move them to the withdrawal balance via [`POST /v1/merchant/balance/transfer`](/api-reference/merchant-balance/transfer-merchant-balance). 2. **Via merchant funding** — send crypto or NGN directly to your [funding accounts](/api-reference/merchant-funding/get-merchant-funding). These go straight into the withdrawal balance. NGN funding deposits are converted to USD at the current rate. --- ## Saved vs Inline Recipients Transfers support two modes for specifying the destination: ### Saved recipients Create a recipient once via [`POST /v1/recipients`](/api-reference/recipients/create-recipient), then reference it by `recipient_id` in future transfers. This is ideal for recurring payments. ### Inline recipients Provide full destination details in the `destination` field of the transfer request. The recipient is created implicitly. This is convenient for one-off payments. For inline USD destinations (`US_BANK_ACCOUNT` or `SWIFT_BANK_ACCOUNT`), `on_behalf_of.customer_id` is effectively required. That customer must have completed [tier 2 verification](/api-reference/customers/submit-tier2-verification). --- ## Transfer Lifecycle | Status | Meaning | Terminal | |--------|---------|----------| | `PROCESSING` | Transfer has been accepted and is being executed | No | | `SETTLED` | Funds delivered to the recipient | Yes | | `FAILED` | Transfer failed permanently | Yes | Webhook events expose the lifecycle in more detail than the external status field: | Event | Meaning | |-------|---------| | `transfer.created` | Transfer record and first attempt were created | | `transfer.processing` | Funds were locked and submission was queued | | `transfer.requires_review` | Transfer was flagged for manual review | | `transfer.submitted` | Provider accepted the transfer submission | | `transfer.completed` | Provider success and ledger finalization completed | | `transfer.failed` | Transfer failed terminally after reconciliation | | `transfer.reversed` | A completed transfer was reversed | Use `/v1/transfers` and `transfer.*` webhooks for merchant-created bank sends. --- ## NGN Transfers When you create an NGN transfer: - The `amount` is in Naira (e.g. `50000.00`) - The `debit_amount` in the response is the total USD debited from your withdrawal balance, including fees - The `rate` field captures the exchange rate snapshot used - A minimum effective USD debit of $0.50 is enforced. For NGN transfers, the corresponding Naira amount varies with the effective `SELL` exchange rate. --- ## USD Transfers When you create a USD transfer: - The `amount` is in USD (e.g. `100.00`) - `debit_currency` remains `USD` - The recipient's customer must have completed tier 2 verification --- ## Idempotency `POST /v1/transfers` is idempotent. Include an `Idempotency-Key` header (or `X-Idempotency-Key` as a fallback) with a unique value to safely retry requests without creating duplicate transfers. --- ## API Routes - [Create Transfer](/api-reference/transfers/create-transfer) - [List Transfers](/api-reference/transfers/list-transfers) - [Get Transfer](/api-reference/transfers/get-transfer) --- ## Next Steps Send funds to a bank recipient Save a recipient for reuse Check your available balance #### USD Virtual Accounts Path: /concepts/virtual-accounts Description: USD virtual accounts for tier-2-verified customers ## What is a USD virtual account? A **USD virtual account** is a US dollar bank account provisioned for a customer. Customers can receive USD deposits into this account. Deposits settle into the merchant [collection balance](/api-reference/merchant-balance/get-merchant-balance) and a [webhook](/api-reference/webhooks/events) is sent. USD virtual accounts are separate from funding accounts. Use [Funding Accounts](/concepts/funding-accounts) when customers need to send NGN or crypto into Daya. Use USD virtual accounts when customers need US dollar bank details. --- ## Prerequisites The customer must have completed **tier 2 verification** before a virtual account can be created. For businesses, tier 2 business KYB satisfies the tier 2 requirement. See [Submit Tier 2 Verification](/api-reference/customers/submit-tier2-verification) for individual KYC and business/entity KYB. Individual verification flow: 1. Create a customer via [`POST /v1/customers`](/api-reference/customers/create-customer) 2. Complete tier 1 verification (BVN + phone number + selfie) via [`POST /v1/customers/{id}/tier1-verification`](/api-reference/customers/submit-tier1-verification) 3. Complete tier 2 verification (KYC documents) via [`POST /v1/customers/{id}/tier2-verification`](/api-reference/customers/submit-tier2-verification) 4. Once tier 2 status reaches `VERIFIED`, create the USD virtual account Business/entity verification flow: 1. Create a customer record via [`POST /v1/customers`](/api-reference/customers/create-customer) 2. Submit business KYB via [`POST /v1/customers/{id}/tier2-verification`](/api-reference/customers/submit-tier2-verification) with `customer_type: "business"` 3. Wait for `tier_2_kyc_complete` to become `true` 4. Create the USD virtual account for that `customer_id` --- ## USD Virtual Account Properties Each virtual account includes: | Property | Description | |----------|-------------| | `id` | Unique USD virtual account identifier | | `customer_id` | The customer this account belongs to | | `currency` | Source currency (`usd`) | | `status` | Account status (`active` or `inactive`) | | `developer_fee` | Developer fee percentage used for deposits received through this account | | `deposit_instructions` | Bank details for receiving deposits (see below) | ### `deposit_instructions` fields | Field | Description | |-------|-------------| | `bank_beneficiary_name` | Name of the beneficiary on the receiving account | | `bank_beneficiary_address` | Beneficiary mailing address | | `bank_name` | Name of the receiving bank | | `bank_address` | Address of the receiving bank | | `bank_account_number` | Account number to deposit into | | `bank_routing_number` | US routing number (ACH/domestic wire) | | `payment_rails` | Supported rails for this account (e.g. `ach`, `wire`) | --- ## How USD Deposits Work When a USD deposit is received into the account: 1. The deposit is received at the bank details in `deposit_instructions` 2. Supported payment rails include ACH and wire transfers 3. Funds settle into the merchant [collection balance](/api-reference/merchant-balance/get-merchant-balance) 4. A [webhook](/api-reference/webhooks/events) is sent to notify you of the deposit 5. The payment appears in the [USD account deposits API](/api-reference/virtual-account-deposits/list-usd-account-deposits) with type `USD_DEPOSIT` Use `/v1/virtual-account-deposits` for USD virtual account payments. Use `/v1/deposits` for NGN and crypto funding-account deposits. ## Developer Fees Developer fees let you keep a percentage of each USD deposit received through a virtual account. Add `developer_fee.percentage` when creating the virtual account. Use a decimal string from `0` to `50`. Omit `developer_fee` to use `0%`. `developer_fee.percentage` uses percentage values, not basis points. For example, `0.5` means `0.5%`, `2` means `2%`, and `50` means `50%`. Virtual account responses include `developer_fee.percentage`. USD virtual account deposit responses and deposit webhooks include `developer_fee` and `customer_amount` after Daya calculates the split. --- ## Relationship to Tier 2 Verification Tier 2 verification is the gateway for customer banking features: | Feature | Requires Tier 2 | |---------|:---------------:| | USD virtual accounts | Yes | | `US_BANK_ACCOUNT` recipients | Yes | | `SWIFT_BANK_ACCOUNT` recipients | Yes | | USD transfers (ACH, wire, SWIFT) | Yes | For individual customers, submit tier 2 KYC. For business/entity customers, submit tier 2 KYB with `customer_type: "business"`. Both use [Submit Tier 2 Verification](/api-reference/customers/submit-tier2-verification). --- ## API Routes - [Create USD Virtual Account](/api-reference/virtual-accounts/create-virtual-account) - [List USD Virtual Accounts](/api-reference/virtual-accounts/list-virtual-accounts) - [Get USD Virtual Account](/api-reference/virtual-accounts/get-virtual-account) - [List Customer USD Virtual Accounts](/api-reference/virtual-accounts/list-customer-virtual-accounts) - [List USD Virtual Account Deposits](/api-reference/virtual-account-deposits/list-usd-account-deposits) --- ## Next Steps Provision a USD account for a customer Complete the prerequisite KYC step View payments into USD virtual accounts #### Merchant Withdrawals Path: /concepts/withdrawals Description: How merchant-balance withdrawals are created, tracked, and completed ## What is a Withdrawal? A **withdrawal** moves funds from your Daya **withdrawal balance** to an on-chain destination address. Withdrawals are separate from deposits and transfers. A deposit may settle into the merchant collection balance first, then be transferred to the withdrawal balance, and the withdrawal happens later as its own lifecycle. The merchant balance is split into a **collection balance** and a **withdrawal balance**. Withdrawals draw from the withdrawal balance. Use `POST /v1/merchant/balance/transfer` to move funds from collection to withdrawal. --- ## How the Merchant Balance Works Your merchant balance has two sides: | Balance | What goes in | What goes out | |---------|-------------|---------------| | **Collection balance** | Funding account deposits and USD virtual account deposits | Balance transfers to withdrawal balance | | **Withdrawal balance** | Balance transfers from collection, [merchant funding](/api-reference/merchant-funding/get-merchant-funding) deposits | [Transfers](/api-reference/transfers/create-transfer) to bank recipients, on-chain withdrawals | To use collection funds for transfers or withdrawals, move them to your withdrawal balance via [`POST /v1/merchant/balance/transfer`](/api-reference/merchant-balance/transfer-merchant-balance). You can also fund the withdrawal balance directly: - **Crypto deposits** — send stablecoins to your [merchant funding](/api-reference/merchant-funding/get-merchant-funding) crypto wallet addresses on any supported chain. - **NGN deposits** — transfer Naira to your permanent [merchant funding](/api-reference/merchant-funding/get-merchant-funding) NGN bank account. These are converted to USD at the current rate. --- ## When Withdrawals Are Used Withdrawals apply when you want to move funds from your Daya withdrawal balance to an on-chain crypto address. For sending funds to bank accounts, use the [Transfers API](/api-reference/transfers/create-transfer) instead. Typical flow: 1. Fund your balance — funding account deposits and USD virtual account deposits land in the collection balance, or fund directly via merchant funding 2. Move funds to the withdrawal balance via [`POST /v1/merchant/balance/transfer`](/api-reference/merchant-balance/transfer-merchant-balance) (skip this step if funded via merchant funding) 3. Create a withdrawal to a supported chain and token 4. Daya tracks the withdrawal until it settles or fails --- ## Withdrawal Properties Each withdrawal is represented by the following fields: | Property | Description | |--------|-------------| | `id` | Unique withdrawal identifier | | `amount_usd` | Amount withdrawn from merchant balance | | `chain` | Destination chain | | `token` | Token sent on-chain | | `destination_address` | Recipient address | | `status` | Current withdrawal status | | `failure_code` | Failure code if unsuccessful | | `failure_message` | Human-readable failure detail | | `provider_tx_id` | Provider-side transfer identifier | | `tx_hash` | On-chain transaction hash after submission/settlement | | `created_at` | When the withdrawal record was created | | `updated_at` | Last update time | | `submitted_at` | When the withdrawal was submitted to the provider | | `settled_at` | When the withdrawal completed | --- ## Withdrawal Statuses | Status | Meaning | Terminal | |------|---------|----------| | `PENDING` | Withdrawal record exists but has not been submitted yet | No | | `SUBMITTED` | Sent to the provider and awaiting final outcome | No | | `SETTLED` | Withdrawal completed successfully | Yes | | `FAILED` | Withdrawal failed permanently | Yes | --- ## Lifecycle At a high level, withdrawals move through these stages: 1. **Created** A withdrawal request is accepted against available merchant balance 2. **Submitted** The withdrawal is successfully initiated with the provider 3. **Completed** The withdrawal becomes `SETTLED` or `FAILED` Withdrawal progress can be tracked via both the withdrawal read APIs and merchant webhook events. --- ## Webhook Events Merchant-configured webhook endpoints receive withdrawal lifecycle events: - `withdrawal.created` - `withdrawal.submitted` - `withdrawal.completed` - `withdrawal.failed` Each webhook uses the common merchant webhook envelope: - `event` - `timestamp` - `data` (the public withdrawal object) --- ## Supported Chains and Tokens Withdrawals are constrained by token-level support, not just chain-level support. Withdrawals currently support chains: **SOLANA**, **TRON**, **APTOS**, **BASE**, **POLYGON**, **ETHEREUM** with tokens **USDC** and **USDT**. Use [List Supported Chains](/api-reference/supported-chains/list-supported-chains) to fetch the current live matrix of supported chains and token combinations. --- ## API Routes Use these routes to work with balances and withdrawals: - [Get Merchant Balance](/api-reference/merchant-balance/get-merchant-balance) — check collection and withdrawal balances - [Transfer Merchant Balance](/api-reference/merchant-balance/transfer-merchant-balance) — move funds from collection to withdrawal - [Get Merchant Funding](/api-reference/merchant-funding/get-merchant-funding) — view funding instructions (NGN account + crypto wallets) - [Create Transfer](/api-reference/transfers/create-transfer) — send funds to bank recipients (NGN, ACH, wire, SWIFT) - [Withdraw Merchant Balance](/api-reference/merchant-balance/withdraw-merchant-balance) — withdraw to an on-chain address - [List Withdrawals](/api-reference/merchant-balance/list-withdrawals) - [Get Withdrawal](/api-reference/merchant-balance/get-withdrawal) --- ## Next Steps Check available funds before creating withdrawals Validate supported chain and token combinations Track withdrawal lifecycle asynchronously #### Rates & Settlement Path: /concepts/rates-and-settlement Description: How exchange rates are applied and how funds are delivered ## Overview Rates and settlement define **how value moves** through the Daya API: - **Rates** determine *how much* stablecoin a deposit converts into - **Settlement** determines *where* the converted funds are delivered These rules are deterministic and depend on the funding account type and settlement destination. --- ## Rates A **rate** is a firm FX quote used to convert between NGN and stablecoins. Each rate includes: - A fixed exchange rate - A **side** (`BUY` or `SELL`) - A defined validity window (TTL) - A unique `rate_id` for funding account settlement requests that need a firm quote ### Buy vs Sell Rates | Side | Use Case | |------|----------| | `BUY` | Merchant is buying crypto (receiving NGN deposits, converting to stablecoin) | | `SELL` | Merchant is selling crypto (converting stablecoin to NGN settlement) | New rates are generated approximately every **10 minutes** and expire about **30 minutes** after creation. --- ## Rate Properties (Conceptual) Each rate defines the following attributes: | Property | Meaning | |--------|--------| | `rate_id` | Identifier used to bind a funding account settlement request | | `from` / `to` | Source and destination currencies | | `side` | `BUY` or `SELL` | | `rate` | Conversion rate | | `expires_at` | When the rate becomes invalid | Rates are **immutable** once published. --- ## Rate Semantics by Funding Account Type How a rate is applied depends on the funding account. ### Temporary Funding Accounts (Firm Rates) Temporary funding accounts that use a quoted conversion are **bound to a specific `rate_id` at creation**. - Deposits received **within the validity window** execute at the bound rate - Deposits received **after expiry** do not settle automatically - No slippage occurs within the validity window This includes: - Temporary `NGN_VIRTUAL_ACCOUNT` funding accounts that settle to `INTERNAL_BALANCE` - Temporary `NGN_VIRTUAL_ACCOUNT` funding accounts that settle through `ONCHAIN` - Temporary `CRYPTO_ADDRESS` funding accounts that settle through `NGN_PAYOUT` This provides price certainty for time-sensitive or one-time transactions. --- ### Permanent NGN Funding Accounts (Floating Rates) Permanent NGN funding accounts are **not bound** to a specific rate. - Each deposit uses the **current rate** at the time of execution - Rates may differ between deposits on the same funding account - Subject to market movement and spread This is best suited for recurring or long-lived deposit flows. --- ## Rate Expiry Behavior If a deposit arrives after a bound rate expires: - The deposit is **FLAGGED** - No FX or settlement occurs automatically - Manual review is required Rates **cannot** be extended, reused, or retroactively applied. --- ## Settlement Settlement defines **where converted stablecoins are sent** after FX conversion. Settlement behavior is configured on the funding account. --- ## Settlement Modes ### On-chain (`ONCHAIN`) Converted stablecoins are **sent to a blockchain address**. - Settlement occurs after FX conversion - Subject to risk checks - Finalized once the on-chain transaction confirms Used for direct wallet settlement and real-time delivery. --- ### Internal balance (`INTERNAL_BALANCE`) Converted funds are **credited to the merchant’s Daya balance**. - No immediate on-chain transaction - Funds can be withdrawn later - Useful for aggregation and batch withdrawals --- ### NGN Payout (`NGN_PAYOUT`) For crypto funding account deposits — converted funds are **paid out as NGN to a Nigerian bank account**. - Temporary crypto funding accounts require a `rate_id` (SELL rate) and `destination_bank` - Settlement occurs after crypto deposit is confirmed and FX conversion is executed --- ## Guarantees & Invariants Daya enforces the following guarantees: - FX conversion and settlement are **atomic** - Partial execution is **not possible** - Rates are **firm** for temporary rate-locked funding accounts - Settlement behavior is **immutable** per temporary funding account (permanent funding accounts can update settlement) --- ## What Rates and Settlement Do *Not* Guarantee - Rates do not guarantee settlement if deposits arrive late - Settlement does not occur if deposits are flagged or failed - Permanent funding accounts do not guarantee a fixed price --- ## Next Steps See how receive-money settlement is configured Understand the deposit lifecycle View the API reference #### Supported Chains Path: /concepts/supported-chains Description: Blockchain networks and assets supported by Daya ## Overview Daya supports a defined set of blockchain networks for stablecoin settlement. Supported chains differ between **Sandbox** and **Production** environments. Support is directional. A token can be enabled for deposits on a chain without being enabled for withdrawals on that same chain. Need the same information programmatically? Use [List Supported Chains](/api-reference/supported-chains/list-supported-chains). Use the API endpoint for the authoritative live matrix of deposit and withdrawal support. Do not hard-code this table into your product. --- ## Environment Support Sandbox is designed for testing and integration. It supports a **limited set of chains** with test behavior that mirrors production. ### Supported Assets | Chain | USDC Deposit | USDC Withdraw | USDT Deposit | USDT Withdraw | |-------|:-----------:|:-------------:|:-----------:|:-------------:| | Ethereum | Yes | Yes | Yes | No | Sandbox uses **Ethereum only**. Sandbox supports **only Ethereum**. Requests specifying other chains will fail. Production supports multiple blockchain networks for real settlement. | Chain | USDC Deposit | USDC Withdraw | USDT Deposit | USDT Withdraw | |-------|:-----------:|:-------------:|:-----------:|:-------------:| | Polygon | Yes | No | Yes | No | | Ethereum | Yes | No | Yes | No | | Base | Yes | Yes | Yes | No | | Optimism | Yes | No | Yes | No | | BNB Smart Chain | Yes | No | Yes | No | | Solana | Yes | Yes | Yes | Yes | | Sui | Yes | No | No | No | | Tempo | No | No | Yes | No | | Tron | No | No | Yes | No | | Aptos | Yes | Yes | Yes | Yes | The production table is a snapshot. Your integration should still call `/v1/supported-chains` before showing chain options or submitting a request. --- ## Important Notes Always ensure that: - The **asset** (USDC or USDT) - The **chain** - The **destination address format** all match exactly. Funds sent to an unsupported chain or incorrect network **might not be recoverable**. --- ## How Chains Are Used Chains are specified when configuring **ONCHAIN** settlement for an NGN funding account, creating a crypto funding account address, or withdrawing merchant balance onchain. Use the endpoint flags this way: | Flag | Use it for | | --- | --- | | `deposit_enabled` | Crypto funding account receive addresses and crypto-to-NGN offramp deposits. | | `withdraw_enabled` | ONCHAIN settlement for NGN funding accounts and merchant balance withdrawals to wallets. | Rules to keep in mind: - Chain selection is per funding account. - Different funding accounts may use different chains. - Merchant balance settlement does **not** require a chain until withdrawal. - A chain name appearing in an older guide, enum, or conversation does not mean it is available for every flow. The asset, chain, environment, and direction must all be enabled by `/v1/supported-chains`. See: - [Funding Accounts](/concepts/funding-accounts) - [Rates & Settlement](/concepts/rates-and-settlement) --- ## Adding New Chains Support for additional networks may be added over time. If you require a chain not listed here, contact [support@daya.co](mailto:support@daya.co). --- ## Next Steps Learn how on-chain settlement works Configure settlement via the API Fetch the latest chain support in your integration ### Guides #### Limits Path: /limits/overview Description: Merchant-facing limits and deposit constraints ## Overview Daya enforces a small set of limits to ensure system reliability and safe operation. This page documents **merchant-facing limits only**. Limits apply across both **Sandbox** and **Production**, unless stated otherwise. --- ## Deposit Limits ### Minimum Deposit Each deposit must meet a minimum value to be processed successfully. - **Minimum deposit:** **~$1 USD equivalent in NGN** - The exact NGN amount depends on the current FX rate - Deposits below this threshold are **rejected** The minimum deposit exists to ensure FX execution and settlement costs are covered. --- ### Per-Transaction Maximum Each deposit or payout has a maximum allowable transaction size. | Limit Type | Value | |-----------|-------| | **Per-deposit maximum** | $10,000 USD equivalent | | **Per-payout maximum** | $10,000 USD equivalent | Transactions exceeding these limits are flagged for review rather than processed automatically. --- ## Merchant Limits ### Rolling 24-Hour Volume Limits Volume is measured over a rolling 24-hour window across deposits and payouts. | Limit | Value | |------|-------| | **Merchant 24-hour limit** | $1,000,000 USD equivalent | | **System-wide 24-hour limit** | $1,000,000 USD equivalent | This limit applies to: - All deposits and payouts combined - All funding account types and settlement modes - All merchants for the system-wide limit Once reached, further transactions may be **flagged or rejected** until enough prior volume falls outside the rolling 24-hour window. --- ### Funding Account Creation Limit To prevent abuse, merchants are limited in how many funding accounts they can create per day. | Limit | Value | |------|-------| | **Funding accounts per day** | 1,000 | Exceeding this limit may temporarily block new funding account creation. --- ## What Happens When a Limit Is Hit When a limit is exceeded, one of the following may occur: - Deposit is **FLAGGED** for review - Payout is **FLAGGED** for review - Deposit is **rejected** immediately - Funding account creation is temporarily blocked The exact behavior depends on which limit is exceeded. Limits are enforced automatically. Merchants cannot override them via the API. --- ## Sandbox vs Production - **Sandbox** mirrors production limits for realistic testing - No real funds move in sandbox - Limit behavior (errors, flags) matches production This allows you to test limit handling before going live. --- ## Increasing Limits If your business requires limits above the standard profile, contact [support@daya.co](mailto:support@daya.co) with: - Your merchant ID - Expected daily volume - Use case and settlement pattern Limit increases are evaluated on a case-by-case basis. --- ## Next Steps Learn how deposits move through the system Learn how amount mismatches are handled for temporary VAs View all supported blockchain networks #### Handling Amount Mismatch Path: /limits/amount-mismatches Description: How Daya handles underpayments and overpayments for temporary NGN funding accounts # Handling Underpayments and Overpayments ## Overview When creating a **temporary NGN funding account**, merchants submit a principal `amount`. The create response returns an `amount` that is the exact amount the customer must transfer. The response value may be higher because the payment provider can add a collection charge. Always use the response `amount` as the payment instruction. Do not calculate the provider charge yourself or use the request value. This page explains what happens when a customer sends an amount that **does not match the response `amount`**. This behavior applies **only to temporary NGN funding accounts**. Permanent NGN funding accounts do not enforce a specific amount and process any transfer received. --- ## Underpayments If a customer sends **less than the response `amount`**, the deposit is **automatically refunded** to the sender's bank account. ### Example - **API response `amount`:** ₦50,000 - **Customer sends:** ₦45,000 - **Result:** ₦45,000 is refunded to sender's bank account The funding account remains **ACTIVE** until expiry, and the customer can attempt the transfer again with the correct amount. --- ## Overpayments If a customer sends **more than the response `amount`**: 1. The **correct amount is processed** normally 2. The **excess amount is refunded** to the sender ### Example - **API response `amount`:** ₦50,000 - **Customer sends:** ₦55,000 - **Result:** - ₦50,000 is processed and converted to stablecoin - ₦5,000 is refunded to sender's bank account --- ## Fidelity Bank Exception **Fidelity Bank behaves differently:** For transfers originating from Fidelity Bank, any mismatch between the response `amount` and the actual transfer will cause the transaction to **fail entirely**. - **Underpayment from Fidelity Bank** → Transaction fails - **Overpayment from Fidelity Bank** → Transaction fails No funds are processed. The customer must initiate a new transfer with the exact response **`amount`**. ### Why This Happens This is due to technical constraints specific to Fidelity Bank's transfer processing system. ### Recommendation When displaying bank details to customers, **always emphasize the exact amount** if there's a possibility they are using Fidelity Bank. --- ## How Refunds Work Refunds are processed automatically by Daya and typically arrive within: - **1-2 business days** for most banks - May take longer depending on the sender's bank Merchants are **not charged** for refunded amounts. Refunds are handled transparently. No merchant action is required. --- ## API Behavior When an amount mismatch occurs: | Event | Deposit Status | Webhook Event | |------|---------------|---------------| | Underpayment (non-Fidelity) | Not created | None | | Overpayment (non-Fidelity) | `SETTLED` (for correct amount) | `deposit.completed` | | Mismatch (Fidelity) | `FAILED` | `deposit.failed` | The deposit object reflects only the **processed amount**, not the refunded portion (for overpayments). --- ## Which Banks Are Affected? This automatic refund behavior applies to transfers from **all Nigerian banks except Fidelity Bank**. ### Common Banks (Automatic Refund) - Access Bank - GTBank - UBA - Zenith Bank - First Bank - Kuda - Opay - And all others except Fidelity ### Fidelity Bank (Transaction Fails) Exact amount match is **required**. --- ## Best Practices When showing bank details to customers, make the create response `amount` **highly visible** and emphasize that the transfer must match it exactly. Do not substitute the request value or calculate the provider charge yourself. If you know or suspect a customer is using Fidelity Bank, explicitly warn them that the amount must be exact. Sandbox mirrors production behavior for amount mismatches. Test your flow with under/overpayments to see how your system handles refunds. Set up webhook listeners for `deposit.failed` events to catch Fidelity Bank mismatches and notify customers to retry. --- ## Related Pages Learn about temporary NGN funding accounts Understand deposit lifecycle and statuses See other merchant-facing limits #### Sandbox testing Path: /limits/sandbox-testing Description: Test funding accounts, deposits, webhooks, and flagged edge cases in sandbox ## Overview Sandbox is designed to help you test **real production behaviors** in a safe environment. In particular, you can trigger a mock deposit for an NGN or crypto funding account: - [Create a sandbox deposit](/api-reference/sandbox/create-sandbox-deposit) ## What it validates - Your **deposit lifecycle UI/state machine** - Your **webhook endpoint** (signature verification + idempotency + retries) - Your handling of **edge cases** like deposits that require review ## Testing specific deposit outcomes Use `scenario` with `POST /v1/sandbox/deposits` when you need to trigger a specific lifecycle state. ```json { "funding_account_id": "6b0e8400-e29b-41d4-a716-446655440000", "scenario": "COMPLETED" } ``` Supported scenarios: | Scenario | Webhooks to expect | What to verify | |----------|--------------------|----------------| | `PROCESSING` | `deposit.received`, `deposit.processing` | Your app shows an in-progress state and keeps polling/listening | | `COMPLETED` | `deposit.received`, `deposit.completed` | Your app only credits the user when the final completed state arrives | | `REQUIRES_REVIEW` | `deposit.received`, `deposit.requires_review` | Your app holds the transaction and shows a review state | | `FAILED` | `deposit.received`, `deposit.failed` | Your app shows failure messaging and does not credit the user | If you omit `scenario`, sandbox runs the default processing flow. Use the default flow when you want to test production-like behavior and natural intermediate states, and use `scenario` when you want deterministic state testing. `PROCESSING` and `COMPLETED` scenarios still use the funding account's active settlement destination. Make sure the funding account is active, has an active settlement destination, and includes any required `rate_id`, destination wallet, or destination bank details before triggering those outcomes. ## Flagging behavior (important) Sandbox follows the same rules engine as production. As a result, simulated deposits may require review automatically: - **Rate expired**: If the quote/rate tied to the funding account is expired, the deposit will be flagged. - **Re-used temporary NGN account**: If a deposit has already been created for the same temporary receive instruction, subsequent simulated deposits may be flagged. If you want a clean happy path test, create a fresh funding account, then trigger the sandbox deposit immediately. For crypto funding accounts, you can either use this helper with `funding_account_id` or send testnet USDC/USDT to the returned address. ## Sandbox Bank Accounts When testing crypto funding accounts with `NGN_PAYOUT` settlement, use the following test bank accounts: | Field | Value | |-------|-------| | **Bank code** | `044` | | **Account numbers** | `0690000031`, `0690000032`, `0690000033`, ... | Increment the last digit of the account number to generate additional test accounts. Resolve them with `POST /v1/banks/resolve` before use — just like production. ## Recommended end-to-end flow ### NGN receive instruction 1. Call `GET /v1/rates?side=BUY` to get a fresh `rate_id` 2. Create a sandbox NGN funding account with `rail: NGN_VIRTUAL_ACCOUNT` 3. Call `POST /v1/sandbox/deposits` with the returned funding account `id` as `funding_account_id` 4. Observe status progression and confirm you receive the expected webhooks (`deposit.received`, then `deposit.completed` / `deposit.failed` / `deposit.requires_review`) ### Crypto funding account 1. Call `POST /v1/banks/resolve` to verify the test bank account 2. Call `GET /v1/rates?side=SELL` to get a fresh `rate_id` 3. Call `POST /v1/funding-accounts` with `rail: CRYPTO_ADDRESS`, `asset`, `chain`, and `NGN_PAYOUT` settlement 4. Call `POST /v1/sandbox/deposits` with the returned funding account `id` as `funding_account_id`, or send test crypto to the returned `address` 5. Observe deposit webhook events (`deposit.received`, then `deposit.completed` / `deposit.failed` / `deposit.requires_review`) ## Getting Testnet USDC To test crypto deposits in sandbox, you need testnet USDC. Use the Circle faucet to get free testnet USDC: Get free testnet USDC to send to your sandbox crypto funding account addresses ## Next Steps Ensure your webhook endpoint is receiving deposit events Verify signatures correctly and handle retries safely ### Rates & Chains #### Retrieve rates Path: /api-reference/rates/get-rates Description: Get current exchange rates for NGN to USDC/USDT conversions ## Overview Request firm FX quotes with guaranteed exchange rates. Each rate has a ~30-minute validity window and is identified by a unique `rate_id`. ## Authentication Your merchant API key ``` X-Api-Key: YOUR_API_KEY ``` ## Query Parameters Source currency. Currently only `NGN` is supported. **Allowed values:** `NGN` Destination currency. **Allowed values:** `USDC`, `USDT`, `USD` `USD` is treated as equivalent to USDC/USDT in v0.1. Depeg scenarios are not handled. Rate side. Determines whether the quote is for buying or selling crypto. **Allowed values:** `BUY`, `SELL` - `BUY` — Merchant is buying crypto (NGN deposits → stablecoin). Use for NGN funding accounts that settle onchain. - `SELL` — Merchant is selling crypto (stablecoin → NGN). Use for crypto funding accounts that settle to an NGN bank account. ## Request Examples ```bash cURL curl --request GET \ --url 'https://api.daya.co/v1/rates?from=NGN&to=USDC&side=BUY' \ --header 'X-Api-Key: YOUR_API_KEY' ``` ```javascript JavaScript const response = await fetch( 'https://api.daya.co/v1/rates?from=NGN&to=USDC&side=BUY', { headers: { 'X-Api-Key': 'YOUR_API_KEY' } } ); const rate = await response.json(); console.log(rate); ``` ```python Python import requests response = requests.get( 'https://api.daya.co/v1/rates', params={'from': 'NGN', 'to': 'USDC', 'side': 'BUY'}, headers={'X-Api-Key': 'YOUR_API_KEY'} ) rate = response.json() print(rate) ``` ```go Go package main import ( "fmt" "io" "net/http" ) func main() { url := "https://api.daya.co/v1/rates?from=NGN&to=USDC&side=BUY" req, _ := http.NewRequest("GET", url, nil) req.Header.Add("X-Api-Key", "YOUR_API_KEY") res, _ := http.DefaultClient.Do(req) defer res.Body.Close() body, _ := io.ReadAll(res.Body) fmt.Println(string(body)) } ``` ## Response Unique identifier for this rate snapshot. Use this when creating funding accounts that need a quoted conversion. **Example:** `rate_8x7k2mq9p` Source currency **Example:** `NGN` Destination currency **Example:** `USDC` Rate side — `BUY` or `SELL` **Example:** `BUY` Conversion rate from source to destination (e.g., 1 USDC = X NGN) **Example:** `1545.50` This rate already includes Daya's spread/fee. You don't need to calculate fees separately. Inverse conversion rate (e.g., 1 NGN = X USDC) **Example:** `0.000647` Fee in basis points (1 bps = 0.01%) **Example:** `50` (0.5%) Minimum NGN deposit amount for this rate **Example:** `1500.00` (~$1.00) Deposits below this amount will be rejected with status `FAILED`. When this rate was generated (ISO 8601 timestamp) **Example:** `2026-01-14T15:05:00Z` When this rate becomes invalid (ISO 8601 timestamp) **Example:** `2026-01-14T15:35:00Z` Always check this before using `rate_id` to create a funding account. Expired rates will be rejected. ### Success Response ```json 200 OK { "rate_id": "rate_8x7k2mq9p", "from": "NGN", "to": "USDC", "side": "BUY", "rate": 1545.50, "inverse_rate": 0.000647, "fee_bps": 50, "min_deposit_ngn": 1500.00, "created_at": "2026-01-14T15:05:00Z", "expires_at": "2026-01-14T15:35:00Z" } ``` ## Error Responses ```json 400 Bad Request - Missing from parameter { "error": { "code": "missing_parameter", "message": "Required parameter 'from' is missing", "details": "Query parameter 'from' must be provided" } } ``` ```json 400 Bad Request - Invalid currency { "error": { "code": "invalid_currency", "message": "Invalid currency specified", "details": "Only NGN is supported for 'from' parameter" } } ``` ```json 503 Service Unavailable - No rates available { "error": { "code": "rate_unavailable", "message": "No valid exchange rates available at this time", "details": "Please try again in a few minutes" } } ``` ```json 401 Unauthorized { "error": { "code": "unauthorized", "message": "Invalid or missing API key" } } ``` ## Rate Lifecycle New rates are generated approximately **every 10 minutes** and expire after **~30 minutes**. ``` Time Rate ID Valid Until ───────────────────────────────────── 15:05 rate_abc123 15:35 15:15 rate_def456 15:45 15:25 rate_ghi789 15:55 15:35 rate_jkl012 16:05 ``` Request a fresh rate immediately before creating each temporary funding account that uses a quoted conversion. ## Usage Notes ### Rate Guarantee For **temporary funding accounts** with a quoted settlement destination, the rate is guaranteed for deposits within the validity window: - Funding account created with `rate_id` at 15:10 - Rate expires at 15:35 - Deposit at 15:20 → Uses guaranteed rate ✅ - Deposit at 15:40 → Flagged (expired) ❌ ### Caching Rates You can cache rates client-side but must respect `expires_at`: ```javascript class RateCache { constructor() { this.rate = null; } async getValidRate() { if (!this.rate || new Date() >= new Date(this.rate.expires_at)) { const response = await fetch('https://api.daya.co/v1/rates?from=NGN'); this.rate = await response.json(); } return this.rate; } } ``` ### Checking Time Remaining Calculate remaining validity time: ```javascript function getSecondsRemaining(expiresAt) { const now = new Date(); const expiry = new Date(expiresAt); return Math.max(0, (expiry - now) / 1000); } const rate = await getRates(); const remaining = getSecondsRemaining(rate.expires_at); console.log(`Rate valid for ${remaining} seconds`); ``` ## Rate Calculation The displayed rate includes Daya's fee: ``` Displayed Rate = Market Rate × (1 - fee_bps / 10000) ``` **Example:** - Market rate: 1550 NGN/USDC - Fee: 50 bps (0.5%) - Displayed rate: 1550 × (1 - 0.005) = **1545.50 NGN/USDC** ## Common Patterns **Recommended flow:** 1. Call `GET /v1/rates` 2. Display rate to user 3. User confirms 4. Call `POST /v1/funding-accounts` with `rate_id` ```javascript async function createFundingAccountWithRate(customerId, destinationAddress) { // Step 1: Get current rate const rate = await getRates(); // Step 2: Show to user (await confirmation) await showRateToUser(rate); // Step 3: Create funding account with rate_id const fundingAccount = await createFundingAccount({ type: 'TEMPORARY', rail: 'NGN_VIRTUAL_ACCOUNT', customer: { customer_id: customerId }, currency: 'NGN', amount: 50000, settlement_destination: { type: 'ONCHAIN', rate_id: rate.rate_id, destination_asset: 'USDC', destination_chain: 'BASE', destination_address: destinationAddress } }); return fundingAccount; } ``` If FX venue is down, gracefully handle `rate_unavailable` error: ```javascript async function getRatesWithRetry(maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { try { const response = await fetch('https://api.daya.co/v1/rates?from=NGN'); if (response.status === 503) { await new Promise(resolve => setTimeout(resolve, 5000)); // Wait 5s continue; } return await response.json(); } catch (error) { if (i === maxRetries - 1) throw error; } } throw new Error('Rates unavailable after retries'); } ``` ## Rate Limits - **100 requests per minute** per API key - No specific rate limit on this endpoint (non-mutating) ## Next Steps Use the rate_id to create a funding account Learn more about rate semantics #### List supported chains Path: /api-reference/supported-chains/list-supported-chains Description: Return the currently supported settlement chains and their deposit or withdrawal availability ## Overview Returns the chains Daya currently supports for settlement, including chain metadata and token availability. Use this endpoint to power chain selectors, validate user input, or decide whether a chain supports deposits, withdrawals, or both. This endpoint is public and does not require authentication. Use the API response for the authoritative live matrix of deposit and withdrawal support per chain and token. For a human-readable environment matrix, see [Supported Chains](/concepts/supported-chains). ## Request Examples ```bash cURL curl --request GET \ --url https://api.daya.co/v1/supported-chains ``` ```javascript JavaScript const response = await fetch('https://api.daya.co/v1/supported-chains'); const chains = await response.json(); console.log(chains); ``` ## Response List of supported chain entries. Internal chain identifier. **Example:** `POLYGON` Human-readable chain name. **Example:** `Polygon` Numeric chain ID where applicable. **Example:** `137` Icon URL for the chain. Token-level support metadata for this chain. Token symbol. **Example:** `USDT` Human-readable token name. **Example:** `Tether USD` Icon URL for the token. Token decimals. **Example:** `6` Token contract address on the chain, when applicable. Whether this token can be used for deposits on the chain. Whether this token can be used for withdrawals on the chain. ### Success Response ```json 200 OK { "data": [ { "chain": "POLYGON", "display_name": "Polygon", "icon": "https://cryptologos.cc/logos/polygon-matic-logo.svg", "chain_id": 137, "tokens": [ { "symbol": "USDT", "display_name": "Tether USD", "icon": "https://cryptologos.cc/logos/tether-usdt-logo.svg", "decimals": 6, "contract_address": "0xc2132D05D31c914a87C6611C10748AEb04B58e8F", "deposit_enabled": true, "withdraw_enabled": false }, { "symbol": "USDC", "display_name": "USD Coin", "icon": "https://cryptologos.cc/logos/usd-coin-usdc-logo.svg", "decimals": 6, "contract_address": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", "deposit_enabled": true, "withdraw_enabled": false } ] }, { "chain": "TEMPO", "display_name": "Tempo", "icon": "https://tempo.xyz/favicon.svg", "chain_id": 4217, "tokens": [ { "symbol": "USDT", "display_name": "Tether USD", "icon": "https://cryptologos.cc/logos/tether-usdt-logo.svg", "decimals": 6, "contract_address": "0x20c00000000000000000000014f22ca97301eb73", "deposit_enabled": true, "withdraw_enabled": false } ] } ] } ``` The Tempo entry is returned only when Tempo deposits are enabled in the current environment. Its public token symbol is `USDT`; the `contract_address` identifies the official USDT0 token used on Tempo. ## Next Steps Use chain support data when creating crypto accounts or on-chain settlement Send balance funds to a supported chain and token pair ### Customers #### Create Customer Path: /api-reference/customers/create-customer Description: Create a new customer under the authenticated merchant ## Overview Create a customer record that can be referenced by `customer_id` in funding account, transfer, and USD virtual account requests. Customers are scoped to the authenticated merchant. For an individual customer, create the customer record, complete tier 1 verification with [`POST /v1/customers/{id}/tier1-verification`](/api-reference/customers/submit-tier1-verification), then submit individual tier 2 KYC with [`POST /v1/customers/{id}/tier2-verification`](/api-reference/customers/submit-tier2-verification). For a business or entity, create the customer record first, then submit tier 2 KYB with [`POST /v1/customers/{id}/tier2-verification`](/api-reference/customers/submit-tier2-verification) using `customer_type: "business"` and the business KYB fields. The `first_name` and `last_name` fields can be omitted for business customer records. ## Authentication Your merchant API key ## Request Body Customer email address (must be valid) **Example:** `customer@example.com` Email is normalized to lowercase and trimmed before storage. Customer first name (1–100 characters) **Example:** `John` Customer last name (1–100 characters) **Example:** `Doe` ## Request Example ```json JSON { "email": "customer@example.com", "first_name": "John", "last_name": "Doe" } ``` ```bash cURL curl --request POST \ --url https://api.daya.co/v1/customers \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "email": "customer@example.com", "first_name": "John", "last_name": "Doe" }' ``` ## Response Unique customer identifier (UUID) Customer email address Customer first name Customer last name Whether the customer has passed verification. `false` on creation. Whether tier 1 KYC has been completed. `false` on creation. Whether tier 2 KYC has been completed. `false` on creation. Customer capabilities and their current status. Empty on creation until verification starts. Current blocking verification or capability issues. Empty on creation. When the customer was created (ISO 8601 timestamp) When the customer was last updated (ISO 8601 timestamp) ### Success Response ```json 201 Created { "id": "650e8400-e29b-41d4-a716-446655440000", "email": "customer@example.com", "first_name": "John", "last_name": "Doe", "is_verified": false, "tier_1_kyc_complete": false, "tier_2_kyc_complete": false, "capabilities": [], "rejection_reasons": [], "created_at": "2026-01-05T15:04:05Z", "updated_at": "2026-01-05T15:04:05Z" } ``` ## Error Responses ```json 400 Bad Request - Validation failed { "error": { "code": "VALIDATION_FAILED", "message": "Validation failed", "request_id": "550e8400-e29b-41d4-a716-446655440000", "validation": "email is required and must be a valid email address" } } ``` ```json 409 Conflict - Duplicate email { "error": { "code": "CONFLICT", "message": "Customer with this email already exists for this merchant", "request_id": "550e8400-e29b-41d4-a716-446655440000" } } ``` ## Notes - Customers are scoped to the authenticated merchant — the same email can exist under different merchants. - Once created, a customer can be referenced by `customer_id` in funding account requests. - Business/entity customers use the same customer object and response shape as individual customers. Submit business KYB before creating a USD virtual account for a business beneficiary. #### List Customers Path: /api-reference/customers/list-customers Description: List customers for the authenticated merchant ## Overview Retrieve a paginated list of customers belonging to the authenticated merchant. Supports filtering by email. ## Authentication Your merchant API key ## Query Parameters Results per page **Default:** `50` | **Max:** `200` Page number to retrieve **Default:** `1` Filter by exact email address **Example:** `customer@example.com` ## Request Example ```bash cURL curl --request GET \ --url 'https://api.daya.co/v1/customers?limit=50' \ --header 'X-Api-Key: YOUR_API_KEY' ``` ```bash cURL - Filter by email curl --request GET \ --url 'https://api.daya.co/v1/customers?email=customer@example.com' \ --header 'X-Api-Key: YOUR_API_KEY' ``` ## Response Array of customer objects Current page number Results per page Total number of customers matching the query Total number of pages available ### Success Response ```json 200 OK { "data": [ { "id": "650e8400-e29b-41d4-a716-446655440000", "email": "customer@example.com", "first_name": "John", "last_name": "Doe", "is_verified": false, "tier_1_kyc_complete": false, "tier_2_kyc_complete": false, "capabilities": [], "rejection_reasons": [], "created_at": "2026-01-05T15:04:05Z", "updated_at": "2026-01-05T15:04:05Z" } ], "page": 1, "limit": 50, "total": 1, "total_pages": 1 } ``` ## Pagination This endpoint uses **page-based pagination**. 1. Make your initial request (optionally with `limit` and `page`) 2. Check `total_pages` to determine how many pages are available 3. Increment `page` to fetch subsequent pages until you reach `total_pages` #### Get Customer Path: /api-reference/customers/get-customer Description: Retrieve a single customer by ID ## Overview Retrieve details for a specific customer belonging to the authenticated merchant. ## Authentication Your merchant API key ## Path Parameters Customer ID (UUID) **Example:** `650e8400-e29b-41d4-a716-446655440000` ## Request Example ```bash cURL curl --request GET \ --url https://api.daya.co/v1/customers/650e8400-e29b-41d4-a716-446655440000 \ --header 'X-Api-Key: YOUR_API_KEY' ``` ## Response Unique customer identifier (UUID) Customer email address Customer first name Customer last name Whether the customer has passed verification. Whether tier 1 KYC has been completed. Whether tier 2 KYC has been completed. Customer capabilities and their current status. Capability identifier. Current values: `base`, `usd_banking`. Capability status: `approved`, `pending`, `rejected`, or `missing`. Whether the customer can receive a Paystack instruction for a permanent NGN funding account. `READY` or `REQUIRES_INFORMATION`. Bank code submitted through Tier 1 KYC, when available. Last four digits of the submitted account number. The full account number is never returned. Fields to add to Tier 1 KYC before requesting a Paystack instruction. Current blocking verification or capability issues. Empty array when the customer is approved. When the customer was created (ISO 8601 timestamp) When the customer was last updated (ISO 8601 timestamp) ### Success Response ```json 200 OK { "id": "650e8400-e29b-41d4-a716-446655440000", "email": "customer@example.com", "first_name": "John", "last_name": "Doe", "is_verified": false, "tier_1_kyc_complete": false, "tier_2_kyc_complete": false, "capabilities": [ { "name": "base", "status": "pending" } ], "paystack_funding_account_readiness": { "status": "REQUIRES_INFORMATION", "missing_fields": [ "bank_account.account_number", "bank_account.bank_code" ] }, "rejection_reasons": [], "created_at": "2026-01-05T15:04:05Z", "updated_at": "2026-01-05T15:04:05Z" } ``` ## Error Responses ```json 400 Bad Request - Invalid UUID { "error": { "code": "BAD_REQUEST", "message": "Invalid UUID format", "request_id": "550e8400-e29b-41d4-a716-446655440000" } } ``` ```json 404 Not Found { "error": { "code": "NOT_FOUND", "message": "Customer not found", "request_id": "550e8400-e29b-41d4-a716-446655440000" } } ``` #### Update customer Path: /api-reference/customers/update-customer Description: Update an existing customer's details ## Overview Update the details of an existing customer. Only provided fields are updated; omitted fields remain unchanged. ## Authentication Your merchant API key ## Path Parameters Customer ID (UUID) **Example:** `650e8400-e29b-41d4-a716-446655440000` ## Request Body Customer email address. Must be a valid email if provided. **Example:** `customer@example.com` Customer first name (1-100 characters) **Example:** `Jane` Customer last name (1-100 characters) **Example:** `Doe` ## Request Examples ```json JSON { "email": "customer@example.com", "first_name": "Jane", "last_name": "Doe" } ``` ```bash cURL curl --request PATCH \ --url https://api.daya.co/v1/customers/650e8400-e29b-41d4-a716-446655440000 \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "email": "customer@example.com", "first_name": "Jane", "last_name": "Doe" }' ``` ```javascript JavaScript const response = await fetch( 'https://api.daya.co/v1/customers/650e8400-e29b-41d4-a716-446655440000', { method: 'PATCH', headers: { 'X-Api-Key': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ email: 'customer@example.com', first_name: 'Jane', last_name: 'Doe' }) } ); const customer = await response.json(); console.log(customer); ``` ## Response Returns the updated customer object. See [Get customer](/api-reference/customers/get-customer#response) for the full field list. ### Success Response ```json 200 OK { "id": "650e8400-e29b-41d4-a716-446655440000", "email": "customer@example.com", "first_name": "Jane", "last_name": "Doe", "is_verified": false, "tier_1_kyc_complete": false, "tier_2_kyc_complete": false, "capabilities": [], "rejection_reasons": [], "created_at": "2026-01-05T15:04:05Z", "updated_at": "2026-01-06T10:30:00Z" } ``` ## Error Responses ```json 400 Bad Request - Invalid UUID { "error": { "code": "BAD_REQUEST", "message": "Invalid UUID format", "request_id": "550e8400-e29b-41d4-a716-446655440000" } } ``` ```json 400 Bad Request - Validation failed { "error": { "code": "VALIDATION_FAILED", "message": "Validation failed", "request_id": "550e8400-e29b-41d4-a716-446655440000", "validation": "first_name must be between 1 and 100 characters" } } ``` ```json 404 Not Found { "error": { "code": "NOT_FOUND", "message": "Customer not found", "request_id": "550e8400-e29b-41d4-a716-446655440000" } } ``` ```json 409 Conflict - Email already exists { "error": { "code": "CONFLICT", "message": "Customer with this email already exists for this merchant", "request_id": "550e8400-e29b-41d4-a716-446655440000" } } ``` ## Next Steps Retrieve the full customer record Verify a customer's identity for permanent NGN funding accounts #### Submit Tier 1 verification Path: /api-reference/customers/submit-tier1-verification Description: Submit identity verification for a customer ## Overview Submit Tier 1 identity verification (BVN + phone number + selfie) for a customer. This is required before creating permanent NGN funding accounts. Include a verified customer bank account only if you want a Paystack funding-account instruction. Flutterwave does not require it. Tier 1 KYC is complete once the BVN check succeeds. ## Authentication Your merchant API key ## Path Parameters Customer ID (UUID) **Example:** `650e8400-e29b-41d4-a716-446655440000` ## Request Body 11-digit Bank Verification Number **Example:** `22345678901` Must be exactly 11 digits. The BVN is validated against the national identity database. Customer's phone number. Send the 11-digit national form or the `+234` international form. **Example:** `+2348012345678` Face image for identity matching. Accepts an HTTPS URL to a jpg/png image, or base64-encoded image data up to 1 MiB decoded. **Example:** `https://example.com/selfie.jpg` For base64, include the data URI prefix: `data:image/jpeg;base64,/9j/4AAQ...`. The decoded payload must not exceed 1 MiB. Optional NGN bank account belonging to the customer. This is not the funding account the customer will receive. The customer's 10-digit NGN bank account number. The bank code returned by [`GET /v1/banks`](/api-reference/banks/list-banks). Before including `bank_account`, get the supported `bank_code` from [`GET /v1/banks`](/api-reference/banks/list-banks) and verify the account with [`POST /v1/banks/resolve`](/api-reference/banks/resolve-bank-account). Then send the verified `account_number` and `bank_code` together. If the bank details are rejected after the BVN check succeeds, the customer remains Tier 1 verified. Correct the bank details with the [Update Tier 1 bank account](/api-reference/customers/update-tier1-verification) endpoint. ## Request Examples ```json JSON - URL Image { "bvn": "22345678901", "phone_number": "+2348012345678", "image_url": "https://example.com/selfie.jpg", "bank_account": { "account_number": "0123456789", "bank_code": "058" } } ``` ```json JSON - Base64 Image { "bvn": "22345678901", "phone_number": "+2348012345678", "image_url": "data:image/jpeg;base64,/9j/4AAQSkZJRg..." } ``` ```bash cURL curl --request POST \ --url https://api.daya.co/v1/customers/650e8400-e29b-41d4-a716-446655440000/tier1-verification \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "bvn": "22345678901", "phone_number": "+2348012345678", "image_url": "https://example.com/selfie.jpg", "bank_account": { "account_number": "0123456789", "bank_code": "058" } }' ``` ```javascript JavaScript const response = await fetch( 'https://api.daya.co/v1/customers/650e8400-e29b-41d4-a716-446655440000/tier1-verification', { method: 'POST', headers: { 'X-Api-Key': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ bvn: '22345678901', phone_number: '+2348012345678', image_url: 'https://example.com/selfie.jpg', bank_account: { account_number: '0123456789', bank_code: '058' } }) } ); const result = await response.json(); console.log(result); ``` ## Response Returns the updated customer object. See [Get customer](/api-reference/customers/get-customer#response) for the full field list. On success, `tier_1_kyc_complete` flips to `true`. ### Success Response ```json 200 OK { "id": "650e8400-e29b-41d4-a716-446655440000", "email": "customer@example.com", "first_name": "John", "last_name": "Doe", "is_verified": false, "tier_1_kyc_complete": true, "tier_2_kyc_complete": false, "capabilities": [ { "name": "base", "status": "approved" } ], "paystack_funding_account_readiness": { "status": "READY", "bank_code": "058", "account_number_last4": "6789", "missing_fields": [], "updated_at": "2026-01-06T12:00:00Z" }, "rejection_reasons": [], "created_at": "2026-01-05T15:04:05Z", "updated_at": "2026-01-06T12:00:00Z" } ``` ## Bank Account Verification Result The result webhook includes `customer_id`, `provider`, `status`, and `identity_version`. Failed results also include `failure_code` and `failure_message` so you can tell the customer what needs to be corrected. See [Bank Account Verification Events](/api-reference/webhooks/events#bank-account-verification-events) for complete payloads and retry-safe handling. ## Error Responses ```json 400 Bad Request - Validation failed { "error": { "code": "VALIDATION_FAILED", "message": "Validation failed", "request_id": "550e8400-e29b-41d4-a716-446655440000", "validation": "bvn must be exactly 11 digits" } } ``` ```json 400 Bad Request - BVN verification failed { "error": { "code": "BVN_VERIFICATION_FAILED", "message": "BVN verification failed", "request_id": "550e8400-e29b-41d4-a716-446655440000" } } ``` ```json 400 Bad Request - Face mismatch { "error": { "code": "FACE_MISMATCH", "message": "Face verification failed: selfie does not match BVN record", "request_id": "550e8400-e29b-41d4-a716-446655440000" } } ``` ```json 404 Not Found { "error": { "code": "NOT_FOUND", "message": "Customer not found", "request_id": "550e8400-e29b-41d4-a716-446655440000" } } ``` ```json 502 Bad Gateway - Provider unavailable { "error": { "code": "INTEGRATION_FAILED", "message": "Verification provider unavailable, please try again", "request_id": "550e8400-e29b-41d4-a716-446655440000" } } ``` ## Next Steps Create a permanent funding account for the verified customer Add bank details for an already verified customer Check the customer's current verification status Handle Paystack bank account verification results #### Update Tier 1 bank account Path: /api-reference/customers/update-tier1-verification Description: Add or replace bank details for a Tier 1 verified customer ## Overview Add or replace the bank account attached to a customer's completed Tier 1 KYC. Use this when the customer finished Tier 1 without bank details, when their previous bank details need to be corrected, or when an older verification needs the customer's phone number added. This endpoint does not repeat BVN or selfie verification, and it does not create a funding account. It starts a new asynchronous Paystack bank account verification for the submitted details, even when the customer does not yet have a permanent NGN funding account. If Paystack is temporarily unavailable, Daya keeps the verification pending and starts it automatically after the provider recovers. ## Authentication Your merchant API key ## Path Parameters Customer ID (UUID) ## Request Body Customer's phone number. Send the 11-digit national form or the `+234` international form. **Example:** `+2348012345678` NGN bank account belonging to the customer. This is not the funding account the customer will receive. The customer's 10-digit NGN bank account number. The bank code returned by [`GET /v1/banks`](/api-reference/banks/list-banks). Before submitting `bank_account`, get the supported `bank_code` from [`GET /v1/banks`](/api-reference/banks/list-banks) and verify the account with [`POST /v1/banks/resolve`](/api-reference/banks/resolve-bank-account). Then send the verified `account_number` and `bank_code` together. ## Request Example ```bash curl --request PATCH \ --url https://api.daya.co/v1/customers/650e8400-e29b-41d4-a716-446655440000/tier1-verification \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "phone_number": "+2348012345678", "bank_account": { "account_number": "0123456789", "bank_code": "058" } }' ``` ## Response Returns the customer with `paystack_funding_account_readiness.status` set to `READY` when the bank details are accepted locally. This does not mean Paystack has completed verification. ```json 200 OK { "id": "650e8400-e29b-41d4-a716-446655440000", "tier_1_kyc_complete": true, "paystack_funding_account_readiness": { "status": "READY", "bank_code": "058", "account_number_last4": "6789", "missing_fields": [], "updated_at": "2026-01-06T12:00:00Z" } } ``` The response shows only the last four digits of the account number, not the full account number. ## Verification Result Subscribe to: - `customer.bank_account_verification.succeeded` - `customer.bank_account_verification.failed` Each result includes an `identity_version`. A later bank detail update increments the version, so ignore a result for an older version after you have received a newer result. Failed results include `failure_code` and `failure_message` that you can use to explain what the customer should correct. If verification succeeds and the customer already has a permanent NGN funding account, Daya starts creating its Paystack instruction. Daya sends `funding_account.active` when Paystack makes a pending account usable, or `funding_account.updated` when another instruction was already active. If the customer does not have a permanent account, Daya retains the successful verification until you create one. See [Bank Account Verification Events](/api-reference/webhooks/events#bank-account-verification-events) for the payload fields and examples. ## Next Steps Handle the asynchronous verification result Create a permanent NGN funding account when the customer needs one #### Submit tier 2 verification Path: /api-reference/customers/submit-tier2-verification Description: Submit tier 2 KYC or business KYB verification for a customer ## Overview Submits tier 2 verification for an existing customer. Use the individual KYC payload for people, or pass `customer_type: "business"` with business KYB fields for an entity customer. Tier 2 verification is required before the customer can use Bridge-backed banking features. Business KYB can also be submitted through the compatibility route `POST /v1/customers/{id}/business-verification`. New integrations should prefer this tier 2 endpoint with `customer_type: "business"`. **Tier 2 verification is required for:** - Creating USD virtual accounts - Creating `US_BANK_ACCOUNT` or `SWIFT_BANK_ACCOUNT` recipients - Sending USD transfers to ACH, wire, or SWIFT recipients **Idempotency behavior:** - If tier 2 status is already `PENDING`, resubmission is blocked. - If tier 2 status is already `VERIFIED`, resubmission is blocked. ## Authentication Your merchant API key ## Path Parameters Customer ID (UUID format) **Example:** `650e8400-e29b-41d4-a716-446655440000` ## Request Body Verification subject type. Omit this field or use `individual` for individual KYC. Use `business` for business/entity KYB. **Allowed values:** `individual` | `business` ### Individual KYC fields Use these fields when `customer_type` is omitted or set to `individual`. Customer's residential address. Street address line 1 **Example:** `123 Main St` City **Example:** `Lagos` Short state or subdivision code. Must be 10 characters or fewer. **Example:** `LA` Postal code **Example:** `100001` ISO 3166-1 alpha-3 country code **Example:** `NGA` ISO 3166-1 alpha-3 country code for the customer's nationality. **Example:** `NGA` Date of birth in `YYYY-MM-DD` format. **Example:** `1990-05-15` Array of identity documents and tax IDs. Non-US customers should include at least one government photo ID (`passport`, `drivers_license`, or `national_id`) with `image_front`. Add `tin` when Daya or the verification provider asks for a tax identification number. Identification type. Use `tin` when Daya or the verification provider asks for the customer's tax identification number. **Allowed values:** `passport` | `drivers_license` | `national_id` | `tin` ISO 3166-1 alpha-3 country code of the issuing country. **Example:** `NGA` Document or tax identification number. Required when `type` is `tin`. **Example:** `A12345678` Document expiration date in `YYYY-MM-DD` format. **Example:** `2030-01-01` Front image of the document. Required for government photo ID types: `passport`, `drivers_license`, and `national_id`. Not required for `tin` when you are only submitting the tax identification number. If Daya specifically asks for a tax document image, send it as a base64 data URL up to 1 MiB decoded. Compress large camera images before encoding. **Example:** `data:image/png;base64,iVBORw0KGgo...` Back image of the document. Must be a base64 data URL up to 1 MiB decoded when provided. **Example:** `data:image/png;base64,iVBORw0KGgo...` Tax ID is not required for every customer. Some higher-risk customers or provider follow-up reviews may require it. For non-US customers, `tin` is additional information and should be sent alongside a government photo ID. Send `type: "tin"` in Daya's API; Daya maps it to the provider's country-specific tax identifier type before submission. Source of the customer's funds. **Allowed values:** `company_funds` | `ecommerce_reseller` | `gambling_proceeds` | `gifts` | `government_benefits` | `inheritance` | `investments_loans` | `pension_retirement` | `salary` | `sale_of_assets_real_estate` | `savings` | `someone_elses_funds` Purpose of the account. **Allowed values:** `charitable_donations` | `ecommerce_retail_payments` | `investment_purposes` | `operating_a_company` | `other` | `payments_to_friends_or_family_abroad` | `personal_or_living_expenses` | `protect_wealth` | `purchase_goods_and_services` | `receive_payment_for_freelancing` | `receive_salary` Expected monthly payment volume bracket. **Allowed values:** `0_4999` | `5000_9999` | `10000_49999` | `50000_plus` Whether the customer is acting as an intermediary for a third party. **Example:** `false` Customer's current employment status. **Allowed values:** `employed` | `homemaker` | `retired` | `self_employed` | `student` | `unemployed` Customer's most recent occupation category. **Allowed values:** `BUSINESS_ADMIN` | `STEM` | `HEALTHCARE_SOCIAL` | `EDUCATION_ARTS_MEDIA` | `SERVICE_PUBLIC_SAFETY` | `TRADES_LABOR` Supporting documents. Optional at the schema level, but **conditionally required** when `expected_monthly_payments_usd` is above the `0_4999` bucket. For `5000_9999`, `10000_49999`, and `50000_plus` buckets, supporting documents should be provided. Document file. Must be a base64 data URL up to 5 MiB decoded. **Example:** `data:image/png;base64,iVBORw0KGgo...` Document purposes (at least one required). **Allowed values:** `proof_of_address` | `proof_of_source_of_funds` ### Business KYB fields Use these fields when `customer_type` is `business`. Daya will submit the customer to the business verification provider flow and update the same customer object. Legal name of the business or entity. Daya also uses this as the trade name when submitting to the verification provider. **Example:** `Ada Labs Ltd` Short description of what the business does. Provider-supported business type, such as `corporation`, `llc`, or another type approved for your use case. Registered business address using the same address shape as `residential_address`. Physical operating address using the same address shape as `residential_address`. If omitted, Daya uses `registered_address` as the physical address. Whether the entity is a DAO. Directors, signers, controllers, and beneficial owners associated with the business. At least one associated person is required. At least one associated person must have `has_control: true`, and at least one must have `is_signer: true`. First name. Last name. Email address. Residential address using the same address shape as `residential_address`. Date of birth in `YYYY-MM-DD` format. Whether the person is a beneficial owner. Whether the person has control over the entity. At least one associated person must be a control person. Whether the person is authorized to sign for the entity. At least one associated person must be a signer. The person's title at the entity. Required when `has_control` is `true`. ISO 3166-1 alpha-3 nationality code. Government ID and tax identification details for the associated person. Estimated annual revenue bracket. **Example:** `1000000_4999999` Expected monthly payment volume bracket. **Allowed values:** `0_4999` | `5000_9999` | `10000_49999` | `50000_plus` Whether the business operates in prohibited countries. Purpose of the account. **Example:** `operating_a_company` Source of business funds. **Example:** `company_funds` Whether the business conducts money services. Business identifying information. Include the business tax ID as an item with `type: "tin"` and `number`. Business supporting documents. Include at least one `proof_of_address` document and one `proof_of_source_of_funds` document. For business KYB, associated persons carry the required government ID documents. A non-US business tax ID does not require the business itself to submit a passport. ## Request Example ### Individual KYC example ```bash cURL curl --request POST \ --url https://api.daya.co/v1/customers/650e8400-e29b-41d4-a716-446655440000/tier2-verification \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "residential_address": { "street_line_1": "123 Main St", "city": "Lagos", "subdivision": "LA", "postal_code": "100001", "country": "NGA" }, "nationality": "NGA", "birth_date": "1990-05-15", "identifying_information": [ { "type": "passport", "issuing_country": "NGA", "number": "A12345678", "expiration": "2030-01-01", "image_front": "data:image/png;base64,iVBORw0KGgo..." } ], "source_of_funds": "salary", "account_purpose": "personal_or_living_expenses", "expected_monthly_payments_usd": "0_4999", "acting_as_intermediary": false, "employment_status": "employed", "most_recent_occupation": "BUSINESS_ADMIN" }' ``` ### Business KYB example ```bash cURL curl --request POST \ --url https://api.daya.co/v1/customers/650e8400-e29b-41d4-a716-446655440000/tier2-verification \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "customer_type": "business", "business_legal_name": "Ada Labs Ltd", "business_description": "Cross-border collection and treasury operations", "business_type": "corporation", "registered_address": { "street_line_1": "10 Market Street", "city": "London", "subdivision": "LDN", "postal_code": "EC1A 1AA", "country": "GBR" }, "is_dao": false, "associated_persons": [ { "first_name": "Jane", "last_name": "Doe", "email": "jane@example.com", "residential_address": { "street_line_1": "10 Market Street", "city": "London", "subdivision": "LDN", "postal_code": "EC1A 1AA", "country": "GBR" }, "birth_date": "1990-05-15", "has_ownership": true, "has_control": true, "is_signer": true, "is_director": true, "ownership_percentage": 50, "relationship_established_at": "2020-01-01", "nationality": "GBR", "identifying_information": [ { "type": "passport", "issuing_country": "GBR", "number": "123456789", "expiration": "2030-01-01", "image_front": "data:image/png;base64,iVBORw0KGgo..." } ] } ], "estimated_annual_revenue_usd": "1000000_4999999", "expected_monthly_payments_usd": "50000_plus", "operates_in_prohibited_countries": false, "account_purpose": "operating_a_company", "source_of_funds": "company_funds", "conducts_money_services": false, "identifying_information": [ { "type": "tin", "issuing_country": "GBR", "number": "GB123456789" } ], "documents": [ { "purposes": ["proof_of_address"], "file": "data:image/png;base64,iVBORw0KGgo..." }, { "purposes": ["proof_of_source_of_funds"], "file": "data:image/png;base64,iVBORw0KGgo..." } ] }' ``` ### Add a tax ID when requested If Daya or the verification provider asks for the customer's tax identification number, add a `tin` item to `identifying_information` and resubmit Tier 2 verification: ```json { "identifying_information": [ { "type": "passport", "issuing_country": "NGA", "number": "A12345678", "expiration": "2030-01-01", "image_front": "data:image/png;base64,iVBORw0KGgo..." }, { "type": "tin", "issuing_country": "NGA", "number": "1234567890" } ] } ``` ## Response Returns the updated customer object with verification status. Customer ID (UUID) Customer email address Customer first name Customer last name Whether the customer has passed verification Whether tier 1 KYC has been completed. Whether tier 2 KYC has been completed. `true` once Bridge approves the submitted data. Customer capabilities and their current status. Capability name. Current values: `base`, `usd_banking`. Capability status: `approved`, `pending`, `rejected`, or `missing`. Current blocking verification/capability issues. Empty array when the customer is approved for the relevant capability. When the customer was created (ISO 8601) When the customer was last updated (ISO 8601) ```json 200 OK { "id": "650e8400-e29b-41d4-a716-446655440000", "email": "customer@example.com", "first_name": "John", "last_name": "Doe", "is_verified": false, "tier_1_kyc_complete": true, "tier_2_kyc_complete": false, "capabilities": [ { "name": "base", "status": "approved" }, { "name": "usd_banking", "status": "pending" } ], "rejection_reasons": [], "created_at": "2026-01-05T15:04:05Z", "updated_at": "2026-01-05T16:00:00Z" } ``` ```json 400 Bad Request - Already pending { "error": { "code": "TIER2_ALREADY_PENDING", "message": "Tier 2 verification is already pending", "request_id": "550e8400-e29b-41d4-a716-446655440000" } } ``` ```json 400 Bad Request - Already verified { "error": { "code": "TIER2_ALREADY_VERIFIED", "message": "Customer has already completed tier 2 verification", "request_id": "550e8400-e29b-41d4-a716-446655440000" } } ``` ### Funding Accounts #### Create a funding account Path: /api-reference/funding-accounts/create-funding-account Description: Create a way for a customer to send NGN or crypto into Daya ```bash Create NGN funding account curl --request POST \ --url https://api.daya.co/v1/funding-accounts \ --header 'Content-Type: application/json' \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'X-Idempotency-Key: create-funding-account-001' \ --data '{ "type": "TEMPORARY", "rail": "NGN_VIRTUAL_ACCOUNT", "customer": { "customer_id": "650e8400-e29b-41d4-a716-446655440000" }, "currency": "NGN", "amount": 50000, "developer_fee": { "percentage": "2.5" }, "settlement_destination": { "type": "ONCHAIN", "rate_id": "550e8400-e29b-41d4-a716-446655440000", "destination_asset": "USDC", "destination_chain": "BASE", "destination_address": "0x742d35cc6634c0532925a3b844bc9e7595f2bd18" } }' ``` ```bash Create crypto funding account curl --request POST \ --url https://api.daya.co/v1/funding-accounts \ --header 'Content-Type: application/json' \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'X-Idempotency-Key: create-funding-account-002' \ --data '{ "type": "PERMANENT", "rail": "CRYPTO_ADDRESS", "customer": { "customer_id": "650e8400-e29b-41d4-a716-446655440000" }, "asset": "USDC", "chain": "BASE", "settlement_destination": { "type": "INTERNAL_BALANCE" } }' ``` ```bash Create Tempo USDT funding account curl --request POST \ --url https://api.daya.co/v1/funding-accounts \ --header 'Content-Type: application/json' \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'X-Idempotency-Key: create-funding-account-003' \ --data '{ "type": "PERMANENT", "rail": "CRYPTO_ADDRESS", "customer": { "customer_id": "650e8400-e29b-41d4-a716-446655440000" }, "asset": "USDT", "chain": "TEMPO", "settlement_destination": { "type": "INTERNAL_BALANCE" } }' ``` ## Overview Create a funding account for an existing customer. The response includes the public funding account object and the payment details the customer can use once setup succeeds. Permanent NGN funding accounts return provider instructions for Flutterwave and, when enabled, Paystack. Add the customer's verified bank details through Tier 1 KYC so Paystack can verify the customer. Daya evaluates both providers when creating the account, so Paystack can supply the usable instruction while Flutterwave is unavailable. Show the customer any instruction whose `status` is `ACTIVE`; do not send a provider in this request. `customer.customer_id` is required. This endpoint does not create customers inline. ## Authentication Your merchant API key Unique idempotency key for request deduplication ## Request Body Funding account type. Allowed values: `TEMPORARY`, `PERMANENT`. Receive rail. Allowed values: `NGN_VIRTUAL_ACCOUNT`, `CRYPTO_ADDRESS`. Existing customer details. Include `customer_id`. For permanent NGN virtual accounts, the customer must have completed Tier 1 KYC. Required for `NGN_VIRTUAL_ACCOUNT`. Must be `NGN`. Required for `CRYPTO_ADDRESS`. Supported values: `USDC`, `USDT`. Required for `CRYPTO_ADDRESS`. Allowed values: `APTOS`, `BASE`, `BSC`, `CELO`, `ETHEREUM`, `POLYGON`, `SOLANA`, `SUI`, `TEMPO`, `TRON`. Chain availability depends on the asset, environment, and direction. Use `GET /v1/supported-chains` as the authoritative live source. When enabled in production, `TEMPO` accepts `USDT` deposits only; it is not available in Sandbox. Required for temporary NGN virtual accounts. Accepted only for `TEMPORARY` accounts. This is the principal amount requested before any payment-provider collection charge. Do not use the request value as the customer's payment instruction; use `amount` from the create response. Optional fee that your merchant account keeps from each deposit received through this funding account. Omit to use `0%`. Percentage of each received deposit that your merchant account keeps. Use a decimal string from `0` to `50`. This is a percentage value, not basis points: `0.5` means `0.5%`, `2` means `2%`, and `50` means `50%`. **Example:** `"2.5"` ### `settlement_destination` `settlement_destination` is a nested object. Do not send dotted keys such as `settlement_destination.type`. | Field | Type | Required when | |-------|------|---------------| | `type` | string | Always. Allowed values depend on the funding account `rail` and `type`; see the matrix below. | | `rate_id` | string | Required for temporary funding accounts that use a quoted conversion: temporary `NGN_VIRTUAL_ACCOUNT` accounts settling to `INTERNAL_BALANCE` or `ONCHAIN`, and temporary `CRYPTO_ADDRESS` accounts settling through `NGN_PAYOUT`. Do not send it for permanent accounts. | | `destination_asset` | string | `rail` is `NGN_VIRTUAL_ACCOUNT` and `type` is `ONCHAIN`. Supported values: `USDC`, `USDT`. | | `destination_chain` | string | `rail` is `NGN_VIRTUAL_ACCOUNT` and `type` is `ONCHAIN`. | | `destination_address` | string | `rail` is `NGN_VIRTUAL_ACCOUNT` and `type` is `ONCHAIN`. | | `destination_bank` | object | `rail` is `CRYPTO_ADDRESS` and `type` is `NGN_PAYOUT`. Fixed request shape: `account_number`, `bank_code`. | #### Allowed settlement destination types | `rail` | Funding account `type` | Allowed `settlement_destination.type` values | |--------|-------------------------|----------------------------------------------| | `NGN_VIRTUAL_ACCOUNT` | `TEMPORARY` | `INTERNAL_BALANCE`, `ONCHAIN` | | `NGN_VIRTUAL_ACCOUNT` | `PERMANENT` | `INTERNAL_BALANCE`, `ONCHAIN` | | `CRYPTO_ADDRESS` | `TEMPORARY` | `NGN_PAYOUT` | | `CRYPTO_ADDRESS` | `PERMANENT` | `INTERNAL_BALANCE`, `NGN_PAYOUT` | Do not send `NGN_PAYOUT` for `NGN_VIRTUAL_ACCOUNT` funding accounts, and do not send `ONCHAIN` for `CRYPTO_ADDRESS` funding accounts. #### `destination_bank` | Field | Type | Required | |-------|------|----------| | `account_number` | string | Yes | | `bank_code` | string | Yes | Before sending `destination_bank`, fetch supported banks with [`GET /v1/banks`](/api-reference/banks/list-banks), then verify the account with [`POST /v1/banks/resolve`](/api-reference/banks/resolve-bank-account). Use the verified `bank_code` and `account_number` in the funding account request. Do not send `account_name`; Daya returns the resolved account name in the funding account response. ## Settlement Destination Shapes ```json Internal balance { "settlement_destination": { "type": "INTERNAL_BALANCE", "rate_id": "550e8400-e29b-41d4-a716-446655440000" } } ``` For temporary funding accounts, `rate_id` is required and the rate is locked for the temporary account's validity window. For permanent funding accounts, omit `rate_id`; the rate is applied when each deposit is processed. ```json NGN virtual account to onchain settlement { "settlement_destination": { "type": "ONCHAIN", "rate_id": "550e8400-e29b-41d4-a716-446655440000", "destination_asset": "USDC", "destination_chain": "BASE", "destination_address": "0x742d35cc6634c0532925a3b844bc9e7595f2bd18" } } ``` ```json Crypto address to NGN payout settlement { "settlement_destination": { "type": "NGN_PAYOUT", "rate_id": "550e8400-e29b-41d4-a716-446655440000", "destination_bank": { "account_number": "0123456789", "bank_code": "058" } } } ``` For `NGN_PAYOUT`, `destination_bank` has a fixed request shape: `account_number` and `bank_code`. Get the supported bank first, resolve the account, then send the verified details. ## Request Examples ```json NGN temporary to onchain { "type": "TEMPORARY", "rail": "NGN_VIRTUAL_ACCOUNT", "customer": { "customer_id": "650e8400-e29b-41d4-a716-446655440000" }, "currency": "NGN", "amount": 50000, "developer_fee": { "percentage": "2.5" }, "settlement_destination": { "type": "ONCHAIN", "rate_id": "550e8400-e29b-41d4-a716-446655440000", "destination_asset": "USDC", "destination_chain": "BASE", "destination_address": "0x742d35cc6634c0532925a3b844bc9e7595f2bd18" } } ``` ```json Crypto permanent to balance { "type": "PERMANENT", "rail": "CRYPTO_ADDRESS", "customer": { "customer_id": "650e8400-e29b-41d4-a716-446655440000" }, "asset": "USDC", "chain": "BASE", "settlement_destination": { "type": "INTERNAL_BALANCE" } } ``` ```json Tempo USDT permanent to balance { "type": "PERMANENT", "rail": "CRYPTO_ADDRESS", "customer": { "customer_id": "650e8400-e29b-41d4-a716-446655440000" }, "asset": "USDT", "chain": "TEMPO", "settlement_destination": { "type": "INTERNAL_BALANCE" } } ``` ## Response Returns a `funding_account` object. For NGN funding accounts, the bank account the customer should pay into is returned in `instructions`. Read `provider`, `status`, `provider_availability`, and `bank_name` on every provider-backed instruction. `provider` identifies the funding provider; `bank_name` is the bank the customer pays. These values may be different. `bank_code` is included when the provider returns it or Daya can safely resolve it from the assigned `bank_name`. If it is omitted, use the returned `bank_name` and `account_number`; do not substitute a code from the request. Every permanent NGN funding account response includes Flutterwave and Paystack. If a Paystack account has not been requested because the customer's bank details are missing, the Paystack entry is `REQUIRES_INFORMATION` and lists the fields you must submit. It has no virtual account number until Daya attempts provisioning and the provider returns account details. Show only `ACTIVE` instructions with account details to the customer. For a temporary NGN funding account, the response `amount` is the exact amount the customer must transfer. It may differ from the request `amount` because the payment provider can add a collection charge. Display and transfer the response value exactly; do not recalculate or round it. ```json NGN funding account response { "object": "funding_account", "id": "750e8400-e29b-41d4-a716-446655440000", "type": "TEMPORARY", "status": "ACTIVE", "rail": "NGN_VIRTUAL_ACCOUNT", "customer_id": "650e8400-e29b-41d4-a716-446655440000", "currency": "NGN", "amount": "50000.50", "developer_fee": { "percentage": "2.5" }, "settlement_destination": { "type": "ONCHAIN", "rate_id": "550e8400-e29b-41d4-a716-446655440000", "destination_asset": "USDC", "destination_chain": "BASE", "destination_address": "0x742d35cc6634c0532925a3b844bc9e7595f2bd18" }, "instructions": [ { "type": "NGN_VIRTUAL_ACCOUNT", "provider": "FLUTTERWAVE", "provider_availability": { "status": "DEGRADED", "message": "This provider is experiencing funding-account delays." }, "status": "ACTIVE", "bank_name": "Wema Bank", "bank_code": "035", "account_number": "1234567890", "account_name": "Daya - Ada Lovelace", "currency": "NGN" } ], "created_at": "2026-01-05T15:04:05Z", "updated_at": "2026-01-05T15:04:10Z" } ``` ### Provider availability For provider-backed payment details, `provider_availability` describes the provider's current funding-account service state: | Field | Meaning | |-------|---------| | `status` | Current provider state: `OPERATIONAL`, `DEGRADED`, or `UNAVAILABLE`. | | `message` | A user-facing explanation when service is disrupted. It is omitted when the provider is operational. You can show this message to affected customers. | The availability states mean: | State | Meaning | |-------|---------| | `OPERATIONAL` | The provider is operating normally and accepts new requests. | | `DEGRADED` | The provider accepts new requests but is working with a known delay or limitation. Read `message` for current guidance. | | `UNAVAILABLE` | The provider is temporarily unavailable for new requests. Read `message` for the current guidance. | For permanent NGN accounts, availability is evaluated per provider. If Flutterwave is `UNAVAILABLE` but Paystack can accept the request and the customer has verified bank details, creation continues through Paystack. Flutterwave remains in the returned `instructions` array with `provider_availability.status: UNAVAILABLE`. The same applies in reverse when Paystack is unavailable and Flutterwave can create the account. Daya returns HTTP `503` with error code `PROVIDER_UNAVAILABLE` only when neither configured permanent NGN provider can accept the new request. Temporary NGN accounts and crypto accounts use one provider and return this error when that provider is unavailable. For crypto funding accounts, `asset` identifies the stablecoin the address should receive, `chain` identifies the network, and `instructions` contains the wallet address. ### Permanent NGN response ```json { "object": "funding_account", "id": "750e8400-e29b-41d4-a716-446655440000", "type": "PERMANENT", "status": "ACTIVE", "rail": "NGN_VIRTUAL_ACCOUNT", "customer_id": "650e8400-e29b-41d4-a716-446655440000", "currency": "NGN", "instructions": [ { "type": "NGN_VIRTUAL_ACCOUNT", "provider": "FLUTTERWAVE", "provider_availability": { "status": "UNAVAILABLE", "message": "Flutterwave funding accounts are temporarily unavailable." }, "status": "PENDING", "currency": "NGN", "required_fields": [], "failure": null }, { "type": "NGN_VIRTUAL_ACCOUNT", "provider": "PAYSTACK", "provider_availability": { "status": "OPERATIONAL" }, "status": "ACTIVE", "bank_name": "Wema Bank", "bank_code": "035", "account_number": "9876543210", "account_name": "Ada Lovelace", "currency": "NGN", "required_fields": [], "failure": null } ] } ``` This example shows the fallback explicitly: the Flutterwave instruction lifecycle is `PENDING`, while its current service state is `provider_availability.status: UNAVAILABLE`; the Paystack instruction is `ACTIVE` and can be shown to the customer. Show only instructions with `status: ACTIVE`. For a pending account that becomes usable after Paystack verification, wait for `funding_account.active`. Later changes to an instruction are sent as `funding_account.updated`. If an instruction is `REQUIRES_INFORMATION`, submit the fields listed in `required_fields`, then call this endpoint again with the same account details. Daya returns the existing live permanent funding account with its latest instructions. #### List funding accounts Path: /api-reference/funding-accounts/list-funding-accounts Description: List funding accounts with optional filters ## Overview Retrieve a paginated list of funding accounts for the authenticated merchant. ## Authentication Your merchant API key ## Query Parameters Filter by customer ID. Filter by `TEMPORARY` or `PERMANENT`. Filter by `NGN_VIRTUAL_ACCOUNT` or `CRYPTO_ADDRESS`. Filter by `PENDING`, `ACTIVE`, `FAILED`, or `DISABLED`. Page number. Default: `1`. Results per page. Default: `50`. Max: `200`. ## Request Example ```bash curl --request GET \ --url 'https://api.daya.co/v1/funding-accounts?rail=NGN_VIRTUAL_ACCOUNT&status=ACTIVE' \ --header 'X-Api-Key: YOUR_API_KEY' ``` ## Response Returns a paginated list of funding account objects. Each object includes `developer_fee.percentage`, which is the fee percentage used for deposits received through that funding account. Provider-backed instructions include `provider_availability`, which reports the provider's current service condition. See [Provider availability](/concepts/funding-accounts#provider-availability) for the state definitions and recommended handling. #### Get a funding account Path: /api-reference/funding-accounts/get-funding-account Description: Retrieve a funding account by ID ## Overview Get one funding account for the authenticated merchant. ## Authentication Your merchant API key ## Path Parameters Funding account ID. ## Request Example ```bash curl --request GET \ --url https://api.daya.co/v1/funding-accounts/750e8400-e29b-41d4-a716-446655440000 \ --header 'X-Api-Key: YOUR_API_KEY' ``` ## Response Returns the funding account object, including payment details, the active settlement destination, and the configured `developer_fee.percentage`. Provider-backed instructions also include `provider_availability`. Read `provider_availability.message` to explain a current provider delay or outage. See [Provider availability](/concepts/funding-accounts#provider-availability) for all states and their meaning. For `NGN_VIRTUAL_ACCOUNT`, the NGN bank account details are in `instructions`: ```json { "rail": "NGN_VIRTUAL_ACCOUNT", "currency": "NGN", "developer_fee": { "percentage": "2.5" }, "instructions": [ { "type": "NGN_VIRTUAL_ACCOUNT", "provider": "FLUTTERWAVE", "provider_availability": { "status": "DEGRADED", "message": "This provider is experiencing funding-account delays." }, "status": "ACTIVE", "bank_name": "Wema Bank", "bank_code": "035", "account_number": "1234567890", "account_name": "Daya - Ada Lovelace", "currency": "NGN" } ] } ``` #### Disable a funding account Path: /api-reference/funding-accounts/disable-funding-account Description: Disable an active funding account and its active payment details ## Overview Disable an active funding account. Already disabled accounts return the current resource without emitting another `funding_account.disabled` webhook. ## Authentication Your merchant API key Unique idempotency key for request deduplication ## Path Parameters Funding account ID. ## Request Example ```bash curl --request POST \ --url https://api.daya.co/v1/funding-accounts/750e8400-e29b-41d4-a716-446655440000/disable \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'X-Idempotency-Key: disable-funding-account-001' ``` ## Response Returns the funding account object with `status: DISABLED`. #### Update funding account settlement destination Path: /api-reference/funding-accounts/update-settlement-destination Description: Rotate the settlement destination for a permanent active funding account ```bash Update NGN virtual account to onchain settlement curl --request PATCH \ --url https://api.daya.co/v1/funding-accounts/750e8400-e29b-41d4-a716-446655440000/settlement-destination \ --header 'Content-Type: application/json' \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'X-Idempotency-Key: update-funding-account-001' \ --data '{ "type": "ONCHAIN", "destination_asset": "USDC", "destination_chain": "BASE", "destination_address": "0x742d35cc6634c0532925a3b844bc9e7595f2bd18" }' ``` ```bash Update crypto address to NGN payout settlement curl --request PATCH \ --url https://api.daya.co/v1/funding-accounts/750e8400-e29b-41d4-a716-446655440000/settlement-destination \ --header 'Content-Type: application/json' \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'X-Idempotency-Key: update-funding-account-002' \ --data '{ "type": "NGN_PAYOUT", "destination_bank": { "account_number": "0123456789", "bank_code": "058" } }' ``` ## Overview Rotate the active settlement destination for a permanent active funding account. This operation does not create new payment details. ## Authentication Your merchant API key Unique idempotency key for request deduplication ## Path Parameters Funding account ID. ## Request Body Settlement destination type. Allowed values depend on the funding account rail; see the matrix below. Required for `NGN_VIRTUAL_ACCOUNT` funding accounts when `type` is `ONCHAIN`. Required for `NGN_VIRTUAL_ACCOUNT` funding accounts when `type` is `ONCHAIN`. Required for `NGN_VIRTUAL_ACCOUNT` funding accounts when `type` is `ONCHAIN`. ### Allowed settlement destination types This endpoint only updates permanent active funding accounts. The rail comes from the funding account identified by `id`; do not send `rail` in this request. | Funding account rail | Allowed `type` values | |----------------------|-----------------------| | `NGN_VIRTUAL_ACCOUNT` | `INTERNAL_BALANCE`, `ONCHAIN` | | `CRYPTO_ADDRESS` | `INTERNAL_BALANCE`, `NGN_PAYOUT` | Do not send `NGN_PAYOUT` for `NGN_VIRTUAL_ACCOUNT` funding accounts, and do not send `ONCHAIN` for `CRYPTO_ADDRESS` funding accounts. ### `destination_bank` `destination_bank` is a nested object. Do not send dotted keys such as `destination_bank.account_number`. | Field | Type | Required | |-------|------|----------| | `account_number` | string | Yes, for `CRYPTO_ADDRESS` funding accounts when `type` is `NGN_PAYOUT`. | | `bank_code` | string | Yes, for `CRYPTO_ADDRESS` funding accounts when `type` is `NGN_PAYOUT`. | Before sending `destination_bank`, fetch supported banks with [`GET /v1/banks`](/api-reference/banks/list-banks), then verify the account with [`POST /v1/banks/resolve`](/api-reference/banks/resolve-bank-account). Use the verified `bank_code` and `account_number` in this request. Daya returns the resolved `account_name` in the funding account response. `rate_id` is rejected for permanent funding accounts. ## Destination Shapes ```json Internal balance (NGN virtual account or crypto address) { "type": "INTERNAL_BALANCE" } ``` ```json NGN virtual account to onchain settlement { "type": "ONCHAIN", "destination_asset": "USDC", "destination_chain": "BASE", "destination_address": "0x742d35cc6634c0532925a3b844bc9e7595f2bd18" } ``` ```json Crypto address to NGN payout settlement { "type": "NGN_PAYOUT", "destination_bank": { "account_number": "0123456789", "bank_code": "058" } } ``` ## Request Example ```bash Update NGN virtual account to onchain settlement curl --request PATCH \ --url https://api.daya.co/v1/funding-accounts/750e8400-e29b-41d4-a716-446655440000/settlement-destination \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'X-Idempotency-Key: update-funding-account-001' \ --header 'Content-Type: application/json' \ --data '{ "type": "ONCHAIN", "destination_asset": "USDC", "destination_chain": "BASE", "destination_address": "0x742d35cc6634c0532925a3b844bc9e7595f2bd18" }' ``` ## Response Returns the funding account object with the new active settlement destination. ### Transfers #### Create transfer Path: /api-reference/transfers/create-transfer Description: Create a merchant-initiated transfer ## Overview Create a transfer to send funds to a saved recipient or an inline destination. Transfers support both NGN bank transfers and USD ACH/wire/SWIFT transfers. Transfers are funded from your **merchant balance**. Before creating a transfer, ensure your balance has sufficient funds. You can fund your balance in two ways: - **Crypto deposits** — send stablecoins to your [merchant funding](/api-reference/merchant-funding/get-merchant-funding) crypto wallet addresses. - **NGN deposits** — transfer Naira to your permanent [merchant funding](/api-reference/merchant-funding/get-merchant-funding) NGN bank account (converted to USD at the current rate). Funding account deposits are pooled into your **collection balance**. To use those funds for transfers or withdrawals, first move them to your **withdrawal balance** via the [balance transfer endpoint](/api-reference/merchant-balance/transfer-merchant-balance). Merchant funding deposits go directly into your withdrawal balance. Exactly one of `recipient_id` or `destination` must be provided. **Saved vs inline recipients:** - **Saved recipients** are created separately via `POST /v1/recipients` and referenced by `recipient_id`. - **Inline recipients** are created implicitly when `destination` is provided. - For inline USD destinations, `on_behalf_of.customer_id` is effectively required because the inline recipient-creation flow needs a customer context. - That customer must already be tier-2 verified; otherwise inline USD recipient creation fails. **Idempotency:** This endpoint is idempotent. Use `Idempotency-Key`; `X-Idempotency-Key` is also accepted as a fallback. ## Authentication Your merchant API key Unique idempotency key for request deduplication. `X-Idempotency-Key` is also accepted. **Example:** `550e8400-e29b-41d4-a716-446655440000` ## Request Body Transfer currency. **Allowed values:** `NGN` | `USD` - `NGN` transfers: only `BANK_ACCOUNT` recipients. - `USD` transfers: only `US_BANK_ACCOUNT` or `SWIFT_BANK_ACCOUNT` recipients. Transfer amount as a decimal string in the target currency. **Example:** `50000.00` Merchant-provided unique reference for the transfer. **Example:** `txn_abc123` ID of a previously saved recipient (UUID). Mutually exclusive with `destination`. **Example:** `750e8400-e29b-41d4-a716-446655440000` Inline destination details. Mutually exclusive with `recipient_id`. Destination type. **Allowed values:** `BANK_ACCOUNT` | `US_BANK_ACCOUNT` | `SWIFT_BANK_ACCOUNT` Nigerian bank account details. Required when `destination.type` is `BANK_ACCOUNT`. Bank account number Bank code (CBN code) Account holder name (resolved automatically if omitted) US bank account details. Required when `destination.type` is `US_BANK_ACCOUNT`. Owner of the bank account. 1-256 characters. ACH and wire transfers enforce regex constraints on this field. Account number Routing number Bank name (1-256 characters) **Allowed values:** `checking` | `savings` **Allowed values:** `ach` | `wire` Address of the beneficiary. US addresses used to receive wires must include a street number. `country` must be an ISO 3166-1 alpha-3 code. State must be a valid ISO 3166-2 subdivision code for US addresses. SWIFT bank account details. Required when `destination.type` is `SWIFT_BANK_ACCOUNT`. For individual recipients, provide `first_name` and `last_name`. For business recipients, provide `business_name`. Owner of the bank account. 1-256 characters. ACH and wire transfers enforce regex constraints on this field. For `individual`, provide `first_name` and `last_name`. For `business`, provide `business_name`. **Allowed values:** `individual` | `business` ISO 3166-1 alpha-3 country code where the bank account is held (3 characters) International Bank Account Number Bank Identifier Code (optional) Bank name (1-256 characters, optional) Required when `account_owner_type` is `individual` Required when `account_owner_type` is `individual` Required when `account_owner_type` is `business` The context of business operations. **Allowed values:** `client` | `parent_company` | `subsidiary` | `supplier` The nature of the transactions this account will participate in. At least one value required. **Allowed values:** `intra_group_transfer` | `invoice_for_goods_and_services` How the business uses the funds. Address of the beneficiary. `country` must be an ISO 3166-1 alpha-3 code. US addresses used to receive wires must include a street number. Bank address. `country` must be an ISO 3166-1 alpha-3 code. Optional metadata linking the transfer to a customer. Effectively required for inline USD destinations. Customer ID (UUID) ## Request Examples ```bash cURL - NGN Bank Transfer (saved recipient) curl --request POST \ --url https://api.daya.co/v1/transfers \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000' \ --header 'Content-Type: application/json' \ --data '{ "currency": "NGN", "amount": "50000.00", "reference": "txn_ngn_001", "recipient_id": "750e8400-e29b-41d4-a716-446655440000" }' ``` ```bash cURL - USD Transfer (inline US bank) curl --request POST \ --url https://api.daya.co/v1/transfers \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'Idempotency-Key: 660e8400-e29b-41d4-a716-446655440000' \ --header 'Content-Type: application/json' \ --data '{ "currency": "USD", "amount": "100.00", "reference": "txn_usd_001", "on_behalf_of": { "customer_id": "650e8400-e29b-41d4-a716-446655440000" }, "destination": { "type": "US_BANK_ACCOUNT", "us_bank_account": { "account_owner_name": "Jane Smith", "account_number": "123456789", "routing_number": "021000021", "bank_name": "Chase", "account_type": "checking", "payout_scheme": "ach", "address": { "street_line_1": "123 Main St", "city": "New York", "state": "NY", "postal_code": "10001", "country": "USA" } } } }' ``` ## Response Transfer ID (UUID) Merchant-provided reference External status: `PROCESSING`, `SETTLED`, or `FAILED` Transfer rail: `NGN_BANK` or `USD_BANK` Transfer currency (`NGN` or `USD`) Transfer amount in the target currency Currency debited from the merchant balance (`USD`) Total amount debited from the merchant withdrawal balance, including fees. Fee amount. Captured exchange rate snapshot. Present for NGN transfers. Rate side (e.g. `SELL`) Rate value When the rate was captured (ISO 8601) Resolved recipient details Customer metadata if provided When the transfer was created (ISO 8601) When the transfer settled (ISO 8601). Null if not yet settled. ### Success Response ```json 201 Created - NGN Transfer { "id": "850e8400-e29b-41d4-a716-446655440000", "reference": "txn_ngn_001", "status": "PROCESSING", "rail": "NGN_BANK", "currency": "NGN", "amount": "50000.00", "debit_currency": "USD", "debit_amount": "33.07", "fee": "0.81", "rate": { "side": "SELL", "value": "1550.00", "captured_at": "2026-01-05T15:04:05Z" }, "recipient": { "id": "750e8400-e29b-41d4-a716-446655440000", "type": "BANK_ACCOUNT", "bank_account": { "account_name": "John Doe", "account_number_last4": "7890", "bank_code": "044", "bank_name": "Access Bank" } }, "on_behalf_of": null, "created_at": "2026-01-05T15:04:05Z", "settled_at": null } ``` ```json 201 Created - USD Transfer { "id": "950e8400-e29b-41d4-a716-446655440000", "reference": "txn_usd_001", "status": "PROCESSING", "rail": "USD_BANK", "currency": "USD", "amount": "100.00", "debit_currency": "USD", "debit_amount": "100.00", "fee": "0.00", "rate": null, "recipient": { "id": "860e8400-e29b-41d4-a716-446655440000", "type": "US_BANK_ACCOUNT", "us_bank_account": { "account_owner_name": "Jane Smith", "account_number_last4": "6789", "bank_name": "Chase", "account_type": "checking", "payout_scheme": "ach" } }, "on_behalf_of": { "customer_id": "650e8400-e29b-41d4-a716-446655440000" }, "created_at": "2026-01-05T15:04:05Z", "settled_at": null } ``` ## Notes - A minimum effective USD debit of $0.50 is enforced. For NGN transfers, the corresponding Naira amount varies with the effective `SELL` exchange rate. - For NGN transfers, `amount` is the NGN sent to the recipient and `debit_amount` is the total USD debited from your withdrawal balance, including fees. - Saved USD recipients can be reused later without resending full destination details. - Transfers emit `transfer.*` webhooks: `transfer.created`, `transfer.processing`, `transfer.submitted`, then `transfer.completed` or `transfer.failed`. `transfer.requires_review` is sent when a transfer is flagged. #### List transfers Path: /api-reference/transfers/list-transfers Description: List merchant-initiated transfers ## Overview Lists all merchant-initiated transfers for the authenticated merchant with optional filtering and pagination. ## Authentication Your merchant API key ## Query Parameters Number of results per page (default: 20, max: 100) Page number (default: 1) Filter by external status **Allowed values:** `PROCESSING` | `SETTLED` | `FAILED` Filter by transfer currency **Allowed values:** `NGN` | `USD` ## Request Example ```bash cURL curl --request GET \ --url 'https://api.daya.co/v1/transfers?limit=20&page=1&status=PROCESSING' \ --header 'X-Api-Key: YOUR_API_KEY' ``` ## Response Array of transfer objects Current page number Results per page Total number of transfers Total number of pages ```json 200 OK { "data": [ { "id": "850e8400-e29b-41d4-a716-446655440000", "reference": "txn_ngn_001", "status": "PROCESSING", "rail": "NGN_BANK", "currency": "NGN", "amount": "50000.00", "debit_currency": "USD", "debit_amount": "33.07", "fee": "0.81", "rate": { "side": "SELL", "value": "1550.00", "captured_at": "2026-01-05T15:04:05Z" }, "recipient": { "id": "750e8400-e29b-41d4-a716-446655440000", "type": "BANK_ACCOUNT", "bank_account": { "account_name": "John Doe", "account_number_last4": "7890", "bank_code": "044", "bank_name": "Access Bank" } }, "on_behalf_of": null, "created_at": "2026-01-05T15:04:05Z", "settled_at": null } ], "page": 1, "limit": 20, "total": 1, "total_pages": 1 } ``` #### Get transfer Path: /api-reference/transfers/get-transfer Description: Retrieve a single transfer by ID ## Overview Retrieves a single merchant-initiated transfer by ID for the authenticated merchant. ## Authentication Your merchant API key ## Path Parameters Transfer ID (UUID format) **Example:** `850e8400-e29b-41d4-a716-446655440000` ## Request Example ```bash cURL curl --request GET \ --url https://api.daya.co/v1/transfers/850e8400-e29b-41d4-a716-446655440000 \ --header 'X-Api-Key: YOUR_API_KEY' ``` ## Response Transfer ID (UUID) Merchant-provided reference External status: `PROCESSING`, `SETTLED`, or `FAILED` Transfer rail: `NGN_BANK` or `USD_BANK` Transfer currency (`NGN` or `USD`) Transfer amount in the target currency Currency debited from the merchant balance (`USD`) Total amount debited from the merchant withdrawal balance, including fees Fee charged for the transfer Captured exchange rate snapshot (present for NGN transfers) Resolved recipient details Customer metadata if provided When the transfer was created (ISO 8601) When the transfer settled (ISO 8601) ```json 200 OK { "id": "850e8400-e29b-41d4-a716-446655440000", "reference": "txn_ngn_001", "status": "SETTLED", "rail": "NGN_BANK", "currency": "NGN", "amount": "50000.00", "debit_currency": "USD", "debit_amount": "33.07", "fee": "0.81", "rate": { "side": "SELL", "value": "1550.00", "captured_at": "2026-01-05T15:04:05Z" }, "recipient": { "id": "750e8400-e29b-41d4-a716-446655440000", "type": "BANK_ACCOUNT", "bank_account": { "account_name": "John Doe", "account_number_last4": "7890", "bank_code": "044", "bank_name": "Access Bank" } }, "on_behalf_of": null, "created_at": "2026-01-05T15:04:05Z", "settled_at": "2026-01-05T15:10:00Z" } ``` ```json 404 Not Found { "error": { "code": "NOT_FOUND", "message": "Transfer not found", "request_id": "550e8400-e29b-41d4-a716-446655440000" } } ``` ### Deposits #### List all deposits Path: /api-reference/deposits/list-deposits Description: List NGN and crypto deposits with optional filters ## Overview Retrieve a paginated list of deposits for the authenticated merchant. Deposits are inbound NGN and crypto funds received through funding accounts, such as NGN virtual accounts and crypto addresses. Use [USD account deposits](/api-reference/virtual-account-deposits/list-usd-account-deposits) for payments into USD virtual accounts. ## Authentication Your merchant API key ## Query Parameters Filter by deposit type **Allowed values:** `NGN_DEPOSIT` | `CRYPTO_DEPOSIT` Filter by deposit status **Allowed values:** `PENDING` | `RECEIVED` | `PROCESSING` | `REQUIRES_REVIEW` | `COMPLETED` | `FLAGGED` | `FAILED` | `REVERSED` `FLAGGED` is accepted for older integrations and maps to `REQUIRES_REVIEW`. Filter by exact payment reference. Only applies to NGN deposits. Filter by legacy onramp ID (UUID). Only applies to migrated NGN deposits. **Example:** `550e8400-e29b-41d4-a716-446655440000` Filter by legacy offramp ID (UUID). Only applies to migrated crypto deposits. **Example:** `550e8400-e29b-41d4-a716-446655440000` Filter deposits created from this time (RFC 3339, inclusive). **Example:** `2026-01-01T00:00:00Z` Filter deposits created before this time (RFC 3339, exclusive). **Example:** `2026-01-31T23:59:59Z` Results per page **Default:** `50` | **Max:** `200` Page number to retrieve **Default:** `1` ## Request Examples ```bash All Deposits curl --request GET \ --url 'https://api.daya.co/v1/deposits' \ --header 'X-Api-Key: YOUR_API_KEY' ``` ```bash By Legacy Onramp curl --request GET \ --url 'https://api.daya.co/v1/deposits?onramp_id=550e8400-e29b-41d4-a716-446655440000' \ --header 'X-Api-Key: YOUR_API_KEY' ``` ```bash By Status curl --request GET \ --url 'https://api.daya.co/v1/deposits?status=COMPLETED' \ --header 'X-Api-Key: YOUR_API_KEY' ``` ```bash Date Range curl --request GET \ --url 'https://api.daya.co/v1/deposits?from=2026-01-01T00:00:00Z&to=2026-01-31T23:59:59Z' \ --header 'X-Api-Key: YOUR_API_KEY' ``` ```javascript JavaScript const response = await fetch( 'https://api.daya.co/v1/deposits?status=COMPLETED&limit=50&page=1', { headers: { 'X-Api-Key': 'YOUR_API_KEY' } } ); const data = await response.json(); ``` ## Response Array of deposit objects. Unique deposit identifier (UUID). Deposit type: `NGN_DEPOSIT` or `CRYPTO_DEPOSIT`. Legacy onramp ID for migrated NGN deposits, when present. Legacy offramp ID for migrated crypto deposits, when present. Associated customer ID. Amount received into Daya. Deposit currency. Amount delivered to the settlement destination. Settlement destination currency, such as `USD` for internal balance, `NGN` for NGN payout, or a stablecoin for onchain settlement. Exchange rate applied. FX rate identifier used for settlement. Current status: `RECEIVED`, `PROCESSING`, `REQUIRES_REVIEW`, `COMPLETED`, `FAILED`, or `REVERSED`. Settlement progress. Settlement mode (e.g., `ONCHAIN`, `INTERNAL_BALANCE`, `NGN_PAYOUT`). Stablecoin symbol for crypto deposits (`USDC` or `USDT`). Blockchain network (for crypto deposits). On-chain transaction hash (for crypto deposits). Funding account that received the funds. Fees applied to the deposit. Deposit fee. Fee amount. Fee currency. Withdrawal fee component. Fee amount. Fee currency. Total fee expressed in USD. Merchant developer fee kept from this deposit, when configured. This is separate from Daya fees and is deducted before `customer_amount` is finalized. Configured percentage as a decimal string. Amount kept by your merchant account after settlement. For onramps and offramps, this is deducted before the final customer amount is calculated rather than added as a separate charge. Currency of the developer fee amount. Amount left for the customer after Daya fees and the developer fee, when available. Amount left for the customer. Currency of the customer amount. Flag code (if flagged). Flag description (if flagged). Time the deposit was received. Last status update time. Current page number. Results per page. Total number of deposits matching filters. Total number of pages available. ### Success Response ```json 200 OK { "data": [ { "id": "7a4e8400-e29b-41d4-a716-446655440000", "type": "NGN_DEPOSIT", "funding_account_id": "6b0e8400-e29b-41d4-a716-446655440000", "onramp_id": "550e8400-e29b-41d4-a716-446655440000", "customer_id": "650e8400-e29b-41d4-a716-446655440000", "amount": "15000.00", "currency": "NGN", "settled_amount": "9.70", "settled_currency": "USDC", "rate": "1545.50", "rate_id": "rate_8x7k2mq9p", "status": "COMPLETED", "settlement_status": "COMPLETED", "settlement_mode": "ONCHAIN", "chain": "BASE", "tx_hash": "0x8f3e2d1c0b9a8e7f6d5c4b3a2e1f0d9c8b7a6e5f4d3c2b1a", "fees": { "deposit_fee": { "amount": "0.05", "currency": "USD" }, "total_fee_usd": "0.05" }, "developer_fee": { "percentage": "2.5", "amount": "0.25", "currency": "USD" }, "customer_amount": { "amount": "9.45", "currency": "USDC" }, "created_at": "2026-01-14T15:06:30Z", "updated_at": "2026-01-14T15:08:15Z" } ], "page": 1, "limit": 50, "total": 1, "total_pages": 1 } ``` ## Next Steps Get a specific deposit by ID Learn about deposit lifecycle #### Retrieve a deposit Path: /api-reference/deposits/get-deposit Description: Get a specific deposit by ID ## Overview Retrieve detailed information about a specific NGN or crypto deposit. Use [Get USD virtual account deposit](/api-reference/virtual-account-deposits/get-usd-account-deposit) for payments into USD virtual accounts. ## Authentication Your merchant API key ## Path Parameters Deposit ID (UUID format). **Example:** `7a4e8400-e29b-41d4-a716-446655440000` ## Request Examples ```bash cURL curl --request GET \ --url https://api.daya.co/v1/deposits/7a4e8400-e29b-41d4-a716-446655440000 \ --header 'X-Api-Key: YOUR_API_KEY' ``` ```javascript JavaScript const response = await fetch( 'https://api.daya.co/v1/deposits/7a4e8400-e29b-41d4-a716-446655440000', { headers: { 'X-Api-Key': 'YOUR_API_KEY' } } ); const deposit = await response.json(); ``` ```python Python import requests response = requests.get( 'https://api.daya.co/v1/deposits/7a4e8400-e29b-41d4-a716-446655440000', headers={'X-Api-Key': 'YOUR_API_KEY'} ) deposit = response.json() ``` ## Response Returns a deposit object. See [List deposits](/api-reference/deposits/list-deposits#response) for the full field list. If a developer fee was configured for the funding account, the deposit object includes `developer_fee` and `customer_amount`. Use `developer_fee` to see the amount and currency kept by your merchant account, and use `customer_amount` to see the final amount left for the customer. ### Success Response ```json 200 OK - NGN Deposit (Completed) { "id": "7a4e8400-e29b-41d4-a716-446655440000", "type": "NGN_DEPOSIT", "funding_account_id": "6b0e8400-e29b-41d4-a716-446655440000", "onramp_id": "550e8400-e29b-41d4-a716-446655440000", "customer_id": "650e8400-e29b-41d4-a716-446655440000", "amount": "15000.00", "currency": "NGN", "settled_amount": "9.70", "settled_currency": "USDC", "rate": "1545.50", "rate_id": "rate_8x7k2mq9p", "status": "COMPLETED", "settlement_status": "COMPLETED", "settlement_mode": "ONCHAIN", "chain": "BASE", "tx_hash": "0x8f3e2d1c0b9a8e7f6d5c4b3a2e1f0d9c8b7a6e5f4d3c2b1a", "fees": { "deposit_fee": { "amount": "0.05", "currency": "USD" }, "total_fee_usd": "0.05" }, "developer_fee": { "percentage": "2.5", "amount": "0.25", "currency": "USD" }, "customer_amount": { "amount": "9.45", "currency": "USDC" }, "created_at": "2026-01-14T15:06:30Z", "updated_at": "2026-01-14T15:08:15Z" } ``` ```json 200 OK - Crypto Deposit (Completed) { "id": "8b4e8400-e29b-41d4-a716-446655440000", "type": "CRYPTO_DEPOSIT", "funding_account_id": "6b0e8400-e29b-41d4-a716-446655440002", "offramp_id": "550e8400-e29b-41d4-a716-446655440000", "customer_id": "650e8400-e29b-41d4-a716-446655440000", "amount": "1.00", "currency": "USDC", "settled_amount": "1.00", "settled_currency": "USD", "status": "COMPLETED", "settlement_status": "COMPLETED", "settlement_mode": "INTERNAL_BALANCE", "asset": "USDC", "chain": "ETHEREUM", "tx_hash": "0xa1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4", "fees": { "total_fee_usd": "0.01" }, "developer_fee": { "percentage": "2.5", "amount": "0.02", "currency": "USD" }, "customer_amount": { "amount": "0.97", "currency": "USD" }, "created_at": "2026-01-14T16:00:00Z", "updated_at": "2026-01-14T16:02:30Z" } ``` ```json 200 OK - Requires Review { "id": "7a4e8400-e29b-41d4-a716-446655440001", "type": "NGN_DEPOSIT", "funding_account_id": "6b0e8400-e29b-41d4-a716-446655440000", "onramp_id": "550e8400-e29b-41d4-a716-446655440000", "customer_id": "650e8400-e29b-41d4-a716-446655440000", "amount": "15000.00", "currency": "NGN", "status": "REQUIRES_REVIEW", "settlement_status": "REQUIRES_REVIEW", "flag_code": "late_deposit", "flag_message": "Deposit received after onramp expiry", "created_at": "2026-01-14T15:35:00Z", "updated_at": "2026-01-14T15:35:05Z" } ``` ```json 404 Not Found { "error": { "code": "NOT_FOUND", "message": "Deposit not found", "request_id": "550e8400-e29b-41d4-a716-446655440000" } } ``` #### Create a sandbox deposit Path: /api-reference/sandbox/create-sandbox-deposit Description: Trigger a mock deposit in sandbox to test the full deposit → settlement/webhook flow ## Overview This sandbox-only endpoint creates a mock deposit for an existing funding account. By default, Daya runs the simulated deposit through the same processing pipeline as a real deposit: - Deposit creation - Settlement to internal balance, onchain payout, or NGN payout - Webhook dispatch You can also pass `scenario` to trigger a specific sandbox lifecycle outcome. Use scenarios when you need to confirm your app handles specific deposit states without waiting for the sandbox processor to produce them naturally. Use this to validate deposit processing and webhook handling before going to production. Create the receive instruction with `/v1/funding-accounts`, then pass the returned funding account `id` to this endpoint. This endpoint is **not available in production**. Calls in production return `403 Not available in production`. For end-to-end guidance (recommended flow, what to validate, and common flagging scenarios), see [Sandbox testing](/limits/sandbox-testing). ## Authentication Your merchant API key ## Request Body Funding account ID to simulate a deposit for. Daya uses the funding account to decide whether the simulated deposit is NGN or crypto, and where settlement should go. **Example:** `6b0e8400-e29b-41d4-a716-446655440000` Optional sandbox lifecycle outcome to trigger. Allowed values: - `PROCESSING` - `COMPLETED` - `REQUIRES_REVIEW` - `FAILED` Omit this field to use the default sandbox processing flow. ## Request Examples ```bash Default flow curl --request POST \ --url https://api.sandbox.daya.co/v1/sandbox/deposits \ --header 'X-Api-Key: YOUR_SANDBOX_KEY' \ --header 'Content-Type: application/json' \ --data '{ "funding_account_id": "6b0e8400-e29b-41d4-a716-446655440000" }' ``` ```bash Trigger completed curl --request POST \ --url https://api.sandbox.daya.co/v1/sandbox/deposits \ --header 'X-Api-Key: YOUR_SANDBOX_KEY' \ --header 'Content-Type: application/json' \ --data '{ "funding_account_id": "6b0e8400-e29b-41d4-a716-446655440000", "scenario": "COMPLETED" }' ``` ```bash Trigger review curl --request POST \ --url https://api.sandbox.daya.co/v1/sandbox/deposits \ --header 'X-Api-Key: YOUR_SANDBOX_KEY' \ --header 'Content-Type: application/json' \ --data '{ "funding_account_id": "6b0e8400-e29b-41d4-a716-446655440000", "scenario": "REQUIRES_REVIEW" }' ``` ## Response Returns a confirmation message and the simulated deposit status. Use `/v1/deposits` and webhook events to track the simulated deposit. - `message`: Human-readable status message - `deposit_id`: Created deposit ID - `status`: Current deposit status - `scenario`: Scenario that was applied, if provided ```json { "message": "Sandbox deposit created for processing.", "deposit_id": "4b4a0f1f-f1dc-4f4c-a77d-3f4ebc8b4f42", "status": "COMPLETED", "scenario": "COMPLETED" } ``` ## Testing lifecycle outcomes Use `scenario` to test the state machine in your integration: | Scenario | Expected webhook path | Use this to test | |----------|------------------------|------------------| | `PROCESSING` | `deposit.received` → `deposit.processing` | In-progress UI and retry-safe polling | | `COMPLETED` | `deposit.received` → `deposit.completed` | Crediting/final success logic | | `REQUIRES_REVIEW` | `deposit.received` → `deposit.requires_review` | Review/hold states | | `FAILED` | `deposit.received` → `deposit.failed` | Failure messaging and recovery | Scenario outcomes are for testing a specific lifecycle state in sandbox. `PROCESSING` and `COMPLETED` still use the funding account's active settlement destination, so the funding account must include any required `rate_id`, destination wallet, or destination bank details. To test the production-like settlement processor and any natural intermediate states, omit `scenario`. ## Error Responses This endpoint may return: - `400`: Invalid request - `401`: Unauthorized - `403`: Not available in production - `404`: Funding account not found - `500`: Internal server error ## Next Steps Verify your webhook handling with sandbox events Recommended end-to-end sandbox flow + common flagging scenarios ### Recipients #### Create recipient Path: /api-reference/recipients/create-recipient Description: Create a saved transfer or settlement destination ## Overview Create a saved recipient that can be reused across transfers and settlement configuration. Supported types: `BANK_ACCOUNT`, `CRYPTO_ADDRESS`, `US_BANK_ACCOUNT`, and `SWIFT_BANK_ACCOUNT`. Recipients are deduplicated by fingerprint — submitting the same account details will return the existing recipient rather than creating a duplicate. **Tier 2 verification required for USD recipients:** - `customer_id` is **required** for `US_BANK_ACCOUNT` and `SWIFT_BANK_ACCOUNT` types. - The referenced customer must have completed [tier 2 verification](/api-reference/customers/submit-tier2-verification). - If the customer does not have a Bridge banking partner ID, USD recipient creation will fail. ## Authentication Your merchant API key Unique idempotency key to prevent duplicate requests **Example:** `550e8400-e29b-41d4-a716-446655440000` ## Request Body Recipient type **Allowed values:** `BANK_ACCOUNT` | `CRYPTO_ADDRESS` | `US_BANK_ACCOUNT` | `SWIFT_BANK_ACCOUNT` Customer ID to associate with this recipient (UUID). **Required** for `US_BANK_ACCOUNT` and `SWIFT_BANK_ACCOUNT` types. The customer must have completed tier 2 verification. **Example:** `650e8400-e29b-41d4-a716-446655440000` Bank account details. Required when `type` is `BANK_ACCOUNT`. Bank account number **Example:** `1234567890` Bank code (e.g. CBN bank code) **Example:** `044` Account holder name. If omitted, the name will be resolved automatically. **Example:** `John Doe` Crypto address details. Required when `type` is `CRYPTO_ADDRESS`. Crypto asset **Allowed values:** `USDC` | `USDT` Blockchain network **Allowed values:** `ETHEREUM` | `BASE` | `POLYGON` | `ARBITRUM` | `OPTIMISM` | `CELO` Wallet address **Example:** `0x1234567890abcdef1234567890abcdef12345678` US bank account details. Required when `type` is `US_BANK_ACCOUNT`. Owner of the bank account. 1-256 characters. For ACH transfers must match: `^(?!\s*$)[\x20-\x7E]*$`. For wire transfers must match: `` ^[ \w!"#$%&'()+,\-./:;<=>?@\\_`~]*$ ``. **Example:** `Jane Smith` Bank account number **Example:** `123456789` Bank routing number **Example:** `021000021` Bank name. 1-256 characters. **Example:** `Chase` Account type **Allowed values:** `checking` | `savings` Payment scheme **Allowed values:** `ach` | `wire` Address of the beneficiary. Please ensure the address is valid. US addresses used to receive wires must include a street number. Street address line 1 (4-35 characters) Street address line 2 (maximum 35 characters) City (minimum 1 character) ISO 3166-2 subdivision code. Must be supplied for US addresses (1-3 characters). Postal code. Must be supplied for countries that use postal codes. ISO 3166-1 alpha-3 country code (3 characters). Example: `USA`. SWIFT bank account details. Required when `type` is `SWIFT_BANK_ACCOUNT`. For individual recipients, provide `first_name` and `last_name`. For business recipients, provide `business_name`. Owner of the bank account. 1-256 characters. For ACH transfers must match: `^(?!\s*$)[\x20-\x7E]*$`. For wire transfers must match: `` ^[ \w!"#$%&'()+,\-./:;<=>?@\\_`~]*$ ``. **Example:** `Hans Mueller` Owner type. For `individual` ownership, `first_name` and `last_name` are required. For `business` ownership, `business_name` is required. **Allowed values:** `individual` | `business` ISO 3166-1 alpha-3 country code where the bank account is held (3 characters). **Example:** `DEU` International Bank Account Number that will be used to send the funds. Minimum length 1. **Example:** `DE89370400440532013000` Bank Identifier Code that will be used to send the funds. Minimum length 1. Optional. **Example:** `COBADEFFXXX` Bank name. 1-256 characters. Optional. **Example:** `Commerzbank` First name. Required when `account_owner_type` is `individual`. Last name. Required when `account_owner_type` is `individual`. Business name. Required when `account_owner_type` is `business`. The context of business operations. **Allowed values:** `client` | `parent_company` | `subsidiary` | `supplier` The nature of the transactions this account will participate in. At least one value required. **Allowed values:** `intra_group_transfer` | `invoice_for_goods_and_services` How the business uses the funds. Address of the beneficiary. Please ensure the address is valid. US addresses used to receive wires must include a street number. Street address line 1 (4-35 characters) Street address line 2 (maximum 35 characters) City (minimum 1 character) ISO 3166-2 subdivision code. Must be supplied for US addresses (1-3 characters). Postal code. Must be supplied for countries that use postal codes. ISO 3166-1 alpha-3 country code (3 characters). Example: `DEU`. Bank address. Same shape as `swift_bank_account.address`. `country` must be an ISO 3166-1 alpha-3 code. ## Request Example ```bash cURL - Bank Account curl --request POST \ --url https://api.daya.co/v1/recipients \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'X-Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000' \ --header 'Content-Type: application/json' \ --data '{ "type": "BANK_ACCOUNT", "bank_account": { "account_number": "1234567890", "bank_code": "044", "account_name": "John Doe" } }' ``` ```bash cURL - Crypto Address curl --request POST \ --url https://api.daya.co/v1/recipients \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'X-Idempotency-Key: 660e8400-e29b-41d4-a716-446655440000' \ --header 'Content-Type: application/json' \ --data '{ "type": "CRYPTO_ADDRESS", "crypto_address": { "asset": "USDC", "chain": "BASE", "address": "0x1234567890abcdef1234567890abcdef12345678" } }' ``` ```javascript JavaScript - Bank Account const response = await fetch("https://api.daya.co/v1/recipients", { method: "POST", headers: { "X-Api-Key": "YOUR_API_KEY", "X-Idempotency-Key": "550e8400-e29b-41d4-a716-446655440000", "Content-Type": "application/json", }, body: JSON.stringify({ type: "BANK_ACCOUNT", bank_account: { account_number: "1234567890", bank_code: "044", account_name: "John Doe", }, }), }); const recipient = await response.json(); ``` ```bash cURL - US Bank Account curl --request POST \ --url https://api.daya.co/v1/recipients \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'X-Idempotency-Key: 770e8400-e29b-41d4-a716-446655440000' \ --header 'Content-Type: application/json' \ --data '{ "type": "US_BANK_ACCOUNT", "customer_id": "650e8400-e29b-41d4-a716-446655440000", "us_bank_account": { "account_owner_name": "Jane Smith", "account_number": "123456789", "routing_number": "021000021", "bank_name": "Chase", "account_type": "checking", "payout_scheme": "ach", "address": { "street_line_1": "123 Main St", "city": "New York", "state": "NY", "postal_code": "10001", "country": "USA" } } }' ``` ```bash cURL - SWIFT Bank Account (individual) curl --request POST \ --url https://api.daya.co/v1/recipients \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'X-Idempotency-Key: 880e8400-e29b-41d4-a716-446655440000' \ --header 'Content-Type: application/json' \ --data '{ "type": "SWIFT_BANK_ACCOUNT", "customer_id": "650e8400-e29b-41d4-a716-446655440000", "swift_bank_account": { "account_owner_name": "Hans Mueller", "account_owner_type": "individual", "first_name": "Hans", "last_name": "Mueller", "account_country": "DEU", "iban": "DE89370400440532013000", "bic": "COBADEFFXXX", "bank_name": "Commerzbank", "category": "client", "purpose_of_funds": ["invoice_for_goods_and_services"], "short_business_description": "Receives payments for design services", "address": { "street_line_1": "Kaiserstrasse 16", "city": "Frankfurt", "postal_code": "60311", "country": "DEU" }, "bank_address": { "street_line_1": "Kaiserplatz", "city": "Frankfurt", "postal_code": "60311", "country": "DEU" } } }' ``` ## Response Unique recipient identifier (UUID) Recipient type (`BANK_ACCOUNT`, `CRYPTO_ADDRESS`, `US_BANK_ACCOUNT`, or `SWIFT_BANK_ACCOUNT`) Recipient first name (resolved from account details) Recipient last name (resolved from account details) Bank account details. Present when `type` is `BANK_ACCOUNT`, `null` otherwise. Bank account number Resolved account holder name Bank code Resolved bank name Crypto address details. Present when `type` is `CRYPTO_ADDRESS`, `null` otherwise. Crypto asset (`USDC` or `USDT`) Blockchain network Wallet address US bank account details. Present when `type` is `US_BANK_ACCOUNT`, `null` otherwise. Account owner name Last 4 digits of account number Routing number Bank name Account type (`checking` or `savings`) Payment scheme (`ach` or `wire`) Recipient address SWIFT bank account details. Present when `type` is `SWIFT_BANK_ACCOUNT`, `null` otherwise. Account owner name Owner type (`individual` or `business`) ISO 3166-1 alpha-3 country code Last 4 digits of IBAN Bank Identifier Code Bank name First name (present when `account_owner_type` is `individual`) Last name (present when `account_owner_type` is `individual`) Business name (present when `account_owner_type` is `business`) Recipient category Purpose of funds Short business description Recipient address Bank address When the recipient was created (ISO 8601 timestamp) ### Success Response ```json 201 Created - Bank Account { "id": "750e8400-e29b-41d4-a716-446655440000", "type": "BANK_ACCOUNT", "first_name": "John", "last_name": "Doe", "bank_account": { "account_number": "1234567890", "account_name": "John Doe", "bank_code": "044", "bank_name": "Access Bank" }, "crypto_address": null, "created_at": "2026-01-05T15:04:05Z" } ``` ```json 201 Created - Crypto Address { "id": "850e8400-e29b-41d4-a716-446655440000", "type": "CRYPTO_ADDRESS", "first_name": null, "last_name": null, "bank_account": null, "crypto_address": { "asset": "USDC", "chain": "BASE", "address": "0x1234567890abcdef1234567890abcdef12345678" }, "created_at": "2026-01-05T15:04:05Z" } ``` ```json 201 Created - US Bank Account { "id": "950e8400-e29b-41d4-a716-446655440000", "type": "US_BANK_ACCOUNT", "first_name": "Jane", "last_name": "Smith", "us_bank_account": { "account_owner_name": "Jane Smith", "account_number_last4": "6789", "routing_number": "021000021", "bank_name": "Chase", "account_type": "checking", "payout_scheme": "ach", "address": { "street_line_1": "123 Main St", "city": "New York", "state": "NY", "postal_code": "10001", "country": "USA" } }, "created_at": "2026-01-05T15:04:05Z" } ``` ```json 201 Created - SWIFT Bank Account { "id": "a50e8400-e29b-41d4-a716-446655440000", "type": "SWIFT_BANK_ACCOUNT", "first_name": "Hans", "last_name": "Mueller", "swift_bank_account": { "account_owner_name": "Hans Mueller", "account_owner_type": "individual", "first_name": "Hans", "last_name": "Mueller", "account_country": "DEU", "iban_last4": "3000", "bic": "COBADEFFXXX", "bank_name": "Commerzbank", "category": "client", "purpose_of_funds": ["personal_or_living_expenses"], "short_business_description": "Personal account for living expenses", "address": { "street_line_1": "Kaiserstrasse 16", "city": "Frankfurt", "postal_code": "60311", "country": "DEU" }, "bank_address": { "street_line_1": "Kaiserplatz", "city": "Frankfurt", "postal_code": "60311", "country": "DEU" } }, "created_at": "2026-01-05T15:04:05Z" } ``` ## Error Responses ```json 400 Bad Request - Validation failed { "error": { "code": "validation_failed", "message": "Validation failed", "details": "bank_account is required when type is BANK_ACCOUNT" } } ``` ```json 401 Unauthorized { "error": { "code": "unauthorized", "message": "Invalid or missing API key" } } ``` ## Notes - Recipients are deduplicated by fingerprint. If you submit the same account details, the existing recipient is returned instead of creating a duplicate. - When `account_name` is omitted for a `BANK_ACCOUNT` recipient, the name is resolved automatically from the bank. - `customer_id` is optional for `BANK_ACCOUNT` and `CRYPTO_ADDRESS` types, but **required** for `US_BANK_ACCOUNT` and `SWIFT_BANK_ACCOUNT` types. - For USD recipient types, the referenced customer must have completed tier 2 verification and have a Bridge banking partner ID. - For `US_BANK_ACCOUNT` and `SWIFT_BANK_ACCOUNT`, the `address.country` field must be an ISO 3166-1 alpha-3 country code (e.g. `USA`, `DEU`, `GBR`). ## Next Steps Retrieve all saved recipients Look up a single recipient by ID #### List recipients Path: /api-reference/recipients/list-recipients Description: List saved recipients with optional filters ## Overview Retrieve a paginated list of saved recipients belonging to the authenticated merchant. Supports filtering by type, customer, and search term. ## Authentication Your merchant API key ## Query Parameters Filter by recipient type. **Allowed values:** `BANK_ACCOUNT` | `CRYPTO_ADDRESS` Filter by associated customer ID (UUID) **Example:** `650e8400-e29b-41d4-a716-446655440000` Search by account name, account number, or wallet address **Example:** `John` Results per page **Default:** `20` | **Max:** `100` Page number to retrieve **Default:** `1` ## Request Example ```bash cURL curl --request GET \ --url 'https://api.daya.co/v1/recipients?limit=20&page=1' \ --header 'X-Api-Key: YOUR_API_KEY' ``` ```bash cURL - Filter by type curl --request GET \ --url 'https://api.daya.co/v1/recipients?type=BANK_ACCOUNT&limit=20' \ --header 'X-Api-Key: YOUR_API_KEY' ``` ```bash cURL - Filter by customer curl --request GET \ --url 'https://api.daya.co/v1/recipients?customer_id=650e8400-e29b-41d4-a716-446655440000' \ --header 'X-Api-Key: YOUR_API_KEY' ``` ```javascript JavaScript const params = new URLSearchParams({ type: "BANK_ACCOUNT", limit: "20", page: "1", }); const response = await fetch( `https://api.daya.co/v1/recipients?${params.toString()}`, { method: "GET", headers: { "X-Api-Key": "YOUR_API_KEY", }, } ); const recipients = await response.json(); ``` ## Response Array of recipient objects Current page number Results per page Total number of recipients matching the query Total number of pages available ### Success Response ```json 200 OK { "data": [ { "id": "750e8400-e29b-41d4-a716-446655440000", "type": "BANK_ACCOUNT", "first_name": "John", "last_name": "Doe", "bank_account": { "account_name": "John Doe", "account_number_last4": "7890", "bank_code": "044", "bank_name": "Access Bank" }, "crypto_address": null, "us_bank_account": null, "swift_bank_account": null, "created_at": "2026-01-05T15:04:05Z" }, { "id": "850e8400-e29b-41d4-a716-446655440000", "type": "CRYPTO_ADDRESS", "first_name": null, "last_name": null, "bank_account": null, "crypto_address": { "asset": "USDC", "chain": "BASE", "address": "0x1234567890abcdef1234567890abcdef12345678" }, "us_bank_account": null, "swift_bank_account": null, "created_at": "2026-01-05T15:04:05Z" } ], "page": 1, "limit": 20, "total": 2, "total_pages": 1 } ``` ## Error Responses ```json 400 Bad Request - Invalid query parameter { "error": { "code": "VALIDATION_FAILED", "message": "Validation failed", "request_id": "550e8400-e29b-41d4-a716-446655440000", "validation": "type must be one of: BANK_ACCOUNT, CRYPTO_ADDRESS" } } ``` ```json 401 Unauthorized { "error": { "code": "UNAUTHORIZED", "message": "Invalid or missing API key", "request_id": "550e8400-e29b-41d4-a716-446655440000" } } ``` ## Pagination This endpoint uses **page-based pagination**. 1. Make your initial request (optionally with `limit` and `page`) 2. Check `total_pages` to determine how many pages are available 3. Increment `page` to fetch subsequent pages until you reach `total_pages` ## Next Steps Create a new recipient Retrieve a single recipient by ID #### Get recipient Path: /api-reference/recipients/get-recipient Description: Retrieve a single recipient by ID ## Overview Retrieve details for a specific recipient belonging to the authenticated merchant. The response shape depends on the recipient's `type`: `BANK_ACCOUNT`, `CRYPTO_ADDRESS`, `US_BANK_ACCOUNT`, or `SWIFT_BANK_ACCOUNT`. Only the field matching the type is populated; the others are `null`. ## Authentication Your merchant API key ## Path Parameters Recipient ID (UUID) **Example:** `750e8400-e29b-41d4-a716-446655440000` ## Request Example ```bash cURL curl --request GET \ --url https://api.daya.co/v1/recipients/750e8400-e29b-41d4-a716-446655440000 \ --header 'X-Api-Key: YOUR_API_KEY' ``` ```javascript JavaScript const response = await fetch( "https://api.daya.co/v1/recipients/750e8400-e29b-41d4-a716-446655440000", { method: "GET", headers: { "X-Api-Key": "YOUR_API_KEY", }, } ); const recipient = await response.json(); ``` ## Response Unique recipient identifier (UUID) Recipient type. **Allowed values:** `BANK_ACCOUNT` | `CRYPTO_ADDRESS` | `US_BANK_ACCOUNT` | `SWIFT_BANK_ACCOUNT` Recipient first name (for individual recipients). Recipient last name (for individual recipients). NGN bank account details. Present when `type` is `BANK_ACCOUNT`, `null` otherwise. Resolved account holder name. Last four digits of the account number. Bank code. Resolved bank name. Crypto address details. Present when `type` is `CRYPTO_ADDRESS`, `null` otherwise. Crypto asset (`USDC` or `USDT`). Blockchain network. Wallet address. US bank account details. Present when `type` is `US_BANK_ACCOUNT`, `null` otherwise. Account owner name. Last four digits of the account number. ABA routing number. Bank name. Account type (e.g., `checking`, `savings`). Payout scheme (e.g., `ach`, `wire`). Recipient address. Street line 1. Street line 2. City. State or subdivision. Postal code. ISO 3166-1 alpha-3 country code. SWIFT bank account details. Present when `type` is `SWIFT_BANK_ACCOUNT`, `null` otherwise. Account owner name. Account owner type: `individual` or `business`. First name (for individual owners). Last name (for individual owners). Business name (for business owners). Short business description. Relationship category: `client`, `parent_company`, `subsidiary`, or `supplier`. ISO 3166-1 alpha-3 country code of the account. Bank name. Bank Identifier Code (SWIFT/BIC). Last four characters of the IBAN. Declared purpose(s) of funds. Recipient address (same shape as `us_bank_account.address`). Bank address (same shape as `us_bank_account.address`). When the recipient was created (ISO 8601 timestamp) ### Success Response ```json 200 OK - Bank Account (NGN) { "id": "750e8400-e29b-41d4-a716-446655440000", "type": "BANK_ACCOUNT", "first_name": "John", "last_name": "Doe", "bank_account": { "account_name": "John Doe", "account_number_last4": "7890", "bank_code": "044", "bank_name": "Access Bank" }, "crypto_address": null, "us_bank_account": null, "swift_bank_account": null, "created_at": "2026-01-05T15:04:05Z" } ``` ```json 200 OK - Crypto Address { "id": "850e8400-e29b-41d4-a716-446655440000", "type": "CRYPTO_ADDRESS", "first_name": null, "last_name": null, "bank_account": null, "crypto_address": { "asset": "USDC", "chain": "BASE", "address": "0x1234567890abcdef1234567890abcdef12345678" }, "us_bank_account": null, "swift_bank_account": null, "created_at": "2026-01-05T15:04:05Z" } ``` ```json 200 OK - US Bank Account { "id": "950e8400-e29b-41d4-a716-446655440000", "type": "US_BANK_ACCOUNT", "first_name": "Jane", "last_name": "Smith", "bank_account": null, "crypto_address": null, "us_bank_account": { "account_owner_name": "Jane Smith", "account_number_last4": "6789", "routing_number": "021000021", "bank_name": "JPMorgan Chase", "account_type": "checking", "payout_scheme": "ach", "address": { "street_line_1": "123 Main St", "city": "New York", "state": "NY", "postal_code": "10001", "country": "USA" } }, "swift_bank_account": null, "created_at": "2026-01-05T15:04:05Z" } ``` ```json 200 OK - SWIFT Bank Account { "id": "a50e8400-e29b-41d4-a716-446655440000", "type": "SWIFT_BANK_ACCOUNT", "first_name": "Aisha", "last_name": "Ahmed", "bank_account": null, "crypto_address": null, "us_bank_account": null, "swift_bank_account": { "account_owner_name": "Aisha Ahmed", "account_owner_type": "individual", "first_name": "Aisha", "last_name": "Ahmed", "category": "client", "account_country": "GBR", "bank_name": "HSBC UK", "bic": "HBUKGB4B", "iban_last4": "3210", "purpose_of_funds": ["personal_or_living_expenses"], "address": { "street_line_1": "10 Downing St", "city": "London", "postal_code": "SW1A 2AA", "country": "GBR" }, "bank_address": { "street_line_1": "8 Canada Square", "city": "London", "postal_code": "E14 5HQ", "country": "GBR" } }, "created_at": "2026-01-05T15:04:05Z" } ``` ## Error Responses ```json 400 Bad Request - Invalid UUID { "error": { "code": "BAD_REQUEST", "message": "Invalid UUID format", "request_id": "550e8400-e29b-41d4-a716-446655440000" } } ``` ```json 404 Not Found { "error": { "code": "NOT_FOUND", "message": "Recipient not found", "request_id": "550e8400-e29b-41d4-a716-446655440000" } } ``` ## Next Steps Retrieve all saved recipients Remove a payout recipient #### Delete recipient Path: /api-reference/recipients/delete-recipient Description: Soft-delete a recipient ## Overview Soft-delete a recipient. The recipient will no longer appear in list results and cannot be used for new transfers or settlement destinations. This action is irreversible. ## Authentication Your merchant API key ## Path Parameters Recipient ID (UUID) **Example:** `750e8400-e29b-41d4-a716-446655440000` ## Request Example ```bash cURL curl --request DELETE \ --url https://api.daya.co/v1/recipients/750e8400-e29b-41d4-a716-446655440000 \ --header 'X-Api-Key: YOUR_API_KEY' ``` ```javascript JavaScript const response = await fetch( "https://api.daya.co/v1/recipients/750e8400-e29b-41d4-a716-446655440000", { method: "DELETE", headers: { "X-Api-Key": "YOUR_API_KEY", }, } ); // 204 No Content on success if (response.status === 204) { console.log("Recipient deleted successfully"); } ``` ## Response A successful deletion returns a `204 No Content` response with an empty body. ### Success Response ```text 204 No Content (empty response body) ``` ## Error Responses ```json 400 Bad Request - Invalid UUID { "error": { "code": "validation_failed", "message": "Invalid UUID format" } } ``` ```json 404 Not Found { "error": { "code": "not_found", "message": "Recipient not found" } } ``` ```json 401 Unauthorized { "error": { "code": "unauthorized", "message": "Invalid or missing API key" } } ``` ## Notes - This is a soft-delete operation. The recipient record is retained internally but is no longer accessible via the API. - Any in-progress movements referencing this recipient will not be affected. - Deleting a recipient that has already been deleted will return a `404 Not Found` error. ## Next Steps Create a new recipient Retrieve all saved recipients ### Banks #### List supported banks Path: /api-reference/banks/list-banks Description: Return the list of supported Nigerian banks ## Overview Returns the list of Nigerian banks supported for NGN bank transfers and NGN payout settlement. Use this endpoint to populate bank selectors in your UI. ## Authentication Your merchant API key ## Request Examples ```bash cURL curl --request GET \ --url https://api.daya.co/v1/banks \ --header 'X-Api-Key: YOUR_API_KEY' ``` ```javascript JavaScript const response = await fetch('https://api.daya.co/v1/banks', { headers: { 'X-Api-Key': 'YOUR_API_KEY' } }); const banks = await response.json(); console.log(banks); ``` ## Response Array of supported bank objects Nigerian bank code (e.g. CBN institution code) **Example:** `044` Human-readable bank name **Example:** `Access Bank` ### Success Response ```json 200 OK { "data": [ { "code": "044", "name": "Access Bank" }, { "code": "058", "name": "GTBank" }, { "code": "011", "name": "First Bank of Nigeria" }, { "code": "033", "name": "United Bank for Africa" }, { "code": "057", "name": "Zenith Bank" } ] } ``` ## Error Responses ```json 401 Unauthorized { "error": { "code": "unauthorized", "message": "Invalid or missing API key" } } ``` ## Next Steps Verify a bank account number and get the account holder's name Send funds to a Nigerian bank account #### Resolve bank account Path: /api-reference/banks/resolve-bank-account Description: Verify a bank account number and get the account holder's name ## Overview Verify that a bank account number is valid and retrieve the account holder's name. Use this before creating a recipient to confirm account details with the user. ## Authentication Your merchant API key ## Request Body Bank account number to verify (typically 10 digits) **Example:** `1234567890` Bank code from the [List Banks](/api-reference/banks/list-banks) endpoint **Example:** `044` ## Request Examples ```json JSON { "account_number": "1234567890", "bank_code": "044" } ``` ```bash cURL curl --request POST \ --url https://api.daya.co/v1/banks/resolve \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "account_number": "1234567890", "bank_code": "044" }' ``` ```javascript JavaScript const response = await fetch('https://api.daya.co/v1/banks/resolve', { method: 'POST', headers: { 'X-Api-Key': 'YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ account_number: '1234567890', bank_code: '044' }) }); const result = await response.json(); console.log(result); ``` ## Response The verified bank account number The account holder's full name as registered with the bank ### Success Response ```json 200 OK { "account_number": "1234567890", "account_name": "JOHN DOE" } ``` ## Error Responses ```json 400 Bad Request - Validation failed { "error": { "code": "validation_failed", "message": "Validation failed", "details": "account_number is required" } } ``` ```json 400 Bad Request - Invalid account { "error": { "code": "validation_failed", "message": "Could not resolve account number for the specified bank" } } ``` ```json 400 Bad Request - Invalid bank code { "error": { "code": "validation_failed", "message": "Invalid bank code" } } ``` ```json 502 Bad Gateway - Provider unavailable { "error": { "code": "INTEGRATION_FAILED", "message": "Bank verification provider unavailable, please try again" } } ``` ## Next Steps Get bank codes to use with this endpoint Send funds to a Nigerian bank account ### USD Virtual Accounts #### Create USD virtual account Path: /api-reference/virtual-accounts/create-virtual-account Description: Create a USD virtual account for a customer ## Overview Creates a new USD virtual account for a customer. The customer must have completed tier 2 verification before creating a USD virtual account. Payments into the account settle into the merchant [collection balance](/api-reference/merchant-balance/get-merchant-balance), are listed as [USD virtual account deposits](/api-reference/virtual-account-deposits/list-usd-account-deposits), and send a [webhook](/api-reference/webhooks/events). **Prerequisite:** The customer must have completed tier 2 verification (`POST /v1/customers/{id}/tier2-verification`). ## Authentication Your merchant API key ## Request Body Customer ID (UUID). The customer must have completed tier 2 verification. **Example:** `650e8400-e29b-41d4-a716-446655440000` Currency for the virtual account. Defaults to `usd` if omitted. Currently only `usd` is supported. **Example:** `usd` Optional fee that your merchant account keeps from each USD deposit received through this virtual account. Omit to use `0%`. Percentage of each received USD deposit that your merchant account keeps. Use a decimal string from `0` to `50`. This is a percentage value, not basis points: `0.5` means `0.5%`, `2` means `2%`, and `50` means `50%`. **Example:** `"1.5"` ## Request Example ```bash cURL curl --request POST \ --url https://api.daya.co/v1/virtual-accounts \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "customer_id": "650e8400-e29b-41d4-a716-446655440000", "currency": "usd", "developer_fee": { "percentage": "1.5" } }' ``` ## Response Virtual account ID (UUID) Associated customer ID Merchant ID Source currency (e.g. `usd`) Account status (e.g. `active`) Virtual account provider (e.g. `bridge`) Developer fee percentage used for deposits received through this virtual account. Configured percentage as a decimal string. Bank deposit instructions for funding the virtual account. Beneficiary name Beneficiary address Bank name Bank address Bank routing number (US domestic ACH/wire) Bank account number Supported payment rails (e.g. `ach`, `wire`) Crypto destination where settled funds are sent. Payment rail (e.g. `base`) Destination currency (e.g. `usdc`) Destination address When the account was created (ISO 8601) When the account was last updated (ISO 8601) ```json 201 Created { "id": "650e8400-e29b-41d4-a716-446655440000", "customer_id": "650e8400-e29b-41d4-a716-446655440002", "merchant_id": "650e8400-e29b-41d4-a716-446655440001", "currency": "usd", "status": "active", "provider": "bridge", "developer_fee": { "percentage": "1.5" }, "deposit_instructions": { "bank_beneficiary_name": "Bridge Trust", "bank_beneficiary_address": "123 Finance St, New York, NY", "bank_name": "Lead Bank", "bank_address": "1801 Main St, Kansas City, MO", "bank_routing_number": "101019644", "bank_account_number": "1234567890", "payment_rails": ["ach", "wire"] }, "destination": { "payment_rail": "base", "currency": "usdc", "address": "0x1234567890abcdef1234567890abcdef12345678" }, "created_at": "2026-01-05T15:04:05Z", "updated_at": "2026-01-05T15:04:05Z" } ``` ```json 400 Bad Request { "error": { "code": "TIER2_NOT_VERIFIED", "message": "Customer has not completed tier 2 verification", "request_id": "550e8400-e29b-41d4-a716-446655440000" } } ``` #### List USD virtual accounts Path: /api-reference/virtual-accounts/list-virtual-accounts Description: List all USD virtual accounts ## Overview Lists all USD virtual accounts for the authenticated merchant with optional filtering by customer and status. Each virtual account includes `developer_fee.percentage`. ## Authentication Your merchant API key ## Query Parameters Number of results per page (default: 50, max: 200) Page number (default: 1) Filter by customer ID (UUID) Filter by status **Allowed values:** `active` | `inactive` ## Request Example ```bash cURL curl --request GET \ --url 'https://api.daya.co/v1/virtual-accounts?limit=50&page=1' \ --header 'X-Api-Key: YOUR_API_KEY' ``` ## Response Array of virtual account objects Current page number Results per page Total number of virtual accounts Total number of pages ```json 200 OK { "data": [ { "id": "650e8400-e29b-41d4-a716-446655440000", "customer_id": "650e8400-e29b-41d4-a716-446655440002", "merchant_id": "650e8400-e29b-41d4-a716-446655440001", "currency": "usd", "status": "active", "provider": "bridge", "developer_fee": { "percentage": "1.5" }, "deposit_instructions": { "bank_beneficiary_name": "Bridge Trust", "bank_beneficiary_address": "123 Finance St, New York, NY", "bank_name": "Lead Bank", "bank_address": "1801 Main St, Kansas City, MO", "bank_routing_number": "101019644", "bank_account_number": "1234567890", "payment_rails": ["ach", "wire"] }, "destination": { "payment_rail": "base", "currency": "usdc", "address": "0x1234567890abcdef1234567890abcdef12345678" }, "created_at": "2026-01-05T15:04:05Z", "updated_at": "2026-01-05T15:04:05Z" } ], "page": 1, "limit": 50, "total": 1, "total_pages": 1 } ``` #### Get USD virtual account Path: /api-reference/virtual-accounts/get-virtual-account Description: Retrieve a single virtual account by ID ## Overview Retrieves a single USD virtual account by ID for the authenticated merchant. Response includes deposit instructions, destination, status, provider, and the configured `developer_fee.percentage`. ## Authentication Your merchant API key ## Path Parameters USD virtual account ID (UUID format) **Example:** `650e8400-e29b-41d4-a716-446655440000` ## Request Example ```bash cURL curl --request GET \ --url https://api.daya.co/v1/virtual-accounts/650e8400-e29b-41d4-a716-446655440000 \ --header 'X-Api-Key: YOUR_API_KEY' ``` ## Response Returns the full virtual account object including deposit instructions and destination details. ```json 200 OK { "id": "650e8400-e29b-41d4-a716-446655440000", "customer_id": "650e8400-e29b-41d4-a716-446655440002", "merchant_id": "650e8400-e29b-41d4-a716-446655440001", "currency": "usd", "status": "active", "provider": "bridge", "developer_fee": { "percentage": "1.5" }, "deposit_instructions": { "bank_beneficiary_name": "Bridge Trust", "bank_beneficiary_address": "123 Finance St, New York, NY", "bank_name": "Lead Bank", "bank_address": "1801 Main St, Kansas City, MO", "bank_routing_number": "101019644", "bank_account_number": "1234567890", "payment_rails": ["ach", "wire"] }, "destination": { "payment_rail": "base", "currency": "usdc", "address": "0x1234567890abcdef1234567890abcdef12345678" }, "created_at": "2026-01-05T15:04:05Z", "updated_at": "2026-01-05T15:04:05Z" } ``` ```json 404 Not Found { "error": { "code": "NOT_FOUND", "message": "Virtual account not found", "request_id": "550e8400-e29b-41d4-a716-446655440000" } } ``` #### List customer USD virtual accounts Path: /api-reference/virtual-accounts/list-customer-virtual-accounts Description: List USD virtual accounts for a specific customer ## Overview Lists all USD virtual accounts for a specific customer under the authenticated merchant. The customer must have completed tier 2 verification to have virtual accounts. Each virtual account includes `developer_fee.percentage`. ## Authentication Your merchant API key ## Path Parameters Customer ID (UUID format) **Example:** `650e8400-e29b-41d4-a716-446655440002` ## Query Parameters Number of results per page (default: 50, max: 200) Page number (default: 1) Filter by status **Allowed values:** `active` | `inactive` ## Request Example ```bash cURL curl --request GET \ --url 'https://api.daya.co/v1/customers/650e8400-e29b-41d4-a716-446655440002/virtual-accounts' \ --header 'X-Api-Key: YOUR_API_KEY' ``` ## Response Array of virtual account objects for the customer Current page number Results per page Total number of virtual accounts Total number of pages ```json 200 OK { "data": [ { "id": "650e8400-e29b-41d4-a716-446655440000", "customer_id": "650e8400-e29b-41d4-a716-446655440002", "merchant_id": "650e8400-e29b-41d4-a716-446655440001", "currency": "usd", "status": "active", "provider": "bridge", "developer_fee": { "percentage": "1.5" }, "deposit_instructions": { "bank_beneficiary_name": "Bridge Trust", "bank_beneficiary_address": "123 Finance St, New York, NY", "bank_name": "Lead Bank", "bank_address": "1801 Main St, Kansas City, MO", "bank_routing_number": "101019644", "bank_account_number": "1234567890", "payment_rails": ["ach", "wire"] }, "destination": { "payment_rail": "base", "currency": "usdc", "address": "0x1234567890abcdef1234567890abcdef12345678" }, "created_at": "2026-01-05T15:04:05Z", "updated_at": "2026-01-05T15:04:05Z" } ], "page": 1, "limit": 50, "total": 1, "total_pages": 1 } ``` ### USD Virtual Account Deposits #### List USD virtual account deposits Path: /api-reference/virtual-account-deposits/list-usd-account-deposits Description: List payments received into USD virtual accounts ## Overview Retrieve payments received into USD virtual accounts. Use this endpoint when a customer pays into USD bank details created with `/v1/virtual-accounts`. Use [List deposits](/api-reference/deposits/list-deposits) for NGN and crypto deposits received through funding accounts. ## Authentication Your merchant API key ## Query Parameters Filter by customer ID (UUID). Filter by USD virtual account ID (UUID). Filter by USD account deposit status. **Allowed values:** `PENDING` | `COMPLETED` | `FLAGGED` | `FAILED` Filter deposits created from this time (RFC 3339, inclusive). Filter deposits created before this time (RFC 3339, exclusive). Results per page. **Default:** `50` | **Max:** `200` Page number to retrieve. **Default:** `1` ## Request Examples ```bash List USD Virtual Account Deposits curl --request GET \ --url 'https://api.daya.co/v1/virtual-account-deposits' \ --header 'X-Api-Key: YOUR_API_KEY' ``` ```bash By Virtual Account curl --request GET \ --url 'https://api.daya.co/v1/virtual-account-deposits?virtual_account_id=550e8400-e29b-41d4-a716-446655440000' \ --header 'X-Api-Key: YOUR_API_KEY' ``` ```javascript JavaScript const response = await fetch( 'https://api.daya.co/v1/virtual-account-deposits?status=COMPLETED&limit=50&page=1', { headers: { 'X-Api-Key': 'YOUR_API_KEY' } } ); const data = await response.json(); ``` ## Response Array of USD account deposit objects. Unique deposit identifier (UUID). Always `USD_DEPOSIT`. USD account deposit ID. Associated customer ID. Amount received in USD. Always `USD`. Amount credited after fees, when settled. Settlement currency, usually `USD`. Payment rail used, such as `ach` or `wire`. Sender information when available. Public status: `PENDING`, `COMPLETED`, `FLAGGED`, or `FAILED`. Settlement progress. Fees applied to the deposit. Merchant developer fee kept from this USD deposit, when configured. This is separate from Daya fees. Configured percentage as a decimal string. Amount kept by your merchant account after settlement. Currency of the developer fee amount. Amount left for the customer after Daya fees and the developer fee, when available. Amount left for the customer. Currency of the customer amount. Time the payment was received. Last status update time. Current page number. Results per page. Total number of deposits matching filters. Total number of pages available. ### Success Response ```json 200 OK { "data": [ { "id": "9c4e8400-e29b-41d4-a716-446655440000", "type": "USD_DEPOSIT", "va_deposit_id": "9c4e8400-e29b-41d4-a716-446655440000", "customer_id": "650e8400-e29b-41d4-a716-446655440000", "amount": "100.00", "currency": "USD", "settled_amount": "99.50", "settled_currency": "USD", "payment_rail": "ach", "sender": { "name": "Jane Doe", "account_number": "****1234", "bank_name": "Chase" }, "status": "COMPLETED", "settlement_status": "COMPLETED", "fees": { "deposit_fee": { "amount": "0.50", "currency": "USD" }, "total_fee_usd": "0.50" }, "developer_fee": { "percentage": "1.5", "amount": "1.49", "currency": "USD" }, "customer_amount": { "amount": "98.01", "currency": "USD" }, "created_at": "2026-01-14T17:00:00Z", "updated_at": "2026-01-14T17:01:30Z" } ], "page": 1, "limit": 50, "total": 1, "total_pages": 1 } ``` ## Next Steps Get a specific USD virtual account deposit by ID Learn how USD virtual accounts work #### Get USD virtual account deposit Path: /api-reference/virtual-account-deposits/get-usd-account-deposit Description: Retrieve a payment received into a USD virtual account ## Overview Retrieve a specific USD virtual account deposit by ID. Use this endpoint for payments into USD virtual accounts. Use [Get deposit](/api-reference/deposits/get-deposit) for NGN and crypto deposits received through funding accounts. ## Authentication Your merchant API key ## Path Parameters USD account deposit ID (UUID format). **Example:** `9c4e8400-e29b-41d4-a716-446655440000` ## Request Examples ```bash cURL curl --request GET \ --url https://api.daya.co/v1/virtual-account-deposits/9c4e8400-e29b-41d4-a716-446655440000 \ --header 'X-Api-Key: YOUR_API_KEY' ``` ```javascript JavaScript const response = await fetch( 'https://api.daya.co/v1/virtual-account-deposits/9c4e8400-e29b-41d4-a716-446655440000', { headers: { 'X-Api-Key': 'YOUR_API_KEY' } } ); const deposit = await response.json(); ``` ## Response Returns a USD account deposit object. See [List USD account deposits](/api-reference/virtual-account-deposits/list-usd-account-deposits#response) for the full field list. ### Success Response ```json 200 OK - Completed { "id": "9c4e8400-e29b-41d4-a716-446655440000", "type": "USD_DEPOSIT", "va_deposit_id": "9c4e8400-e29b-41d4-a716-446655440000", "customer_id": "650e8400-e29b-41d4-a716-446655440000", "amount": "100.00", "currency": "USD", "settled_amount": "99.50", "settled_currency": "USD", "payment_rail": "ach", "sender": { "name": "Jane Doe", "account_number": "****1234", "bank_name": "Chase" }, "status": "COMPLETED", "settlement_status": "COMPLETED", "fees": { "deposit_fee": { "amount": "0.50", "currency": "USD" }, "total_fee_usd": "0.50" }, "developer_fee": { "percentage": "1.5", "amount": "1.49", "currency": "USD" }, "customer_amount": { "amount": "98.01", "currency": "USD" }, "created_at": "2026-01-14T17:00:00Z", "updated_at": "2026-01-14T17:01:30Z" } ``` ```json 404 Not Found { "error": { "code": "NOT_FOUND", "message": "Deposit not found", "request_id": "550e8400-e29b-41d4-a716-446655440000" } } ``` ### Merchant Balance #### Get merchant balance Path: /api-reference/merchant-balance/get-merchant-balance Description: Retrieve the current merchant USD collection and withdrawal balances ## Overview Returns your current merchant USD balances. The response includes separate `collection_balance_usd` and `withdrawal_balance_usd` fields. The collection balance accumulates from funding account deposits and USD virtual account deposits, and can be transferred to the withdrawal balance via the [balance transfer endpoint](/api-reference/merchant-balance/transfer-merchant-balance). The withdrawal balance is used for transfers and on-chain withdrawals. ## Authentication Your merchant API key ## Request Examples ```bash cURL curl --request GET \ --url https://api.daya.co/v1/merchant/balance \ --header 'X-Api-Key: YOUR_API_KEY' ``` ```javascript JavaScript const response = await fetch('https://api.daya.co/v1/merchant/balance', { headers: { 'X-Api-Key': 'YOUR_API_KEY' } }); const balance = await response.json(); console.log(balance); ``` ## Response Current merchant balance details. Collection balance in USD. Accumulates from funding account deposits and USD virtual account deposits. **Example:** `500.1234` Withdrawal balance in USD. Used for on-chain withdrawals and merchant funding. **Example:** `100.1234` ### Success Response ```json 200 OK { "data": { "collection_balance_usd": "500.1234", "withdrawal_balance_usd": "100.1234" } } ``` ## Error Responses This endpoint may return: - `401`: Unauthorized - `500`: Internal server error ## Next Steps Move funds from collection to withdrawal balance Transfer withdrawal balance to an on-chain address View funding instructions to top up your balance #### Transfer merchant balance Path: /api-reference/merchant-balance/transfer-merchant-balance Description: Transfer funds from collection to withdrawal balance ## Overview Transfers USD from the merchant collection balance into the merchant withdrawal balance. This is a USD-only operation. ## Authentication Your merchant API key Unique idempotency key for request deduplication **Example:** `550e8400-e29b-41d4-a716-446655440000` ## Request Body Amount in USD to transfer from collection to withdrawal balance. **Example:** `100.00` ## Request Example ```bash cURL curl --request POST \ --url https://api.daya.co/v1/merchant/balance/transfer \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'X-Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000' \ --header 'Content-Type: application/json' \ --data '{ "amount_usd": "100.00" }' ``` ## Response Updated merchant balance after the transfer. Updated collection balance in USD Updated withdrawal balance in USD ```json 200 OK { "data": { "collection_balance_usd": "400.0000", "withdrawal_balance_usd": "600.0000" } } ``` ```json 400 Bad Request { "error": { "code": "INSUFFICIENT_BALANCE", "message": "Insufficient collection balance", "request_id": "550e8400-e29b-41d4-a716-446655440000" } } ``` #### List balance transfers Path: /api-reference/merchant-balance/list-balance-transfers Description: List collection-to-withdrawal balance transfer history ## Overview Lists the history of collection-to-withdrawal balance transfers for the authenticated merchant. This is the audit trail for balance moves executed via [`POST /v1/merchant/balance/transfer`](/api-reference/merchant-balance/transfer-merchant-balance). This endpoint returns the history of balance moves. To execute a new balance transfer, use [`POST /v1/merchant/balance/transfer`](/api-reference/merchant-balance/transfer-merchant-balance), which returns the updated balances immediately. ## Authentication Your merchant API key ## Query Parameters Number of results per page (default: 50) Page number (default: 1) Filter by transfer status **Allowed values:** `PENDING` | `COMPLETED` | `FAILED` ## Request Example ```bash cURL curl --request GET \ --url 'https://api.daya.co/v1/merchant/balance/transfers?limit=50&page=1' \ --header 'X-Api-Key: YOUR_API_KEY' ``` ## Response Array of balance transfer records. Unique transfer identifier (UUID) Amount transferred in USD Transfer status: `PENDING`, `COMPLETED`, or `FAILED` Collection balance before the transfer Collection balance after the transfer Withdrawal balance before the transfer Withdrawal balance after the transfer Human-readable failure detail (present when status is `FAILED`) When the transfer was created (ISO 8601) When the transfer was last updated (ISO 8601) Current page number Results per page Total number of balance transfers Total number of pages ```json 200 OK { "data": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "amount_usd": "100.0000", "status": "COMPLETED", "collection_balance_before_usd": "500.0000", "collection_balance_after_usd": "400.0000", "withdrawal_balance_before_usd": "200.0000", "withdrawal_balance_after_usd": "300.0000", "failure_message": null, "created_at": "2026-01-05T15:04:05Z", "updated_at": "2026-01-05T15:04:05Z" } ], "page": 1, "limit": 50, "total": 1, "total_pages": 1 } ``` #### Withdraw merchant balance Path: /api-reference/merchant-balance/withdraw-merchant-balance Description: Send USD from your merchant balance to a supported on-chain destination ## Overview Creates a withdrawal from your **withdrawal balance** to a supported on-chain address. Withdrawals draw from the withdrawal balance only — not directly from the collection balance. If the needed funds are still in the collection balance, move them first via [`POST /v1/merchant/balance/transfer`](/api-reference/merchant-balance/transfer-merchant-balance). Provide a unique `X-Idempotency-Key` for each withdrawal attempt so retries do not create duplicate transfers. Before creating a withdrawal, check [Get Merchant Balance](/api-reference/merchant-balance/get-merchant-balance) and validate the destination chain with [List Supported Chains](/api-reference/supported-chains/list-supported-chains). Only `USDT` on `POLYGON` is withdrawal-enabled right now. Use the token-level metadata from `GET /v1/supported-chains` to validate supported combinations before submitting a withdrawal. ## Authentication Your merchant API key ## Headers Unique request identifier used to deduplicate retries. **Example:** `withdrawal-20260310-0001` ## Request Body Amount to withdraw in USD decimal format. **Example:** `12.3400` Token to send on-chain. **Allowed values:** `USDC`, `USDT` Destination chain for the withdrawal. **Allowed values:** `SOLANA`, `TRON`, `APTOS`, `BASE`, `POLYGON`, `ETHEREUM` Address that will receive the withdrawal. **Example:** `4vJ9JU1bJJE96FWSJN` Make sure the address format matches the selected chain and token pair. ## Request Examples ```bash cURL curl --request POST \ --url https://api.daya.co/v1/merchant/withdrawals \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --header 'X-Idempotency-Key: withdrawal-20260310-0001' \ --data '{ "amount_usd": "12.3400", "token": "USDC", "chain": "SOLANA", "destination_address": "4vJ9JU1bJJE96FWSJN" }' ``` ```json Request Body { "amount_usd": "12.3400", "token": "USDC", "chain": "SOLANA", "destination_address": "4vJ9JU1bJJE96FWSJN" } ``` ```javascript JavaScript const response = await fetch('https://api.daya.co/v1/merchant/withdrawals', { method: 'POST', headers: { 'X-Api-Key': 'YOUR_API_KEY', 'Content-Type': 'application/json', 'X-Idempotency-Key': 'withdrawal-20260310-0001' }, body: JSON.stringify({ amount_usd: '12.3400', token: 'USDC', chain: 'SOLANA', destination_address: '4vJ9JU1bJJE96FWSJN' }) }); const withdrawal = await response.json(); console.log(withdrawal); ``` ## Response Withdrawal creation result. Identifier for the created transfer. **Example:** `transactions/abc123` ### Success Response ```json 201 Created { "data": { "transaction_id": "transactions/abc123" } } ``` ## Error Responses ```json 400 Bad Request { "error": { "code": "VALIDATION_FAILED", "message": "Invalid request", "request_id": "550e8400-e29b-41d4-a716-446655440000", "validation": "amount_usd must be greater than zero" } } ``` ```json 409 Conflict { "error": { "code": "IDEMPOTENCY_CONFLICT", "message": "Idempotency key already used with a different payload", "request_id": "550e8400-e29b-41d4-a716-446655440000" } } ``` #### List withdrawals Path: /api-reference/merchant-balance/list-withdrawals Description: List withdrawals for the authenticated merchant with optional status filtering and pagination ## Overview Returns merchant withdrawal history for the authenticated merchant. Withdrawals draw from the **withdrawal balance** only — if the needed funds are still in the collection balance, use [`POST /v1/merchant/balance/transfer`](/api-reference/merchant-balance/transfer-merchant-balance) to move them first. Use this endpoint to track pending, submitted, settled, or failed withdrawals and to paginate through historical activity. ## Authentication Your merchant API key ## Query Parameters Number of results per page. **Default:** `50` Page number to retrieve. **Default:** `1` Filter by withdrawal status. **Allowed values:** `PENDING`, `SUBMITTED`, `SETTLED`, `FAILED` ## Request Examples ```bash cURL curl --request GET \ --url 'https://api.daya.co/v1/merchant/withdrawals?status=SETTLED&limit=20' \ --header 'X-Api-Key: YOUR_API_KEY' ``` ```javascript JavaScript const response = await fetch( 'https://api.daya.co/v1/merchant/withdrawals?status=SETTLED&limit=20', { headers: { 'X-Api-Key': 'YOUR_API_KEY' } } ); const withdrawals = await response.json(); console.log(withdrawals); ``` ## Response Withdrawal records matching the current filter. Unique withdrawal identifier. Withdrawal amount in USD. Fee charged for the withdrawal, in USD. Token sent on-chain. Destination chain. Recipient on-chain address. Current withdrawal status. Provider-side transfer identifier, when available. On-chain transaction hash, when available. Provider or platform failure code for failed withdrawals. Human-readable failure detail for failed withdrawals. Time the withdrawal was created. Time the withdrawal was submitted to the provider. Time the withdrawal was confirmed as settled. Last update time for the withdrawal. Current page number. Results per page. Total number of withdrawals matching filters. Total number of pages available. ### Success Response ```json 200 OK { "data": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "amount_usd": "12.3400", "fee_usd": "0.50", "token": "USDC", "chain": "SOLANA", "destination_address": "4vJ9JU1bJJE96FWSJN", "status": "SETTLED", "provider_tx_id": "transactions/abc123", "tx_hash": "4D5uX4exampleTxHash", "created_at": "2026-03-10T09:00:00Z", "submitted_at": "2026-03-10T09:01:00Z", "settled_at": "2026-03-10T09:03:00Z", "updated_at": "2026-03-10T09:03:00Z" } ], "page": 1, "limit": 50, "total": 1, "total_pages": 1 } ``` ## Error Responses This endpoint may return: - `400`: Invalid query parameters - `401`: Unauthorized - `500`: Internal server error ## Next Steps Retrieve a single withdrawal by ID Initiate a new merchant balance withdrawal #### Get withdrawal Path: /api-reference/merchant-balance/get-withdrawal Description: Retrieve a single merchant balance withdrawal by ID ## Overview Returns one merchant withdrawal record for the authenticated merchant. Use this endpoint when you already have a withdrawal ID and need its latest state, timestamps, or transaction references. The older `GET /v1/withdrawals/{id}` route still exists. This merchant-prefixed route is an alias added for consistency with `POST /v1/merchant/withdrawals`. ## Authentication Your merchant API key ## Path Parameters Withdrawal ID in UUID format. **Example:** `550e8400-e29b-41d4-a716-446655440000` ## Request Examples ```bash cURL curl --request GET \ --url https://api.daya.co/v1/merchant/withdrawals/550e8400-e29b-41d4-a716-446655440000 \ --header 'X-Api-Key: YOUR_API_KEY' ``` ```javascript JavaScript const response = await fetch( 'https://api.daya.co/v1/merchant/withdrawals/550e8400-e29b-41d4-a716-446655440000', { headers: { 'X-Api-Key': 'YOUR_API_KEY' } } ); const withdrawal = await response.json(); console.log(withdrawal); ``` ## Response Unique withdrawal identifier. Withdrawal amount in USD. Fee charged for the withdrawal, in USD. Token sent on-chain. Destination chain. Recipient on-chain address. Current withdrawal status. **Common values:** `PENDING`, `SUBMITTED`, `SETTLED`, `FAILED` Provider-side transfer identifier, when available. On-chain transaction hash, when available. Failure code for unsuccessful withdrawals. Human-readable failure detail for unsuccessful withdrawals. Time the withdrawal was created. Time the withdrawal was submitted to the provider. Time the withdrawal was settled. Last update time for the withdrawal. ### Success Response ```json 200 OK { "id": "550e8400-e29b-41d4-a716-446655440000", "amount_usd": "12.3400", "fee_usd": "0.50", "token": "USDC", "chain": "SOLANA", "destination_address": "4vJ9JU1bJJE96FWSJN", "status": "SETTLED", "provider_tx_id": "transactions/abc123", "tx_hash": "4D5uX4exampleTxHash", "created_at": "2026-03-10T09:00:00Z", "submitted_at": "2026-03-10T09:01:00Z", "settled_at": "2026-03-10T09:03:00Z", "updated_at": "2026-03-10T09:03:00Z" } ``` ```json 404 Not Found { "error": { "code": "NOT_FOUND", "message": "Withdrawal not found", "request_id": "550e8400-e29b-41d4-a716-446655440000" } } ``` ## Error Responses This endpoint may return: - `400`: Invalid withdrawal ID format - `401`: Unauthorized - `404`: Withdrawal not found - `500`: Internal server error ## Next Steps Browse recent withdrawals and paginate through history Initiate another merchant balance withdrawal ### Merchant Funding #### Get merchant funding Path: /api-reference/merchant-funding/get-merchant-funding Description: Get merchant funding instructions ## Overview Returns the merchant's funding setup status, permanent NGN collection account, and supported crypto wallet deposit addresses. Merchant funding deposits top up the merchant withdrawal balance. NGN funding deposits are converted to USD at the current rate before crediting. Successful merchant funding credits trigger a merchant email notification. ## Authentication Your merchant API key ## Request Example ```bash cURL curl --request GET \ --url https://api.daya.co/v1/merchant/funding \ --header 'X-Api-Key: YOUR_API_KEY' ``` ## Response Merchant funding details. Current funding setup status (e.g. `ACTIVE`) Permanent NGN bank account for funding deposits. Bank account number Account name Bank name Bank code Crypto wallet with deposit addresses across supported chains. Wallet identifier Array of deposit addresses, one per supported chain. Supported chains: `ETHEREUM`, `BASE`, `OPTIMISM`, `BSC`, `POLYGON`, `APTOS`, `TRON` Each address object contains `chain` and `address` fields. ```json 200 OK { "data": { "setup_status": "ACTIVE", "ngn_account": { "account_number": "0123456789", "account_name": "Acme Ltd", "bank_name": "Wema Bank", "bank_code": "035" }, "crypto_wallet": { "wallet_id": "wallet_abc123", "addresses": [ { "chain": "ETHEREUM", "address": "0xabcdef1234567890abcdef1234567890abcdef12" }, { "chain": "BASE", "address": "0xabcdef1234567890abcdef1234567890abcdef12" }, { "chain": "OPTIMISM", "address": "0xabcdef1234567890abcdef1234567890abcdef12" }, { "chain": "BSC", "address": "0xabcdef1234567890abcdef1234567890abcdef12" }, { "chain": "POLYGON", "address": "0xabcdef1234567890abcdef1234567890abcdef12" }, { "chain": "APTOS", "address": "0xabcdef1234567890abcdef1234567890abcdef12" }, { "chain": "TRON", "address": "TAbcdef1234567890abcdef1234567890ab" } ] } } } ``` ```json 404 Not Found { "error": { "code": "NOT_FOUND", "message": "Merchant funding configuration not found", "request_id": "550e8400-e29b-41d4-a716-446655440000" } } ``` ## Notes - Merchant funding surfaces include a permanent NGN virtual account plus crypto deposit addresses. - NGN funding deposits are converted to USD at the current rate before crediting the merchant withdrawal balance. - Successful funding credits trigger an email notification to the merchant. ### Webhooks #### Webhooks Overview Path: /api-reference/webhooks/overview Description: Real-time event notifications for money movement lifecycle events ## What are Webhooks? Webhooks let you receive real-time HTTP notifications when funding account, deposit, transfer, withdrawal, or customer verification lifecycle events occur, instead of polling the API for status changes. Webhooks are the recommended way to track funding account provisioning, deposit settlement, merchant-initiated bank transfers, crypto withdrawal progress, and customer verification changes. ## Supported Events **Deposit Events:** | Event | Resource | Description | |-------|----------|-------------| | `deposit.received` | Deposit | Deposit received (NGN or crypto) | | `deposit.processing` | Deposit | Deposit settlement has started | | `deposit.requires_review` | Deposit | Deposit needs review before it can continue | | `deposit.completed` | Deposit | Deposit reached its settlement destination | | `deposit.failed` | Deposit | Deposit failed | | `deposit.reversed` | Deposit | Completed deposit reversed | **Funding Account Events:** | Event | Resource | Description | |-------|----------|-------------| | `funding_account.created` | Funding Account | Funding account record created in pending state | | `funding_account.active` | Funding Account | Receive instructions provisioned successfully | | `funding_account.updated` | Funding Account | An instruction changed status or received updated payment details | | `funding_account.failed` | Funding Account | Receive instruction provisioning failed | | `funding_account.disabled` | Funding Account | Funding account disabled | **Withdrawal Events:** | Event | Resource | Description | |-------|----------|-------------| | `withdrawal.created` | Withdrawal | Withdrawal request accepted | | `withdrawal.submitted` | Withdrawal | Withdrawal submitted to chain | | `withdrawal.completed` | Withdrawal | Withdrawal confirmed on-chain | | `withdrawal.failed` | Withdrawal | Withdrawal failed | **Transfer Events:** | Event | Resource | Description | |-------|----------|-------------| | `transfer.created` | Transfer | Transfer record created | | `transfer.processing` | Transfer | Funds locked and transfer submission queued | | `transfer.requires_review` | Transfer | Transfer flagged for manual review | | `transfer.submitted` | Transfer | Provider accepted the transfer submission | | `transfer.completed` | Transfer | Transfer settled after provider success and ledger finalization | | `transfer.failed` | Transfer | Transfer failed terminally | | `transfer.reversed` | Transfer | Completed transfer reversed | **Customer Verification Events:** | Event | Resource | Description | |-------|----------|-------------| | `customer.verification.submitted` | Customer | Verification submitted for review | | `customer.verification.approved` | Customer | Verification approved | | `customer.verification.information_requested` | Customer | More information is required to continue verification | | `customer.verification.rejected` | Customer | Verification rejected | **Bank Account Verification Events:** | Event | Resource | Description | |-------|----------|-------------| | `customer.bank_account_verification.succeeded` | Bank account verification result | Paystack verified the customer's submitted bank details | | `customer.bank_account_verification.failed` | Bank account verification result | Paystack rejected the customer's submitted bank details | Use `transfer.*` events for `POST /v1/transfers`. For deposits, track settlement with `deposit.*` events; settlement delivery work that Daya performs in the background is not surfaced as a separate payout webhook. ## Webhook Configuration Configure webhook endpoints in your [Daya Dashboard](https://dashboard.daya.co/webhooks): 1. Navigate to **Webhooks** 2. Add your webhook URL 3. Generate or copy your webhook secret 4. Subscribe to the events you want to receive The `event_types` setting controls which events an endpoint receives. Omit `event_types` or set it to `[]` to receive all events. Use a non-empty array, such as `["deposit.received", "deposit.completed"]`, to receive only those exact event names. Unknown event names are rejected. Use HTTPS in production. HTTP should be limited to local or sandbox development. ## Webhook Payload All merchant webhooks use the same envelope: ```json { "event": "transfer.completed", "id": "550e8400-e29b-41d4-a716-446655440000", "data": { "id": "650e8400-e29b-41d4-a716-446655440000", "status": "SETTLED", "rail": "NGN_BANK", "reference": "txn_ngn_001" }, "timestamp": "2026-03-10T09:03:00Z" } ``` ### Common Fields Event type, such as `deposit.completed`, `transfer.completed`, or `withdrawal.failed`. Unique webhook event identifier. Store this value to deduplicate deliveries. Resource payload for the event. Funding account events send a funding account object, deposit events send a deposit object, transfer events send a transfer object, withdrawal events send a withdrawal object, and `customer.verification.*` events send a customer object. Bank account verification events send the result fields documented in [Webhook Events](/api-reference/webhooks/events#bank-account-verification-events). RFC3339 timestamp for when the event was emitted. See [Webhook Events](/api-reference/webhooks/events) for the full event list and resource payload mapping. ## Delivery Guarantees Webhooks may be delivered more than once. Your handler must deduplicate repeated deliveries. Events can arrive out of order. Reconcile the resource's latest state instead of assuming delivery order. For bank account verification results, prefer the highest `identity_version`; for two results with the same version, keep the one with the later `timestamp`. If your endpoint returns a non-2xx response or times out, Daya retries with backoff. Build your handler to be safe for repeated delivery. Respond quickly and offload heavy work asynchronously. Slow handlers increase duplicate deliveries. ## Webhook Verification All webhook requests include an HMAC-SHA256 signature in the `X-Daya-Signature` header. See [Webhook Verification](/api-reference/webhooks/verification) for implementation details. ## Implementing a Webhook Endpoint Your endpoint should: 1. Verify the signature 2. Build a stable deduplication key from the payload 3. Return `2xx` quickly 4. Process the event asynchronously if work is non-trivial ```javascript Express.js const express = require('express'); const crypto = require('crypto'); const app = express(); app.use(express.json()); function verifySignature(payload, signature, secret) { const expected = crypto .createHmac('sha256', secret) .update(payload) .digest('hex'); return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expected) ); } function webhookSubjectId(event) { return event.data.id || event.data.deposit_id || event.data.customer_id; } function dedupeKey(event) { return event.id || `${event.event}:${webhookSubjectId(event)}:${event.timestamp}`; } app.post('/webhooks/daya', (req, res) => { const signature = req.headers['x-daya-signature']; const payload = JSON.stringify(req.body); if (!verifySignature(payload, signature, process.env.DAYA_WEBHOOK_SECRET)) { return res.status(401).send('Invalid signature'); } const key = dedupeKey(req.body); if (isProcessed(key)) { return res.status(200).send('Already processed'); } queue.add('process-webhook', req.body); markAsProcessed(key); res.status(200).send('OK'); }); ``` ```python Flask from flask import Flask, request, jsonify import hashlib import hmac app = Flask(__name__) def verify_signature(payload, signature, secret): expected = hmac.new( secret.encode(), payload, hashlib.sha256 ).hexdigest() return hmac.compare_digest(signature, expected) def webhook_subject_id(event): return event["data"].get("id") or event["data"].get("deposit_id") or event["data"].get("customer_id") def dedupe_key(event): return event.get("id") or f'{event["event"]}:{webhook_subject_id(event)}:{event["timestamp"]}' @app.route('/webhooks/daya', methods=['POST']) def handle_webhook(): signature = request.headers.get('X-Daya-Signature') payload = request.get_data() if not verify_signature(payload, signature, DAYA_WEBHOOK_SECRET): return jsonify({'error': 'Invalid signature'}), 401 event = request.json key = dedupe_key(event) if is_processed(key): return jsonify({'status': 'already_processed'}), 200 queue.enqueue('process_webhook', event) mark_as_processed(key) return jsonify({'status': 'ok'}), 200 ``` ## Best Practices Always verify `X-Daya-Signature` before trusting the payload. Store the webhook event `id` as your primary deduplication key. ```sql CREATE TABLE processed_webhook_events ( webhook_event_id VARCHAR(255) PRIMARY KEY, processed_at TIMESTAMP ); ``` Acknowledge receipt immediately and queue heavier downstream work. Use `timestamp` and the underlying resource state to reconcile event order safely. Track failed deliveries and alert on sustained retries. ## Testing Webhooks For deposit webhook flows: 1. Create a sandbox receive instruction 2. Trigger a sandbox deposit with [Create a sandbox deposit](/api-reference/sandbox/create-sandbox-deposit) 3. Observe the resulting deposit lifecycle webhooks For withdrawal webhook flows: 1. Fund merchant balance through a merchant-balance settlement flow 2. Create a withdrawal 3. Observe `withdrawal.created`, `withdrawal.submitted`, `withdrawal.completed`, or `withdrawal.failed` ## Troubleshooting Check endpoint reachability, TLS configuration, and whether your handler is returning non-2xx responses. Duplicate delivery is expected under at-least-once semantics. Deduplicate using the webhook event `id`. Sort or reconcile events using `timestamp` instead of arrival order. Verify you are using the correct webhook secret and the raw request body. ## Next Steps Event inventory and resource payload mapping Implement HMAC verification #### Webhook Events Path: /api-reference/webhooks/events Description: High-level event inventory and webhook payload envelope ## Overview Daya sends merchant webhooks for funding account, deposit, transfer, withdrawal, customer verification, and customer bank-account verification lifecycle changes. Every webhook uses the same envelope: ```json { "event": "transfer.completed", "id": "550e8400-e29b-41d4-a716-446655440000", "data": { "id": "650e8400-e29b-41d4-a716-446655440000", "status": "SETTLED", "rail": "NGN_BANK" }, "timestamp": "2026-03-10T09:03:00Z" } ``` `event` tells you what changed, `id` identifies the webhook event for idempotency, and `data` describes the resource or verification result that changed. ## Common Payload Fields Event type for the lifecycle transition that occurred. Unique webhook event identifier. Use this value for idempotency. Payload for the event. Resource lifecycle events follow the corresponding public API response, while bank-account verification events use the result shape documented below. RFC3339 timestamp for when the event was emitted. ## Data Shapes Webhook events are documented here at the event-name level. Resource lifecycle events use the corresponding public API response object; bank-account verification events use the result object documented on this page. | Event family | `data` shape | Resource docs | |--------------|--------------|---------------| | `funding_account.*` | Funding account response object | [Get Funding Account](/api-reference/funding-accounts/get-funding-account) | | `deposit.*` | Deposit response object | [Get Deposit](/api-reference/deposits/get-deposit) | | `transfer.*` | Transfer response object | [Get Transfer](/api-reference/transfers/get-transfer) | | `withdrawal.*` | Withdrawal response object | [Get Withdrawal](/api-reference/merchant-balance/get-withdrawal) | | `customer.verification.*` | Customer response object with the latest verification state | [Get Customer](/api-reference/customers/get-customer) | | `customer.bank_account_verification.*` | Bank-account verification result with customer, provider, and identity version | [Bank Account Verification Events](#customer-bank-account-verification-events) | For resource lifecycle events, fetch the latest resource by the `id` in `data` when needed. Bank-account verification events identify the customer with `data.customer_id`. ## Deposit Events | Event | Terminal? | When Sent | |-------|-----------|-----------| | `deposit.received` | No | Deposit received (NGN or crypto) | | `deposit.processing` | No | Settlement has started and may be waiting on conversion or delivery | | `deposit.requires_review` | No | Deposit needs review before it can continue | | `deposit.completed` | Yes | Deposit reached its settlement destination | | `deposit.failed` | Yes | Deposit permanently fails | | `deposit.reversed` | Yes | A completed deposit was reversed | Deposit payloads for NGN and crypto receive flows include `funding_account_id`. When a developer fee is configured, deposit payloads include `developer_fee` and `customer_amount`. The developer fee is deducted before the final customer amount is calculated; it is not added as a separate charge. Track the merchant-facing settlement lifecycle with `deposit.*` events. ## Funding Account Events Funding account events track receive-instruction provisioning and disablement. The `data` object is the public funding account response. | Event | Terminal? | When Sent | |-------|-----------|-----------| | `funding_account.created` | No | Funding account record has been created in `PENDING` status | | `funding_account.active` | No | Payment details are ready and the account is active | | `funding_account.updated` | No | A funding-account instruction changed status or received updated payment details | | `funding_account.failed` | Yes | Receive instruction provisioning failed | | `funding_account.disabled` | Yes | Active funding account was disabled | ## Transfer Events Transfer events are emitted for `POST /v1/transfers` and follow the merchant-created transfer lifecycle. | Event | Terminal? | When Sent | |-------|-----------|-----------| | `transfer.created` | No | Transfer record and initial attempt have been created | | `transfer.processing` | No | Funds have been locked and provider submission has been queued | | `transfer.requires_review` | No | Risk or ops policy flagged the transfer for review | | `transfer.submitted` | No | Provider accepted the transfer submission | | `transfer.completed` | Yes | Provider success and ledger finalization are complete | | `transfer.failed` | Yes | Transfer failed terminally after release or failure reconciliation | | `transfer.reversed` | Yes | A previously completed transfer was reversed | ## Withdrawal Events | Event | Terminal? | When Sent | |-------|-----------|-----------| | `withdrawal.created` | No | Withdrawal request has been accepted | | `withdrawal.submitted` | No | Withdrawal submitted to chain | | `withdrawal.completed` | Yes | Withdrawal confirmed on-chain | | `withdrawal.failed` | Yes | Withdrawal failed | ## Customer Verification Events | Event | Terminal? | When Sent | |-------|-----------|-----------| | `customer.verification.submitted` | No | Customer verification has been submitted for review | | `customer.verification.approved` | Yes | Customer verification has been approved | | `customer.verification.information_requested` | No | More information is required to continue customer verification | | `customer.verification.rejected` | Yes | Customer verification has been rejected | ## Customer Bank Account Verification Events These events report Paystack's asynchronous verification result for the bank details submitted through Tier 1 KYC. They do not change the customer's Tier 1 KYC status. | Event | Terminal? | When Sent | |-------|-----------|-----------| | `customer.bank_account_verification.succeeded` | Yes | Paystack verified the customer's current bank-account identity | | `customer.bank_account_verification.failed` | Yes | Paystack rejected the customer's current bank-account identity | Both events include `customer_id`, `provider`, `status`, and `identity_version`. A failed event also includes `failure_code` and `failure_message`. When the customer submits new bank details, Daya increments `identity_version`; ignore an older result after you have received a result for a newer version. All event families use the same webhook envelope and top-level `id` for delivery deduplication. Funding account payloads use the public funding account response shape. ## Bank Account Verification Events When you include `bank_account` in a Tier 1 verification request, Daya resolves and stores the bank details, then starts Paystack's asynchronous bank account verification. If Paystack is temporarily unavailable, Daya keeps the verification pending and starts it automatically after the provider recovers. The Tier 1 API response confirms that the submission was accepted; receive the Paystack result through one of these events: | Event | Terminal? | When Sent | |-------|-----------|-----------| | `customer.bank_account_verification.succeeded` | No | Paystack currently reports the submitted bank details as verified | | `customer.bank_account_verification.failed` | No | Paystack currently reports the submitted bank details as not verified | The `data` object contains: | Field | Type | Description | |-------|------|-------------| | `customer_id` | string | Customer whose bank details were checked | | `provider` | string | Verification provider; currently `PAYSTACK` | | `status` | string | `VERIFIED` for success or `FAILED` for failure | | `identity_version` | integer | Version of the customer's submitted bank details | | `failure_code` | string | Stable failure category; included only when verification fails | | `failure_message` | string | Explanation of the failure; included only when verification fails | Bank detail updates increment `identity_version`. Daya checks Paystack's current customer state whenever it processes a signed identification callback, so a later result can supersede an earlier result for the same version. If results arrive out of order, prefer the highest `identity_version`; for two results with the same version, keep the one with the later `timestamp`. Deduplicate repeated delivery of the same event with the top-level `id`. On failure, use `failure_code` and `failure_message` to tell the customer what to correct, then submit the corrected bank details with [`PATCH /v1/customers/{id}/tier1-verification`](/api-reference/customers/update-tier1-verification). On success, Daya can create the Paystack instruction. If the customer already has a permanent NGN funding account, Daya sends `funding_account.active` when Paystack makes a pending account usable, or `funding_account.updated` when another instruction was already active. Use the Paystack instruction only after its status is `ACTIVE`. If the customer does not yet have an account, Daya retains the successful verification and uses it when you create the permanent NGN funding account later. ## Lifecycle Examples These examples show how webhook names follow the public resource being reconciled. ### Funding Account Deposit When a customer sends NGN to a funding account's virtual account, the incoming money is a deposit. The payment detail is the funding account; the money movement is tracked with `deposit.*` events. | Step | Event | Notes | |------|-------|-------| | Funds arrive | `deposit.received` | Daya records the NGN deposit. | | Settlement starts | `deposit.processing` | Sent when delivery or conversion work begins. | | Deposit completes | `deposit.completed` | Funds have reached the configured settlement destination. | | Review or failure path | `deposit.requires_review` or `deposit.failed` | Sent if the deposit cannot proceed automatically. | ### Internal Balance to NGN Bank Transfer When a merchant sends withdrawal balance to an NGN bank account through `POST /v1/transfers`, the public resource is a transfer. | Step | Event | Notes | |------|-------|-------| | Request accepted | `transfer.created` | Transfer record and initial attempt exist. | | Funds locked | `transfer.processing` | Funds are held and provider submission is queued. | | Manual review, if needed | `transfer.requires_review` | Risk or ops policy paused the transfer. | | Provider accepts submission | `transfer.submitted` | Provider accepted the transfer request. | | Final success | `transfer.completed` | Provider success and ledger finalization are complete. | | Final failure | `transfer.failed` | Transfer failed terminally after release or failure reconciliation. | ### Crypto Wallet Withdrawal When a merchant sends stablecoin or crypto from the withdrawal balance to an on-chain wallet, the public resource is a withdrawal. | Step | Event | Notes | |------|-------|-------| | Request accepted | `withdrawal.created` | Withdrawal record exists. | | On-chain transaction submitted | `withdrawal.submitted` | Daya submitted the withdrawal to the chain/provider. | | On-chain transaction confirmed | `withdrawal.completed` | Withdrawal is confirmed and complete. | | Final failure | `withdrawal.failed` | Withdrawal failed terminally. | ## Payload Examples These examples are intentionally representative. The full `data` object follows the linked resource response shape for each event family. ### Funding Account Active ```json { "event": "funding_account.active", "id": "550e8400-e29b-41d4-a716-446655440000", "data": { "object": "funding_account", "id": "750e8400-e29b-41d4-a716-446655440000", "type": "PERMANENT", "status": "ACTIVE", "rail": "NGN_VIRTUAL_ACCOUNT", "customer_id": "650e8400-e29b-41d4-a716-446655440000", "currency": "NGN", "developer_fee": { "percentage": "2.5" }, "settlement_destination": { "type": "INTERNAL_BALANCE" }, "instructions": [ { "type": "NGN_VIRTUAL_ACCOUNT", "provider": "FLUTTERWAVE", "provider_availability": { "status": "OPERATIONAL" }, "status": "ACTIVE", "bank_name": "Wema Bank", "bank_code": "035", "account_number": "1234567890", "account_name": "Daya - Ada Lovelace", "currency": "NGN", "required_fields": [], "failure": null }, { "type": "NGN_VIRTUAL_ACCOUNT", "provider": "PAYSTACK", "provider_availability": { "status": "OPERATIONAL" }, "status": "PENDING", "currency": "NGN", "required_fields": [], "failure": null } ], "created_at": "2026-01-05T15:04:05Z", "updated_at": "2026-01-05T15:04:10Z" }, "timestamp": "2026-01-05T15:04:10Z" } ``` ### Deposit Completed ```json { "event": "deposit.completed", "id": "550e8400-e29b-41d4-a716-446655440001", "data": { "type": "CRYPTO_DEPOSIT", "id": "850e8400-e29b-41d4-a716-446655440000", "funding_account_id": "750e8400-e29b-41d4-a716-446655440000", "customer_id": "650e8400-e29b-41d4-a716-446655440000", "amount": "12.50", "currency": "USDC", "status": "COMPLETED", "settlement_status": "COMPLETED", "settlement_mode": "NGN_PAYOUT", "asset": "USDC", "chain": "BASE", "fees": { "total_fee_usd": "0.0500" }, "developer_fee": { "percentage": "10", "amount": "1.25", "currency": "USD" }, "customer_amount": { "amount": "16800.00", "currency": "NGN" }, "created_at": "2026-01-05T15:04:05Z", "updated_at": "2026-01-05T15:04:10Z" }, "timestamp": "2026-01-05T15:04:10Z" } ``` ### Transfer Completed ```json { "event": "transfer.completed", "id": "550e8400-e29b-41d4-a716-446655440000", "data": { "id": "850e8400-e29b-41d4-a716-446655440000", "reference": "txn_ngn_001", "status": "SETTLED", "rail": "NGN_BANK", "currency": "NGN", "amount": "50000.000000", "debit_currency": "USD", "debit_amount": "33.070000", "fee": "0.810000", "rate": { "side": "SELL", "value": "1550.00", "captured_at": "2026-01-05T15:04:05Z" }, "on_behalf_of": null, "created_at": "2026-01-05T15:04:05Z", "settled_at": "2026-01-05T15:10:00Z" }, "timestamp": "2026-01-05T15:10:00Z" } ``` ### Customer Verification Approved ```json { "event": "customer.verification.approved", "id": "450e8400-e29b-41d4-a716-446655440004", "data": { "id": "650e8400-e29b-41d4-a716-446655440000", "email": "customer@example.com", "first_name": "John", "last_name": "Doe", "tier_1_kyc_complete": true, "tier_2_kyc_complete": true, "is_verified": true, "capabilities": [ { "name": "base", "status": "approved" }, { "name": "usd_banking", "status": "approved" } ], "rejection_reasons": [], "created_at": "2026-01-05T15:04:05Z", "updated_at": "2026-01-05T16:00:00Z" }, "timestamp": "2026-01-05T16:00:00Z" } ``` ### Bank Account Verification Succeeded ```json { "event": "customer.bank_account_verification.succeeded", "id": "3da53d16-dd57-4bf8-a8af-39944f637d81", "data": { "customer_id": "650e8400-e29b-41d4-a716-446655440000", "provider": "PAYSTACK", "status": "VERIFIED", "identity_version": 1 }, "timestamp": "2026-08-10T12:05:00Z" } ``` ### Bank Account Verification Failed ```json { "event": "customer.bank_account_verification.failed", "id": "93de81e1-268f-4c79-80bd-ccaa4349b742", "data": { "customer_id": "650e8400-e29b-41d4-a716-446655440000", "provider": "PAYSTACK", "status": "FAILED", "identity_version": 1, "failure_code": "BVN_OR_ACCOUNT_NAME_MISMATCH", "failure_message": "Account name or BVN is incorrect" }, "timestamp": "2026-08-10T12:05:00Z" } ``` ## Next Steps Delivery guarantees, retries, and handler guidance Verify the HMAC signature on incoming requests #### Webhook Verification Path: /api-reference/webhooks/verification Description: Verify webhook authenticity using HMAC signatures ## Overview All Daya webhooks include an `X-Daya-Signature` header containing an HMAC-SHA256 signature of the payload. **Always verify** this signature to ensure the webhook came from Daya. Never process unverified webhooks. Attackers could send fake webhooks to manipulate your system. ## Signature Header ``` X-Daya-Signature: a8f5f167f44f4964e6c998dee827110c447be52d40d67b6a60b78c1e3e01b7e8 ``` ## Verification Algorithm 1. Get raw request body as string 2. Compute HMAC-SHA256 using your webhook secret 3. Compare computed signature with `X-Daya-Signature` header 4. Use timing-safe comparison to prevent timing attacks ## Implementation Examples ```javascript Node.js const crypto = require('crypto'); function verifyWebhookSignature(payload, signature, secret) { // Compute expected signature const expectedSignature = crypto .createHmac('sha256', secret) .update(payload, 'utf8') .digest('hex'); // Timing-safe comparison try { return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expectedSignature) ); } catch (error) { return false; } } // Express.js middleware app.post('/webhooks/daya', express.raw({ type: 'application/json' }), (req, res) => { const signature = req.headers['x-daya-signature']; const payload = req.body.toString('utf8'); if (!verifyWebhookSignature(payload, signature, process.env.DAYA_WEBHOOK_SECRET)) { return res.status(401).json({ error: 'Invalid signature' }); } // Process webhook... const event = JSON.parse(payload); res.status(200).send('OK'); }); ``` ```python Python import hmac import hashlib def verify_webhook_signature(payload: bytes, signature: str, secret: str) -> bool: """Verify webhook HMAC signature""" expected_signature = hmac.new( secret.encode('utf-8'), payload, hashlib.sha256 ).hexdigest() # Timing-safe comparison return hmac.compare_digest(signature, expected_signature) # Flask example from flask import Flask, request, jsonify @app.route('/webhooks/daya', methods=['POST']) def handle_webhook(): signature = request.headers.get('X-Daya-Signature') payload = request.get_data() if not verify_webhook_signature(payload, signature, DAYA_WEBHOOK_SECRET): return jsonify({'error': 'Invalid signature'}), 401 # Process webhook... event = request.json return jsonify({'status': 'ok'}), 200 ``` ```go Go package main import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "io" "net/http" ) func verifyWebhookSignature(payload []byte, signature string, secret string) bool { mac := hmac.New(sha256.New, []byte(secret)) mac.Write(payload) expectedSignature := hex.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(signature), []byte(expectedSignature)) } func handleWebhook(w http.ResponseWriter, r *http.Request) { signature := r.Header.Get("X-Daya-Signature") payload, _ := io.ReadAll(r.Body) if !verifyWebhookSignature(payload, signature, DAYA_WEBHOOK_SECRET) { http.Error(w, "Invalid signature", http.StatusUnauthorized) return } // Process webhook... w.WriteHeader(http.StatusOK) } ``` ```php PHP 'Invalid signature']); exit; } // Process webhook... $event = json_decode($payload, true); http_response_code(200); echo json_encode(['status' => 'ok']); ``` ## Important Notes **Critical:** Compute HMAC on the **raw request body** before parsing JSON. Parsing changes whitespace and ordering, breaking the signature. ```javascript // ✅ Correct: Use raw body app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => { const payload = req.body.toString('utf8'); verify(payload, signature, secret); }); // ❌ Wrong: JSON.stringify changes format app.post('/webhooks', express.json(), (req, res) => { const payload = JSON.stringify(req.body); // Wrong! verify(payload, signature, secret); }); ``` Regular string comparison (==) is vulnerable to timing attacks. Use constant-time comparison: - Node.js: `crypto.timingSafeEqual()` - Python: `hmac.compare_digest()` - Go: `hmac.Equal()` - PHP: `hash_equals()` - Store webhook secret in environment variables - Never commit secrets to version control - Rotate secrets regularly - Use different secrets for sandbox and production ## Testing Verification Generate test signatures for local testing: ```bash CLI # Generate test signature echo -n '{"event":"withdrawal.completed","timestamp":"2026-03-10T09:03:00Z","data":{"id":"550e8400-e29b-41d4-a716-446655440000"}}' | \ openssl dgst -sha256 -hmac "your_webhook_secret" | \ awk '{print $2}' ``` ```javascript Node.js const crypto = require('crypto'); function generateTestSignature(payload, secret) { return crypto .createHmac('sha256', secret) .update(payload, 'utf8') .digest('hex'); } const payload = '{"event":"withdrawal.completed","timestamp":"2026-03-10T09:03:00Z","data":{"id":"550e8400-e29b-41d4-a716-446655440000"}}'; const signature = generateTestSignature(payload, 'your_webhook_secret'); console.log(signature); ``` ## Common Issues **Possible causes:** - Using wrong webhook secret - Not using raw request body - Character encoding issues **Debug:** ```javascript console.log('Received signature:', signature); console.log('Expected signature:', expectedSignature); console.log('Payload:', payload); console.log('Secret (first 4 chars):', secret.substring(0, 4)); ``` **Cause:** Parsing JSON before verification **Fix:** Always compute HMAC on raw body, then parse JSON ## Next Steps Learn about webhook delivery and event types Event schemas and payloads ### Legacy API / Guides #### Legacy Migration Guide Path: /legacy/migration-guide Description: Move older onramp, offramp, payout, and webhook integrations to the current API model ## Overview Older Daya integrations may still use onramp, offramp, payout, and legacy webhook names. Those routes remain available for existing integrations, but new receive-money work should use funding accounts and deposits. This guide shows what to use instead. No removal date has been published for the legacy routes. Daya will share sunset dates before any legacy route is removed. ## What Changed | Older integration path | Current path | |------------------------|--------------| | Create or list NGN onramps with `/v1/onramps` | Create or list funding accounts with `rail: NGN_VIRTUAL_ACCOUNT` | | Create or list crypto offramps with `/v1/offramps` | Create or list funding accounts with `rail: CRYPTO_ADDRESS`, `asset`, and `chain` | | Reconcile onramp or offramp deposits by legacy receive-flow IDs | Reconcile deposits with `/v1/deposits` and store `funding_account_id` | | Track settlement delivery with payout events | Track receive-flow settlement with `deposit.*` events | | Send money to a bank recipient through payout-style flows | Create a transfer with `/v1/transfers` | | Track old webhook names such as `deposit.settled` | Use current names such as `deposit.completed` | ## Receive NGN Use a funding account with `rail: NGN_VIRTUAL_ACCOUNT` when a customer needs NGN bank details. ```json { "type": "TEMPORARY", "rail": "NGN_VIRTUAL_ACCOUNT", "customer": { "customer_id": "650e8400-e29b-41d4-a716-446655440000" }, "currency": "NGN", "amount": 50000, "settlement_destination": { "type": "INTERNAL_BALANCE" } } ``` The response includes `instructions` with the bank account details to show your customer. When money arrives, the deposit response includes `funding_account_id`. Use these endpoints: | Task | Endpoint | |------|----------| | Create NGN payment details | [`POST /v1/funding-accounts`](/api-reference/funding-accounts/create-funding-account) | | List NGN payment details | [`GET /v1/funding-accounts?rail=NGN_VIRTUAL_ACCOUNT`](/api-reference/funding-accounts/list-funding-accounts) | | Reconcile received money | [`GET /v1/deposits`](/api-reference/deposits/list-deposits) | ## Receive Crypto Use a funding account with `rail: CRYPTO_ADDRESS` when a customer needs to send USDC or USDT to Daya. ```json { "type": "TEMPORARY", "rail": "CRYPTO_ADDRESS", "customer": { "customer_id": "650e8400-e29b-41d4-a716-446655440000" }, "asset": "USDC", "chain": "BASE", "settlement_destination": { "type": "NGN_PAYOUT", "rate_id": "550e8400-e29b-41d4-a716-446655440000", "destination_bank": { "account_number": "0690000031", "bank_code": "044" } } } ``` The response includes `instructions` with the wallet address. When crypto lands, the deposit response includes `funding_account_id`, `asset`, `chain`, and `tx_hash`. Use these endpoints: | Task | Endpoint | |------|----------| | Create crypto payment details | [`POST /v1/funding-accounts`](/api-reference/funding-accounts/create-funding-account) | | List crypto payment details | [`GET /v1/funding-accounts?rail=CRYPTO_ADDRESS`](/api-reference/funding-accounts/list-funding-accounts) | | Reconcile received money | [`GET /v1/deposits`](/api-reference/deposits/list-deposits) | ## Payouts and Transfers Payouts are legacy read-only records for settlement delivery from older receive-money flows. Do not build new send-money workflows on `/v1/payouts`. Use: | Goal | Current API | |------|-------------| | Send NGN or USD to a bank account | [`POST /v1/transfers`](/api-reference/transfers/create-transfer) | | Save reusable bank or wallet details | [`POST /v1/recipients`](/api-reference/recipients/create-recipient) | | Move collection balance to withdrawal balance | [`POST /v1/merchant/balance/transfer`](/api-reference/merchant-balance/transfer-merchant-balance) | | Send stablecoins on-chain from withdrawal balance | [`POST /v1/merchant/balance/withdraw`](/api-reference/merchant-balance/withdraw-merchant-balance) | If you only need to inspect older payout records, use the legacy [List payouts](/api-reference/payouts/list-payouts) and [Get payout](/api-reference/payouts/get-payout) routes. ## Webhook Names Use the current webhook names when creating or updating webhook filters. | Older name | Current name | |------------|--------------| | `deposit.flagged` | `deposit.requires_review` | | `deposit.settled` | `deposit.completed` | | `transfer.flagged` | `transfer.requires_review` | | `transfer.settled` | `transfer.completed` | | `withdrawal.settled` | `withdrawal.completed` | | `virtual_account.created` | `funding_account.active` | | `onramp.created` | `funding_account.active` | | `onramp.failed` | `funding_account.failed` | Existing endpoints that receive all events may temporarily receive both the current and legacy event name for the same state change. See [Legacy Webhooks](/legacy/webhooks) for the compatibility list. ## Migration Checklist 1. Create new receive instructions with `/v1/funding-accounts`. 2. Store `funding_account_id` from funding account responses and deposit payloads. 3. Reconcile NGN and crypto payments from `/v1/deposits`. 4. Replace payout-based send flows with `/v1/transfers` or merchant withdrawals. 5. Update webhook filters and handlers to use current event names. 6. Keep legacy route support only for existing customers or historical records. ## Legacy Routes The following routes remain documented for older integrations: | Resource | Routes | |----------|--------| | Onramps | [`POST /v1/onramps`](/api-reference/onramp/create-onramp), [`GET /v1/onramps`](/api-reference/onramp/list-onramps), [`GET /v1/onramps/{id}`](/api-reference/onramp/get-onramp) | | Offramps | [`POST /v1/offramps`](/api-reference/offramps/create-offramp), [`GET /v1/offramps`](/api-reference/offramps/list-offramps), [`GET /v1/offramps/{id}`](/api-reference/offramps/get-offramp) | | Payouts | [`GET /v1/payouts`](/api-reference/payouts/list-payouts), [`GET /v1/payouts/{id}`](/api-reference/payouts/get-payout) | Legacy receive-flow routes are compatibility routes. Funding accounts, deposits, transfers, and withdrawals are the current integration path. #### Legacy Webhooks Path: /legacy/webhooks Description: Webhook event names kept temporarily for older integrations ## Overview New integrations should use the event names documented in [Webhook Events](/api-reference/webhooks/events). Some older integrations may still receive previous event names during the migration window. These names are delivered only for compatibility and are not accepted when creating or updating `event_types`. If your endpoint receives all events, you may see both the current event and its legacy counterpart for the same resource transition. ## Event Name Mapping | Current event | Legacy event | |---------------|--------------| | `deposit.requires_review` | `deposit.flagged` | | `deposit.completed` | `deposit.settled` | | `transfer.requires_review` | `transfer.flagged` | | `transfer.completed` | `transfer.settled` | | `withdrawal.completed` | `withdrawal.settled` | | `funding_account.active` | `virtual_account.created` | | `funding_account.active` | `onramp.created` | | `funding_account.failed` | `onramp.failed` | The funding account aliases apply to NGN receive instructions that older integrations knew as onramps or virtual accounts. Crypto funding accounts do not emit onramp aliases. ## Migration Guidance Use the current event names in webhook filters and application logic. Keep handling legacy names only if you already receive them in production. When you no longer need the legacy names, update your webhook handler to process only the current event names. For route-level migration steps, see the [Legacy Migration Guide](/legacy/migration-guide). ### Legacy API / Onramps #### Legacy: Create an onramp Path: /api-reference/onramp/create-onramp Description: Legacy route for creating an NGN receive flow ## Overview Create an onramp through the legacy compatibility route. New integrations should use [`POST /v1/funding-accounts`](/api-reference/funding-accounts/create-funding-account) with `rail: NGN_VIRTUAL_ACCOUNT`. ## Authentication Your merchant API key Unique idempotency key to prevent duplicate onramp creation ## Request Body Onramp type **Allowed values:** `TEMPORARY`, `PERMANENT` - `TEMPORARY`: Short-lived VA (25 minutes), locked to `rate_id` - `PERMANENT`: Long-lived VA, uses current rate at settlement Customer information. Either `customer_id` or `email` must be provided. UUID of an existing customer. Either this or `customer.email` is required. **Example:** `650e8400-e29b-41d4-a716-446655440000` Email for auto-creating a customer. Either this or `customer.customer_id` is required. **Example:** `user@example.com` Customer first name Customer last name Verification data. Required for permanent onramps when creating a new customer (no `customer_id`). For existing customers, required only if the customer is not yet verified. 11-digit Bank Verification Number **Example:** `22345678901` Customer's phone number in 11-digit national or `+234` international form. **Example:** `+2348012345678` Face image for identity matching. Accepts an HTTPS URL to a jpg/png image, or base64-encoded image data up to 1 MiB decoded. **Example:** `https://example.com/selfie.jpg` Rate identifier from `GET /v1/rates` **Example:** `rate_8x7k2mq9p` **Required** for temporary onramps. **Not allowed** for permanent onramps. Principal amount in NGN before any payment-provider collection charge. The exact amount the customer must transfer is returned as `amount` in the create response. **Example:** `50000` **Required** for temporary onramps. Ignored for permanent onramps. Optional fee that your merchant account keeps from each onramp deposit. Omit to use `0%`. Percentage of each received deposit that your merchant account keeps. Use a decimal string from `0` to `50`. This is a percentage value, not basis points: `0.5` means `0.5%`, `2` means `2%`, and `50` means `50%`. **Example:** `"2.5"` Settlement configuration Settlement mode **Allowed values:** - `ONCHAIN` - Settle directly on-chain to the destination address - `INTERNAL_BALANCE` - Credit merchant USD balance Asset type **Allowed values:** `USDC`, `USDT` Permanent onramps only support `USDC` in v1. Blockchain network. Required for `ONCHAIN` mode. **Allowed values:** `APTOS`, `BASE`, `CELO`, `ETHEREUM`, `POLYGON`, `SOLANA`, `TRON` See [Supported Chains](/concepts/supported-chains) for details on which assets are available on each chain. On-chain destination address. Required for `ONCHAIN` mode. **Example:** `0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb` ## Request Examples ```json Temporary - On-chain { "type": "TEMPORARY", "customer": { "email": "user@example.com" }, "rate_id": "rate_8x7k2mq9p", "amount": 50000, "developer_fee": { "percentage": "2.5" }, "settlement": { "mode": "ONCHAIN", "asset": "USDC", "chain": "BASE", "destination_address": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb" } } ``` ```json Temporary - Internal Balance { "type": "TEMPORARY", "customer": { "email": "user@example.com" }, "rate_id": "rate_8x7k2mq9p", "amount": 50000, "settlement": { "mode": "INTERNAL_BALANCE", "asset": "USDC" } } ``` ```json Permanent - New Customer { "type": "PERMANENT", "customer": { "email": "user@example.com", "first_name": "John", "last_name": "Doe", "verification": { "bvn": "22345678901", "phone_number": "+2348012345678", "image_url": "https://example.com/selfie.jpg" } }, "settlement": { "mode": "ONCHAIN", "asset": "USDC", "chain": "BASE", "destination_address": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb" } } ``` ```json Permanent - Existing Verified Customer { "type": "PERMANENT", "customer": { "customer_id": "650e8400-e29b-41d4-a716-446655440000" }, "settlement": { "mode": "ONCHAIN", "asset": "USDC", "chain": "SOLANA", "destination_address": "7EcDhSYGxXyscszYEp35KHN8vvw3svAuLKTzXwCFLtV" } } ``` ```json Permanent - Existing Unverified Customer { "type": "PERMANENT", "customer": { "customer_id": "650e8400-e29b-41d4-a716-446655440000", "verification": { "bvn": "22345678901", "phone_number": "+2348012345678", "image_url": "data:image/jpeg;base64,/9j/4AAQSkZJRg..." } }, "settlement": { "mode": "ONCHAIN", "asset": "USDC", "chain": "BASE", "destination_address": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb" } } ``` ```bash cURL - Temporary curl --request POST \ --url https://api.daya.co/v1/onramps \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'X-Idempotency-Key: unique-key-123' \ --header 'Content-Type: application/json' \ --data '{ "type": "TEMPORARY", "customer": { "email": "user@example.com" }, "rate_id": "rate_8x7k2mq9p", "amount": 50000, "developer_fee": { "percentage": "2.5" }, "settlement": { "mode": "ONCHAIN", "asset": "USDC", "chain": "BASE", "destination_address": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb" } }' ``` ```bash cURL - Permanent curl --request POST \ --url https://api.daya.co/v1/onramps \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'X-Idempotency-Key: unique-key-456' \ --header 'Content-Type: application/json' \ --data '{ "type": "PERMANENT", "customer": { "email": "user@example.com", "first_name": "John", "last_name": "Doe", "verification": { "bvn": "22345678901", "phone_number": "+2348012345678", "image_url": "https://example.com/selfie.jpg" } }, "settlement": { "mode": "ONCHAIN", "asset": "USDC", "chain": "BASE", "destination_address": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb" } }' ``` ## Response ### Temporary Onramp Response Unique identifier for this onramp `TEMPORARY` Current onramp status. New onramps start as `ACTIVE`. Exact amount the customer must transfer in NGN. The payment provider may add a collection charge, so this can differ from the `amount` submitted in the request. Required and always returned for temporary onramps as a decimal string. Display and transfer this value exactly; do not recalculate or round it. **Example:** `"50000.50"` Associated rate identifier Unique payment reference for the transfer Developer fee percentage used for deposits received through this onramp. Configured percentage as a decimal string. NGN bank account details for receiving deposits Virtual account number Account name Bank name When the virtual account expires (temporary onramps only) When onramp expires (~25 minutes from creation) Settlement configuration (same as request) When onramp was created (ISO 8601 timestamp) ### Permanent Onramp Response `PERMANENT` Unique identifier for the permanent onramp configuration Identifier of the currently active settlement configuration The customer this permanent onramp belongs to Active settlement configuration Permanent NGN bank account details Virtual account number Bank name Account name (typically "Daya-Customer Name") ### Success Responses ```json 201 Created - Temporary On-chain { "onramp_id": "onramp_3j5k8n2q", "type": "TEMPORARY", "status": "ACTIVE", "amount": "50000.50", "rate_id": "rate_8x7k2mq9p", "payment_reference": "DAYA-3J5K8N2Q", "developer_fee": { "percentage": "2.5" }, "virtual_account": { "account_number": "9876543210", "account_name": "Daya - user@example.com", "bank_name": "Wema Bank", "expires_at": "2026-01-14T15:30:00Z" }, "expires_at": "2026-01-14T15:30:00Z", "settlement": { "mode": "ONCHAIN", "asset": "USDC", "chain": "BASE", "destination_address": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb" }, "created_at": "2026-01-14T15:05:12Z" } ``` ```json 201 Created - Temporary Internal Balance { "onramp_id": "onramp_7k3m5n8q", "type": "TEMPORARY", "status": "ACTIVE", "amount": "50000.50", "rate_id": "rate_8x7k2mq9p", "payment_reference": "DAYA-7K3M5N8Q", "virtual_account": { "account_number": "1122334455", "account_name": "Daya - user@example.com", "bank_name": "Wema Bank", "expires_at": "2026-01-14T15:30:00Z" }, "expires_at": "2026-01-14T15:30:00Z", "settlement": { "mode": "INTERNAL_BALANCE" }, "created_at": "2026-01-14T15:05:12Z" } ``` ```json 201 Created - Permanent { "type": "PERMANENT", "permanent_onramp_id": "850e8400-e29b-41d4-a716-446655440000", "active_settlement_id": "950e8400-e29b-41d4-a716-446655440000", "customer_id": "650e8400-e29b-41d4-a716-446655440000", "settlement": { "mode": "ONCHAIN", "asset": "USDC", "chain": "BASE", "destination_address": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb" }, "virtual_account": { "account_number": "1234567890", "bank_name": "Wema Bank", "account_name": "Daya-John Doe" } } ``` ## Error Responses ```json 400 Bad Request - Missing customer { "error": { "code": "validation_error", "message": "either customer.customer_id or customer.email is required" } } ``` ```json 400 Bad Request - Rate not allowed for permanent { "error": { "code": "validation_error", "message": "rate_id is not allowed for permanent onramps" } } ``` ```json 400 Bad Request - Invalid settlement mode { "error": { "code": "validation_error", "message": "invalid settlement mode" } } ``` ```json 400 Bad Request - Unsupported asset { "error": { "code": "validation_error", "message": "permanent onramps only support USDC in v1" } } ``` ```json 400 Bad Request - Customer not verified { "error": { "code": "validation_error", "message": "customer is not verified and no verification data provided" } } ``` ```json 400 Bad Request - Verification failed { "error": { "code": "VALIDATION_FAILED", "message": "BVN verification failed" } } ``` ```json 502 Bad Gateway - Verification provider error { "error": { "code": "INTEGRATION_FAILED", "message": "Verification provider unavailable, please try again" } } ``` ```json 400 Bad Request - Rate expired { "error": { "code": "rate_expired", "message": "The specified rate_id has expired", "details": "Request a new rate via GET /v1/rates" } } ``` ```json 400 Bad Request - Invalid address { "error": { "code": "invalid_address", "message": "destination_address is not valid for chain BASE" } } ``` ```json 429 Too Many Requests - Daily limit { "error": { "code": "onramp_creation_limit_exceeded", "message": "Merchant has exceeded daily onramp creation limit (1,000/day)" } } ``` ## Validation Rules Either `customer.customer_id` or `customer.email` must be provided (not both optional, at least one required). - `rate_id` is **required** and must be a valid, non-expired rate snapshot - `amount` is **required** - Settlement modes: `ONCHAIN` or `INTERNAL_BALANCE` - `customer.verification` is not required - For `ONCHAIN`: `chain` and `destination_address` are required - For `INTERNAL_BALANCE`: `chain` and `destination_address` must NOT be set - `rate_id` must **not** be provided - `amount` is silently ignored - Settlement modes: `ONCHAIN` or `INTERNAL_BALANCE` - For `ONCHAIN`: `chain` and `destination_address` are required - For `INTERNAL_BALANCE`: `chain` and `destination_address` must NOT be set - If `customer.customer_id` is not provided, `customer.verification` with `bvn`, `phone_number`, and `image_url` is required - If `customer.customer_id` is provided, the customer must either be already verified or `customer.verification` must be included Verification uses BVN + phone number + face matching via an identity provider. **Two paths:** **New customer** (`customer.email` provided, no `customer_id`): - `customer.verification` is required with `bvn`, `phone_number`, and `image_url` - The system creates or finds the customer by email, runs verification, then provisions the virtual account **Existing customer** (`customer.customer_id` provided): - If already verified: proceeds directly - If not verified + `customer.verification` provided: runs verification first - If not verified + no verification data: returns error ## Permanent Onramp Behavior **Settlement updates:** If a permanent onramp already exists for a customer, calling this endpoint again updates the settlement configuration (chain, address) without creating a new virtual account. The previous settlement is deactivated and the new one becomes active. **Virtual accounts for permanent onramps do not expire.** The same account number is reused across settlement updates. ## Best Practices Always call `GET /v1/rates` immediately before creating a temporary onramp to ensure maximum validity window. Use a blockchain library to validate addresses before submitting: ```javascript import { isAddress } from 'ethers'; if (!isAddress(destinationAddress)) { throw new Error('Invalid Ethereum address'); } ``` Create customers once via `POST /v1/customers`, then reference them by `customer_id` in subsequent onramp requests. For permanent onramps, verification may fail due to invalid BVN, face mismatch, or provider issues. Handle `400 VALIDATION_FAILED` and `502 INTEGRATION_FAILED` separately. Always include a unique `X-Idempotency-Key` header to prevent duplicate onramp creation on retries. ## Rate Limits - **1,000 onramp creations per day** per merchant - **100 API requests per minute** per key ## Next Steps Query deposits for an onramp Pre-create customers before onramp requests #### Legacy: List onramps Path: /api-reference/onramp/list-onramps Description: Legacy route for listing NGN receive flows ## Overview Retrieve onramps through the legacy compatibility route. New integrations should use [`GET /v1/funding-accounts`](/api-reference/funding-accounts/list-funding-accounts) with `rail=NGN_VIRTUAL_ACCOUNT`. ## Authentication Your merchant API key ## Query Parameters Filter by onramp type **Allowed values:** `TEMPORARY`, `PERMANENT` Page number (1-indexed) **Default:** `1` Results per page **Default:** `50` **Max:** `200` ## Request Examples ```bash All Onramps curl --request GET \ --url 'https://api.daya.co/v1/onramps' \ --header 'X-Api-Key: YOUR_API_KEY' ``` ```bash Permanent Only curl --request GET \ --url 'https://api.daya.co/v1/onramps?type=PERMANENT' \ --header 'X-Api-Key: YOUR_API_KEY' ``` ```bash Temporary Only curl --request GET \ --url 'https://api.daya.co/v1/onramps?type=TEMPORARY' \ --header 'X-Api-Key: YOUR_API_KEY' ``` ```bash Paginated curl --request GET \ --url 'https://api.daya.co/v1/onramps?page=2&limit=20' \ --header 'X-Api-Key: YOUR_API_KEY' ``` ```javascript JavaScript const response = await fetch( 'https://api.daya.co/v1/onramps?type=PERMANENT&page=1&limit=20', { headers: { 'X-Api-Key': 'YOUR_API_KEY' } } ); const data = await response.json(); ``` ```python Python import requests response = requests.get( 'https://api.daya.co/v1/onramps', params={'type': 'PERMANENT', 'page': 1, 'limit': 20}, headers={'X-Api-Key': 'YOUR_API_KEY'} ) data = response.json() ``` ## Response Array of onramp objects Unique onramp identifier (UUID) Onramp type: `TEMPORARY` or `PERMANENT` Associated customer ID (UUID) Virtual account provisioning status: `PENDING`, `ACTIVE`, or `FAILED` Expected deposit amount in NGN, returned as a decimal string. Always present for temporary onramps (required at creation); not present for permanent onramps. Associated rate identifier (temporary onramps only) When the locked rate expires (temporary onramps only) Developer fee percentage used for deposits received through this onramp. Configured percentage as a decimal string. Settlement configuration Settlement mode: `ONCHAIN` or `INTERNAL_BALANCE` Asset type: `USDC` or `USDT` Blockchain network On-chain destination address Virtual bank account details Virtual account number Account name Bank name When the virtual account expires (temporary onramps only) When the onramp was created (ISO 8601 timestamp) Total number of onramps matching filters Current page number Results per page Total number of pages ### Success Response ```json 200 OK { "data": [ { "id": "750e8400-e29b-41d4-a716-446655440000", "type": "PERMANENT", "customer_id": "650e8400-e29b-41d4-a716-446655440000", "provisioning_status": "ACTIVE", "settlement": { "mode": "ONCHAIN", "asset": "USDC", "chain": "BASE", "destination_address": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb" }, "virtual_account": { "account_number": "1234567890", "account_name": "Daya-John Doe", "bank_name": "Wema Bank" }, "created_at": "2026-01-10T12:00:00Z" }, { "id": "850e8400-e29b-41d4-a716-446655440000", "type": "TEMPORARY", "customer_id": "660e8400-e29b-41d4-a716-446655440000", "provisioning_status": "ACTIVE", "amount": "50000.50", "rate_id": "550e8400-e29b-41d4-a716-446655440000", "rate_expires_at": "2026-01-14T15:30:00Z", "settlement": { "mode": "ONCHAIN", "asset": "USDC", "chain": "SOLANA", "destination_address": "7EcDhSYGxXyscszYEp35KHN8vvw3svAuLKTzXwCFLtV" }, "virtual_account": { "account_number": "9876543210", "account_name": "Daya - user@example.com", "bank_name": "Wema Bank", "expires_at": "2026-01-14T15:30:00Z" }, "created_at": "2026-01-14T15:05:00Z" } ], "total": 2, "page": 1, "limit": 50, "total_pages": 1 } ``` ## Error Responses ```json 400 Bad Request { "error": { "code": "BAD_REQUEST", "message": "Invalid query parameters", "request_id": "550e8400-e29b-41d4-a716-446655440000" } } ``` ```json 401 Unauthorized { "error": { "code": "UNAUTHORIZED", "message": "Unauthorized", "request_id": "550e8400-e29b-41d4-a716-446655440000" } } ``` ## Next Steps Get a specific onramp by ID Create a new onramp #### Legacy: Retrieve an onramp Path: /api-reference/onramp/get-onramp Description: Legacy route for retrieving an NGN receive flow ## Overview Retrieve an onramp through the legacy compatibility route. New integrations should use [`GET /v1/funding-accounts/{id}`](/api-reference/funding-accounts/get-funding-account). ## Authentication Your merchant API key ## Path Parameters Onramp ID (UUID format) **Example:** `750e8400-e29b-41d4-a716-446655440000` ## Request Examples ```bash cURL curl --request GET \ --url https://api.daya.co/v1/onramps/750e8400-e29b-41d4-a716-446655440000 \ --header 'X-Api-Key: YOUR_API_KEY' ``` ```javascript JavaScript const response = await fetch( 'https://api.daya.co/v1/onramps/750e8400-e29b-41d4-a716-446655440000', { headers: { 'X-Api-Key': 'YOUR_API_KEY' } } ); const onramp = await response.json(); ``` ```python Python import requests response = requests.get( 'https://api.daya.co/v1/onramps/750e8400-e29b-41d4-a716-446655440000', headers={'X-Api-Key': 'YOUR_API_KEY'} ) onramp = response.json() ``` ## Response Unique onramp identifier (UUID) Onramp type: `TEMPORARY` or `PERMANENT` Associated customer ID (UUID) Virtual account provisioning status: `PENDING`, `ACTIVE`, or `FAILED` Expected deposit amount in NGN. Always present for temporary onramps (required at creation). Not present for permanent onramps. Decimal string. **Example:** `"50000.50"` Associated rate identifier (temporary onramps only) When the locked rate expires (temporary onramps only) Developer fee percentage used for deposits received through this onramp. Configured percentage as a decimal string. Settlement configuration Settlement mode: `ONCHAIN` or `INTERNAL_BALANCE` Asset type: `USDC` or `USDT` Blockchain network On-chain destination address Virtual bank account details Virtual account number Account name Bank name When the virtual account expires (temporary onramps only) When the onramp was created (ISO 8601 timestamp) ### Success Responses ```json 200 OK - Permanent Onramp { "id": "750e8400-e29b-41d4-a716-446655440000", "type": "PERMANENT", "customer_id": "650e8400-e29b-41d4-a716-446655440000", "provisioning_status": "ACTIVE", "developer_fee": { "percentage": "2.5" }, "settlement": { "mode": "ONCHAIN", "asset": "USDC", "chain": "BASE", "destination_address": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb" }, "virtual_account": { "account_number": "1234567890", "account_name": "Daya-John Doe", "bank_name": "Wema Bank" }, "created_at": "2026-01-10T12:00:00Z" } ``` ```json 200 OK - Temporary Onramp { "id": "850e8400-e29b-41d4-a716-446655440000", "type": "TEMPORARY", "customer_id": "660e8400-e29b-41d4-a716-446655440000", "provisioning_status": "ACTIVE", "amount": "50000.50", "rate_id": "550e8400-e29b-41d4-a716-446655440000", "rate_expires_at": "2026-01-14T15:30:00Z", "developer_fee": { "percentage": "2.5" }, "settlement": { "mode": "ONCHAIN", "asset": "USDC", "chain": "SOLANA", "destination_address": "7EcDhSYGxXyscszYEp35KHN8vvw3svAuLKTzXwCFLtV" }, "virtual_account": { "account_number": "9876543210", "account_name": "Daya - user@example.com", "bank_name": "Wema Bank", "expires_at": "2026-01-14T15:30:00Z" }, "created_at": "2026-01-14T15:05:00Z" } ``` ```json 400 Bad Request { "error": { "code": "BAD_REQUEST", "message": "Invalid onramp ID format", "request_id": "550e8400-e29b-41d4-a716-446655440000" } } ``` ```json 404 Not Found { "error": { "code": "NOT_FOUND", "message": "Onramp not found", "request_id": "550e8400-e29b-41d4-a716-446655440000" } } ``` ## Next Steps List all onramps with filters View deposits for an onramp ### Legacy API / Offramps #### Legacy: Create an offramp Path: /api-reference/offramps/create-offramp Description: Legacy route for creating a crypto receive flow ## Overview Create an offramp through the legacy compatibility route. New integrations should use [`POST /v1/funding-accounts`](/api-reference/funding-accounts/create-funding-account) with `rail: CRYPTO_ADDRESS`, `asset`, and `chain`. ## Authentication Your merchant API key Unique idempotency key to prevent duplicate offramp creation ## Request Body Offramp type **Allowed values:** `TEMPORARY`, `PERMANENT` - `TEMPORARY`: Short-lived deposit address, locked to `rate_id`. Must use `NGN_PAYOUT` settlement. - `PERMANENT`: Long-lived deposit address, uses current rate at settlement. Supports both `INTERNAL_BALANCE` and `NGN_PAYOUT` settlement. Customer information. Either `customer_id` or `email` must be provided. UUID of an existing customer. Either this or `customer.email` is required. **Example:** `650e8400-e29b-41d4-a716-446655440000` Email for auto-creating a customer. Either this or `customer.customer_id` is required. **Example:** `user@example.com` Customer first name Customer last name Blockchain network for the deposit address **Allowed values:** `APTOS`, `BASE`, `BSC`, `CELO`, `ETHEREUM`, `POLYGON`, `SOLANA`, `SUI`, `TEMPO`, `TRON` See [Supported Chains](/concepts/supported-chains) for details on which assets are available on each chain. Stablecoin asset to receive **Allowed values:** `USDC`, `USDT` Optional fee that your merchant account keeps from each offramp deposit. Omit to use `0%`. Percentage of each received deposit that your merchant account keeps. Use a decimal string from `0` to `50`. This is a percentage value, not basis points: `0.5` means `0.5%`, `2` means `2%`, and `50` means `50%`. **Example:** `"2.5"` Settlement configuration Settlement mode **Allowed values:** - `INTERNAL_BALANCE` - Credit merchant USD balance (only valid when `type` is `PERMANENT`) - `NGN_PAYOUT` - Settle as NGN to a bank account Rate identifier from `GET /v1/rates` **Example:** `rate_8x7k2mq9p` **Required** for `NGN_PAYOUT` mode. Not used for `INTERNAL_BALANCE`. Bank account for NGN payout. Required for `NGN_PAYOUT` mode. Bank account holder name **Example:** `John Doe` Bank account number (6-32 characters) **Example:** `0123456789` Bank code (3-16 characters) **Example:** `058` ## Request Examples ```json Temporary - NGN Payout { "type": "TEMPORARY", "customer": { "customer_id": "650e8400-e29b-41d4-a716-446655440000" }, "chain": "BASE", "asset": "USDC", "developer_fee": { "percentage": "2.5" }, "settlement": { "mode": "NGN_PAYOUT", "rate_id": "rate_8x7k2mq9p", "destination_bank": { "account_name": "John Doe", "account_number": "0123456789", "bank_code": "058" } } } ``` ```json Permanent - Internal Balance { "type": "PERMANENT", "customer": { "email": "user@example.com", "first_name": "John", "last_name": "Doe" }, "chain": "SOLANA", "asset": "USDT", "settlement": { "mode": "INTERNAL_BALANCE" } } ``` ```json Permanent - NGN Payout { "type": "PERMANENT", "customer": { "customer_id": "650e8400-e29b-41d4-a716-446655440000" }, "chain": "TRON", "asset": "USDT", "settlement": { "mode": "NGN_PAYOUT", "rate_id": "rate_8x7k2mq9p", "destination_bank": { "account_name": "John Doe", "account_number": "0123456789", "bank_code": "058" } } } ``` ```bash cURL - Permanent with NGN Payout curl --request POST \ --url https://api.daya.co/v1/offramps \ --header 'X-Api-Key: YOUR_API_KEY' \ --header 'X-Idempotency-Key: unique-key-456' \ --header 'Content-Type: application/json' \ --data '{ "type": "PERMANENT", "customer": { "customer_id": "650e8400-e29b-41d4-a716-446655440000" }, "chain": "TRON", "asset": "USDT", "settlement": { "mode": "NGN_PAYOUT", "rate_id": "rate_8x7k2mq9p", "destination_bank": { "account_name": "John Doe", "account_number": "0123456789", "bank_code": "058" } } }' ``` ```javascript JavaScript const response = await fetch('https://api.daya.co/v1/offramps', { method: 'POST', headers: { 'X-Api-Key': 'YOUR_API_KEY', 'X-Idempotency-Key': 'unique-key-123', 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'PERMANENT', customer: { email: 'user@example.com' }, chain: 'ETHEREUM', asset: 'USDC', settlement: { mode: 'INTERNAL_BALANCE' } }) }); const offramp = await response.json(); ``` ## Response Unique offramp identifier (UUID) Offramp type: `TEMPORARY` or `PERMANENT` Associated customer ID (UUID) Generated crypto deposit address Blockchain network: `APTOS`, `BASE`, `BSC`, `CELO`, `ETHEREUM`, `POLYGON`, `SOLANA`, `SUI`, `TEMPO`, or `TRON` Stablecoin asset: `USDC` or `USDT` Current offramp status. New offramps start as `ACTIVE`. Developer fee percentage used for deposits received through this offramp. Configured percentage as a decimal string. Settlement configuration Settlement mode: `INTERNAL_BALANCE` or `NGN_PAYOUT` Associated rate identifier (nullable, present for `NGN_PAYOUT`) Bank account details (present for `NGN_PAYOUT`) Bank account holder name Bank account number Bank code When the offramp was created (ISO 8601 timestamp) When the offramp was last updated (ISO 8601 timestamp) ### Success Responses ```json 201 Created - Internal Balance { "id": "a50e8400-e29b-41d4-a716-446655440000", "type": "PERMANENT", "customer_id": "650e8400-e29b-41d4-a716-446655440000", "address": "0x1a2b3c4d5e6f7890abcdef1234567890abcdef12", "chain": "ETHEREUM", "asset": "USDC", "status": "ACTIVE", "developer_fee": { "percentage": "2.5" }, "settlement": { "mode": "INTERNAL_BALANCE", "rate_id": null, "destination_bank": null }, "created_at": "2026-01-05T15:04:05Z", "updated_at": "2026-01-05T15:04:05Z" } ``` ```json 201 Created - NGN Payout { "id": "b60e8400-e29b-41d4-a716-446655440000", "type": "PERMANENT", "customer_id": "650e8400-e29b-41d4-a716-446655440000", "address": "TN3W4H6rK2ce4vX9YnFQHwKENnHjoxb3m9", "chain": "TRON", "asset": "USDT", "status": "ACTIVE", "settlement": { "mode": "NGN_PAYOUT", "rate_id": "rate_8x7k2mq9p", "destination_bank": { "account_name": "John Doe", "account_number": "0123456789", "bank_code": "058" } }, "created_at": "2026-01-05T15:04:05Z", "updated_at": "2026-01-05T15:04:05Z" } ``` ## Error Responses ```json 400 Bad Request - Missing customer { "error": { "code": "VALIDATION_FAILED", "message": "either customer.customer_id or customer.email is required", "request_id": "550e8400-e29b-41d4-a716-446655440000", "validation": "customer.customer_id or customer.email is required" } } ``` ```json 400 Bad Request - Invalid chain { "error": { "code": "VALIDATION_FAILED", "message": "Validation failed", "request_id": "550e8400-e29b-41d4-a716-446655440000", "validation": "chain must be one of APTOS, BASE, BSC, CELO, ETHEREUM, POLYGON, SOLANA, SUI, TEMPO, TRON" } } ``` ```json 400 Bad Request - Missing rate for NGN payout { "error": { "code": "VALIDATION_FAILED", "message": "Validation failed", "request_id": "550e8400-e29b-41d4-a716-446655440000", "validation": "rate_id is required for NGN_PAYOUT settlement mode" } } ``` ```json 400 Bad Request - Rate expired { "error": { "code": "RATE_EXPIRED", "message": "The specified rate_id has expired. Request a new rate via GET /v1/rates", "request_id": "550e8400-e29b-41d4-a716-446655440000" } } ``` ```json 400 Bad Request - Invalid bank details { "error": { "code": "VALIDATION_FAILED", "message": "Validation failed", "request_id": "550e8400-e29b-41d4-a716-446655440000", "validation": "destination_bank is required for NGN_PAYOUT settlement mode" } } ``` ```json 400 Bad Request - Invalid account number { "error": { "code": "VALIDATION_FAILED", "message": "Validation failed", "request_id": "550e8400-e29b-41d4-a716-446655440000", "validation": "account_number must be between 6 and 32 characters" } } ``` ```json 401 Unauthorized { "error": { "code": "UNAUTHORIZED", "message": "Unauthorized", "request_id": "550e8400-e29b-41d4-a716-446655440000" } } ``` ```json 429 Too Many Requests { "error": { "code": "RATE_LIMITED", "message": "Merchant has exceeded daily offramp creation limit", "request_id": "550e8400-e29b-41d4-a716-446655440000" } } ``` ## Validation Rules Either `customer.customer_id` or `customer.email` must be provided (not both optional, at least one required). - Generates a short-lived crypto deposit address - Settlement mode **must** be `NGN_PAYOUT` — `INTERNAL_BALANCE` is not supported on temporary offramps - `settlement.rate_id` and `settlement.destination_bank` are required - Generates a long-lived crypto deposit address - Uses the current rate at the time of deposit settlement - Supports both `INTERNAL_BALANCE` and `NGN_PAYOUT` settlement modes - For `NGN_PAYOUT`: `settlement.rate_id` and `settlement.destination_bank` are required - For `INTERNAL_BALANCE`: `settlement.rate_id` and `settlement.destination_bank` must NOT be set When `settlement.mode` is `NGN_PAYOUT`: - `destination_bank.account_name` is required - `destination_bank.account_number` must be 6-32 characters - `destination_bank.bank_code` must be 3-16 characters Always resolve the bank account first using [`POST /v1/banks/resolve`](/api-reference/banks/resolve-bank-account) before creating the offramp. Invalid bank accounts will be rejected. ## Best Practices Always call [`POST /v1/banks/resolve`](/api-reference/banks/resolve-bank-account) to verify the account number and get the account holder's name before creating an offramp with `NGN_PAYOUT`. Invalid accounts will be rejected. Call `GET /v1/rates?side=SELL` immediately before creating an offramp with `NGN_PAYOUT` to ensure maximum validity window. Create customers once via `POST /v1/customers`, then reference them by `customer_id` in subsequent offramp requests. Always include a unique `X-Idempotency-Key` header to prevent duplicate offramp creation on retries. Consider transaction fees and confirmation times when selecting a chain. See [Supported Chains](/concepts/supported-chains) for details. ## Next Steps Query offramps with filters Fetch current exchange rates before creating an offramp Pre-create customers before offramp requests Listen for deposit and settlement events #### Legacy: List offramps Path: /api-reference/offramps/list-offramps Description: Legacy route for listing crypto receive flows ## Overview Retrieve offramps through the legacy compatibility route. New integrations should use [`GET /v1/funding-accounts`](/api-reference/funding-accounts/list-funding-accounts) with `rail=CRYPTO_ADDRESS`. ## Authentication Your merchant API key ## Query Parameters Filter by offramp type **Allowed values:** `TEMPORARY`, `PERMANENT` Filter by blockchain network **Allowed values:** `APTOS`, `BASE`, `BSC`, `CELO`, `ETHEREUM`, `POLYGON`, `SOLANA`, `SUI`, `TEMPO`, `TRON` Filter by stablecoin asset **Allowed values:** `USDC`, `USDT` Filter by offramp status **Allowed values:** `ACTIVE`, `INACTIVE`, `EXPIRED` Filter by settlement mode **Allowed values:** `INTERNAL_BALANCE`, `NGN_PAYOUT` Page number (1-indexed) **Default:** `1` Results per page **Default:** `50` **Max:** `200` ## Request Examples ```bash All Offramps curl --request GET \ --url 'https://api.daya.co/v1/offramps' \ --header 'X-Api-Key: YOUR_API_KEY' ``` ```bash Permanent Only curl --request GET \ --url 'https://api.daya.co/v1/offramps?type=PERMANENT' \ --header 'X-Api-Key: YOUR_API_KEY' ``` ```bash Filter by Chain and Asset curl --request GET \ --url 'https://api.daya.co/v1/offramps?chain=ETHEREUM&asset=USDC' \ --header 'X-Api-Key: YOUR_API_KEY' ``` ```bash Paginated curl --request GET \ --url 'https://api.daya.co/v1/offramps?page=2&limit=20' \ --header 'X-Api-Key: YOUR_API_KEY' ``` ```javascript JavaScript const response = await fetch( 'https://api.daya.co/v1/offramps?type=PERMANENT&chain=ETHEREUM&page=1&limit=20', { headers: { 'X-Api-Key': 'YOUR_API_KEY' } } ); const data = await response.json(); ``` ```python Python import requests response = requests.get( 'https://api.daya.co/v1/offramps', params={ 'type': 'PERMANENT', 'chain': 'ETHEREUM', 'page': 1, 'limit': 20 }, headers={'X-Api-Key': 'YOUR_API_KEY'} ) data = response.json() ``` ## Response Array of offramp objects Unique offramp identifier (UUID) Offramp type: `TEMPORARY` or `PERMANENT` Associated customer ID (UUID) Crypto deposit address Blockchain network Stablecoin asset: `USDC` or `USDT` Offramp status: `ACTIVE`, `INACTIVE`, or `EXPIRED` Developer fee percentage used for deposits received through this offramp. Configured percentage as a decimal string. Settlement configuration Settlement mode: `INTERNAL_BALANCE` or `NGN_PAYOUT` Associated rate identifier (nullable) Bank account details (nullable) Bank account holder name Bank account number Bank code When the offramp was created (ISO 8601 timestamp) When the offramp was last updated (ISO 8601 timestamp) Total number of offramps matching filters Current page number Results per page Total number of pages ### Success Response ```json 200 OK { "data": [ { "id": "a50e8400-e29b-41d4-a716-446655440000", "type": "PERMANENT", "customer_id": "650e8400-e29b-41d4-a716-446655440000", "address": "0x1a2b3c4d5e6f7890abcdef1234567890abcdef12", "chain": "ETHEREUM", "asset": "USDC", "status": "ACTIVE", "settlement": { "mode": "INTERNAL_BALANCE", "rate_id": null, "destination_bank": null }, "created_at": "2026-01-10T12:00:00Z", "updated_at": "2026-01-10T12:00:00Z" }, { "id": "b60e8400-e29b-41d4-a716-446655440000", "type": "TEMPORARY", "customer_id": "660e8400-e29b-41d4-a716-446655440000", "address": "TN3W4H6rK2ce4vX9YnFQHwKENnHjoxb3m9", "chain": "TRON", "asset": "USDT", "status": "ACTIVE", "settlement": { "mode": "NGN_PAYOUT", "rate_id": "rate_8x7k2mq9p", "destination_bank": { "account_name": "John Doe", "account_number": "0123456789", "bank_code": "058" } }, "created_at": "2026-01-14T15:05:00Z", "updated_at": "2026-01-14T15:05:00Z" } ], "total": 2, "page": 1, "limit": 50, "total_pages": 1 } ``` ## Error Responses ```json 400 Bad Request { "error": { "code": "BAD_REQUEST", "message": "Invalid query parameters", "request_id": "550e8400-e29b-41d4-a716-446655440000" } } ``` ```json 401 Unauthorized { "error": { "code": "UNAUTHORIZED", "message": "Unauthorized", "request_id": "550e8400-e29b-41d4-a716-446655440000" } } ``` ## Next Steps Get a specific offramp by ID Create a new offramp #### Legacy: Get offramp Path: /api-reference/offramps/get-offramp Description: Legacy route for retrieving a crypto receive flow ## Overview Retrieve an offramp through the legacy compatibility route. New integrations should use [`GET /v1/funding-accounts/{id}`](/api-reference/funding-accounts/get-funding-account). ## Authentication Your merchant API key ## Path Parameters Offramp ID (UUID format) **Example:** `a50e8400-e29b-41d4-a716-446655440000` ## Request Examples ```bash cURL curl --request GET \ --url https://api.daya.co/v1/offramps/a50e8400-e29b-41d4-a716-446655440000 \ --header 'X-Api-Key: YOUR_API_KEY' ``` ```javascript JavaScript const response = await fetch( 'https://api.daya.co/v1/offramps/a50e8400-e29b-41d4-a716-446655440000', { headers: { 'X-Api-Key': 'YOUR_API_KEY' } } ); const offramp = await response.json(); ``` ```python Python import requests response = requests.get( 'https://api.daya.co/v1/offramps/a50e8400-e29b-41d4-a716-446655440000', headers={'X-Api-Key': 'YOUR_API_KEY'} ) offramp = response.json() ``` ## Response Unique offramp identifier (UUID) Offramp type: `TEMPORARY` or `PERMANENT` Associated customer ID (UUID) Crypto deposit address Blockchain network: `APTOS`, `BASE`, `BSC`, `CELO`, `ETHEREUM`, `POLYGON`, `SOLANA`, `SUI`, `TEMPO`, or `TRON` Stablecoin asset: `USDC` or `USDT` Offramp status: `ACTIVE`, `INACTIVE`, or `EXPIRED` Developer fee percentage used for deposits received through this offramp. Configured percentage as a decimal string. Settlement configuration Settlement mode: `INTERNAL_BALANCE` or `NGN_PAYOUT` Associated rate identifier (nullable) Bank account details (nullable) Bank account holder name Bank account number Bank code When the offramp was created (ISO 8601 timestamp) When the offramp was last updated (ISO 8601 timestamp) ### Success Responses ```json 200 OK - Internal Balance { "id": "a50e8400-e29b-41d4-a716-446655440000", "type": "PERMANENT", "customer_id": "650e8400-e29b-41d4-a716-446655440000", "address": "0x1a2b3c4d5e6f7890abcdef1234567890abcdef12", "chain": "ETHEREUM", "asset": "USDC", "status": "ACTIVE", "developer_fee": { "percentage": "2.5" }, "settlement": { "mode": "INTERNAL_BALANCE", "rate_id": null, "destination_bank": null }, "created_at": "2026-01-05T15:04:05Z", "updated_at": "2026-01-05T15:04:05Z" } ``` ```json 200 OK - NGN Payout { "id": "b60e8400-e29b-41d4-a716-446655440000", "type": "PERMANENT", "customer_id": "650e8400-e29b-41d4-a716-446655440000", "address": "TN3W4H6rK2ce4vX9YnFQHwKENnHjoxb3m9", "chain": "TRON", "asset": "USDT", "status": "ACTIVE", "developer_fee": { "percentage": "2.5" }, "settlement": { "mode": "NGN_PAYOUT", "rate_id": "rate_8x7k2mq9p", "destination_bank": { "account_name": "John Doe", "account_number": "0123456789", "bank_code": "058" } }, "created_at": "2026-01-05T15:04:05Z", "updated_at": "2026-01-05T15:04:05Z" } ``` ```json 400 Bad Request { "error": { "code": "BAD_REQUEST", "message": "Invalid offramp ID format", "request_id": "550e8400-e29b-41d4-a716-446655440000" } } ``` ```json 404 Not Found { "error": { "code": "NOT_FOUND", "message": "Offramp not found", "request_id": "550e8400-e29b-41d4-a716-446655440000" } } ``` ## Next Steps List all offramps with filters Create a new offramp ### Legacy API / Payouts #### Legacy: List payouts Path: /api-reference/payouts/list-payouts Description: Legacy route for listing settlement payouts ## Overview Retrieve a paginated list of read-only settlement payouts. Payouts are legacy read-only settlement artifacts. Merchant-initiated sends use the [Transfers API](/api-reference/transfers/create-transfer). ## Authentication Your merchant API key ## Query Parameters Filter by payout type **Allowed values:** `CRYPTO_PAYOUT`, `NGN_PAYOUT` Filter by payout status **Allowed values:** `PROCESSING`, `SETTLED`, `FAILED` Results per page **Default:** `20` **Max:** `100` Page number (1-indexed) **Default:** `1` ## Request Examples ```bash All Payouts curl --request GET \ --url 'https://api.daya.co/v1/payouts' \ --header 'X-Api-Key: YOUR_API_KEY' ``` ```bash By Type curl --request GET \ --url 'https://api.daya.co/v1/payouts?type=NGN_PAYOUT' \ --header 'X-Api-Key: YOUR_API_KEY' ``` ```bash By Status curl --request GET \ --url 'https://api.daya.co/v1/payouts?status=SETTLED' \ --header 'X-Api-Key: YOUR_API_KEY' ``` ```javascript JavaScript const response = await fetch( 'https://api.daya.co/v1/payouts?status=SETTLED&page=1&limit=20', { headers: { 'X-Api-Key': 'YOUR_API_KEY' } } ); const data = await response.json(); ``` ## Response Array of payout objects Payout type (`NGN_PAYOUT` or `CRYPTO_PAYOUT`) Unique payout identifier (UUID) Payout reference string Current status (`PROCESSING`, `SETTLED`, or `FAILED`) Currency funds are sent from (e.g. `USD`) Currency funds are delivered in (e.g. `NGN`) Amount in source currency Amount in destination currency Fee charged for the payout FX rate applied to the payout Rate side (`SELL`) Exchange rate value When the rate was captured (ISO 8601 timestamp) Payout recipient details Recipient identifier (UUID) Recipient type (`BANK_ACCOUNT` or `CRYPTO_ADDRESS`) Bank account details (present for NGN payouts, `null` for crypto payouts) Bank account number Account holder name Bank code Bank name Crypto wallet address (present for crypto payouts, `null` for NGN payouts) When the recipient was created (ISO 8601 timestamp) Entity that initiated the payout Sender type (`MERCHANT` or `CUSTOMER`) Sender identifier (UUID) On-chain transaction hash (for crypto payouts, `null` for NGN payouts) When the payout was created (ISO 8601 timestamp) When the payout settled (ISO 8601 timestamp, `null` if not yet settled) Current page number Results per page Total number of payouts matching filters Total number of pages ### Success Response ```json 200 OK { "data": [ { "type": "NGN_PAYOUT", "id": "b2c3d4e5-f6a7-8901-bcde-f23456789abc", "reference": "PAY-NGN-20250105-001", "status": "SETTLED", "source_currency": "USD", "destination_currency": "NGN", "source_amount": "100.00", "destination_amount": "155050.00", "fee": "1.50", "rate": { "side": "SELL", "value": "1550.50", "captured_at": "2025-01-05T15:04:05Z" }, "recipient": { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "type": "BANK_ACCOUNT", "bank_account": { "account_number": "1234567890", "account_name": "John Doe", "bank_code": "044", "bank_name": "Access Bank" }, "crypto_address": null, "created_at": "2025-01-05T15:04:05Z" }, "sender": { "type": "MERCHANT", "id": "c3d4e5f6-a7b8-9012-cdef-345678901abc" }, "tx_hash": null, "created_at": "2025-01-05T15:04:05Z", "settled_at": "2025-01-05T15:10:00Z" } ], "page": 1, "limit": 20, "total": 42, "total_pages": 3 } ``` ## Next Steps Get a specific payout by ID View incoming deposits #### Legacy: Get payout Path: /api-reference/payouts/get-payout Description: Legacy route for retrieving a settlement payout ## Overview Retrieve a read-only settlement payout by ID. Payouts are legacy read-only settlement artifacts. Merchant-initiated sends use the [Transfers API](/api-reference/transfers/create-transfer). ## Authentication Your merchant API key ## Path Parameters Unique payout identifier (UUID) **Example:** `b2c3d4e5-f6a7-8901-bcde-f23456789abc` ## Request Examples ```bash cURL curl --request GET \ --url https://api.daya.co/v1/payouts/b2c3d4e5-f6a7-8901-bcde-f23456789abc \ --header 'X-Api-Key: YOUR_API_KEY' ``` ```javascript JavaScript const response = await fetch( 'https://api.daya.co/v1/payouts/b2c3d4e5-f6a7-8901-bcde-f23456789abc', { headers: { 'X-Api-Key': 'YOUR_API_KEY' } } ); const payout = await response.json(); ``` ## Response Same structure as individual payout objects in list response. ### Success Response ```json 200 OK - NGN Payout { "type": "NGN_PAYOUT", "id": "b2c3d4e5-f6a7-8901-bcde-f23456789abc", "reference": "PAY-NGN-20250105-001", "status": "SETTLED", "source_currency": "USD", "destination_currency": "NGN", "source_amount": "100.00", "destination_amount": "155050.00", "fee": "1.50", "rate": { "side": "SELL", "value": "1550.50", "captured_at": "2025-01-05T15:04:05Z" }, "recipient": { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "type": "BANK_ACCOUNT", "bank_account": { "account_number": "1234567890", "account_name": "John Doe", "bank_code": "044", "bank_name": "Access Bank" }, "crypto_address": null, "created_at": "2025-01-05T15:04:05Z" }, "sender": { "type": "MERCHANT", "id": "c3d4e5f6-a7b8-9012-cdef-345678901abc" }, "tx_hash": null, "created_at": "2025-01-05T15:04:05Z", "settled_at": "2025-01-05T15:10:00Z" } ``` ```json 200 OK - Crypto Payout { "type": "CRYPTO_PAYOUT", "id": "d4e5f6a7-b8c9-0123-defg-456789012bcd", "reference": "PAY-CRYPTO-20250105-002", "status": "SETTLED", "source_currency": "USD", "destination_currency": "USDC", "source_amount": "500.00", "destination_amount": "499.25", "fee": "0.75", "rate": { "side": "SELL", "value": "1.00", "captured_at": "2025-01-05T16:20:00Z" }, "recipient": { "id": "e5f6a7b8-c9d0-1234-efgh-567890123cde", "type": "CRYPTO_ADDRESS", "bank_account": null, "crypto_address": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18", "created_at": "2025-01-05T16:18:00Z" }, "sender": { "type": "CUSTOMER", "id": "f6a7b8c9-d0e1-2345-fghi-678901234def" }, "tx_hash": "0x9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8", "created_at": "2025-01-05T16:20:00Z", "settled_at": "2025-01-05T16:22:30Z" } ``` ```json 404 Not Found { "error": { "code": "payout_not_found", "message": "Payout not found", "details": "No payout with ID b2c3d4e5-f6a7-8901-bcde-000000000000 found for this merchant" } } ``` ```json 401 Unauthorized { "error": { "code": "unauthorized", "message": "Invalid or missing API key", "details": "Provide a valid API key via the X-Api-Key header" } } ``` ## Next Steps View all payouts with filters View incoming deposits ## Pro ### Getting Started #### Pro Overview Path: /pro/overview Description: Programmatic access to Daya's currency trading platform ## Introduction Daya Pro API provides programmatic access to our currency trading platform. Build trading bots, integrate market data into your applications, or automate your trading strategies. Pro is a separate platform from Daya Onramp. It requires its own API keys and uses different authentication methods. ## What You Can Build Automate trading strategies with market and limit orders Access real-time orderbook and market data Track orders, trades, and account activity Analyze trading patterns and market trends ## Available Markets Pro currently supports the following trading pairs: **Unified Balance:** You can use `USDT-NGN`, `USDC-NGN`, or `USD-NGN` as the symbol. All are treated equivalently and map to a single unified USD balance. | Input Symbol | Description | |--------------|-------------| | `USDT-NGN` | Tether USD to Nigerian Naira (recommended) | | `USDC-NGN` | USD Coin to Nigerian Naira | | `USD-NGN` | US Dollar to Nigerian Naira | ## API Capabilities ### Public Endpoints (No Authentication) Access market data without an API key: - **Markets** - List all available trading pairs - **Orderbook** - Real-time orderbook snapshots with configurable depth ### Authenticated Endpoints With an API key, you can: | Scope Required | Capabilities | |----------------|--------------| | **Read** | View your orders, trades, balances, account data, deposit addresses, and withdrawal options | | **Trade** | Place market/limit orders, cancel orders, and initiate withdrawals | | **Write** | Manage support-enabled webhook configuration | ## Key Differences from Onramp API | Aspect | Onramp API | Pro API | |--------|------------|-----------------| | **Purpose** | Fiat-to-crypto conversion | Currency trading | | **Auth Header** | `X-Api-Key` | `X-Api-Key` | | **Key Prefix** | `sk_sandbox_` / `sk_live_` | `daya_sk_` | | **Environments** | Sandbox + Production | Production only | | **Scopes** | Full access | Read, Trade, Write | | **Management** | dashboard.daya.co | Contact support | ## Getting Started API keys are generated by the Daya team. Contact [support@daya.co](mailto:support@daya.co) to request access. Contact [support@daya.co](mailto:support@daya.co) to set up your Pro account and request API keys with the scopes you need Fetch market data without authentication to verify connectivity Add your API key to start accessing orders and placing trades ## Next Steps Set up API keys and learn about scopes Place your first order in minutes #### Quick Start Path: /pro/quickstart Description: Place your first trade with the Pro API ## Overview This guide walks you through placing your first order using the Pro API. You'll learn to fetch market data, check your account, and execute a trade. **Prerequisites:** You need a Pro account and an API key with **Trade** scope. Contact [support@daya.co](mailto:support@daya.co) to request access. See [Authentication](/pro/authentication) for details. ## Step 1: Fetch Available Markets First, check which markets are available. This endpoint doesn't require authentication: ```bash cURL curl --request GET \ --url https://api.pro.daya.co/public/v1/markets ``` ```javascript JavaScript const response = await fetch('https://api.pro.daya.co/public/v1/markets'); const result = await response.json(); console.log(result.data); ``` ```python Python import requests response = requests.get('https://api.pro.daya.co/public/v1/markets') result = response.json() print(result['data']) ``` ```go Go package main import ( "encoding/json" "fmt" "net/http" ) func main() { resp, err := http.Get("https://api.pro.daya.co/public/v1/markets") if err != nil { panic(err) } defer resp.Body.Close() var result map[string]interface{} json.NewDecoder(resp.Body).Decode(&result) fmt.Println(result["data"]) } ``` **Response:** ```json { "success": true, "message": "Markets retrieved successfully", "data": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "symbol": "USD-NGN", "base_asset": "USD", "quote_asset": "NGN", "status": "active", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ], "timestamp": "2024-01-15T10:30:00Z" } ``` ## Step 2: Check the Orderbook View current market depth for your chosen pair: ```bash cURL curl --request GET \ --url 'https://api.pro.daya.co/public/v1/orderbook/USDT-NGN?depth=10' ``` ```javascript JavaScript const response = await fetch( 'https://api.pro.daya.co/public/v1/orderbook/USDT-NGN?depth=10' ); const result = await response.json(); console.log('Best bid:', result.data.bids[0]); console.log('Best ask:', result.data.asks[0]); ``` ```python Python import requests response = requests.get( 'https://api.pro.daya.co/public/v1/orderbook/USDT-NGN', params={'depth': 10} ) result = response.json() print(f"Best bid: {result['data']['bids'][0]}") print(f"Best ask: {result['data']['asks'][0]}") ``` ```go Go package main import ( "encoding/json" "fmt" "net/http" ) func main() { resp, err := http.Get("https://api.pro.daya.co/public/v1/orderbook/USDT-NGN?depth=10") if err != nil { panic(err) } defer resp.Body.Close() var result map[string]interface{} json.NewDecoder(resp.Body).Decode(&result) data := result["data"].(map[string]interface{}) bids := data["bids"].([]interface{}) asks := data["asks"].([]interface{}) fmt.Println("Best bid:", bids[0]) fmt.Println("Best ask:", asks[0]) } ``` ## Step 3: Place a Limit Order Now place an order. This requires an API key with **Trade** scope: ```bash cURL curl --request POST \ --url https://api.pro.daya.co/public/v1/orders \ --header 'X-Api-Key: daya_sk_YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "symbol": "USDT-NGN", "side": "buy", "type": "limit", "price": "1545.00", "quantity": "100.00" }' ``` ```javascript JavaScript const response = await fetch('https://api.pro.daya.co/public/v1/orders', { method: 'POST', headers: { 'X-Api-Key': 'daya_sk_YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ symbol: 'USDT-NGN', side: 'buy', type: 'limit', price: '1545.00', quantity: '100.00' }) }); const order = await response.json(); console.log('Order placed:', order); ``` ```python Python import requests headers = { 'X-Api-Key': 'daya_sk_YOUR_API_KEY', 'Content-Type': 'application/json' } data = { 'symbol': 'USDT-NGN', 'side': 'buy', 'type': 'limit', 'price': '1545.00', 'quantity': '100.00' } response = requests.post( 'https://api.pro.daya.co/public/v1/orders', headers=headers, json=data ) order = response.json() print(f"Order placed: {order}") ``` ```go Go package main import ( "bytes" "encoding/json" "fmt" "net/http" ) func main() { order := map[string]string{ "symbol": "USDT-NGN", "side": "buy", "type": "limit", "price": "1545.00", "quantity": "100.00", } body, _ := json.Marshal(order) req, _ := http.NewRequest("POST", "https://api.pro.daya.co/public/v1/orders", bytes.NewBuffer(body)) req.Header.Set("X-Api-Key", "daya_sk_YOUR_API_KEY") req.Header.Set("Content-Type", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { panic(err) } defer resp.Body.Close() var result map[string]interface{} json.NewDecoder(resp.Body).Decode(&result) fmt.Println("Order placed:", result) } ``` **Response:** ```json { "success": true, "message": "Order placed successfully", "data": { "order_id": "550e8400-e29b-41d4-a716-446655440000", "status": "pending_settlement", "side": "buy", "type": "limit", "symbol": "USD-NGN", "requested_quantity": "100.00000000", "filled_quantity": "0.00000000", "remaining_quantity": "100.00000000", "avg_fill_price": "", "total_fee_charged": "", "created_at": "2024-01-15T10:35:00Z", "updated_at": "2024-01-15T10:35:00Z" }, "timestamp": "2024-01-15T10:35:00Z" } ``` ## Step 4: Check Your Orders View your open orders: ```bash cURL curl --request GET \ --url 'https://api.pro.daya.co/public/v1/orders?symbol=USDT-NGN' \ --header 'X-Api-Key: daya_sk_YOUR_API_KEY' ``` ```javascript JavaScript const response = await fetch( 'https://api.pro.daya.co/public/v1/orders?symbol=USDT-NGN', { headers: { 'X-Api-Key': 'daya_sk_YOUR_API_KEY' } } ); const data = await response.json(); console.log('Your orders:', data.data); ``` ```python Python import requests headers = {'X-Api-Key': 'daya_sk_YOUR_API_KEY'} response = requests.get( 'https://api.pro.daya.co/public/v1/orders', headers=headers, params={'symbol': 'USDT-NGN'} ) orders = response.json() print(f"Your orders: {orders['data']}") ``` ```go Go package main import ( "encoding/json" "fmt" "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://api.pro.daya.co/public/v1/orders?symbol=USDT-NGN", nil) req.Header.Set("X-Api-Key", "daya_sk_YOUR_API_KEY") client := &http.Client{} resp, err := client.Do(req) if err != nil { panic(err) } defer resp.Body.Close() var data map[string]interface{} json.NewDecoder(resp.Body).Decode(&data) fmt.Println("Your orders:", data["data"]) } ``` ## Step 5: Cancel an Order Cancel an open order if needed: ```bash cURL curl --request DELETE \ --url https://api.pro.daya.co/public/v1/orders/550e8400-e29b-41d4-a716-446655440000 \ --header 'X-Api-Key: daya_sk_YOUR_API_KEY' ``` ```javascript JavaScript const response = await fetch( 'https://api.pro.daya.co/public/v1/orders/550e8400-e29b-41d4-a716-446655440000', { method: 'DELETE', headers: { 'X-Api-Key': 'daya_sk_YOUR_API_KEY' } } ); if (response.ok) { console.log('Order cancelled'); } ``` ```python Python import requests headers = {'X-Api-Key': 'daya_sk_YOUR_API_KEY'} response = requests.delete( 'https://api.pro.daya.co/public/v1/orders/550e8400-e29b-41d4-a716-446655440000', headers=headers ) if response.ok: print('Order cancelled') ``` ```go Go package main import ( "fmt" "net/http" ) func main() { req, _ := http.NewRequest("DELETE", "https://api.pro.daya.co/public/v1/orders/550e8400-e29b-41d4-a716-446655440000", nil) req.Header.Set("X-Api-Key", "daya_sk_YOUR_API_KEY") client := &http.Client{} resp, err := client.Do(req) if err != nil { panic(err) } defer resp.Body.Close() if resp.StatusCode == 200 { fmt.Println("Order cancelled") } } ``` ## Order Types | Type | Description | Required Fields | |------|-------------|-----------------| | **limit** | Execute at specified price or better | `price`, `quantity` | | **market** | Execute immediately at best available price | `quantity` | ## Order Status | Status | Description | |--------|-------------| | `pending_settlement` | Order is awaiting settlement | | `new` | Order has been created | | `open` | Order is active on the orderbook | | `partially_filled` | Order is partially executed | | `filled` | Order is completely executed | | `cancelled` | Order was cancelled by user | | `rejected` | Order was rejected by the system | | `failed` | Order failed to execute | ## Next Steps Full markets and orderbook reference Complete order management reference #### Authentication Path: /pro/authentication Description: Secure your Pro API requests with API keys ## Overview Pro API uses secret bearer API keys for authentication. Each key is tied to your user account and has configurable permission scopes. API keys grant access to your Daya Pro account. Treat them like passwords: **never** expose them in client-side code, share them publicly, or commit them to version control. ## API Keys ### Generating Keys API keys are generated by the Daya team. Contact [support@daya.co](mailto:support@daya.co) to request API access and specify the scopes you need. **Store your API key securely.** The full key is only shared once. If you lose it, you'll need to request a new key. ### Key Format All Pro API keys use this format: ``` daya_sk_[random-32-bytes-base64] ``` Example: `daya_sk_xK9mN2pL8qR4sT6vW0yZaBcDeFgHiJkLmNoPqRsTuV` The `daya_sk_` prefix helps identify Daya keys in code scanning and secret detection tools. ### Permission Scopes | Scope | Permissions | Use Case | |-------|-------------|----------| | **Read** | View orders, trades, balances, account data, deposit addresses, and withdrawal options | Dashboards, analytics, monitoring, reconciliation | | **Trade** | Place and cancel orders, initiate withdrawals (includes Read) | Trading bots, automated strategies, treasury movement | | **Write** | Manage support-enabled webhooks (includes Read) | Webhook configuration arranged through Daya support | **Trade** scope automatically includes **Read** permissions. You don't need to select both. **Trade** keys authorize fund movement. Only store Trade-scoped keys in trusted server-side systems that are allowed to place orders and initiate withdrawals from the account. ### Key Limits - Maximum **10 API keys** per user - Keys can be revoked immediately ### Key Status | Status | Description | |--------|-------------| | `active` | Key is valid and operational | | `revoked` | Key was manually revoked by user | Revoked keys cannot be reactivated. Create a new key if needed. ## Base URL All Pro API requests use: ``` https://api.pro.daya.co/public/v1 ``` ## Making Authenticated Requests Include your API key in the `X-Api-Key` header: ```bash cURL curl --request GET \ --url https://api.pro.daya.co/public/v1/orders \ --header 'X-Api-Key: daya_sk_YOUR_API_KEY' ``` ```javascript JavaScript const response = await fetch('https://api.pro.daya.co/public/v1/orders', { headers: { 'X-Api-Key': 'daya_sk_YOUR_API_KEY' } }); const data = await response.json(); ``` ```python Python import requests headers = { 'X-Api-Key': 'daya_sk_YOUR_API_KEY' } response = requests.get( 'https://api.pro.daya.co/public/v1/orders', headers=headers ) ``` ```go Go client := &http.Client{} req, _ := http.NewRequest("GET", "https://api.pro.daya.co/public/v1/orders", nil) req.Header.Add("X-Api-Key", "daya_sk_YOUR_API_KEY") resp, _ := client.Do(req) ``` ## Public Endpoints Some endpoints don't require authentication: | Endpoint | Description | |----------|-------------| | `GET /public/v1/markets` | List all trading markets | | `GET /public/v1/markets/{symbol}` | Get a specific market | | `GET /public/v1/orderbook/{symbol}` | Get orderbook snapshot | | `GET /public/v1/last-price/{symbol}` | Get latest price and 24h change | | `GET /public/v1/market-trades/{symbol}` | List recent market trades | ## Security Best Practices Use environment variables or secret management systems: ```bash .env DAYA_PRO_API_KEY=daya_sk_xK9mN2pL8qR4sT6vW0yZ... ``` Never hardcode keys in source code or commit them to Git. Only grant the permissions your application needs: - **Read-only applications** (dashboards, analytics): Use Read scope only - **Trading and treasury automation**: Use Trade scope only for systems allowed to place orders and initiate withdrawals - **Webhook management**: Contact support to configure Pro webhook setup and Write scope Create separate keys for different use cases. ## Error Responses ### 401 Unauthorized Missing or invalid API key: ```json { "success": false, "message": "Unauthorized", "error": { "code": "API_KEY_INVALID", "message": "Invalid or missing API key" }, "timestamp": "2024-01-15T10:30:00Z" } ``` **Common causes:** - Missing `X-Api-Key` header - Invalid key format (must start with `daya_sk_`) - Key has been revoked ### 403 Forbidden Insufficient permissions: ```json { "success": false, "message": "Forbidden", "error": { "code": "API_KEY_INVALID_SCOPE", "message": "Insufficient scope for this operation" }, "timestamp": "2024-01-15T10:30:00Z" } ``` **Common causes:** - Using Read-only key to place orders (requires Trade scope) - Using non-Write key to manage webhooks (requires Write scope) - User account is suspended ## Testing Authentication Verify your API key works: ```bash cURL curl --request GET \ --url https://api.pro.daya.co/public/v1/balances \ --header 'X-Api-Key: daya_sk_YOUR_API_KEY' ``` ```javascript JavaScript async function testAuth() { const response = await fetch( 'https://api.pro.daya.co/public/v1/balances', { headers: { 'X-Api-Key': 'daya_sk_YOUR_API_KEY' } } ); if (response.ok) { console.log('Authentication successful'); const result = await response.json(); console.log('Balances:', result.data); } else { console.error('Authentication failed:', response.status); } } ``` ```python Python import requests headers = {'X-Api-Key': 'daya_sk_YOUR_API_KEY'} response = requests.get( 'https://api.pro.daya.co/public/v1/balances', headers=headers ) if response.ok: print('Authentication successful') print('Balances:', response.json()['data']) else: print('Authentication failed:', response.status_code) ``` Expected response: ```json { "success": true, "message": "Balances retrieved successfully", "data": { "balances": [] }, "timestamp": "2024-01-15T10:30:00Z" } ``` ## Next Steps Place your first order Explore all endpoints ### Markets #### List Markets Path: /pro/api-reference/list-markets Description: Retrieve all available trading markets ## Overview Get a list of all available trading markets on the Pro platform. This endpoint does not require authentication. ## Request Examples ```bash cURL curl --request GET \ --url https://api.pro.daya.co/public/v1/markets ``` ```javascript JavaScript const response = await fetch('https://api.pro.daya.co/public/v1/markets'); const data = await response.json(); console.log(data.data); ``` ```python Python import requests response = requests.get('https://api.pro.daya.co/public/v1/markets') data = response.json() print(data['data']) ``` ```go Go package main import ( "encoding/json" "fmt" "net/http" ) func main() { resp, err := http.Get("https://api.pro.daya.co/public/v1/markets") if err != nil { panic(err) } defer resp.Body.Close() var data map[string]interface{} json.NewDecoder(resp.Body).Decode(&data) fmt.Println(data["data"]) } ``` ## Response Indicates if the request was successful Human-readable response message Array of market objects Market ID (UUID) **Example:** `550e8400-e29b-41d4-a716-446655440000` Trading pair identifier **Example:** `USDT-NGN` Base asset of the pair (always `USD` - unified balance) **Example:** `USD` Quote asset of the pair **Example:** `NGN` Market status **Values:** `active`, `suspended`, `maintenance` ISO 8601 creation timestamp ISO 8601 last update timestamp ### Success Response ```json 200 OK { "success": true, "message": "Markets retrieved successfully", "data": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "symbol": "USD-NGN", "base_asset": "USD", "quote_asset": "NGN", "status": "active", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } ] } ``` ## Available Markets **Unified Balance:** The API accepts `USDT-NGN`, `USDC-NGN`, or `USD-NGN` as input symbols. All are treated equivalently and map to a single unified USD balance. Responses will show `USD` as the base asset. | Input Symbol | Mapped To | Description | |--------------|-----------|-------------| | `USDT-NGN` | USD-NGN | Tether USD to Nigerian Naira (recommended) | | `USDC-NGN` | USD-NGN | USD Coin to Nigerian Naira | | `USD-NGN` | USD-NGN | US Dollar to Nigerian Naira | ## Rate Limits - **100 requests per minute** per IP address - No authentication required ## Next Steps View market depth for a trading pair Place your first trade #### Get Market Path: /pro/api-reference/get-market Description: Retrieve details for a specific trading market ## Overview Get details for a specific trading market by its symbol. This endpoint does not require authentication. ## Path Parameters Trading pair symbol **Example:** `USDT-NGN` **Allowed values:** `USDT-NGN`, `USDC-NGN`, `USD-NGN` ## Request Examples ```bash cURL curl --request GET \ --url https://api.pro.daya.co/public/v1/markets/USDT-NGN ``` ```javascript JavaScript const response = await fetch( 'https://api.pro.daya.co/public/v1/markets/USDT-NGN' ); const data = await response.json(); console.log(data.data); ``` ```python Python import requests response = requests.get('https://api.pro.daya.co/public/v1/markets/USDT-NGN') data = response.json() print(data['data']) ``` ```go Go package main import ( "encoding/json" "fmt" "net/http" ) func main() { resp, err := http.Get("https://api.pro.daya.co/public/v1/markets/USDT-NGN") if err != nil { panic(err) } defer resp.Body.Close() var data map[string]interface{} json.NewDecoder(resp.Body).Decode(&data) fmt.Println(data["data"]) } ``` ## Response Indicates if the request was successful Human-readable response message Market details Market ID (UUID) **Example:** `550e8400-e29b-41d4-a716-446655440000` Trading pair identifier **Example:** `USD-NGN` Base asset of the pair (always `USD` - unified balance) **Example:** `USD` Quote asset of the pair **Example:** `NGN` Market status **Values:** `active`, `suspended`, `maintenance` ISO 8601 creation timestamp ISO 8601 last update timestamp ### Success Response ```json 200 OK { "success": true, "message": "Market retrieved successfully", "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "symbol": "USD-NGN", "base_asset": "USD", "quote_asset": "NGN", "status": "active", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z" } } ``` ## Error Responses ```json 404 Not Found { "success": false, "message": "Market not found", "error": { "code": "SYMBOL_NOT_FOUND", "message": "Market not found", "symbol": "INVALID-PAIR" } } ``` ## Unified Balance **Unified Balance:** The API accepts `USDT-NGN`, `USDC-NGN`, or `USD-NGN` as input symbols. All are treated equivalently and map to a single unified USD balance. The response will show `USD` as the base asset. ## Rate Limits - **100 requests per minute** per IP address - No authentication required ## Next Steps View all available markets Get the latest price for a market #### Get Orderbook Path: /pro/api-reference/get-orderbook Description: Retrieve the current orderbook for a trading pair ## Overview Get the current orderbook (market depth) for a specific trading pair. This endpoint does not require authentication. ## Path Parameters Trading pair symbol **Example:** `USDT-NGN` **Allowed values:** `USDT-NGN`, `USDC-NGN`, `USD-NGN` ## Query Parameters Number of price levels to return **Default:** `20` **Range:** `1` to `100` ## Request Examples ```bash cURL curl --request GET \ --url 'https://api.pro.daya.co/public/v1/orderbook/USDT-NGN?depth=10' ``` ```javascript JavaScript const response = await fetch( 'https://api.pro.daya.co/public/v1/orderbook/USDT-NGN?depth=10' ); const orderbook = await response.json(); console.log('Best bid:', orderbook.data.stats.best_bid); console.log('Best ask:', orderbook.data.stats.best_ask); ``` ```python Python import requests response = requests.get( 'https://api.pro.daya.co/public/v1/orderbook/USDT-NGN', params={'depth': 10} ) orderbook = response.json() print(f"Best bid: {orderbook['data']['stats']['best_bid']}") print(f"Best ask: {orderbook['data']['stats']['best_ask']}") ``` ```go Go package main import ( "encoding/json" "fmt" "net/http" ) func main() { resp, err := http.Get("https://api.pro.daya.co/public/v1/orderbook/USDT-NGN?depth=10") if err != nil { panic(err) } defer resp.Body.Close() var result map[string]interface{} json.NewDecoder(resp.Body).Decode(&result) data := result["data"].(map[string]interface{}) stats := data["stats"].(map[string]interface{}) fmt.Println("Best bid:", stats["best_bid"]) fmt.Println("Best ask:", stats["best_ask"]) } ``` ## Response Indicates if the request was successful Human-readable response message Orderbook data Trading pair symbol Market ID (UUID) Base asset of the pair Quote asset of the pair Buy orders sorted by price (highest first) Bid price Total quantity at this price level Sell orders sorted by price (lowest first) Ask price Total quantity at this price level Orderbook statistics Best (highest) bid price Best (lowest) ask price Spread between best bid and ask Spread as percentage Mid price between best bid and ask Orderbook depth returned ISO 8601 timestamp of the snapshot ### Success Response ```json 200 OK { "success": true, "message": "Orderbook retrieved successfully", "data": { "symbol": "USD-NGN", "market_id": "550e8400-e29b-41d4-a716-446655440000", "base_asset": "USD", "quote_asset": "NGN", "bids": [ {"price": "1545.00", "quantity": "500.00"}, {"price": "1544.50", "quantity": "1200.00"}, {"price": "1544.00", "quantity": "800.00"} ], "asks": [ {"price": "1546.00", "quantity": "800.00"}, {"price": "1546.50", "quantity": "350.00"}, {"price": "1547.00", "quantity": "1500.00"} ], "stats": { "best_bid": "1545.00", "best_ask": "1546.00", "spread": "1.00", "spread_percent": "0.065", "mid_price": "1545.50" }, "depth": 10, "timestamp": "2024-01-15T10:30:00Z" } } ``` ## Error Responses ```json 404 Not Found - Invalid symbol { "success": false, "message": "Market not found", "error": { "code": "SYMBOL_NOT_FOUND", "message": "Market not found", "symbol": "INVALID-PAIR" } } ``` ```json 400 Bad Request - Invalid depth { "success": false, "message": "Validation error", "error": { "code": "VALIDATION_ERROR", "message": "Depth must be between 1 and 100" } } ``` ## Understanding the Orderbook ### Bids vs Asks | Type | Description | Sort Order | |------|-------------|------------| | **Bids** | Buy orders - users willing to purchase at this price | Highest price first | | **Asks** | Sell orders - users willing to sell at this price | Lowest price first | ### Reading the Spread The spread is the difference between the best ask and best bid: ``` Spread = Best Ask - Best Bid Spread % = (Spread / Mid Price) × 100 ``` A tighter spread indicates higher liquidity. ## Rate Limits - **100 requests per minute** per IP address - No authentication required ## Next Steps Execute a trade at current market prices View all available markets #### Get Last Price Path: /pro/api-reference/get-last-price Description: Get the latest trade price and 24h change for a trading pair ## Overview Get the most recent trade price for a specific trading pair, along with the 24-hour price change. This endpoint does not require authentication. ## Path Parameters Trading pair symbol **Example:** `USDT-NGN` **Allowed values:** `USDT-NGN`, `USDC-NGN`, `USD-NGN` ## Request Examples ```bash cURL curl --request GET \ --url https://api.pro.daya.co/public/v1/last-price/USDT-NGN ``` ```javascript JavaScript const response = await fetch( 'https://api.pro.daya.co/public/v1/last-price/USDT-NGN' ); const data = await response.json(); console.log('Price:', data.data.price); console.log('24h change:', data.data.change_24h); ``` ```python Python import requests response = requests.get('https://api.pro.daya.co/public/v1/last-price/USDT-NGN') data = response.json() print(f"Price: {data['data']['price']}") print(f"24h change: {data['data']['change_24h']}") ``` ```go Go package main import ( "encoding/json" "fmt" "net/http" ) func main() { resp, err := http.Get("https://api.pro.daya.co/public/v1/last-price/USDT-NGN") if err != nil { panic(err) } defer resp.Body.Close() var result map[string]interface{} json.NewDecoder(resp.Body).Decode(&result) data := result["data"].(map[string]interface{}) fmt.Println("Price:", data["price"]) fmt.Println("24h change:", data["change_24h"]) } ``` ## Response Indicates if the request was successful Human-readable response message Last price data Trading pair symbol **Example:** `USD-NGN` Most recent trade price **Example:** `1545.50` Price change over the last 24 hours as a percentage **Example:** `0.35` ### Success Response ```json 200 OK { "success": true, "message": "Last price retrieved successfully", "data": { "symbol": "USD-NGN", "price": 1545.50, "change_24h": 0.35 } } ``` ## Error Responses ```json 404 Not Found { "success": false, "message": "Market not found", "error": { "code": "SYMBOL_NOT_FOUND", "message": "Market not found" } } ``` ## Notes - The price reflects the most recent executed trade on the market. - The `change_24h` value is calculated by comparing the latest trade price to the most recent trade price from 24 hours ago. A positive value indicates the price has increased. - If no trades have occurred, the price and change values may be `0`. ## Rate Limits - **100 requests per minute** per IP address - No authentication required ## Next Steps View market depth for a trading pair View recent trades for a market #### List Market Trades Path: /pro/api-reference/list-market-trades Description: List recent trades for a trading pair ## Overview Get a list of recent trades executed on a specific market. This is public market data and does not require authentication. Unlike the [List Trades](/pro/api-reference/list-trades) endpoint, this returns anonymous market-wide trades rather than your personal trade history. ## Path Parameters Trading pair symbol **Example:** `USDT-NGN` **Allowed values:** `USDT-NGN`, `USDC-NGN`, `USD-NGN` ## Query Parameters Maximum number of trades to return **Default:** `50` **Range:** `1` to `100` ## Request Examples ```bash cURL curl --request GET \ --url 'https://api.pro.daya.co/public/v1/market-trades/USDT-NGN?limit=20' ``` ```javascript JavaScript const response = await fetch( 'https://api.pro.daya.co/public/v1/market-trades/USDT-NGN?limit=20' ); const data = await response.json(); console.log('Recent trades:', data.data); ``` ```python Python import requests response = requests.get( 'https://api.pro.daya.co/public/v1/market-trades/USDT-NGN', params={'limit': 20} ) data = response.json() print(f"Recent trades: {data['data']}") ``` ```go Go package main import ( "encoding/json" "fmt" "net/http" ) func main() { resp, err := http.Get("https://api.pro.daya.co/public/v1/market-trades/USDT-NGN?limit=20") if err != nil { panic(err) } defer resp.Body.Close() var result map[string]interface{} json.NewDecoder(resp.Body).Decode(&result) fmt.Println("Recent trades:", result["data"]) } ``` ## Response Indicates if the request was successful Human-readable response message Array of market trade objects Unique trade identifier (UUID) Trading pair symbol **Example:** `USD-NGN` Execution price **Example:** `1545.50` Trade quantity in base asset **Example:** `100.00` Taker side of the trade **Values:** `buy`, `sell` ISO 8601 trade execution timestamp ### Success Response ```json 200 OK { "success": true, "message": "Market trades retrieved successfully", "data": [ { "id": "660e8400-e29b-41d4-a716-446655440000", "symbol": "USD-NGN", "price": "1545.50", "quantity": "100.00", "side": "buy", "created_at": "2024-01-15T10:35:01Z" }, { "id": "660e8400-e29b-41d4-a716-446655440001", "symbol": "USD-NGN", "price": "1545.00", "quantity": "50.00", "side": "sell", "created_at": "2024-01-15T10:34:55Z" } ] } ``` ## Error Responses ```json 404 Not Found { "success": false, "message": "Market not found", "error": { "code": "SYMBOL_NOT_FOUND", "message": "Market not found" } } ``` ## Notes - Trades are returned in reverse chronological order (most recent first). - The `side` field indicates the taker side of the trade (the order that triggered the match). - This endpoint returns anonymous market data. For your personal trade history with fee details, use the authenticated [List Trades](/pro/api-reference/list-trades) endpoint. ## Rate Limits - **100 requests per minute** per IP address - No authentication required ## Next Steps Get the latest price for a market View market depth for a trading pair ### Account #### Get Account Path: /pro/api-reference/get-account Description: Get account information for the authenticated user ## Overview Retrieve account information for the authenticated user, including account status and profile details. This endpoint requires authentication with an API key that has **Read** scope. ## Authentication Your API key with Read scope ``` X-Api-Key: daya_sk_YOUR_API_KEY ``` ## Request Examples ```bash cURL curl --request GET \ --url https://api.pro.daya.co/public/v1/account \ --header 'X-Api-Key: daya_sk_YOUR_API_KEY' ``` ```javascript JavaScript const response = await fetch('https://api.pro.daya.co/public/v1/account', { headers: { 'X-Api-Key': 'daya_sk_YOUR_API_KEY' } }); const result = await response.json(); console.log('Account:', result.data); ``` ```python Python import requests headers = {'X-Api-Key': 'daya_sk_YOUR_API_KEY'} response = requests.get( 'https://api.pro.daya.co/public/v1/account', headers=headers ) result = response.json() print(f"Account: {result['data']}") ``` ```go Go package main import ( "encoding/json" "fmt" "net/http" ) func main() { client := &http.Client{} req, _ := http.NewRequest("GET", "https://api.pro.daya.co/public/v1/account", nil) req.Header.Add("X-Api-Key", "daya_sk_YOUR_API_KEY") resp, err := client.Do(req) if err != nil { panic(err) } defer resp.Body.Close() var data map[string]interface{} json.NewDecoder(resp.Body).Decode(&data) fmt.Println(data["data"]) } ``` ## Response Indicates if the request was successful Human-readable response message Account information User ID (UUID) **Example:** `770e8400-e29b-41d4-a716-446655440000` Account email address **Example:** `user@example.com` Account display name **Example:** `John Doe` Account status **Values:** `active`, `suspended`, `frozen` Whether the account is enabled for trading ISO 8601 account creation timestamp ISO 8601 timestamp of the response ### Success Response ```json 200 OK { "success": true, "message": "Account retrieved successfully", "data": { "id": "770e8400-e29b-41d4-a716-446655440000", "email": "user@example.com", "name": "John Doe", "status": "active", "trading_enabled": true, "created_at": "2024-01-01T00:00:00Z" }, "timestamp": "2024-01-15T10:30:00Z" } ``` ## Error Responses ### 401 Unauthorized ```json 401 Unauthorized { "success": false, "message": "Unauthorized", "error": { "code": "API_KEY_INVALID", "message": "Invalid or missing API key" }, "timestamp": "2024-01-15T10:30:00Z" } ``` **Common causes:** - Missing `X-Api-Key` header - Invalid API key format - API key has been revoked ### 403 Forbidden ```json 403 Forbidden { "success": false, "message": "Forbidden", "error": { "code": "API_KEY_INVALID_SCOPE", "message": "Insufficient scope for this operation" }, "timestamp": "2024-01-15T10:30:00Z" } ``` **Common causes:** - API key does not have Read scope ## Rate Limits - **100 requests per minute** per API key - Requires Read scope ## Next Steps View your account balances Start trading #### Get Balances Path: /pro/api-reference/get-balances Description: Retrieve account balances for all currencies ## Overview Get the current balance for all currencies in your Pro account. This endpoint requires authentication with an API key that has **Read** scope. **Unified Balance:** USDT, USDC, and USD are all mapped to a single unified USD balance. When you trade with any of these symbols (USDT-NGN, USDC-NGN, or USD-NGN), they all use the same USD balance. ## Authentication Your API key with Read scope ``` X-Api-Key: daya_sk_YOUR_API_KEY ``` ## Request Examples ```bash cURL curl --request GET \ --url https://api.pro.daya.co/public/v1/balances \ --header 'X-Api-Key: daya_sk_YOUR_API_KEY' ``` ```javascript JavaScript const response = await fetch('https://api.pro.daya.co/public/v1/balances', { headers: { 'X-Api-Key': 'daya_sk_YOUR_API_KEY' } }); const result = await response.json(); console.log(result.data.balances); ``` ```python Python import requests headers = { 'X-Api-Key': 'daya_sk_YOUR_API_KEY' } response = requests.get( 'https://api.pro.daya.co/public/v1/balances', headers=headers ) result = response.json() print(result['data']['balances']) ``` ```go Go package main import ( "encoding/json" "fmt" "net/http" ) func main() { client := &http.Client{} req, _ := http.NewRequest("GET", "https://api.pro.daya.co/public/v1/balances", nil) req.Header.Add("X-Api-Key", "daya_sk_YOUR_API_KEY") resp, err := client.Do(req) if err != nil { panic(err) } defer resp.Body.Close() var data map[string]interface{} json.NewDecoder(resp.Body).Decode(&data) fmt.Println(data["data"]) } ``` ## Response Indicates if the request was successful Human-readable response message Balances wrapper object Array of balance objects Currency code **Example:** `USD`, `NGN` Total balance (available + held) **Example:** `1500.00` Available balance (can be used for trading) **Example:** `1000.00` Held balance (locked in open orders) **Example:** `500.00` Outstanding credit debt **Example:** `0.00` Credit held in open orders **Example:** `0.00` Available credit to use **Example:** `0.00` Total credit limit **Example:** `0.00` Current USD exchange rate for this currency **Example:** `1.00` ISO 8601 timestamp of the response ### Success Response ```json 200 OK { "success": true, "message": "Balances retrieved successfully", "data": { "balances": [ { "currency": "USD", "total_balance": "1500.00", "available_balance": "1000.00", "held_balance": "500.00", "used_credit": "0.00", "held_credit": "0.00", "available_credit": "0.00", "credit_limit": "0.00", "usd_rate": "1.00" }, { "currency": "NGN", "total_balance": "1545000.00", "available_balance": "1545000.00", "held_balance": "0.00", "used_credit": "0.00", "held_credit": "0.00", "available_credit": "0.00", "credit_limit": "0.00", "usd_rate": "0.00065" } ] }, "timestamp": "2024-01-15T10:30:00Z" } ``` ## Error Responses ### 401 Unauthorized ```json 401 Unauthorized { "success": false, "message": "Unauthorized", "error": { "code": "API_KEY_INVALID", "message": "Invalid or missing API key" }, "timestamp": "2024-01-15T10:30:00Z" } ``` **Common causes:** - Missing `X-Api-Key` header - Invalid API key format - API key has been revoked ### 403 Forbidden ```json 403 Forbidden { "success": false, "message": "Forbidden", "error": { "code": "API_KEY_INVALID_SCOPE", "message": "Insufficient scope for this operation" }, "timestamp": "2024-01-15T10:30:00Z" } ``` **Common causes:** - API key does not have Read scope - User account is suspended ## Rate Limits - **100 requests per minute** per API key - Requires Read scope ## Next Steps Use your balance to place trades View your open and historical orders #### Get Balance Path: /pro/api-reference/get-balance Description: Retrieve account balance for a specific currency ## Overview Get the current balance for a specific currency in your Pro account. This endpoint requires authentication with an API key that has **Read** scope. **Unified Balance:** USDT, USDC, and USD are all mapped to a single unified USD balance. Use `USD` to retrieve your dollar balance. ## Authentication Your API key with Read scope ``` X-Api-Key: daya_sk_YOUR_API_KEY ``` ## Path Parameters Currency code to retrieve balance for **Example:** `USD`, `NGN` ## Request Examples ```bash cURL curl --request GET \ --url https://api.pro.daya.co/public/v1/balances/USD \ --header 'X-Api-Key: daya_sk_YOUR_API_KEY' ``` ```javascript JavaScript const currency = 'USD'; const response = await fetch( `https://api.pro.daya.co/public/v1/balances/${currency}`, { headers: { 'X-Api-Key': 'daya_sk_YOUR_API_KEY' } } ); const result = await response.json(); console.log(result.data); ``` ```python Python import requests currency = 'USD' headers = { 'X-Api-Key': 'daya_sk_YOUR_API_KEY' } response = requests.get( f'https://api.pro.daya.co/public/v1/balances/{currency}', headers=headers ) result = response.json() print(result['data']) ``` ```go Go package main import ( "encoding/json" "fmt" "net/http" ) func main() { currency := "USD" url := fmt.Sprintf("https://api.pro.daya.co/public/v1/balances/%s", currency) client := &http.Client{} req, _ := http.NewRequest("GET", url, nil) req.Header.Add("X-Api-Key", "daya_sk_YOUR_API_KEY") resp, err := client.Do(req) if err != nil { panic(err) } defer resp.Body.Close() var data map[string]interface{} json.NewDecoder(resp.Body).Decode(&data) fmt.Println(data["data"]) } ``` ## Response Indicates if the request was successful Human-readable response message Balance object Currency code **Example:** `USD` Total balance (available + held) **Example:** `1500.00` Available balance (can be used for trading) **Example:** `1000.00` Held balance (locked in open orders) **Example:** `500.00` Outstanding credit debt **Example:** `0.00` Credit held in open orders **Example:** `0.00` Available credit to use **Example:** `0.00` Total credit limit **Example:** `0.00` Current USD exchange rate for this currency **Example:** `1.00` ISO 8601 timestamp of the response ### Success Response ```json 200 OK { "success": true, "message": "Balance retrieved successfully", "data": { "currency": "USD", "total_balance": "1500.00", "available_balance": "1000.00", "held_balance": "500.00", "used_credit": "0.00", "held_credit": "0.00", "available_credit": "0.00", "credit_limit": "0.00", "usd_rate": "1.00" }, "timestamp": "2024-01-15T10:30:00Z" } ``` ## Error Responses ### 401 Unauthorized ```json 401 Unauthorized { "success": false, "message": "Unauthorized", "error": { "code": "API_KEY_INVALID", "message": "Invalid or missing API key" }, "timestamp": "2024-01-15T10:30:00Z" } ``` **Common causes:** - Missing `X-Api-Key` header - Invalid API key format - API key has been revoked ### 403 Forbidden ```json 403 Forbidden { "success": false, "message": "Forbidden", "error": { "code": "API_KEY_INVALID_SCOPE", "message": "Insufficient scope for this operation" }, "timestamp": "2024-01-15T10:30:00Z" } ``` **Common causes:** - API key does not have Read scope - User account is suspended ### 404 Not Found ```json 404 Not Found { "success": false, "message": "Currency not found", "error": { "code": "NOT_FOUND", "message": "The specified currency does not exist" }, "timestamp": "2024-01-15T10:30:00Z" } ``` **Common causes:** - Invalid currency code - Currency not supported on the platform ## Rate Limits - **100 requests per minute** per API key - Requires Read scope ## Next Steps Get all currency balances at once Use your balance to place trades ### Orders #### Place Order Path: /pro/api-reference/place-order Description: Place a new order on the Pro platform ## Overview Place a new limit or market order on the Pro platform. This endpoint requires authentication with an API key that has **Trade** scope. ## Authentication Your API key with Trade scope ``` X-Api-Key: daya_sk_YOUR_API_KEY ``` ## Request Body Trading pair symbol. All symbols map to a unified USD balance. **Example:** `USDT-NGN` (recommended) **Allowed values:** `USDT-NGN`, `USDC-NGN`, `USD-NGN` Order side **Allowed values:** `buy`, `sell` Order type **Allowed values:** `limit`, `market` Order price (required for limit orders) **Example:** `1545.00` Required when `type` is `limit`. Not allowed for market orders. Order quantity in base asset **Example:** `100.00` ## Request Examples ```json Limit Order { "symbol": "USDT-NGN", "side": "buy", "type": "limit", "price": "1545.00", "quantity": "100.00" } ``` ```json Market Order { "symbol": "USDT-NGN", "side": "buy", "type": "market", "quantity": "100.00" } ``` ```bash cURL - Limit Order curl --request POST \ --url https://api.pro.daya.co/public/v1/orders \ --header 'X-Api-Key: daya_sk_YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "symbol": "USDT-NGN", "side": "buy", "type": "limit", "price": "1545.00", "quantity": "100.00" }' ``` ```javascript JavaScript const response = await fetch('https://api.pro.daya.co/public/v1/orders', { method: 'POST', headers: { 'X-Api-Key': 'daya_sk_YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ symbol: 'USDT-NGN', side: 'buy', type: 'limit', price: '1545.00', quantity: '100.00' }) }); const order = await response.json(); console.log('Order placed:', order.data.order_id); ``` ```python Python import requests headers = { 'X-Api-Key': 'daya_sk_YOUR_API_KEY', 'Content-Type': 'application/json' } data = { 'symbol': 'USDT-NGN', 'side': 'buy', 'type': 'limit', 'price': '1545.00', 'quantity': '100.00' } response = requests.post( 'https://api.pro.daya.co/public/v1/orders', headers=headers, json=data ) order = response.json() print(f"Order placed: {order['data']['order_id']}") ``` ```go Go package main import ( "bytes" "encoding/json" "fmt" "net/http" ) func main() { order := map[string]string{ "symbol": "USDT-NGN", "side": "buy", "type": "limit", "price": "1545.00", "quantity": "100.00", } body, _ := json.Marshal(order) req, _ := http.NewRequest("POST", "https://api.pro.daya.co/public/v1/orders", bytes.NewBuffer(body)) req.Header.Set("X-Api-Key", "daya_sk_YOUR_API_KEY") req.Header.Set("Content-Type", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { panic(err) } defer resp.Body.Close() var result map[string]interface{} json.NewDecoder(resp.Body).Decode(&result) data := result["data"].(map[string]interface{}) fmt.Println("Order placed:", data["order_id"]) } ``` ## Response Indicates if the request was successful Human-readable response message Order placement response Unique order identifier (UUID) **Example:** `550e8400-e29b-41d4-a716-446655440000` Initial order status **Values:** `pending_settlement`, `new`, `open` Order side: `buy` or `sell` Order type: `limit` or `market` Trading pair symbol Requested order quantity Quantity filled so far Remaining quantity to fill Average fill price (empty if no fills yet) Total fees charged (empty if no fills yet) ISO 8601 creation timestamp ISO 8601 last update timestamp ISO 8601 timestamp of the response ### Success Response ```json 201 Created - Limit Order { "success": true, "message": "Order placed successfully", "data": { "order_id": "550e8400-e29b-41d4-a716-446655440000", "status": "pending_settlement", "side": "buy", "type": "limit", "symbol": "USD-NGN", "requested_quantity": "100.00000000", "filled_quantity": "0.00000000", "remaining_quantity": "100.00000000", "avg_fill_price": "", "total_fee_charged": "", "created_at": "2024-01-15T10:35:00Z", "updated_at": "2024-01-15T10:35:00Z" }, "timestamp": "2024-01-15T10:35:00Z" } ``` ```json 201 Created - Market Order (Immediate Fill) { "success": true, "message": "Order placed successfully", "data": { "order_id": "550e8400-e29b-41d4-a716-446655440001", "status": "filled", "side": "buy", "type": "market", "symbol": "USD-NGN", "requested_quantity": "100.00000000", "filled_quantity": "100.00000000", "remaining_quantity": "0.00000000", "avg_fill_price": "1545.50", "total_fee_charged": "0.15455000", "created_at": "2024-01-15T10:35:00Z", "updated_at": "2024-01-15T10:35:01Z" }, "timestamp": "2024-01-15T10:35:01Z" } ``` ## Error Responses ```json 400 Bad Request - Insufficient balance { "success": false, "message": "Insufficient balance", "error": { "code": "INSUFFICIENT_BALANCE", "message": "Insufficient balance to place this order" }, "timestamp": "2024-01-15T10:35:00Z" } ``` ```json 400 Bad Request - Invalid order { "success": false, "message": "Invalid order", "error": { "code": "INVALID_ORDER", "message": "Price is required for limit orders" }, "timestamp": "2024-01-15T10:35:00Z" } ``` ```json 403 Forbidden - Insufficient scope { "success": false, "message": "Forbidden", "error": { "code": "API_KEY_INVALID_SCOPE", "message": "API key does not have the required Trade scope" }, "timestamp": "2024-01-15T10:35:00Z" } ``` ```json 404 Not Found - Invalid symbol { "success": false, "message": "Symbol not found", "error": { "code": "SYMBOL_NOT_FOUND", "message": "The specified trading pair does not exist" }, "timestamp": "2024-01-15T10:35:00Z" } ``` ## Order Types | Type | Description | Required Fields | |------|-------------|-----------------| | **limit** | Execute at specified price or better | `price`, `quantity` | | **market** | Execute immediately at best available price | `quantity` | ### Limit Orders Limit orders are placed on the orderbook and wait for a matching order: - **Buy limit**: Executes at the specified price or lower - **Sell limit**: Executes at the specified price or higher - Remains open until filled or cancelled ### Market Orders Market orders execute immediately at the best available price: - May experience slippage if orderbook depth is low - Partially fills if insufficient liquidity ## Order Status Flow ``` pending_settlement → new → open → partially_filled → filled ↓ ↓ cancelled cancelled ↓ failed ``` | Status | Description | |--------|-------------| | `pending_settlement` | Order is awaiting settlement | | `new` | Order has been created | | `open` | Order is active on the orderbook | | `partially_filled` | Order is partially executed | | `filled` | Order is completely executed | | `cancelled` | Order was cancelled by user | | `rejected` | Order was rejected by the system | | `failed` | Order failed to execute | ## Rate Limits - **100 requests per minute** per API key ## Next Steps View your open orders Cancel an open order #### Get Order Quote Path: /pro/api-reference/get-order-quote Description: Get a price quote for an order without placing it ## Overview Get an estimated price quote for an order without actually placing it. This is useful for previewing the expected execution price and fees before committing to a trade. This endpoint requires authentication with an API key that has **Trade** scope. ## Authentication Your API key with Trade scope ``` X-Api-Key: daya_sk_YOUR_API_KEY ``` ## Request Body Trading pair symbol **Example:** `USDT-NGN` **Allowed values:** `USDT-NGN`, `USDC-NGN`, `USD-NGN` Order side **Allowed values:** `buy`, `sell` Order type **Allowed values:** `limit`, `market` Order price (required for limit orders) **Example:** `1545.00` Required when `type` is `limit`. Not allowed for market orders. Order quantity in base asset **Example:** `100.00` ## Request Examples ```bash cURL curl --request POST \ --url https://api.pro.daya.co/public/v1/orders/quote \ --header 'X-Api-Key: daya_sk_YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "symbol": "USDT-NGN", "side": "buy", "type": "market", "quantity": "100.00" }' ``` ```javascript JavaScript const response = await fetch('https://api.pro.daya.co/public/v1/orders/quote', { method: 'POST', headers: { 'X-Api-Key': 'daya_sk_YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ symbol: 'USDT-NGN', side: 'buy', type: 'market', quantity: '100.00' }) }); const quote = await response.json(); console.log('Estimated price:', quote.data.estimated_price); console.log('Estimated fee:', quote.data.estimated_fee); ``` ```python Python import requests headers = { 'X-Api-Key': 'daya_sk_YOUR_API_KEY', 'Content-Type': 'application/json' } data = { 'symbol': 'USDT-NGN', 'side': 'buy', 'type': 'market', 'quantity': '100.00' } response = requests.post( 'https://api.pro.daya.co/public/v1/orders/quote', headers=headers, json=data ) quote = response.json() print(f"Estimated price: {quote['data']['estimated_price']}") print(f"Estimated fee: {quote['data']['estimated_fee']}") ``` ```go Go package main import ( "bytes" "encoding/json" "fmt" "net/http" ) func main() { order := map[string]string{ "symbol": "USDT-NGN", "side": "buy", "type": "market", "quantity": "100.00", } body, _ := json.Marshal(order) req, _ := http.NewRequest("POST", "https://api.pro.daya.co/public/v1/orders/quote", bytes.NewBuffer(body)) req.Header.Set("X-Api-Key", "daya_sk_YOUR_API_KEY") req.Header.Set("Content-Type", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { panic(err) } defer resp.Body.Close() var result map[string]interface{} json.NewDecoder(resp.Body).Decode(&result) data := result["data"].(map[string]interface{}) fmt.Println("Estimated price:", data["estimated_price"]) fmt.Println("Estimated fee:", data["estimated_fee"]) } ``` ## Response Indicates if the request was successful Human-readable response message Order quote details Trading pair symbol **Example:** `USD-NGN` Order side: `buy` or `sell` Order type: `limit` or `market` Order quantity in base asset **Example:** `100.00` Estimated execution price based on current orderbook **Example:** `1545.50` Estimated total value in quote currency **Example:** `154550.00` Estimated fee for the trade **Example:** `0.15455000` ISO 8601 timestamp of the response ### Success Response ```json 200 OK - Market Order Quote { "success": true, "message": "Order quote retrieved successfully", "data": { "symbol": "USD-NGN", "side": "buy", "type": "market", "quantity": "100.00", "estimated_price": "1545.50", "estimated_total": "154550.00", "estimated_fee": "0.15455000" }, "timestamp": "2024-01-15T10:35:00Z" } ``` ```json 200 OK - Limit Order Quote { "success": true, "message": "Order quote retrieved successfully", "data": { "symbol": "USD-NGN", "side": "buy", "type": "limit", "quantity": "100.00", "estimated_price": "1545.00", "estimated_total": "154500.00", "estimated_fee": "0.15450000" }, "timestamp": "2024-01-15T10:35:00Z" } ``` ## Error Responses ```json 400 Bad Request { "success": false, "message": "Validation error", "error": { "code": "VALIDATION_ERROR", "message": "Quantity is required" }, "timestamp": "2024-01-15T10:35:00Z" } ``` ```json 403 Forbidden - Account frozen { "success": false, "message": "Forbidden", "error": { "code": "ACCOUNT_FROZEN", "message": "Trading account is frozen" }, "timestamp": "2024-01-15T10:35:00Z" } ``` ```json 404 Not Found { "success": false, "message": "Symbol not found", "error": { "code": "SYMBOL_NOT_FOUND", "message": "The specified trading pair does not exist" }, "timestamp": "2024-01-15T10:35:00Z" } ``` ## Notes - Quotes are estimates based on the current state of the orderbook and may differ from the actual execution price. - The quote does not reserve liquidity or lock funds. - For market orders, the estimated price is the volume-weighted average price across the available orderbook depth. ## Rate Limits - **100 requests per minute** per API key ## Next Steps Place the order after reviewing the quote View the full orderbook depth #### List Active Orders Path: /pro/api-reference/list-orders Description: List active orders for the authenticated user ## Overview Retrieve a list of active orders for the authenticated user. Active orders include orders that are still being processed or waiting to be filled. For historical/completed orders, use the [Order History](/pro/api-reference/get-order-history) endpoint. **Active order statuses:** `pending_settlement`, `new`, `open`, `partially_filled` ## Authentication Your API key with Read or Trade scope ``` X-Api-Key: daya_sk_YOUR_API_KEY ``` ## Query Parameters Filter by trading pair symbol **Example:** `USDT-NGN` Filter by active order status **Allowed values:** `pending_settlement`, `new`, `open`, `partially_filled` ## Request Examples ```bash cURL curl --request GET \ --url 'https://api.pro.daya.co/public/v1/orders?symbol=USDT-NGN&status=open' \ --header 'X-Api-Key: daya_sk_YOUR_API_KEY' ``` ```javascript JavaScript const response = await fetch( 'https://api.pro.daya.co/public/v1/orders?symbol=USDT-NGN&status=open', { headers: { 'X-Api-Key': 'daya_sk_YOUR_API_KEY' } } ); const data = await response.json(); console.log('Active orders:', data.data); ``` ```python Python import requests headers = {'X-Api-Key': 'daya_sk_YOUR_API_KEY'} response = requests.get( 'https://api.pro.daya.co/public/v1/orders', headers=headers, params={'symbol': 'USDT-NGN', 'status': 'open'} ) orders = response.json() print(f"Active orders: {orders['data']}") ``` ```go Go package main import ( "encoding/json" "fmt" "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://api.pro.daya.co/public/v1/orders?symbol=USDT-NGN&status=open", nil) req.Header.Set("X-Api-Key", "daya_sk_YOUR_API_KEY") client := &http.Client{} resp, err := client.Do(req) if err != nil { panic(err) } defer resp.Body.Close() var result map[string]interface{} json.NewDecoder(resp.Body).Decode(&result) fmt.Println("Active orders:", result["data"]) } ``` ## Response Indicates if the request was successful Human-readable response message Array of active order objects Unique order identifier (UUID) User identifier Trading pair symbol Order side: `buy` or `sell` Order type: `limit` or `market` Order status: `pending_settlement`, `new`, `open`, `partially_filled` Order price (for limit orders) Original order quantity Quantity that has been filled Remaining quantity to fill Average execution price Total order value in quote currency Value of filled portion Base currency code (e.g. `USD`) Quote currency code (e.g. `NGN`) ISO 8601 creation timestamp ISO 8601 last update timestamp ISO 8601 timestamp of the response ### Success Response ```json 200 OK { "success": true, "message": "Orders retrieved successfully", "data": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "user_id": "user_abc123", "symbol": "USD-NGN", "side": "buy", "type": "limit", "status": "open", "price": "1545.00", "quantity": "100.00000000", "filled_quantity": "0.00000000", "remaining_quantity": "100.00000000", "executed_price": "", "total_value": "154500.00", "filled_value": "0.00", "base_currency": "USD", "quote_currency": "NGN", "created_at": "2024-01-15T10:35:00Z", "updated_at": "2024-01-15T10:35:00Z" }, { "id": "550e8400-e29b-41d4-a716-446655440001", "user_id": "user_abc123", "symbol": "USD-NGN", "side": "sell", "type": "limit", "status": "partially_filled", "price": "1548.00", "quantity": "200.00000000", "filled_quantity": "75.00000000", "remaining_quantity": "125.00000000", "executed_price": "1548.00", "total_value": "309600.00", "filled_value": "116100.00", "base_currency": "USD", "quote_currency": "NGN", "created_at": "2024-01-15T09:20:00Z", "updated_at": "2024-01-15T10:15:00Z" } ], "timestamp": "2024-01-15T10:35:00Z" } ``` ## Error Responses ```json 401 Unauthorized { "success": false, "message": "Unauthorized", "error": { "code": "API_KEY_INVALID", "message": "The provided API key is invalid" }, "timestamp": "2024-01-15T10:35:00Z" } ``` ```json 400 Bad Request - Invalid status { "success": false, "message": "Validation error", "error": { "code": "VALIDATION_ERROR", "message": "Invalid status value" }, "timestamp": "2024-01-15T10:35:00Z" } ``` ## Rate Limits - **100 requests per minute** per API key ## Next Steps Get historical/completed orders Get details of a specific order Cancel an open order Place a new order #### Order History Path: /pro/api-reference/get-order-history Description: Get historical orders with filtering and pagination ## Overview Retrieve order history for the authenticated user. This endpoint returns orders regardless of status, with optional filtering and pagination. For active orders, use the [List Active Orders](/pro/api-reference/list-orders) endpoint. ## Authentication Your API key with Read or Trade scope ``` X-Api-Key: daya_sk_YOUR_API_KEY ``` ## Query Parameters Filter by order status **Allowed values:** `filled`, `cancelled`, `rejected`, `partially_filled` Filter by trading pair symbol **Example:** `USDT-NGN` Filter by order side **Allowed values:** `buy`, `sell` Filter by order type **Allowed values:** `market`, `limit` Filter orders created after this time (RFC3339 format) **Example:** `2024-01-01T00:00:00Z` Filter orders created before this time (RFC3339 format) **Example:** `2024-12-31T23:59:59Z` Maximum number of orders to return **Default:** `50` **Range:** `1` to `100` Number of orders to skip for pagination **Default:** `0` ## Request Examples ```bash cURL curl --request GET \ --url 'https://api.pro.daya.co/public/v1/orders/history?symbol=USDT-NGN&status=filled&limit=20' \ --header 'X-Api-Key: daya_sk_YOUR_API_KEY' ``` ```javascript JavaScript const response = await fetch( 'https://api.pro.daya.co/public/v1/orders/history?symbol=USDT-NGN&status=filled&limit=20', { headers: { 'X-Api-Key': 'daya_sk_YOUR_API_KEY' } } ); const data = await response.json(); console.log('Order history:', data.data.orders); ``` ```python Python import requests headers = {'X-Api-Key': 'daya_sk_YOUR_API_KEY'} response = requests.get( 'https://api.pro.daya.co/public/v1/orders/history', headers=headers, params={ 'symbol': 'USDT-NGN', 'status': 'filled', 'limit': 20 } ) result = response.json() print(f"Order history: {result['data']['orders']}") ``` ```go Go package main import ( "encoding/json" "fmt" "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://api.pro.daya.co/public/v1/orders/history?symbol=USDT-NGN&status=filled&limit=20", nil) req.Header.Set("X-Api-Key", "daya_sk_YOUR_API_KEY") client := &http.Client{} resp, err := client.Do(req) if err != nil { panic(err) } defer resp.Body.Close() var result map[string]interface{} json.NewDecoder(resp.Body).Decode(&result) fmt.Println("Order history:", result["data"]) } ``` ## Response Indicates if the request was successful Human-readable response message Paginated order data Array of order objects Unique order identifier (UUID) User identifier Trading pair symbol Order side: `buy` or `sell` Order type: `limit` or `market` Order status: `filled`, `cancelled`, `rejected`, `partially_filled` Order price (for limit orders) Original order quantity Quantity that has been filled Remaining quantity (0 for filled orders) Average execution price Total order value in quote currency Value of filled portion Base currency code (e.g. `USD`) Quote currency code (e.g. `NGN`) ISO 8601 creation timestamp ISO 8601 last update timestamp Total number of orders matching the filter Number of orders returned Offset used for pagination Whether there are more results beyond the current page ISO 8601 timestamp of the response ### Success Response ```json 200 OK { "success": true, "message": "Order history retrieved successfully", "data": { "orders": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "user_id": "user_abc123", "symbol": "USD-NGN", "side": "buy", "type": "limit", "status": "filled", "price": "1545.00", "quantity": "100.00000000", "filled_quantity": "100.00000000", "remaining_quantity": "0.00000000", "executed_price": "1545.00", "total_value": "154500.00", "filled_value": "154500.00", "base_currency": "USD", "quote_currency": "NGN", "created_at": "2024-01-15T10:35:00Z", "updated_at": "2024-01-15T10:35:05Z" }, { "id": "550e8400-e29b-41d4-a716-446655440001", "user_id": "user_abc123", "symbol": "USD-NGN", "side": "sell", "type": "market", "status": "filled", "price": "", "quantity": "50.00000000", "filled_quantity": "50.00000000", "remaining_quantity": "0.00000000", "executed_price": "1546.50", "total_value": "77325.00", "filled_value": "77325.00", "base_currency": "USD", "quote_currency": "NGN", "created_at": "2024-01-14T15:20:00Z", "updated_at": "2024-01-14T15:20:01Z" } ], "total_count": 156, "limit": 20, "offset": 0, "has_more": true }, "timestamp": "2024-01-15T10:35:00Z" } ``` ## Error Responses ```json 401 Unauthorized { "success": false, "message": "Unauthorized", "error": { "code": "API_KEY_INVALID", "message": "The provided API key is invalid" }, "timestamp": "2024-01-15T10:35:00Z" } ``` ```json 400 Bad Request - Invalid filter { "success": false, "message": "Validation error", "error": { "code": "VALIDATION_ERROR", "message": "Invalid start_time format. Use RFC3339 format." }, "timestamp": "2024-01-15T10:35:00Z" } ``` ## Pagination Use `limit` and `offset` for pagination. The response includes `total_count` and `has_more` to help navigate pages. ## Rate Limits - **100 requests per minute** per API key ## Next Steps View your active orders View individual trade executions Get details of a specific order Place a new order #### Get Order Path: /pro/api-reference/get-order Description: Get details of a specific order ## Overview Retrieve details for a specific order by its ID. ## Authentication Your API key with Read or Trade scope ``` X-Api-Key: daya_sk_YOUR_API_KEY ``` ## Path Parameters The unique order identifier (UUID) **Example:** `550e8400-e29b-41d4-a716-446655440000` ## Request Examples ```bash cURL curl --request GET \ --url https://api.pro.daya.co/public/v1/orders/550e8400-e29b-41d4-a716-446655440000 \ --header 'X-Api-Key: daya_sk_YOUR_API_KEY' ``` ```javascript JavaScript const orderId = '550e8400-e29b-41d4-a716-446655440000'; const response = await fetch( `https://api.pro.daya.co/public/v1/orders/${orderId}`, { headers: { 'X-Api-Key': 'daya_sk_YOUR_API_KEY' } } ); const order = await response.json(); console.log('Order details:', order.data); ``` ```python Python import requests order_id = '550e8400-e29b-41d4-a716-446655440000' headers = {'X-Api-Key': 'daya_sk_YOUR_API_KEY'} response = requests.get( f'https://api.pro.daya.co/public/v1/orders/{order_id}', headers=headers ) order = response.json() print(f"Order details: {order['data']}") ``` ```go Go package main import ( "encoding/json" "fmt" "net/http" ) func main() { orderID := "550e8400-e29b-41d4-a716-446655440000" url := fmt.Sprintf("https://api.pro.daya.co/public/v1/orders/%s", orderID) req, _ := http.NewRequest("GET", url, nil) req.Header.Set("X-Api-Key", "daya_sk_YOUR_API_KEY") client := &http.Client{} resp, err := client.Do(req) if err != nil { panic(err) } defer resp.Body.Close() var result map[string]interface{} json.NewDecoder(resp.Body).Decode(&result) fmt.Println("Order details:", result["data"]) } ``` ## Response Indicates if the request was successful Human-readable response message Order details Unique order identifier (UUID) User identifier Trading pair symbol Order side: `buy` or `sell` Order type: `limit` or `market` Order status **Values:** `pending_settlement`, `new`, `open`, `partially_filled`, `filled`, `cancelled`, `rejected`, `failed` Order price (for limit orders) Original order quantity Quantity that has been filled Remaining quantity to fill Average execution price Total order value in quote currency Value of filled portion Base currency code (e.g. `USD`) Quote currency code (e.g. `NGN`) ISO 8601 creation timestamp ISO 8601 last update timestamp ISO 8601 timestamp of the response ### Success Response ```json 200 OK - Open Order { "success": true, "message": "Order retrieved successfully", "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "user_id": "user_abc123", "symbol": "USD-NGN", "side": "buy", "type": "limit", "status": "open", "price": "1545.00", "quantity": "100.00000000", "filled_quantity": "0.00000000", "remaining_quantity": "100.00000000", "executed_price": "", "total_value": "154500.00", "filled_value": "0.00", "base_currency": "USD", "quote_currency": "NGN", "created_at": "2024-01-15T10:35:00Z", "updated_at": "2024-01-15T10:35:00Z" }, "timestamp": "2024-01-15T10:35:00Z" } ``` ```json 200 OK - Filled Order { "success": true, "message": "Order retrieved successfully", "data": { "id": "550e8400-e29b-41d4-a716-446655440001", "user_id": "user_abc123", "symbol": "USD-NGN", "side": "buy", "type": "market", "status": "filled", "price": "", "quantity": "100.00000000", "filled_quantity": "100.00000000", "remaining_quantity": "0.00000000", "executed_price": "1545.50", "total_value": "154550.00", "filled_value": "154550.00", "base_currency": "USD", "quote_currency": "NGN", "created_at": "2024-01-15T10:35:00Z", "updated_at": "2024-01-15T10:35:01Z" }, "timestamp": "2024-01-15T10:35:01Z" } ``` ## Error Responses ```json 401 Unauthorized { "success": false, "message": "Unauthorized", "error": { "code": "API_KEY_INVALID", "message": "The provided API key is invalid" }, "timestamp": "2024-01-15T10:35:00Z" } ``` ```json 404 Not Found { "success": false, "message": "Order not found", "error": { "code": "ORDER_NOT_FOUND", "message": "The specified order does not exist" }, "timestamp": "2024-01-15T10:35:00Z" } ``` ## Rate Limits - **100 requests per minute** per API key ## Next Steps View trade history for this order Cancel an open order #### Replace Order Path: /pro/api-reference/replace-order Description: Atomically cancel and re-place an open order ## Overview Atomically cancel an existing open order and submit a new one in its place. The matching engine performs both steps in one operation, so you never end up holding both. Useful for amending limit orders as the market moves. The replacement is a *new* order, not a mutation — the response returns a fresh order ID. Track that ID for follow-up cancels and webhook events. Requires **Trade** scope. Only orders that are still resting (status `open` or `partially_filled`) can be replaced — terminal orders return 404. ## Authentication Your API key with Trade scope ## Path Parameters Order ID (UUID) to replace ## Request Body Order side for the new order. **Allowed values:** `buy`, `sell` Order type for the new order. **Allowed values:** `limit`, `market` Quantity for the new order. **Example:** `120.00` Limit price (required when `type` is `limit`). **Example:** `1550.00` ## Request Example ```bash cURL curl --request PUT \ --url https://api.pro.daya.co/public/v1/orders/550e8400-e29b-41d4-a716-446655440000 \ --header 'X-Api-Key: daya_sk_YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "side": "buy", "type": "limit", "price": "1550.00", "quantity": "120.00" }' ``` ## Response Same shape as [Get Order](/pro/api-reference/get-order). The returned `id` is the **new** order — track it for follow-up cancels and webhook events. ```json 200 OK { "success": true, "message": "Order replaced successfully", "data": { "id": "8c5b2e8e-1f8e-4b4f-9e5a-2b3c4d5e6f70", "symbol": "USDT-NGN", "side": "buy", "type": "limit", "status": "open", "price": "1550.00", "quantity": "120.00", "filled_quantity": "0.00", "created_at": "2026-05-06T10:00:00Z" } } ``` ## Error Responses | Code | Meaning | |---|---| | `400` | Invalid body — missing side/type/quantity, or price supplied for a market order | | `403` | API key missing Trade scope, or account frozen | | `404` | Order not found, or already terminal | #### Cancel Order Path: /pro/api-reference/cancel-order Description: Cancel an open order ## Overview Cancel an open or partially filled order. Only orders with status `open` or `partially_filled` can be cancelled. ## Authentication Your API key with Trade scope ``` X-Api-Key: daya_sk_YOUR_API_KEY ``` ## Path Parameters The unique order identifier (UUID) to cancel **Example:** `550e8400-e29b-41d4-a716-446655440000` ## Request Examples ```bash cURL curl --request DELETE \ --url https://api.pro.daya.co/public/v1/orders/550e8400-e29b-41d4-a716-446655440000 \ --header 'X-Api-Key: daya_sk_YOUR_API_KEY' ``` ```javascript JavaScript const orderId = '550e8400-e29b-41d4-a716-446655440000'; const response = await fetch( `https://api.pro.daya.co/public/v1/orders/${orderId}`, { method: 'DELETE', headers: { 'X-Api-Key': 'daya_sk_YOUR_API_KEY' } } ); const result = await response.json(); console.log('Order cancelled:', result.success); ``` ```python Python import requests order_id = '550e8400-e29b-41d4-a716-446655440000' headers = {'X-Api-Key': 'daya_sk_YOUR_API_KEY'} response = requests.delete( f'https://api.pro.daya.co/public/v1/orders/{order_id}', headers=headers ) result = response.json() print(f"Order cancelled: {result['success']}") ``` ```go Go package main import ( "encoding/json" "fmt" "net/http" ) func main() { orderID := "550e8400-e29b-41d4-a716-446655440000" url := fmt.Sprintf("https://api.pro.daya.co/public/v1/orders/%s", orderID) req, _ := http.NewRequest("DELETE", url, nil) req.Header.Set("X-Api-Key", "daya_sk_YOUR_API_KEY") client := &http.Client{} resp, err := client.Do(req) if err != nil { panic(err) } defer resp.Body.Close() var result map[string]interface{} json.NewDecoder(resp.Body).Decode(&result) fmt.Println("Order cancelled:", result["success"]) } ``` ## Response Indicates if the request was successful Human-readable response message Returns `null` on successful cancellation ISO 8601 timestamp of the response ### Success Response ```json 200 OK { "success": true, "message": "Order cancelled successfully", "data": null, "timestamp": "2024-01-15T10:40:00Z" } ``` ## Error Responses ```json 401 Unauthorized { "success": false, "message": "Unauthorized", "error": { "code": "API_KEY_INVALID", "message": "The provided API key is invalid" }, "timestamp": "2024-01-15T10:40:00Z" } ``` ```json 403 Forbidden - Insufficient scope { "success": false, "message": "Forbidden", "error": { "code": "API_KEY_INVALID_SCOPE", "message": "API key does not have the required Trade scope" }, "timestamp": "2024-01-15T10:40:00Z" } ``` ```json 404 Not Found { "success": false, "message": "Order not found", "error": { "code": "ORDER_NOT_FOUND", "message": "The specified order does not exist" }, "timestamp": "2024-01-15T10:40:00Z" } ``` ```json 400 Bad Request - Order not cancellable { "success": false, "message": "Order cannot be cancelled", "error": { "code": "ORDER_NOT_CANCELLABLE", "message": "Order is already filled and cannot be cancelled" }, "timestamp": "2024-01-15T10:40:00Z" } ``` ## Cancellable Order Statuses | Status | Can Cancel? | |--------|-------------| | `pending_settlement` | No | | `new` | Yes | | `open` | Yes | | `partially_filled` | Yes | | `filled` | No | | `cancelled` | No | | `rejected` | No | | `failed` | No | When cancelling a partially filled order, the filled portion remains executed. Only the remaining unfilled quantity is cancelled. ## Rate Limits - **100 requests per minute** per API key ## Next Steps Place a new order View your orders ### Trades #### List Trades Path: /pro/api-reference/list-trades Description: List trade history for the authenticated user ## Overview Retrieve a list of executed trades for the authenticated user. Trades represent individual fills against orders. ## Authentication Your API key with Read or Trade scope ``` X-Api-Key: daya_sk_YOUR_API_KEY ``` ## Query Parameters Filter by trading pair symbol **Example:** `USDT-NGN` Filter by order ID **Example:** `550e8400-e29b-41d4-a716-446655440000` Maximum number of trades to return **Default:** `50` **Range:** `1` to `100` Number of trades to skip for pagination **Default:** `0` ## Request Examples ```bash cURL curl --request GET \ --url 'https://api.pro.daya.co/public/v1/trades?symbol=USDT-NGN&limit=20' \ --header 'X-Api-Key: daya_sk_YOUR_API_KEY' ``` ```javascript JavaScript const response = await fetch( 'https://api.pro.daya.co/public/v1/trades?symbol=USDT-NGN&limit=20', { headers: { 'X-Api-Key': 'daya_sk_YOUR_API_KEY' } } ); const data = await response.json(); console.log('Trade history:', data.data); ``` ```python Python import requests headers = {'X-Api-Key': 'daya_sk_YOUR_API_KEY'} response = requests.get( 'https://api.pro.daya.co/public/v1/trades', headers=headers, params={'symbol': 'USDT-NGN', 'limit': 20} ) trades = response.json() print(f"Trade history: {trades['data']}") ``` ```go Go package main import ( "encoding/json" "fmt" "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://api.pro.daya.co/public/v1/trades?symbol=USDT-NGN&limit=20", nil) req.Header.Set("X-Api-Key", "daya_sk_YOUR_API_KEY") client := &http.Client{} resp, err := client.Do(req) if err != nil { panic(err) } defer resp.Body.Close() var result map[string]interface{} json.NewDecoder(resp.Body).Decode(&result) fmt.Println("Trade history:", result["data"]) } ``` ## Response Indicates if the request was successful Human-readable response message Array of trade objects Unique trade identifier (UUID) Trading pair symbol Trade side relative to the authenticated user (`"buy"` or `"sell"`) The authenticated user's order identifier (UUID) Execution price Trade quantity in base asset Trade value in quote asset (price x quantity) Fee charged to the authenticated user for this trade Whether the authenticated user was the maker (resting order) in this trade ISO 8601 trade execution timestamp ISO 8601 timestamp of the response ### Success Response ```json 200 OK { "success": true, "message": "Trades retrieved successfully", "data": [ { "id": "660e8400-e29b-41d4-a716-446655440000", "symbol": "USD-NGN", "side": "buy", "order_id": "550e8400-e29b-41d4-a716-446655440000", "price": "1545.50", "quantity": "100.00000000", "total_value": "154550.00", "fee": "0.15455000", "is_maker": false, "created_at": "2024-01-15T10:35:01Z" }, { "id": "660e8400-e29b-41d4-a716-446655440001", "symbol": "USD-NGN", "side": "sell", "order_id": "550e8400-e29b-41d4-a716-446655440001", "price": "1548.00", "quantity": "50.00000000", "total_value": "77400.00", "fee": "0.05000000", "is_maker": true, "created_at": "2024-01-15T10:20:15Z" } ], "timestamp": "2024-01-15T10:35:01Z" } ``` ## Error Responses ```json 401 Unauthorized { "success": false, "message": "Unauthorized", "error": { "code": "API_KEY_INVALID", "message": "The provided API key is invalid" }, "timestamp": "2024-01-15T10:35:00Z" } ``` ```json 400 Bad Request - Invalid order_id { "success": false, "message": "Validation error", "error": { "code": "VALIDATION_ERROR", "message": "Invalid order_id format" }, "timestamp": "2024-01-15T10:35:00Z" } ``` ## Rate Limits - **100 requests per minute** per API key ## Next Steps View your orders Place a new order #### Get Trade Path: /pro/api-reference/get-trade Description: Get details of a specific trade ## Overview Retrieve details for a specific trade by its ID. Only trades where you are the buyer or seller are accessible. This endpoint requires authentication with an API key that has **Read** scope. ## Authentication Your API key with Read or Trade scope ``` X-Api-Key: daya_sk_YOUR_API_KEY ``` ## Path Parameters The unique trade identifier (UUID) **Example:** `660e8400-e29b-41d4-a716-446655440000` ## Request Examples ```bash cURL curl --request GET \ --url https://api.pro.daya.co/public/v1/trades/660e8400-e29b-41d4-a716-446655440000 \ --header 'X-Api-Key: daya_sk_YOUR_API_KEY' ``` ```javascript JavaScript const tradeId = '660e8400-e29b-41d4-a716-446655440000'; const response = await fetch( `https://api.pro.daya.co/public/v1/trades/${tradeId}`, { headers: { 'X-Api-Key': 'daya_sk_YOUR_API_KEY' } } ); const trade = await response.json(); console.log('Trade details:', trade.data); ``` ```python Python import requests trade_id = '660e8400-e29b-41d4-a716-446655440000' headers = {'X-Api-Key': 'daya_sk_YOUR_API_KEY'} response = requests.get( f'https://api.pro.daya.co/public/v1/trades/{trade_id}', headers=headers ) trade = response.json() print(f"Trade details: {trade['data']}") ``` ```go Go package main import ( "encoding/json" "fmt" "net/http" ) func main() { tradeID := "660e8400-e29b-41d4-a716-446655440000" url := fmt.Sprintf("https://api.pro.daya.co/public/v1/trades/%s", tradeID) req, _ := http.NewRequest("GET", url, nil) req.Header.Set("X-Api-Key", "daya_sk_YOUR_API_KEY") client := &http.Client{} resp, err := client.Do(req) if err != nil { panic(err) } defer resp.Body.Close() var result map[string]interface{} json.NewDecoder(resp.Body).Decode(&result) fmt.Println("Trade details:", result["data"]) } ``` ## Response Indicates if the request was successful Human-readable response message Trade details Unique trade identifier (UUID) Trading pair symbol **Example:** `USD-NGN` Trade side relative to the authenticated user (`"buy"` or `"sell"`) The authenticated user's order identifier (UUID) Execution price Trade quantity in base asset Trade value in quote asset (price x quantity) Fee charged to the authenticated user for this trade Whether the authenticated user was the maker (resting order) in this trade ISO 8601 trade execution timestamp ISO 8601 timestamp of the response ### Success Response ```json 200 OK { "success": true, "message": "Trade retrieved successfully", "data": { "id": "660e8400-e29b-41d4-a716-446655440000", "symbol": "USD-NGN", "side": "buy", "order_id": "550e8400-e29b-41d4-a716-446655440000", "price": "1545.50", "quantity": "100.00000000", "total_value": "154550.00", "fee": "0.15455000", "is_maker": false, "created_at": "2024-01-15T10:35:01Z" }, "timestamp": "2024-01-15T10:35:01Z" } ``` ## Error Responses ```json 401 Unauthorized { "success": false, "message": "Unauthorized", "error": { "code": "API_KEY_INVALID", "message": "The provided API key is invalid" }, "timestamp": "2024-01-15T10:35:00Z" } ``` ```json 404 Not Found { "success": false, "message": "Trade not found", "error": { "code": "TRADE_NOT_FOUND", "message": "The specified trade does not exist" }, "timestamp": "2024-01-15T10:35:00Z" } ``` ## Notes - You can only access trades where you are either the buyer or the seller. Attempting to access a trade you are not party to will return a 404 response. - The `side` and `order_id` fields are relative to your user. If you were the buyer, `side` is `"buy"` and `order_id` is your buy order's ID. ## Rate Limits - **100 requests per minute** per API key ## Next Steps View all your trades View trades for a specific order #### List Order Trades Path: /pro/api-reference/list-order-trades Description: List all trades (fills) for a specific order ## Overview Retrieve all trades (fills) associated with a specific order. This is useful for seeing how a partially filled or fully filled order was executed across multiple trades. This endpoint requires authentication with an API key that has **Read** scope. ## Authentication Your API key with Read or Trade scope ``` X-Api-Key: daya_sk_YOUR_API_KEY ``` ## Path Parameters The unique order identifier (UUID) **Example:** `550e8400-e29b-41d4-a716-446655440000` ## Request Examples ```bash cURL curl --request GET \ --url https://api.pro.daya.co/public/v1/orders/550e8400-e29b-41d4-a716-446655440000/trades \ --header 'X-Api-Key: daya_sk_YOUR_API_KEY' ``` ```javascript JavaScript const orderId = '550e8400-e29b-41d4-a716-446655440000'; const response = await fetch( `https://api.pro.daya.co/public/v1/orders/${orderId}/trades`, { headers: { 'X-Api-Key': 'daya_sk_YOUR_API_KEY' } } ); const data = await response.json(); console.log('Order trades:', data.data); ``` ```python Python import requests order_id = '550e8400-e29b-41d4-a716-446655440000' headers = {'X-Api-Key': 'daya_sk_YOUR_API_KEY'} response = requests.get( f'https://api.pro.daya.co/public/v1/orders/{order_id}/trades', headers=headers ) data = response.json() print(f"Order trades: {data['data']}") ``` ```go Go package main import ( "encoding/json" "fmt" "net/http" ) func main() { orderID := "550e8400-e29b-41d4-a716-446655440000" url := fmt.Sprintf("https://api.pro.daya.co/public/v1/orders/%s/trades", orderID) req, _ := http.NewRequest("GET", url, nil) req.Header.Set("X-Api-Key", "daya_sk_YOUR_API_KEY") client := &http.Client{} resp, err := client.Do(req) if err != nil { panic(err) } defer resp.Body.Close() var result map[string]interface{} json.NewDecoder(resp.Body).Decode(&result) fmt.Println("Order trades:", result["data"]) } ``` ## Response Indicates if the request was successful Human-readable response message Array of trade objects for the order Unique trade identifier (UUID) Trading pair symbol **Example:** `USD-NGN` Trade side relative to the authenticated user (`"buy"` or `"sell"`) The authenticated user's order identifier (UUID) Execution price Trade quantity in base asset Trade value in quote asset (price x quantity) Fee charged to the authenticated user for this trade Whether the authenticated user was the maker (resting order) in this trade ISO 8601 trade execution timestamp ISO 8601 timestamp of the response ### Success Response ```json 200 OK { "success": true, "message": "Order trades retrieved successfully", "data": [ { "id": "660e8400-e29b-41d4-a716-446655440000", "symbol": "USD-NGN", "side": "buy", "order_id": "550e8400-e29b-41d4-a716-446655440000", "price": "1545.00", "quantity": "60.00000000", "total_value": "92700.00", "fee": "0.09270000", "is_maker": false, "created_at": "2024-01-15T10:35:01Z" }, { "id": "660e8400-e29b-41d4-a716-446655440001", "symbol": "USD-NGN", "side": "buy", "order_id": "550e8400-e29b-41d4-a716-446655440000", "price": "1545.50", "quantity": "40.00000000", "total_value": "61820.00", "fee": "0.06182000", "is_maker": false, "created_at": "2024-01-15T10:35:01Z" } ], "timestamp": "2024-01-15T10:35:01Z" } ``` ## Error Responses ```json 401 Unauthorized { "success": false, "message": "Unauthorized", "error": { "code": "API_KEY_INVALID", "message": "The provided API key is invalid" }, "timestamp": "2024-01-15T10:35:00Z" } ``` ```json 404 Not Found { "success": false, "message": "Order not found", "error": { "code": "ORDER_NOT_FOUND", "message": "The specified order does not exist" }, "timestamp": "2024-01-15T10:35:00Z" } ``` ## Notes - You can only view trades for your own orders. Attempting to access trades for another user's order will return a 404 response. - An order may have zero trades (if still open or cancelled before any fills), one trade, or many trades (if filled in multiple partial fills). - Trades are returned in chronological order. ## Rate Limits - **100 requests per minute** per API key ## Next Steps View the order details View all your trades across orders ### Deposits #### List Crypto Deposit Addresses Path: /pro/api-reference/list-crypto-deposit-addresses Description: Fetch your account-level crypto deposit addresses grouped by asset ## Overview Returns your active, provisioned account-level crypto deposit addresses grouped by asset. Use these addresses to collect supported stablecoins directly into your Daya Pro account. Requires **Read** scope. This endpoint is read-only, requires Read scope, and returns your existing account-level deposit addresses. ## Authentication Your API key with Read scope ## Request Example ```bash cURL curl --request GET \ --url https://api.pro.daya.co/public/v1/deposits/addresses \ --header 'X-Api-Key: daya_sk_YOUR_API_KEY' ``` ## Response Deposit addresses grouped by asset. Partner-facing asset symbol, such as `USDT` Human-readable asset name Asset icon path or URL Default asset precision Provisioned chains for this asset Active deposit addresses for the asset. Canonical chain key, such as `ethereum` or `polygon` Human-readable chain name Chain icon path or URL Token contract address when the chain uses one Token precision on this chain Your account-level collection address for this asset and chain QR code URL for the deposit address, when available Current deposit fee amount for this rail Asset used to charge the deposit fee ```json 200 OK { "success": true, "message": "Crypto deposit addresses retrieved successfully", "data": { "assets": [ { "asset": "USDT", "display_name": "Tether USD", "icon": "/assets/v1/logos/stablecoins/usdt.svg", "decimals": 6, "chains": [ { "blockchain": "ethereum", "blockchain_name": "Ethereum (ERC-20)", "icon": "/assets/v1/logos/chains/ethereum.svg", "contract_address": "0xdAC17F958D2ee523a2206206994597C13D831ec7", "decimals": 6, "deposit_address": "0x1234567890abcdef1234567890abcdef12345678", "qr_code_url": "https://api.pro.daya.co/public/assets/qr/usdt-ethereum.png", "fee_amount": "1.50", "fee_asset": "USDT" } ] } ] } } ``` ## Error Responses | Code | Meaning | |---|---| | `401` | Missing or invalid API key | | `403` | Missing Read scope | | `503` | Wallet service temporarily unavailable | ## Notes Subscribe to the [`crypto.deposit.completed`](/pro/webhooks/events#crypto-deposit-completed) webhook event to receive real-time notifications after confirmed on-chain deposits are credited to your Pro balance. ## Next Steps View supported on-chain withdrawal rails and fees Initiate an on-chain withdrawal from your Pro balance #### List Completed Deposits Path: /pro/api-reference/list-completed-deposits Description: Reconcile against NGN bank-transfer deposits that have settled to your Daya Pro balance ## Overview Returns the authenticated user's recent **completed NGN bank-transfer** deposits. Bank-transfer only — on-chain deposits are not included here. Most callers should subscribe to the [`deposit.completed` webhook](/pro/webhooks/events#deposit-completed) instead — this endpoint exists for backfill and reconciliation when you can't trust webhook delivery alone. Requires **Read** scope. ## Authentication Your API key with Read scope ## Query Parameters Number of records to return. Default `20`, max `100`. Pagination offset. Default `0`, max `1,000,000`. For deeper history, prefer paging by `from`/`to` windows over large offsets. Start timestamp. RFC3339 (`2026-05-01T00:00:00Z`) or `YYYY-MM-DD` accepted. End timestamp. Same accepted formats as `from`. ## Request Example ```bash cURL curl --request GET \ --url 'https://api.pro.daya.co/public/v1/deposits/completed?limit=20&from=2026-05-01' \ --header 'X-Api-Key: daya_sk_YOUR_API_KEY' ``` ## Response Array of completed deposit records, newest first. Deposit UUID Always `deposit` Funding rail. Always `bank_transfer` from this endpoint. Always `completed` from this endpoint. Settled amount in NGN as a decimal string Fee in NGN, when applicable Always `NGN` from this endpoint Underlying provider that settled the deposit Provider-side reference for reconciliation Transaction reference (when provider supplies one) Name on the source bank account, when available Internal reference used to attribute the deposit User-supplied or generated reference shown on statements Free-form narration from the source institution Human-readable summary ISO 8601 timestamp when first observed ISO 8601 timestamp when settlement finished Total matching records (across all pages) The limit echo for this page The offset echo for this page True when more records exist beyond this page ```json 200 OK { "success": true, "message": "Completed deposits retrieved successfully", "data": { "deposits": [ { "id": "11111111-1111-1111-1111-111111111111", "type": "deposit", "method": "bank_transfer", "status": "completed", "amount_ngn": "500000.00", "fee_amount_ngn": "50.00", "currency": "NGN", "payment_provider": "flutterwave", "provider_transaction_id": "FLW-RFR-9001", "tx_ref": "txref-001", "originator_name": "Jane Doe", "matching_reference": "DAYA-REF-001", "reference": "INV-9001", "narration": "Customer top-up", "description": "Bank transfer deposit", "created_at": "2026-05-05T14:00:00Z", "completed_at": "2026-05-05T14:00:09Z" } ], "pagination": { "total": 137, "limit": 20, "offset": 0, "has_next": true } } } ``` ### Withdrawals #### List Onchain Withdrawal Options Path: /pro/api-reference/list-onchain-withdrawal-options Description: Fetch enabled on-chain withdrawal rails grouped by asset ## Overview Returns enabled account-level on-chain withdrawal rails grouped by asset. Use this endpoint before initiating a withdrawal to choose the asset, blockchain, and understand current withdrawal fees. Requires **Read** scope. ## Authentication Your API key with Read scope ## Request Example ```bash cURL curl --request GET \ --url https://api.pro.daya.co/public/v1/withdrawals/onchain/options \ --header 'X-Api-Key: daya_sk_YOUR_API_KEY' ``` ## Response Withdrawal rails grouped by asset. Partner-facing asset symbol, such as `USDT` Human-readable asset name Asset icon path or URL Default asset precision Enabled withdrawal chains for this asset Enabled chains for the asset. Canonical chain key, such as `polygon` or `tron` Human-readable chain name Chain icon path or URL Token contract address when the chain uses one Token precision on this chain Current withdrawal fee amount for this rail Asset used to charge the withdrawal fee ```json 200 OK { "success": true, "message": "Onchain withdrawal options retrieved successfully", "data": { "assets": [ { "asset": "USDT", "display_name": "Tether USD", "icon": "/assets/v1/logos/stablecoins/usdt.svg", "decimals": 6, "chains": [ { "blockchain": "polygon", "blockchain_name": "Polygon", "icon": "/assets/v1/logos/chains/polygon.svg", "contract_address": "0xc2132D05D31c914a87C6611C10748AEb04B58e8F", "decimals": 6, "fee_amount": "0.75", "fee_asset": "USDT" } ] } ] } } ``` ## Error Responses | Code | Meaning | |---|---| | `401` | Missing or invalid API key | | `403` | Missing Read scope | | `503` | Wallet service temporarily unavailable | ## Notes Only enabled rails are returned. If an asset or chain is missing, do not attempt to withdraw on that rail. #### Withdraw Onchain Path: /pro/api-reference/withdraw-onchain Description: Initiate an on-chain withdrawal from your Daya Pro stablecoin balance ## Overview Initiates an account-level on-chain withdrawal to a raw destination. The request is accepted synchronously and the withdrawal continues asynchronously on-chain. Requires **Trade** scope. Trade-scoped API keys authorize fund movement. Keep these keys server-side and restrict access to systems that are allowed to initiate withdrawals from the account. Requests are idempotent on `idempotency_key`. Replaying the same key returns the original transaction without creating a duplicate withdrawal. This endpoint does not manage saved withdrawal addresses. Pass the destination directly in `to_address` for each request. ## Authentication Your API key with Trade scope ## Request Body Caller-supplied unique key. Reuse on retry to avoid duplicate withdrawals. **Example:** `onchain-wd-2026-06-05-001` Asset to withdraw. Asset symbols are normalized by Daya, so `usdt` and `USDT` resolve to the same partner-facing asset when supported. **Example:** `USDT` Withdrawal amount as a decimal string. **Example:** `25.505555` Canonical chain key from [List Onchain Withdrawal Options](/pro/api-reference/list-onchain-withdrawal-options). **Example:** `polygon` Destination string for the selected blockchain. Daya forwards this value to the on-chain provider without Daya-side address-format validation, so provider-supported formats may be accepted and unsupported formats are rejected during initiation. **Example:** `0x1234567890abcdef1234567890abcdef12345678` ## Request Example ```bash cURL curl --request POST \ --url https://api.pro.daya.co/public/v1/withdrawals/onchain \ --header 'X-Api-Key: daya_sk_YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "idempotency_key": "onchain-wd-2026-06-05-001", "asset": "usdt", "amount": "25.505555", "blockchain": "polygon", "to_address": "0x1234567890abcdef1234567890abcdef12345678" }' ``` ## Response Daya transaction UUID. Store this as the primary Daya reference for reconciliation and support. Initial withdrawal status. Most accepted withdrawals return as `processing`. **Values:** `pending`, `processing`, `completed`, `failed` Accepted amount as a full-precision decimal string for the asset. Canonical partner-facing asset symbol. Withdrawal fee as a full-precision decimal string for the asset. On-chain transaction hash when available from initiation. This may be omitted if the provider has not returned a hash yet. Canonical chain key used for the withdrawal. ISO 8601 timestamp when the withdrawal was accepted. ```json 200 OK { "success": true, "message": "Withdrawal confirmed and processing", "data": { "transaction_id": "33333333-3333-3333-3333-333333333333", "status": "processing", "amount": "25.505555", "asset": "USDT", "fee": "0.750000", "transaction_hash": "0xwithdrawaltx", "blockchain": "polygon", "created_at": "2026-06-05T11:00:00Z" } } ``` ## Error Responses | Code | Meaning | |---|---| | `400` | Invalid asset, amount, blockchain, destination, or provider rejection during initiation | | `401` | Missing or invalid API key | | `402` | Insufficient balance | | `403` | Missing Trade scope or account restricted | | `409` | Same `idempotency_key` previously used with a different request body | ## Notes on settlement The response confirms the withdrawal was accepted for processing. It does not mean the recipient has received funds on-chain. Store the `transaction_id`, `idempotency_key`, initial `status`, and `transaction_hash` when returned. Use `transaction_hash` for on-chain monitoring, and use `transaction_id` when reconciling the withdrawal with Daya support. The public API does not expose a withdrawal-status endpoint or withdrawal-status webhook. For now, review withdrawal history and status in your Daya Pro account. #### Withdraw to Bank Path: /pro/api-reference/withdraw-to-bank Description: Initiate a fiat withdrawal from your Daya Pro NGN balance to a Nigerian bank account ## Overview Move NGN from your Daya Pro balance to a Nigerian bank account. The withdrawal is queued and dispatched through Daya's payout provider; the response returns immediately with a transaction ID and the initial status. Requires **Trade** scope. Requests are idempotent on `idempotency_key`. Replaying the same key always returns the original transaction without re-running the withdrawal — safe to retry on network failures. There are two ways to specify the destination: 1. **Saved beneficiary** — pass `bank_account_id` (UUID of a beneficiary previously stored on your account). All other bank fields are ignored. 2. **Inline details** — pass `account_number`, `bank_code`, `account_name`, `bank_name` directly. ## Authentication Your API key with Trade scope ## Request Body Caller-supplied unique key. Reuse on retry to avoid double-spend. **Example:** `wd-2026-05-06-01` Withdrawal amount as a decimal string. **Example:** `1000000.00` Currency code. **Example:** `NGN` UUID of a saved bank account on the user. When set, the inline bank fields below are ignored. Destination NUBAN. Required when `bank_account_id` is omitted. **Example:** `0123456789` CBN bank code. Required when `bank_account_id` is omitted. **Example:** `058` Account holder name as registered with the destination bank. Required when `bank_account_id` is omitted. Must match — the provider rejects mismatched names. Display name of the destination bank. Required when `bank_account_id` is omitted. **Example:** `GTBank` Optional free-form narration shown on the recipient's bank statement. ## Request Example ```bash cURL curl --request POST \ --url https://api.pro.daya.co/public/v1/withdrawals/bank \ --header 'X-Api-Key: daya_sk_YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "idempotency_key": "wd-2026-05-06-01", "account_number": "0123456789", "bank_code": "058", "account_name": "Jane Doe", "bank_name": "GTBank", "amount": "1000000.00", "currency": "NGN", "narration": "Payout for invoice 123" }' ``` ## Response Daya transaction UUID. Use this to track status. Initial status. Most withdrawals return as `processing` and transition asynchronously. **Values:** `pending`, `processing`, `completed`, `failed` Echoed amount. Echoed currency. Fee charged for the withdrawal. Provider-side transfer reference (when available). ISO 8601 timestamp when the withdrawal was accepted. ```json 200 OK { "success": true, "message": "Withdrawal initiated successfully", "data": { "transaction_id": "22222222-2222-2222-2222-222222222222", "status": "processing", "amount": "1000000.00", "currency": "NGN", "fee": "100.00", "transfer_id": "FLW-TRF-9001", "created_at": "2026-05-06T12:00:00Z" } } ``` ## Error Responses | Code | Meaning | |---|---| | `400` | Validation error or insufficient balance | | `403` | Missing Trade scope, or account restricted | | `409` | Same `idempotency_key` previously used with a different request body | ## Notes on settlement The response confirms the withdrawal was *accepted*, not that funds have arrived. Final settlement happens through the underlying payout provider and can take seconds to minutes depending on the destination bank. ### Webhooks API #### List Webhooks Path: /pro/api-reference/list-webhooks Description: List all webhooks for the authenticated user ## Overview Retrieve all webhook endpoints configured for the authenticated user. Use this to view your webhook configurations and their status. Pro webhook setup is support-managed. Contact [support@daya.co](mailto:support@daya.co) to configure webhook endpoints for your Pro account. ## Authentication Your API key with Write scope ``` X-Api-Key: daya_sk_YOUR_API_KEY ``` ## Request Examples ```bash cURL curl --request GET \ --url 'https://api.pro.daya.co/public/v1/webhooks' \ --header 'X-Api-Key: daya_sk_YOUR_API_KEY' ``` ```javascript JavaScript const response = await fetch( 'https://api.pro.daya.co/public/v1/webhooks', { headers: { 'X-Api-Key': 'daya_sk_YOUR_API_KEY' } } ); const data = await response.json(); console.log('Webhooks:', data.data); ``` ```python Python import requests headers = {'X-Api-Key': 'daya_sk_YOUR_API_KEY'} response = requests.get( 'https://api.pro.daya.co/public/v1/webhooks', headers=headers ) webhooks = response.json() print(f"Webhooks: {webhooks['data']}") ``` ```go Go package main import ( "encoding/json" "fmt" "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://api.pro.daya.co/public/v1/webhooks", nil) req.Header.Set("X-Api-Key", "daya_sk_YOUR_API_KEY") client := &http.Client{} resp, err := client.Do(req) if err != nil { panic(err) } defer resp.Body.Close() var result map[string]interface{} json.NewDecoder(resp.Body).Decode(&result) fmt.Println("Webhooks:", result["data"]) } ``` ## Response Indicates if the request was successful Human-readable response message Array of webhook objects Unique webhook identifier (UUID) Webhook endpoint URL Webhook description Events this webhook is subscribed to **Values:** `order.created`, `order.filled`, `order.partially_filled`, `order.cancelled`, `order.rejected`, `trade.executed`, `deposit.completed`, `crypto.deposit.completed` Webhook status **Values:** `active`, `paused`, `disabled` Number of consecutive delivery failures Last successful delivery timestamp (ISO 8601) Last failed delivery timestamp (ISO 8601) Reason for last delivery failure ISO 8601 creation timestamp ISO 8601 last update timestamp ### Success Response ```json 200 OK { "success": true, "message": "Webhooks retrieved successfully", "data": [ { "id": "770e8400-e29b-41d4-a716-446655440000", "url": "https://example.com/webhooks/daya", "description": "Order notifications", "events": ["order.filled", "order.cancelled"], "status": "active", "failure_count": 0, "last_success_at": "2024-01-15T10:30:00Z", "last_failure_at": null, "last_failure_reason": null, "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-15T10:30:00Z" }, { "id": "770e8400-e29b-41d4-a716-446655440001", "url": "https://example.com/webhooks/daya-trades", "description": "Trade notifications", "events": ["trade.executed"], "status": "paused", "failure_count": 3, "last_success_at": "2024-01-10T15:00:00Z", "last_failure_at": "2024-01-15T08:00:00Z", "last_failure_reason": "Connection timeout", "created_at": "2024-01-05T00:00:00Z", "updated_at": "2024-01-15T08:00:00Z" } ], "timestamp": "2024-01-15T10:30:00Z" } ``` ## Error Responses ```json 401 Unauthorized { "success": false, "message": "Unauthorized", "error": { "code": "API_KEY_INVALID", "message": "The provided API key is invalid" } } ``` ```json 403 Forbidden { "success": false, "message": "Forbidden", "error": { "code": "API_KEY_INVALID_SCOPE", "message": "Insufficient scope for this operation" } } ``` ## Webhook Status | Status | Description | |--------|-------------| | `active` | Webhook is enabled and receiving events | | `paused` | Webhook is temporarily disabled by user | | `disabled` | Webhook was auto-disabled due to repeated failures | ## Rate Limits - **100 requests per minute** per API key ## Next Steps Create a new webhook endpoint Learn about webhook events and payloads #### Create Webhook Path: /pro/api-reference/create-webhook Description: Create a new webhook endpoint ## Overview Create a new webhook endpoint to receive order, trade, and deposit event notifications. The webhook secret is only returned once at creation, so make sure to store it securely. Pro webhook setup is support-managed. Contact [support@daya.co](mailto:support@daya.co) to enable webhook setup or request webhook configuration changes for your Pro account. ## Authentication Your API key with Write scope ``` X-Api-Key: daya_sk_YOUR_API_KEY ``` ## Request Body Webhook endpoint URL (must be HTTPS in production) **Example:** `https://example.com/webhooks/daya` Events to subscribe to (at least one required) **Allowed values:** `order.created`, `order.filled`, `order.partially_filled`, `order.cancelled`, `order.rejected`, `trade.executed`, `deposit.completed`, `crypto.deposit.completed` Optional description for the webhook **Example:** `My order notifications` ## Request Examples ```bash cURL curl --request POST \ --url 'https://api.pro.daya.co/public/v1/webhooks' \ --header 'X-Api-Key: daya_sk_YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "url": "https://example.com/webhooks/daya", "events": ["order.filled", "order.cancelled", "trade.executed"], "description": "Order and trade notifications" }' ``` ```javascript JavaScript const response = await fetch( 'https://api.pro.daya.co/public/v1/webhooks', { method: 'POST', headers: { 'X-Api-Key': 'daya_sk_YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ url: 'https://example.com/webhooks/daya', events: ['order.filled', 'order.cancelled', 'trade.executed'], description: 'Order and trade notifications' }) } ); const data = await response.json(); // IMPORTANT: Store the secret securely - it's only shown once! console.log('Webhook secret:', data.data.secret); ``` ```python Python import requests headers = { 'X-Api-Key': 'daya_sk_YOUR_API_KEY', 'Content-Type': 'application/json' } payload = { 'url': 'https://example.com/webhooks/daya', 'events': ['order.filled', 'order.cancelled', 'trade.executed'], 'description': 'Order and trade notifications' } response = requests.post( 'https://api.pro.daya.co/public/v1/webhooks', headers=headers, json=payload ) result = response.json() # IMPORTANT: Store the secret securely - it's only shown once! print(f"Webhook secret: {result['data']['secret']}") ``` ```go Go package main import ( "bytes" "encoding/json" "fmt" "net/http" ) func main() { payload := map[string]interface{}{ "url": "https://example.com/webhooks/daya", "events": []string{"order.filled", "order.cancelled", "trade.executed"}, "description": "Order and trade notifications", } jsonPayload, _ := json.Marshal(payload) req, _ := http.NewRequest("POST", "https://api.pro.daya.co/public/v1/webhooks", bytes.NewBuffer(jsonPayload)) req.Header.Set("X-Api-Key", "daya_sk_YOUR_API_KEY") req.Header.Set("Content-Type", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { panic(err) } defer resp.Body.Close() var result map[string]interface{} json.NewDecoder(resp.Body).Decode(&result) // IMPORTANT: Store the secret securely - it's only shown once! fmt.Println("Webhook created:", result["data"]) } ``` ## Response Indicates if the request was successful Human-readable response message Created webhook with secret Unique webhook identifier (UUID) Webhook endpoint URL Webhook description Events this webhook is subscribed to Webhook status (will be `active`) **Signing secret for verifying webhook payloads.** This is only returned once at creation. Store it securely! ISO 8601 creation timestamp ISO 8601 last update timestamp ### Success Response ```json 201 Created { "success": true, "message": "Webhook created successfully", "data": { "id": "770e8400-e29b-41d4-a716-446655440000", "url": "https://example.com/webhooks/daya", "description": "Order and trade notifications", "events": ["order.filled", "order.cancelled", "trade.executed"], "status": "active", "secret": "a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456", "failure_count": 0, "last_success_at": null, "last_failure_at": null, "last_failure_reason": null, "created_at": "2024-01-15T10:00:00Z", "updated_at": "2024-01-15T10:00:00Z" }, "timestamp": "2024-01-15T10:00:00Z" } ``` The `secret` is a 64-character hex string (32 bytes). This is the raw secret used for HMAC-SHA256 signature verification. ## Error Responses ```json 400 Bad Request - Invalid URL { "success": false, "message": "Validation error", "error": { "code": "VALIDATION_ERROR", "message": "URL must be a valid HTTPS URL" } } ``` ```json 400 Bad Request - Invalid events { "success": false, "message": "Validation error", "error": { "code": "VALIDATION_ERROR", "message": "At least one event is required" } } ``` ```json 401 Unauthorized { "success": false, "message": "Unauthorized", "error": { "code": "API_KEY_INVALID", "message": "The provided API key is invalid" } } ``` ```json 429 Too Many Requests - Webhook limit { "success": false, "message": "Webhook limit exceeded", "error": { "code": "WEBHOOK_LIMIT_EXCEEDED", "message": "Maximum number of webhooks reached (5)" } } ``` ## Available Events | Event | Description | |-------|-------------| | `order.created` | New order accepted by matching engine | | `order.filled` | Order completely filled | | `order.partially_filled` | Order partially executed | | `order.cancelled` | Order cancelled | | `order.rejected` | Order rejected | | `trade.executed` | Trade executed involving your order | | `deposit.completed` | NGN bank-transfer deposit settled to your Daya Pro balance | | `crypto.deposit.completed` | Confirmed on-chain deposit credited to your Daya Pro balance | ## Important Notes **Store your webhook secret securely!** The `secret` field is only returned once when the webhook is created. If you lose it, you'll need to [rotate the secret](/pro/api-reference/rotate-webhook-secret). Webhook URLs must use HTTPS in production. HTTP is only allowed for local development testing. ## Rate Limits - **100 requests per minute** per API key - **Maximum 5 webhooks** per account ## Next Steps Learn how to verify webhook signatures See event payload details View all your webhooks Update webhook configuration #### Get Webhook Path: /pro/api-reference/get-webhook Description: Get a specific webhook by ID ## Overview Retrieve details of a specific webhook endpoint by its ID. ## Authentication Your API key with Write scope ``` X-Api-Key: daya_sk_YOUR_API_KEY ``` ## Path Parameters Webhook ID (UUID) **Example:** `770e8400-e29b-41d4-a716-446655440000` ## Request Examples ```bash cURL curl --request GET \ --url 'https://api.pro.daya.co/public/v1/webhooks/770e8400-e29b-41d4-a716-446655440000' \ --header 'X-Api-Key: daya_sk_YOUR_API_KEY' ``` ```javascript JavaScript const webhookId = '770e8400-e29b-41d4-a716-446655440000'; const response = await fetch( `https://api.pro.daya.co/public/v1/webhooks/${webhookId}`, { headers: { 'X-Api-Key': 'daya_sk_YOUR_API_KEY' } } ); const data = await response.json(); console.log('Webhook:', data.data); ``` ```python Python import requests webhook_id = '770e8400-e29b-41d4-a716-446655440000' headers = {'X-Api-Key': 'daya_sk_YOUR_API_KEY'} response = requests.get( f'https://api.pro.daya.co/public/v1/webhooks/{webhook_id}', headers=headers ) webhook = response.json() print(f"Webhook: {webhook['data']}") ``` ## Response Indicates if the request was successful Human-readable response message Webhook object Unique webhook identifier (UUID) Webhook endpoint URL Webhook description Events this webhook is subscribed to Webhook status: `active`, `paused`, `disabled` Number of consecutive delivery failures Last successful delivery timestamp Last failed delivery timestamp Reason for last delivery failure ISO 8601 creation timestamp ISO 8601 last update timestamp ### Success Response ```json 200 OK { "success": true, "message": "Webhook retrieved successfully", "data": { "id": "770e8400-e29b-41d4-a716-446655440000", "url": "https://example.com/webhooks/daya", "description": "Order notifications", "events": ["order.filled", "order.cancelled"], "status": "active", "failure_count": 0, "last_success_at": "2024-01-15T10:30:00Z", "last_failure_at": null, "last_failure_reason": null, "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-15T10:30:00Z" }, "timestamp": "2024-01-15T10:30:00Z" } ``` ## Error Responses ```json 400 Bad Request - Invalid ID { "success": false, "message": "Validation error", "error": { "code": "VALIDATION_ERROR", "message": "Invalid webhook ID format" } } ``` ```json 404 Not Found { "success": false, "message": "Webhook not found", "error": { "code": "WEBHOOK_NOT_FOUND", "message": "No webhook found with the specified ID" } } ``` ```json 401 Unauthorized { "success": false, "message": "Unauthorized", "error": { "code": "API_KEY_INVALID", "message": "The provided API key is invalid" } } ``` ## Rate Limits - **100 requests per minute** per API key ## Next Steps Update webhook configuration View webhook delivery logs #### Update Webhook Path: /pro/api-reference/update-webhook Description: Update a webhook's configuration ## Overview Update a webhook's URL, events, status, or description. All fields are optional - only include the fields you want to update. Pro webhook setup and configuration changes are support-managed. Contact [support@daya.co](mailto:support@daya.co) if you need to add or change webhook endpoints for your Pro account. ## Authentication Your API key with Write scope ``` X-Api-Key: daya_sk_YOUR_API_KEY ``` ## Path Parameters Webhook ID (UUID) **Example:** `770e8400-e29b-41d4-a716-446655440000` ## Request Body New webhook endpoint URL (must be HTTPS) New events to subscribe to **Allowed values:** `order.created`, `order.filled`, `order.partially_filled`, `order.cancelled`, `order.rejected`, `trade.executed`, `deposit.completed`, `crypto.deposit.completed` Webhook status **Allowed values:** `active`, `paused` New description for the webhook ## Request Examples ### Update Events ```bash cURL curl --request PATCH \ --url 'https://api.pro.daya.co/public/v1/webhooks/770e8400-e29b-41d4-a716-446655440000' \ --header 'X-Api-Key: daya_sk_YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "events": ["order.filled", "trade.executed"] }' ``` ```javascript JavaScript const webhookId = '770e8400-e29b-41d4-a716-446655440000'; const response = await fetch( `https://api.pro.daya.co/public/v1/webhooks/${webhookId}`, { method: 'PATCH', headers: { 'X-Api-Key': 'daya_sk_YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ events: ['order.filled', 'trade.executed'] }) } ); const data = await response.json(); console.log('Updated webhook:', data.data); ``` ### Pause Webhook ```bash cURL curl --request PATCH \ --url 'https://api.pro.daya.co/public/v1/webhooks/770e8400-e29b-41d4-a716-446655440000' \ --header 'X-Api-Key: daya_sk_YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "status": "paused" }' ``` ```javascript JavaScript const webhookId = '770e8400-e29b-41d4-a716-446655440000'; const response = await fetch( `https://api.pro.daya.co/public/v1/webhooks/${webhookId}`, { method: 'PATCH', headers: { 'X-Api-Key': 'daya_sk_YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ status: 'paused' }) } ); ``` ### Resume Webhook ```bash curl --request PATCH \ --url 'https://api.pro.daya.co/public/v1/webhooks/770e8400-e29b-41d4-a716-446655440000' \ --header 'X-Api-Key: daya_sk_YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{"status": "active"}' ``` ## Response Indicates if the request was successful Human-readable response message Updated webhook object Unique webhook identifier (UUID) Webhook endpoint URL Webhook description Events this webhook is subscribed to Webhook status: `active`, `paused`, `disabled` Number of consecutive delivery failures ISO 8601 creation timestamp ISO 8601 last update timestamp ### Success Response ```json 200 OK { "success": true, "message": "Webhook updated successfully", "data": { "id": "770e8400-e29b-41d4-a716-446655440000", "url": "https://example.com/webhooks/daya", "description": "Order notifications", "events": ["order.filled", "trade.executed"], "status": "active", "failure_count": 0, "last_success_at": "2024-01-15T10:30:00Z", "last_failure_at": null, "last_failure_reason": null, "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-15T12:00:00Z" }, "timestamp": "2024-01-15T12:00:00Z" } ``` ## Error Responses ```json 400 Bad Request - Invalid status { "success": false, "message": "Validation error", "error": { "code": "VALIDATION_ERROR", "message": "Invalid status. Allowed values: active, paused" } } ``` ```json 400 Bad Request - Invalid URL { "success": false, "message": "Validation error", "error": { "code": "VALIDATION_ERROR", "message": "URL must be a valid HTTPS URL" } } ``` ```json 404 Not Found { "success": false, "message": "Webhook not found", "error": { "code": "WEBHOOK_NOT_FOUND", "message": "No webhook found with the specified ID" } } ``` ```json 401 Unauthorized { "success": false, "message": "Unauthorized", "error": { "code": "API_KEY_INVALID", "message": "The provided API key is invalid" } } ``` ## Rate Limits - **100 requests per minute** per API key ## Next Steps Delete a webhook Generate a new signing secret #### Delete Webhook Path: /pro/api-reference/delete-webhook Description: Delete a webhook and all its delivery logs ## Overview Delete a webhook endpoint. This also deletes all delivery logs associated with the webhook. This action cannot be undone. Pro webhook setup and configuration changes are support-managed. Contact [support@daya.co](mailto:support@daya.co) if you need to remove a webhook endpoint from your Pro account. ## Authentication Your API key with Write scope ``` X-Api-Key: daya_sk_YOUR_API_KEY ``` ## Path Parameters Webhook ID (UUID) **Example:** `770e8400-e29b-41d4-a716-446655440000` ## Request Examples ```bash cURL curl --request DELETE \ --url 'https://api.pro.daya.co/public/v1/webhooks/770e8400-e29b-41d4-a716-446655440000' \ --header 'X-Api-Key: daya_sk_YOUR_API_KEY' ``` ```javascript JavaScript const webhookId = '770e8400-e29b-41d4-a716-446655440000'; const response = await fetch( `https://api.pro.daya.co/public/v1/webhooks/${webhookId}`, { method: 'DELETE', headers: { 'X-Api-Key': 'daya_sk_YOUR_API_KEY' } } ); const data = await response.json(); console.log('Deleted:', data.success); ``` ```python Python import requests webhook_id = '770e8400-e29b-41d4-a716-446655440000' headers = {'X-Api-Key': 'daya_sk_YOUR_API_KEY'} response = requests.delete( f'https://api.pro.daya.co/public/v1/webhooks/{webhook_id}', headers=headers ) result = response.json() print(f"Deleted: {result['success']}") ``` ```go Go package main import ( "encoding/json" "fmt" "net/http" ) func main() { req, _ := http.NewRequest("DELETE", "https://api.pro.daya.co/public/v1/webhooks/770e8400-e29b-41d4-a716-446655440000", nil) req.Header.Set("X-Api-Key", "daya_sk_YOUR_API_KEY") client := &http.Client{} resp, err := client.Do(req) if err != nil { panic(err) } defer resp.Body.Close() var result map[string]interface{} json.NewDecoder(resp.Body).Decode(&result) fmt.Println("Deleted:", result["success"]) } ``` ## Response Indicates if the request was successful Human-readable response message ### Success Response ```json 200 OK { "success": true, "message": "Webhook deleted successfully", "data": null, "timestamp": "2024-01-15T12:00:00Z" } ``` ## Error Responses ```json 400 Bad Request - Invalid ID { "success": false, "message": "Validation error", "error": { "code": "VALIDATION_ERROR", "message": "Invalid webhook ID format" } } ``` ```json 404 Not Found { "success": false, "message": "Webhook not found", "error": { "code": "WEBHOOK_NOT_FOUND", "message": "No webhook found with the specified ID" } } ``` ```json 401 Unauthorized { "success": false, "message": "Unauthorized", "error": { "code": "API_KEY_INVALID", "message": "The provided API key is invalid" } } ``` **This action is permanent.** All delivery logs for this webhook will be deleted and cannot be recovered. ## Rate Limits - **100 requests per minute** per API key ## Next Steps Create a new webhook View all webhooks #### Rotate Webhook Secret Path: /pro/api-reference/rotate-webhook-secret Description: Generate a new signing secret for a webhook ## Overview Generate a new signing secret for a webhook. The old secret becomes invalid immediately. The new secret is only returned once, so make sure to store it securely. Pro webhook setup and secret rotation are support-managed. Contact [support@daya.co](mailto:support@daya.co) if you need to rotate a webhook signing secret for your Pro account. ## Authentication Your API key with Write scope ``` X-Api-Key: daya_sk_YOUR_API_KEY ``` ## Path Parameters Webhook ID (UUID) **Example:** `770e8400-e29b-41d4-a716-446655440000` ## Request Examples ```bash cURL curl --request POST \ --url 'https://api.pro.daya.co/public/v1/webhooks/770e8400-e29b-41d4-a716-446655440000/rotate-secret' \ --header 'X-Api-Key: daya_sk_YOUR_API_KEY' ``` ```javascript JavaScript const webhookId = '770e8400-e29b-41d4-a716-446655440000'; const response = await fetch( `https://api.pro.daya.co/public/v1/webhooks/${webhookId}/rotate-secret`, { method: 'POST', headers: { 'X-Api-Key': 'daya_sk_YOUR_API_KEY' } } ); const data = await response.json(); // IMPORTANT: Store the new secret securely - it's only shown once! console.log('New secret:', data.data.secret); ``` ```python Python import requests webhook_id = '770e8400-e29b-41d4-a716-446655440000' headers = {'X-Api-Key': 'daya_sk_YOUR_API_KEY'} response = requests.post( f'https://api.pro.daya.co/public/v1/webhooks/{webhook_id}/rotate-secret', headers=headers ) result = response.json() # IMPORTANT: Store the new secret securely - it's only shown once! print(f"New secret: {result['data']['secret']}") ``` ```go Go package main import ( "encoding/json" "fmt" "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://api.pro.daya.co/public/v1/webhooks/770e8400-e29b-41d4-a716-446655440000/rotate-secret", nil) req.Header.Set("X-Api-Key", "daya_sk_YOUR_API_KEY") client := &http.Client{} resp, err := client.Do(req) if err != nil { panic(err) } defer resp.Body.Close() var result map[string]interface{} json.NewDecoder(resp.Body).Decode(&result) // IMPORTANT: Store the new secret securely - it's only shown once! fmt.Println("New secret:", result["data"]) } ``` ## Response Indicates if the request was successful Human-readable response message Webhook with new secret Unique webhook identifier (UUID) Webhook endpoint URL Webhook description Events this webhook is subscribed to Webhook status: `active`, `paused`, `disabled` **New signing secret for verifying webhook payloads.** This is only returned once. Store it securely! ISO 8601 creation timestamp ISO 8601 last update timestamp ### Success Response ```json 200 OK { "success": true, "message": "Webhook secret rotated successfully", "data": { "id": "770e8400-e29b-41d4-a716-446655440000", "url": "https://example.com/webhooks/daya", "description": "Order notifications", "events": ["order.filled", "order.cancelled"], "status": "active", "secret": "b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef12345678", "failure_count": 0, "last_success_at": "2024-01-15T10:30:00Z", "last_failure_at": null, "last_failure_reason": null, "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-15T12:00:00Z" }, "timestamp": "2024-01-15T12:00:00Z" } ``` The `secret` is a 64-character hex string (32 bytes). This is the raw secret used for HMAC-SHA256 signature verification. ## Error Responses ```json 400 Bad Request - Invalid ID { "success": false, "message": "Validation error", "error": { "code": "VALIDATION_ERROR", "message": "Invalid webhook ID format" } } ``` ```json 404 Not Found { "success": false, "message": "Webhook not found", "error": { "code": "WEBHOOK_NOT_FOUND", "message": "No webhook found with the specified ID" } } ``` ```json 401 Unauthorized { "success": false, "message": "Unauthorized", "error": { "code": "API_KEY_INVALID", "message": "The provided API key is invalid" } } ``` **The old secret becomes invalid immediately.** After rotating, update your webhook verification code with the new secret before processing any new webhook deliveries. ## Rate Limits - **100 requests per minute** per API key ## Next Steps Learn how to verify webhook signatures View webhook details #### Get Webhook Deliveries Path: /pro/api-reference/get-webhook-deliveries Description: Get delivery logs for a webhook ## Overview Retrieve the delivery history for a specific webhook. Use this to monitor webhook delivery status, debug failures, and track retry attempts. ## Authentication Your API key with Write scope ``` X-Api-Key: daya_sk_YOUR_API_KEY ``` ## Path Parameters Webhook ID (UUID) **Example:** `770e8400-e29b-41d4-a716-446655440000` ## Query Parameters Number of logs to return **Default:** `50` **Range:** `1` to `100` Offset for pagination **Default:** `0` ## Request Examples ```bash cURL curl --request GET \ --url 'https://api.pro.daya.co/public/v1/webhooks/770e8400-e29b-41d4-a716-446655440000/deliveries?limit=20' \ --header 'X-Api-Key: daya_sk_YOUR_API_KEY' ``` ```javascript JavaScript const webhookId = '770e8400-e29b-41d4-a716-446655440000'; const response = await fetch( `https://api.pro.daya.co/public/v1/webhooks/${webhookId}/deliveries?limit=20`, { headers: { 'X-Api-Key': 'daya_sk_YOUR_API_KEY' } } ); const data = await response.json(); console.log('Delivery logs:', data.data); ``` ```python Python import requests webhook_id = '770e8400-e29b-41d4-a716-446655440000' headers = {'X-Api-Key': 'daya_sk_YOUR_API_KEY'} response = requests.get( f'https://api.pro.daya.co/public/v1/webhooks/{webhook_id}/deliveries', headers=headers, params={'limit': 20} ) logs = response.json() print(f"Delivery logs: {logs['data']}") ``` ```go Go package main import ( "encoding/json" "fmt" "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://api.pro.daya.co/public/v1/webhooks/770e8400-e29b-41d4-a716-446655440000/deliveries?limit=20", nil) req.Header.Set("X-Api-Key", "daya_sk_YOUR_API_KEY") client := &http.Client{} resp, err := client.Do(req) if err != nil { panic(err) } defer resp.Body.Close() var result map[string]interface{} json.NewDecoder(resp.Body).Decode(&result) fmt.Println("Delivery logs:", result["data"]) } ``` ## Response Indicates if the request was successful Human-readable response message Array of delivery log objects Unique delivery log identifier (UUID) Event identifier (UUID) Type of event **Values:** `order.created`, `order.filled`, `order.partially_filled`, `order.cancelled`, `order.rejected`, `trade.executed`, `deposit.completed`, `crypto.deposit.completed` Delivery status **Values:** `pending`, `delivered`, `failed`, `retrying` Number of delivery attempts made Maximum delivery attempts allowed HTTP response status code from your endpoint (if any) Last error message (if any) Timestamp of last delivery attempt (ISO 8601) Timestamp of next retry attempt (ISO 8601, if retrying) Timestamp when successfully delivered (ISO 8601) ISO 8601 creation timestamp ### Success Response ```json 200 OK { "success": true, "message": "Delivery logs retrieved successfully", "data": [ { "id": "880e8400-e29b-41d4-a716-446655440000", "event_id": "550e8400-e29b-41d4-a716-446655440000", "event_type": "order.filled", "status": "delivered", "attempts": 1, "max_attempts": 10, "response_status_code": 200, "last_error": null, "last_attempt_at": "2024-01-15T10:30:01Z", "next_retry_at": null, "delivered_at": "2024-01-15T10:30:01Z", "created_at": "2024-01-15T10:30:00Z" }, { "id": "880e8400-e29b-41d4-a716-446655440001", "event_id": "550e8400-e29b-41d4-a716-446655440001", "event_type": "order.cancelled", "status": "retrying", "attempts": 3, "max_attempts": 10, "response_status_code": 500, "last_error": "Internal Server Error", "last_attempt_at": "2024-01-15T10:35:00Z", "next_retry_at": "2024-01-15T10:40:00Z", "delivered_at": null, "created_at": "2024-01-15T10:00:00Z" }, { "id": "880e8400-e29b-41d4-a716-446655440002", "event_id": "550e8400-e29b-41d4-a716-446655440002", "event_type": "trade.executed", "status": "failed", "attempts": 10, "max_attempts": 10, "response_status_code": 0, "last_error": "Connection timeout", "last_attempt_at": "2024-01-14T22:00:00Z", "next_retry_at": null, "delivered_at": null, "created_at": "2024-01-14T08:00:00Z" } ], "timestamp": "2024-01-15T10:35:00Z" } ``` ## Error Responses ```json 400 Bad Request - Invalid ID { "success": false, "message": "Validation error", "error": { "code": "VALIDATION_ERROR", "message": "Invalid webhook ID format" } } ``` ```json 404 Not Found { "success": false, "message": "Webhook not found", "error": { "code": "WEBHOOK_NOT_FOUND", "message": "No webhook found with the specified ID" } } ``` ```json 401 Unauthorized { "success": false, "message": "Unauthorized", "error": { "code": "API_KEY_INVALID", "message": "The provided API key is invalid" } } ``` ## Delivery Status | Status | Description | |--------|-------------| | `pending` | Event queued, not yet attempted | | `delivered` | Successfully delivered (2xx response) | | `retrying` | Delivery failed, will retry | | `failed` | All retry attempts exhausted | ## Retry Behavior Failed deliveries are retried with exponential backoff: | Attempt | Delay After Previous | |---------|---------------------| | 1 | 10 seconds | | 2 | 30 seconds | | 3 | 1 minute | | 4 | 5 minutes | | 5 | 15 minutes | | 6 | 30 minutes | | 7 | 1 hour | | 8 | 2 hours | | 9 | 4 hours | | 10 | 8 hours | After 10 failed attempts, the delivery is marked as `failed` and the webhook may be automatically disabled. ## Rate Limits - **100 requests per minute** per API key ## Next Steps View webhook details Update webhook configuration Learn about webhook events Verify webhook signatures ### Webhooks #### Webhooks Overview Path: /pro/webhooks/overview Description: Real-time event notifications for trading activity on Daya Pro ## What are Webhooks? Webhooks allow you to receive real-time HTTP notifications when trading events occur on your account, eliminating the need to poll the API for order status updates. Webhooks are the **recommended** way to track order execution and trade activity. They provide real-time updates and reduce API load. ## Supported Events | Event | Description | When Triggered | |-------|-------------|----------------| | `order.created` | Order submitted | New order accepted by the matching engine | | `order.filled` | Order completely filled | All quantity executed | | `order.partially_filled` | Order partially filled | Some quantity executed, order still open | | `order.cancelled` | Order cancelled | User cancelled or system cancelled | | `order.rejected` | Order rejected | Validation failed or insufficient balance | | `trade.executed` | Trade executed | A trade matched involving your order | | `deposit.completed` | NGN deposit settled | A bank-transfer deposit reached your Daya Pro balance | | `crypto.deposit.completed` | Crypto deposit settled | A confirmed on-chain deposit was credited to your Daya Pro balance | ## Webhook Configuration Webhook setup for Daya Pro is support-managed. Contact [support@daya.co](mailto:support@daya.co) to set up or update Pro webhook endpoints and event subscriptions. When requesting setup, include: 1. Webhook URL (must be HTTPS) 2. Events to subscribe to 3. Environment and Pro account details Webhook URLs **must** use HTTPS in production. HTTP is only allowed for local development testing. ## HTTP Headers All webhook requests include the following headers: | Header | Description | Example | |--------|-------------|---------| | `Content-Type` | Always `application/json` | `application/json` | | `X-Webhook-Signature` | HMAC-SHA256 signature with `sha256=` prefix | `sha256=a8f5f167f44f...` | | `X-Webhook-Event` | Event type that triggered this webhook | `order.filled` | | `X-Webhook-ID` | Unique event identifier (UUID) | `550e8400-e29b-41d4-a716-446655440000` | | `X-Webhook-Timestamp` | When the event occurred (RFC3339) | `2026-01-14T15:08:15Z` | | `User-Agent` | Identifies Daya as the sender | `Daya-Webhook/1.0` | ## Webhook Payload All webhook events follow this structure: ```json { "event_id": "550e8400-e29b-41d4-a716-446655440000", "type": "order.filled", "timestamp": "2026-01-14T15:08:15Z", "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "symbol": "USD-NGN", "side": "buy", "type": "limit", "status": "filled", "price": "1545.00", "quantity": "100.00", "filled_quantity": "100.00", "executed_price": "1545.00", "base_asset": "USD", "quote_asset": "NGN", "created_at": "2026-01-14T15:06:30Z", "updated_at": "2026-01-14T15:08:15Z" } } ``` ### Common Fields Unique identifier for this event (UUID format) **Use for:** Idempotency (deduplicate multiple deliveries) Event type **Values:** `order.created`, `order.filled`, `order.partially_filled`, `order.cancelled`, `order.rejected`, `trade.executed`, `deposit.completed`, `crypto.deposit.completed` When event occurred (RFC3339 timestamp) Event-specific data (varies by event type) ## Event-Specific Payloads **Sent when:** New order accepted by matching engine ```json { "event_id": "550e8400-e29b-41d4-a716-446655440000", "type": "order.created", "timestamp": "2026-01-14T15:06:30Z", "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "symbol": "USD-NGN", "side": "buy", "type": "limit", "status": "open", "price": "1545.00", "quantity": "100.00", "filled_quantity": "0.00", "base_asset": "USD", "quote_asset": "NGN", "created_at": "2026-01-14T15:06:30Z", "updated_at": "2026-01-14T15:06:30Z" } } ``` **Next steps:** Monitor for `order.filled`, `order.partially_filled`, or `order.cancelled` **Sent when:** Order completely filled ```json { "event_id": "550e8400-e29b-41d4-a716-446655440001", "type": "order.filled", "timestamp": "2026-01-14T15:08:15Z", "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "symbol": "USD-NGN", "side": "buy", "type": "limit", "status": "filled", "price": "1545.00", "quantity": "100.00", "filled_quantity": "100.00", "executed_price": "1545.00", "base_asset": "USD", "quote_asset": "NGN", "created_at": "2026-01-14T15:06:30Z", "updated_at": "2026-01-14T15:08:15Z" } } ``` **Action:** Update UI to show completed order, refresh balances **Sent when:** Order partially executed, still open ```json { "event_id": "550e8400-e29b-41d4-a716-446655440002", "type": "order.partially_filled", "timestamp": "2026-01-14T15:07:30Z", "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "symbol": "USD-NGN", "side": "buy", "type": "limit", "status": "partially_filled", "price": "1545.00", "quantity": "100.00", "filled_quantity": "50.00", "executed_price": "1545.00", "base_asset": "USD", "quote_asset": "NGN", "created_at": "2026-01-14T15:06:30Z", "updated_at": "2026-01-14T15:07:30Z" } } ``` **Action:** Update order status in UI, show partial fill progress **Sent when:** Order cancelled by user or system ```json { "event_id": "550e8400-e29b-41d4-a716-446655440003", "type": "order.cancelled", "timestamp": "2026-01-14T15:10:00Z", "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "symbol": "USD-NGN", "side": "buy", "type": "limit", "status": "cancelled", "price": "1545.00", "quantity": "100.00", "filled_quantity": "50.00", "base_asset": "USD", "quote_asset": "NGN", "created_at": "2026-01-14T15:06:30Z", "updated_at": "2026-01-14T15:10:00Z", "cancelled_at": "2026-01-14T15:10:00Z" } } ``` **Sent when:** Order rejected by validation or matching engine ```json { "event_id": "550e8400-e29b-41d4-a716-446655440004", "type": "order.rejected", "timestamp": "2026-01-14T15:06:31Z", "data": { "id": "550e8400-e29b-41d4-a716-446655440001", "symbol": "USD-NGN", "side": "buy", "type": "limit", "status": "rejected", "price": "1545.00", "quantity": "100.00", "filled_quantity": "0.00", "base_asset": "USD", "quote_asset": "NGN", "created_at": "2026-01-14T15:06:31Z", "updated_at": "2026-01-14T15:06:31Z", "rejection_reason": "insufficient_balance" } } ``` **Rejection reasons:** `insufficient_balance`, `invalid_price`, `invalid_quantity`, `market_closed`, `rate_limit_exceeded` **Sent when:** A trade is executed involving your order ```json { "event_id": "660e8400-e29b-41d4-a716-446655440000", "type": "trade.executed", "timestamp": "2026-01-14T15:08:15Z", "data": { "id": "660e8400-e29b-41d4-a716-446655440000", "order_id": "550e8400-e29b-41d4-a716-446655440000", "symbol": "USD-NGN", "side": "buy", "price": "1545.00", "quantity": "50.00", "total_value": "77250.00", "fee": "15.45", "is_maker": false, "base_asset": "USD", "quote_asset": "NGN", "counterparty_order_id": "770e8400-e29b-41d4-a716-446655440000", "executed_at": "2026-01-14T15:08:15Z" } } ``` **is_maker:** `true` if your order was resting on the book (maker), `false` if your order matched existing orders (taker) **Sent when:** An NGN bank-transfer deposit settles to your Daya Pro balance ```json { "event_id": "770e8400-e29b-41d4-a716-446655440000", "type": "deposit.completed", "timestamp": "2026-05-06T14:00:09Z", "data": { "id": "11111111-1111-1111-1111-111111111111", "type": "deposit", "method": "bank_transfer", "status": "completed", "amount_ngn": "500000.00", "currency": "NGN", "payment_provider": "flutterwave", "provider_transaction_id": "FLW-RFR-9001", "originator_name": "Jane Doe", "matching_reference": "DAYA-REF-001", "reference": "INV-9001", "narration": "Customer top-up", "created_at": "2026-05-06T14:00:00Z", "completed_at": "2026-05-06T14:00:09Z" } } ``` Only fires on the terminal `completed` state. On-chain deposits use `crypto.deposit.completed`. **Sent when:** A confirmed on-chain deposit is credited to your Daya Pro balance ```json { "event_id": "880e8400-e29b-41d4-a716-446655440000", "type": "crypto.deposit.completed", "timestamp": "2026-06-05T12:00:09Z", "data": { "transaction_id": "11111111-1111-1111-1111-111111111111", "status": "completed", "asset": "USDT", "amount": "50.000000", "fee_amount": "0.000000", "fee_asset": "USDT", "blockchain": "ethereum", "transaction_hash": "0xhash456", "from_address": "0xsource", "to_address": "0x1234567890abcdef1234567890abcdef12345678", "created_at": "2026-06-05T11:59:30Z", "completed_at": "2026-06-05T12:00:09Z" } } ``` The payload uses normalized Daya fields and does not include raw provider references. ## Delivery Guarantees Webhooks may be delivered **multiple times**. Your endpoint must handle duplicate deliveries using `event_id` for idempotency. Events may arrive out of order. Use `created_at` timestamps to order events client-side. If your endpoint returns non-2xx status or times out, Daya retries with exponential backoff: | Attempt | Delay After Previous | |---------|---------------------| | 1 | 10 seconds | | 2 | 30 seconds | | 3 | 1 minute | | 4 | 5 minutes | | 5 | 15 minutes | | 6 | 30 minutes | | 7 | 1 hour | | 8 | 2 hours | | 9 | 4 hours | | 10 | 8 hours | After 10 failed attempts, delivery is marked as failed and the webhook may be automatically disabled. Your endpoint must respond within **30 seconds**. Longer responses will timeout and trigger retries. Webhooks are automatically disabled after **10 consecutive delivery failures**. Contact support to re-enable a disabled Pro webhook endpoint. There is no manual redispatch endpoint in this version. Use [Get Webhook Deliveries](/pro/api-reference/get-webhook-deliveries) to inspect delivery logs and retry status. ## Webhook Verification All webhooks include an HMAC-SHA256 signature in the `X-Webhook-Signature` header with a `sha256=` prefix. **Always verify** signatures to prevent spoofing. ``` X-Webhook-Signature: sha256=a8f5f167f44f4964e6c998dee827110c447be52d40d67b6a60b78c1e3e01b7e8 ``` See [Webhook Verification](/pro/webhooks/verification) for implementation details. ## Implementing a Webhook Endpoint ### Required Response Your endpoint must: 1. **Verify signature** (see [Verification](/pro/webhooks/verification)) 2. **Return 2xx status** to acknowledge receipt 3. **Process quickly** (< 10 seconds) or queue for async processing ### Example Implementation ```javascript Express.js const express = require('express'); const crypto = require('crypto'); const app = express(); app.post('/webhooks/daya-pro', express.raw({ type: 'application/json' }), (req, res) => { const signatureHeader = req.headers['x-webhook-signature']; const payload = req.body.toString('utf8'); // 1. Verify signature (strip "sha256=" prefix) const signature = signatureHeader?.replace('sha256=', ''); if (!verifySignature(payload, signature, process.env.DAYA_PRO_WEBHOOK_SECRET)) { return res.status(401).send('Invalid signature'); } // 2. Parse and check idempotency const event = JSON.parse(payload); if (isProcessed(event.event_id)) { return res.status(200).send('Already processed'); } // 3. Handle event switch (event.type) { case 'order.filled': handleOrderFilled(event.data); break; case 'order.partially_filled': handlePartialFill(event.data); break; case 'trade.executed': handleTradeExecuted(event.data); break; case 'crypto.deposit.completed': handleCryptoDepositCompleted(event.data); break; // ... handle other events } markAsProcessed(event.event_id); res.status(200).send('OK'); }); function verifySignature(payload, signature, secret) { const expectedSignature = crypto .createHmac('sha256', secret) .update(payload) .digest('hex'); return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expectedSignature) ); } ``` ```python Flask from flask import Flask, request, jsonify import hmac import hashlib app = Flask(__name__) @app.route('/webhooks/daya-pro', methods=['POST']) def handle_webhook(): signature_header = request.headers.get('X-Webhook-Signature', '') payload = request.get_data() # 1. Verify signature (strip "sha256=" prefix) signature = signature_header.replace('sha256=', '') if not verify_signature(payload, signature, DAYA_PRO_WEBHOOK_SECRET): return jsonify({'error': 'Invalid signature'}), 401 # 2. Parse and check idempotency event = request.json if is_processed(event['event_id']): return jsonify({'status': 'already_processed'}), 200 # 3. Handle event event_type = event['type'] data = event['data'] if event_type == 'order.filled': handle_order_filled(data) elif event_type == 'order.partially_filled': handle_partial_fill(data) elif event_type == 'trade.executed': handle_trade_executed(data) elif event_type == 'crypto.deposit.completed': handle_crypto_deposit_completed(data) mark_as_processed(event['event_id']) return jsonify({'status': 'ok'}), 200 def verify_signature(payload, signature, secret): expected = hmac.new( secret.encode(), payload, hashlib.sha256 ).hexdigest() return hmac.compare_digest(signature, expected) ``` ```go Go package main import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "io" "net/http" "strings" ) func handleWebhook(w http.ResponseWriter, r *http.Request) { signatureHeader := r.Header.Get("X-Webhook-Signature") payload, _ := io.ReadAll(r.Body) // 1. Verify signature (strip "sha256=" prefix) signature := strings.TrimPrefix(signatureHeader, "sha256=") if !verifySignature(payload, signature, dayaProWebhookSecret) { http.Error(w, "Invalid signature", http.StatusUnauthorized) return } // 2. Parse event var event WebhookEvent json.Unmarshal(payload, &event) // 3. Check idempotency if isProcessed(event.EventID) { w.WriteHeader(http.StatusOK) return } // 4. Handle event switch event.Type { case "order.filled": handleOrderFilled(event.Data) case "order.partially_filled": handlePartialFill(event.Data) case "trade.executed": handleTradeExecuted(event.Data) case "crypto.deposit.completed": handleCryptoDepositCompleted(event.Data) } markAsProcessed(event.EventID) w.WriteHeader(http.StatusOK) } func verifySignature(payload []byte, signature string, secret string) bool { mac := hmac.New(sha256.New, []byte(secret)) mac.Write(payload) expectedSignature := hex.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(signature), []byte(expectedSignature)) } ``` ## Best Practices **Always** verify `X-Webhook-Signature` to prevent spoofing attacks. Remember to strip the `sha256=` prefix before comparing. Use `event_id` to deduplicate. Store processed event IDs in your database. ```sql CREATE TABLE processed_webhook_events ( event_id VARCHAR(255) PRIMARY KEY, processed_at TIMESTAMP ); ``` Acknowledge receipt immediately (< 1 second). Queue heavy processing asynchronously. Events may arrive out of order. Use `timestamp` field and order status to reconcile. Periodically call [List Orders](/pro/api-reference/list-orders) to reconcile state in case webhooks are missed. ## Testing Webhooks ### Local Testing For local development, use tools like [ngrok](https://ngrok.com): ```bash # Start ngrok ngrok http 3000 # Share this URL with support for webhook setup https://abc123.ngrok.xyz/webhooks/daya-pro ``` Use ngrok's web interface (http://localhost:4040) to inspect webhook payloads during development. ## Troubleshooting **Possible causes:** - Firewall blocking Daya's IPs - Endpoint returning non-2xx status - SSL certificate issues **Fix:** Check endpoint logs, ensure HTTPS, verify firewall rules **Expected behavior:** At-least-once delivery means duplicates are possible **Fix:** Implement idempotency using `event_id` **Cause:** Endpoint taking > 30 seconds to respond **Fix:** Return 200 immediately, queue processing asynchronously **Cause:** Wrong secret, payload manipulation, or not stripping `sha256=` prefix **Fix:** Verify you're using correct webhook secret and stripping the `sha256=` prefix from the `X-Webhook-Signature` header before comparing ## Next Steps Detailed event schemas Implement HMAC verification #### Webhook Events Path: /pro/webhooks/events Description: Detailed schemas for all Pro Trading API webhook event types ## Event Types Daya Pro sends webhooks for the following trading events: | Event | Terminal? | Description | |-------|-----------|-------------| | `order.created` | No | Order accepted by matching engine | | `order.filled` | Yes | Order completely filled | | `order.partially_filled` | No | Order partially filled, still open | | `order.cancelled` | Yes | Order cancelled | | `order.rejected` | Yes | Order rejected | | `trade.executed` | N/A | Trade executed for your order | | `deposit.completed` | N/A | An NGN bank-transfer deposit settled to your Daya Pro balance | | `crypto.deposit.completed` | N/A | A confirmed on-chain deposit was credited to your Daya Pro balance | ## Order Events All order events share these common fields in the `data` object: Unique order identifier (UUID) Trading pair (e.g., `USD-NGN`) Order side: `buy` or `sell` Order type: `market` or `limit` Order status Limit price (omitted for market orders) Total order quantity Quantity filled so far Volume-weighted average execution price (present when partially or fully filled) Base asset of the trading pair (e.g., `USD`) Quote asset of the trading pair (e.g., `NGN`) ISO 8601 timestamp when order was created ISO 8601 timestamp when order was last updated --- ### order.created Sent when a new order is accepted by the matching engine. **Status:** `open` --- ### order.filled Sent when an order is completely filled. **Status:** `filled` **Additional field:** - `executed_price` - Volume-weighted average execution price --- ### order.partially_filled Sent when an order is partially filled but still has remaining quantity. **Status:** `partially_filled` **Additional field:** - `executed_price` - Average execution price so far --- ### order.cancelled Sent when an order is cancelled. **Status:** `cancelled` **Additional field:** ISO 8601 timestamp when cancelled --- ### order.rejected Sent when an order is rejected. **Status:** `rejected` **Additional field:** Reason for rejection **Values:** - `insufficient_balance` - Not enough funds - `invalid_price` - Price outside allowed range - `invalid_quantity` - Quantity below minimum or above maximum - `market_closed` - Market not accepting orders - `rate_limit_exceeded` - Too many orders --- ## Trade Events ### trade.executed Sent when a trade is executed involving your order. You may receive multiple `trade.executed` events for a single order if it fills across multiple price levels or counterparties. Unique trade identifier (UUID) Your order ID that was filled Trading pair Your side in this trade: `buy` or `sell` Whether you were the maker in this trade - `true` - Your order was resting on the order book (maker) - `false` - Your order matched against resting orders (taker) Execution price Trade quantity Total value in quote currency Trading fee charged Base asset of the trading pair Quote asset of the trading pair The order ID of the counterparty in this trade ISO 8601 timestamp of execution --- ## Deposit Events ### deposit.completed Sent when an NGN bank-transfer deposit settles to your Daya Pro balance. Useful for triggering downstream ledger updates without polling. Bank deposit events fire only on the terminal `completed` state. On-chain deposits use `crypto.deposit.completed`. Deposit transaction identifier Always `deposit` Funding rail used. Always `bank_transfer` for this event Always `completed` for this event Settled amount in NGN as a decimal string Fee charged in NGN, if any Currency code of the original deposit. Always `NGN` for this event Underlying provider that settled the deposit (e.g. `flutterwave`) Provider-side reference for reconciliation Transaction reference (when provider supplies one) Name on the source bank account, when available Internal reference used to attribute the deposit to your account User-supplied or generated reference shown on statements Free-form narration from the source institution Human-readable summary ISO 8601 timestamp when the deposit was first observed ISO 8601 timestamp when the deposit finished settling ### crypto.deposit.completed Sent after a confirmed on-chain deposit is credited to your Daya Pro balance. This event is for account-level crypto collection. It is not a per-customer wallet creation or custody event. Daya transaction identifier Always `completed` for this event Partner-facing asset symbol, such as `USDT` Credited amount as a full-precision decimal string for the asset Fee amount as a full-precision decimal string for the asset Asset used to charge the fee. This currently matches `asset` Canonical chain key, such as `ethereum` or `polygon` On-chain transaction hash when available Source address when available Account-level deposit address that received the funds ISO 8601 timestamp when the deposit transaction was first observed ISO 8601 timestamp when the deposit was credited ```json { "event_id": "880e8400-e29b-41d4-a716-446655440000", "type": "crypto.deposit.completed", "timestamp": "2026-06-05T12:00:09Z", "data": { "transaction_id": "11111111-1111-1111-1111-111111111111", "status": "completed", "asset": "USDT", "amount": "50.000000", "fee_amount": "0.000000", "fee_asset": "USDT", "blockchain": "ethereum", "transaction_hash": "0xhash456", "from_address": "0xsource", "to_address": "0x1234567890abcdef1234567890abcdef12345678", "created_at": "2026-06-05T11:59:30Z", "completed_at": "2026-06-05T12:00:09Z" } } ``` --- ## Order Status Flow ``` +---> rejected | placed ---> created +---> filled | +---> partially_filled ---> filled | | | +---> cancelled | +---> cancelled ``` ## Next Steps Delivery guarantees and implementation Verify webhook authenticity #### Webhook Verification Path: /pro/webhooks/verification Description: Verify Pro Trading API webhook authenticity using HMAC signatures ## Overview All Daya Pro webhooks include an `X-Webhook-Signature` header containing an HMAC-SHA256 signature of the payload with a `sha256=` prefix. **Always verify** this signature to ensure the webhook came from Daya. Never process unverified webhooks. Attackers could send fake webhooks to manipulate your trading system. ## Signature Header ``` X-Webhook-Signature: sha256=a8f5f167f44f4964e6c998dee827110c447be52d40d67b6a60b78c1e3e01b7e8 ``` ## Additional Headers Daya also sends these headers with every webhook request: | Header | Description | |--------|-------------| | `X-Webhook-Event` | Event type (e.g., `order.filled`) | | `X-Webhook-ID` | Unique event identifier (UUID) | | `X-Webhook-Timestamp` | Event timestamp (RFC3339) | | `User-Agent` | `Daya-Webhook/1.0` | ## Verification Algorithm 1. Get raw request body as string 2. Compute HMAC-SHA256 using your webhook secret 3. Strip the `sha256=` prefix from `X-Webhook-Signature` header 4. Compare computed signature with the extracted signature 5. Use timing-safe comparison to prevent timing attacks ## Implementation Examples ```javascript Node.js const crypto = require('crypto'); function verifyWebhookSignature(payload, signature, secret) { // Compute expected signature const expectedSignature = crypto .createHmac('sha256', secret) .update(payload, 'utf8') .digest('hex'); // Timing-safe comparison try { return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expectedSignature) ); } catch (error) { return false; } } // Express.js middleware app.post('/webhooks/daya-pro', express.raw({ type: 'application/json' }), (req, res) => { const signatureHeader = req.headers['x-webhook-signature'] || ''; const signature = signatureHeader.replace('sha256=', ''); // Strip prefix const payload = req.body.toString('utf8'); if (!verifyWebhookSignature(payload, signature, process.env.DAYA_PRO_WEBHOOK_SECRET)) { return res.status(401).json({ error: 'Invalid signature' }); } // Process webhook... const event = JSON.parse(payload); res.status(200).send('OK'); }); ``` ```python Python import hmac import hashlib def verify_webhook_signature(payload: bytes, signature: str, secret: str) -> bool: """Verify webhook HMAC signature""" expected_signature = hmac.new( secret.encode('utf-8'), payload, hashlib.sha256 ).hexdigest() # Timing-safe comparison return hmac.compare_digest(signature, expected_signature) # Flask example from flask import Flask, request, jsonify @app.route('/webhooks/daya-pro', methods=['POST']) def handle_webhook(): signature_header = request.headers.get('X-Webhook-Signature', '') signature = signature_header.replace('sha256=', '') # Strip prefix payload = request.get_data() if not verify_webhook_signature(payload, signature, DAYA_PRO_WEBHOOK_SECRET): return jsonify({'error': 'Invalid signature'}), 401 # Process webhook... event = request.json return jsonify({'status': 'ok'}), 200 ``` ```go Go package main import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "io" "net/http" "strings" ) func verifyWebhookSignature(payload []byte, signature string, secret string) bool { mac := hmac.New(sha256.New, []byte(secret)) mac.Write(payload) expectedSignature := hex.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(signature), []byte(expectedSignature)) } func handleWebhook(w http.ResponseWriter, r *http.Request) { signatureHeader := r.Header.Get("X-Webhook-Signature") signature := strings.TrimPrefix(signatureHeader, "sha256=") // Strip prefix payload, _ := io.ReadAll(r.Body) if !verifyWebhookSignature(payload, signature, dayaProWebhookSecret) { http.Error(w, "Invalid signature", http.StatusUnauthorized) return } // Process webhook... w.WriteHeader(http.StatusOK) } ``` ```php PHP 'Invalid signature']); exit; } // Process webhook... $event = json_decode($payload, true); http_response_code(200); echo json_encode(['status' => 'ok']); ``` ## Important Notes **Critical:** Compute HMAC on the **raw request body** before parsing JSON. Parsing changes whitespace and ordering, breaking the signature. ```javascript // Correct: Use raw body app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => { const payload = req.body.toString('utf8'); verify(payload, signature, secret); }); // Wrong: JSON.stringify changes format app.post('/webhooks', express.json(), (req, res) => { const payload = JSON.stringify(req.body); // Wrong! verify(payload, signature, secret); }); ``` Regular string comparison (==) is vulnerable to timing attacks. Use constant-time comparison: - Node.js: `crypto.timingSafeEqual()` - Python: `hmac.compare_digest()` - Go: `hmac.Equal()` - PHP: `hash_equals()` - Store webhook secret in environment variables - Never commit secrets to version control - Rotate secrets regularly - Use different secrets for different environments ## Testing Verification Generate test signatures for local testing: ```bash CLI # Generate test signature echo -n '{"event":"order.filled","event_id":"evt_pro_test"}' | \ openssl dgst -sha256 -hmac "your_webhook_secret" | \ awk '{print $2}' ``` ```javascript Node.js const crypto = require('crypto'); function generateTestSignature(payload, secret) { return crypto .createHmac('sha256', secret) .update(payload, 'utf8') .digest('hex'); } const payload = '{"event":"order.filled","event_id":"evt_pro_test"}'; const signature = generateTestSignature(payload, 'your_webhook_secret'); console.log(signature); ``` ## Common Issues **Possible causes:** - Using wrong webhook secret - Not using raw request body - Character encoding issues **Debug:** ```javascript console.log('Received signature:', signature); console.log('Expected signature:', expectedSignature); console.log('Payload length:', payload.length); console.log('Secret (first 4 chars):', secret.substring(0, 4)); ``` **Cause:** Parsing JSON before verification **Fix:** Always compute HMAC on raw body, then parse JSON **Cause:** This shouldn't happen - same payload = same signature **Debug:** Log and compare payloads between requests ## Next Steps Delivery guarantees and implementation Event schemas and payloads