A JSON REST API for accepting crypto payments: hosted checkout, invoices,
signed webhooks, balances and payouts. All 24 endpoints below are generated
from the OpenAPI contract the gateway is built against, so this page cannot drift from what
the server actually accepts.
DUALKEEP is a custodial cryptocurrency payment gateway. It issues a fresh deposit address per invoice, monitors the chain for incoming payments, confirms them against a per-currency confirmation policy, credits the merchant's USD ledger balance immediately on confirmation (minus the service fee), delivers signed webhooks, consolidates deposits into a hot treasury wallet in the background, and pays out merchant withdrawals in periodic batches.
The fastest integration is the hosted checkout, and it is three steps:
POST /api/v1/payment/initiate with your order id and a USD amount.
Redirect the customer to https://dualkeep.com/pay/{payment_id}, where they pick a coin, get an address and a QR code, and pay.
Take the signed payment.paid webhook on your callback_url and fulfil the order. Never fulfil on the browser redirect alone.
Build your own payment UI instead with POST /api/v1/payment, which returns a deposit address and exact amount for one chosen currency.
API keys come with a merchant account and are generated on the Integration page of the dashboard. If you use WooCommerce, Joomla or Drupal Commerce, install the plugin and paste the key: it performs the calls above for you.
Every endpoint under /api/v1 requires an API key sent in the X-API-Key request header. Keys are generated on the Integration page of the merchant dashboard and are shown only once. The server stores only a bcrypt hash of the key plus an indexed public prefix.
The /health endpoint is public (unauthenticated), and so are the buyer checkout endpoints (/api/v1/payment/{payment_id}/asset|rate|fix-price|wallet|status|email): the customer's browser holds no API key, so the unguessable UUID payment_id acts as the capability token. Public endpoints are rate-limited by client IP. POST /api/v1/payment/initiate itself is merchant-authenticated.
All cryptocurrency amounts are decimal strings, never floating-point, so full precision survives the round trip. Parse them with a decimal type - 0.1 + 0.2 in a binary float is not 0.3, and the difference is money. Fiat amounts on input accept a JSON number or a decimal string; the exact digits you send are preserved.
Branch on code, not on message: codes are stable, messages are written for humans and may be reworded. Each endpoint below lists the codes it can return.
Code
Status
Meaning
validation_error
400
The request was malformed or failed validation.
unsupported_currency
400
That currency code is not offered.
unauthorized
401
Missing or invalid API key.
forbidden
403
The account is suspended.
not_found
404
No such resource, or it belongs to another merchant.
insufficient_balance
402
The available balance does not cover the request.
invalid_payment_state
409
The resource is not in a state that permits the operation.
rate_limited
429
Slow down and retry (see below).
upstream_error
502
A blockchain or rate provider failed. Safe to retry.
Merchant calls are limited per API key; public checkout calls are limited per client IP. The default allowance is 100 requests per minute with a burst of 20. Over the limit you get 429 rate_limited - back off and retry rather than looping, and poll payment status on an interval rather than continuously.
Creating a payment twice by accident should not charge a customer twice, so the create calls are idempotent on your own reference: POST /api/v1/payment/initiate on order_id and POST /api/v1/payment on merchant_order_id. Repeating a create with the same reference returns the session that already exists instead of opening a second one, and the uniqueness is enforced in the database, so two concurrent retries still resolve to one payment.
Withdrawals take an explicit idempotency_key (or the Idempotency-Key header) for the same reason.
Call GET /api/v1/currencies for the live list: which assets a deployment offers depends on the chain providers it is configured for, so any list written down here goes stale. Codes carry their network where an asset exists on several - USDT_ERC20 (Ethereum), USDT_TRC20 (TRON), USDC_SPL (Solana), and so on.
Invoice created, awaiting first on-chain sighting.
confirming
Fully funded, awaiting required confirmation depth. Never expired.
partially_paid
Received less than the requested amount.
paid
Confirmed portion covers the requested amount; webhook payment.paid fired. Never regresses.
expired
pending/partially_paid past expires_at. Monitored for a 24h grace window; a late full payment transitions it to paid.
failed
Unrecoverable error.
refunded
Fully refunded back to the customer.
amount_received is the sum of all incoming transactions (split payments across several transactions complete). A payment becomes paid only when the confirmed portion covers the requested amount.
When an invoice has a callback_url, DUALKEEP POSTs a WebhookPayload JSON body on state changes. Each request includes:
X-CryptoGate-Event: the event name (payment.paid, payment.expired).
X-CryptoGate-Signature: lowercase-hex HMAC-SHA256 of the raw request body, keyed by the merchant webhook secret.
X-CryptoGate-Delivery: the numeric delivery id.
The signing key is the per-merchant webhook_secret (whsec_<64 hex>) shown once when the merchant account is provisioned. Keep it server-side, next to the API key, and never ship either in frontend code.
Verify the signature against the raw request body before trusting the payload - not against a re-serialised object, whose byte order will differ:
import crypto from "node:crypto";
// express: app.post(path, express.raw({type: "application/json"}), handler)
function verify(rawBody, header, secret) {
const expected = crypto.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
const a = Buffer.from(expected, "utf8");
const b = Buffer.from(header || "", "utf8");
// Length-check first: timingSafeEqual throws on a length mismatch.
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Deliveries are retried with exponential backoff until your endpoint returns 2xx, so the same event can arrive more than once: key your handler on payment_id and ignore an event you have already applied. Answer 2xx as soon as you have stored the event and do the slow work afterwards - a handler that takes too long looks like a failure and gets replayed.
If you sell through a supported platform there is no need to call this API directly. Install the DUALKEEP payment gateway plugin for WooCommerce, Joomla or Drupal Commerce, paste your API key, and crypto appears as a payment method at checkout. The plugins use the hosted-checkout flow described above.
Derives a fresh receiving address from the merchant's HD wallet, persists an invoice, and returns it together with a QR code and a payment URI. Supply either amount (crypto) or fiat_amount + fiat_currency (the gateway converts to crypto at the current rate).
merchant_order_id is an idempotency key: creating again with the same value returns the existing payment (enforced by a unique constraint, so concurrent duplicate creates also resolve to one payment).
XRP invoices use the merchant's shared XRP hot account plus a per-invoice destination_tag (also encoded in payment_uri as ripple:<addr>?amount=X&dt=N); customers must include the tag.
XLM invoices likewise use the merchant's shared XLM hot account plus a per-invoice ID memo, returned as memo / memo_type (also encoded in payment_uri as a SEP-0007 URI: web+stellar:pay?destination=G...&amount=X&memo=N&memo_type=MEMO_ID); customers must include the memo (an ID memo, or a text memo with exactly the numeric ID; both are accepted).
Body parameters
Name
Type
Description
merchant_order_id
string
Your own order reference (optional, indexed for lookup).
currencyrequired
string
A supported currency code, e.g. BTC, ETH, USDT_TRC20, USDC_SPL. Call GET /api/v1/currencies for the live list, including network and confirmation policy per asset.
amount
string
Crypto amount as a decimal string (preferred). Either amount or
fiat_amount + fiat_currency must be supplied.
fiat_amount
number or string
Fiat amount; converted to crypto at the current rate. Accepts a JSON number or a decimal string; the exact digits are preserved (no binary floating point in the money path).
fiat_currency
string
Fiat currency for fiat_amount (e.g. USD).
callback_url
string (url)
Webhook URL notified on payment events. Validated: in production
(APP_ENV=production) it must be https and must not target
private/loopback/internal hosts.
success_url
string (url)
Same URL validation as callback_url.
cancel_url
string (url)
Same URL validation as callback_url.
customer_email
string
expires_in
integer
Invoice validity in seconds (default 3600). Values outside
60..2592000 (30 days) are rejected with 400 validation_error.
Errors
Status
Code
When
400
validation_error
The request was malformed or failed validation.
401
unauthorized
Missing or invalid API key.
429
rate_limited
Too many requests for this API key.
500
internal_error
Unexpected server error.
502
upstream_error
A blockchain or rate provider failed or is unavailable.
Opens a checkout session for an order priced in USD. No currency or deposit address is assigned yet - the customer selects the asset with POST /api/v1/payment/{payment_id}/asset.
Idempotent on (merchant, order_id): repeating the call with the same order_id returns the existing session (enforced by a unique constraint, so concurrent duplicate initiations also resolve to one session).
The response lists every payable asset with its current USD rate (rate_usd is omitted per asset when the rate oracle is unavailable) and uses the buyer-facing status vocabulary (waiting on creation).
Body parameters
Name
Type
Description
order_idrequired
string
Your order reference. Idempotency key: repeating the call with the
same value returns the existing session.
product_namerequired
string
Shown on the payment page and in the confirmation email.
amount_usdrequired
number or string
USD amount. Accepts a JSON number or a decimal string; the exact digits are preserved (no binary floating point in the money path). Must be positive.
email
string
Pre-registers the customer for a confirmation email (max 255 chars).
callback_url
string (url)
Webhook URL notified on payment events. Same validation as the
classic callback_url (https + no private/internal hosts in
production).
success_url
string (url)
Redirect target on success. Same URL validation as callback_url.
expires_in
integer
Session validity in seconds (default 3600). Values outside
60..2592000 (30 days) are rejected with 400 validation_error.
The customer picks the crypto asset. The gateway derives the per-payment deposit address (XRP/XLM use the merchant's shared hot account plus the per-payment destination tag / ID memo), converts the USD amount at the live rate, and starts the rate window (CHECKOUT_RATE_TTL, default 15 minutes).
The asset can be re-selected as long as no funds have been seen; once a deposit is observed (or the session is settled/expired) it is locked (409 invalid_payment_state).
Public: no API key - the unguessable payment_id is the capability token; rate-limited by client IP.
Path parameters
Name
Type
Description
payment_idrequired
string
The DUALKEEP payment id (e.g. pay_2f3c1b7e).
Body parameters
Name
Type
Description
assetrequired
string
A supported currency code, e.g. BTC, ETH, USDT_TRC20, USDC_SPL. Call GET /api/v1/currencies for the live list, including network and confirmation policy per asset.
Errors
Status
Code
When
400
validation_error
The request was malformed or failed validation.
404
not_found
The requested resource was not found.
409
invalid_payment_state
The payment is not in a state that permits this operation.
429
rate_limited
Too many requests for this API key.
500
internal_error
Unexpected server error.
502
upstream_error
A blockchain or rate provider failed or is unavailable.
Returns the current conversion for the payment. For the selected asset, a still-valid (or fixed) rate window returns the stored rate; a lapsed window refreshes the rate at the live price and persists it (rates are no longer refreshed once funds have been observed). Passing a different asset than the selected one returns a non-persisted preview (preview: true).
Public: no API key; rate-limited by client IP.
Path parameters
Name
Type
Description
payment_idrequired
string
The DUALKEEP payment id (e.g. pay_2f3c1b7e).
Query parameters
Name
Type
Description
asset
string
A supported currency code. Defaults to the selected asset; a
different code returns a non-persisted preview. Required when no
asset has been selected yet.
Errors
Status
Code
When
400
validation_error
The request was malformed or failed validation.
404
not_found
The requested resource was not found.
429
rate_limited
Too many requests for this API key.
500
internal_error
Unexpected server error.
502
upstream_error
A blockchain or rate provider failed or is unavailable.
curl -X GET "https://dualkeep.com/api/v1/payment/pay_2f3c1b7e/rate"
const res = await fetch("https://dualkeep.com/api/v1/payment/pay_2f3c1b7e/rate", {
method: "GET",
});
if (!res.ok) throw new Error(`DUALKEEP ${res.status}: ${await res.text()}`);
const data = await res.json();
import os, requests
res = requests.get(
"https://dualkeep.com/api/v1/payment/pay_2f3c1b7e/rate",
timeout=30,
)
res.raise_for_status()
data = res.json()
package main
import (
"encoding/json"
"fmt"
"net/http"
)
func main() {
req, err := http.NewRequest("GET", "https://dualkeep.com/api/v1/payment/pay_2f3c1b7e/rate", nil)
if err != nil {
panic(err)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var data map[string]any
if err := json.NewDecoder(res.Body).Decode(&data); err != nil {
panic(err)
}
fmt.Println(res.StatusCode, data)
}
200 OK
{
"payment_id": "pay_6f0c2a4e-6d9b-4f0e-9df3-6b1d2c8a7e51",
"asset": "BTC",
"network": "native",
"amount_usd": "49.99",
"rate": "60000.00",
"crypto_amount": "0.00083317",
"rate_fixed": false,
"rate_expires_at": "2026-06-24T12:15:00Z",
"confirmations_required": 6,
"rules": [
"Send the exact amount shown; a short payment stays incomplete until topped up.",
"The payment is confirmed after 6 network confirmations."
]
}
Locks the current rate for the fix window (CHECKOUT_FIX_TTL, default 10 minutes) and disables auto-refresh until it lapses. Requires an asset to be selected and the session to still be awaiting funds.
Public: no API key; rate-limited by client IP.
Path parameters
Name
Type
Description
payment_idrequired
string
The DUALKEEP payment id (e.g. pay_2f3c1b7e).
Errors
Status
Code
When
404
not_found
The requested resource was not found.
409
invalid_payment_state
The payment is not in a state that permits this operation.
429
rate_limited
Too many requests for this API key.
500
internal_error
Unexpected server error.
502
upstream_error
A blockchain or rate provider failed or is unavailable.
curl -X POST "https://dualkeep.com/api/v1/payment/pay_2f3c1b7e/fix-price"
const res = await fetch("https://dualkeep.com/api/v1/payment/pay_2f3c1b7e/fix-price", {
method: "POST",
});
if (!res.ok) throw new Error(`DUALKEEP ${res.status}: ${await res.text()}`);
const data = await res.json();
import os, requests
res = requests.post(
"https://dualkeep.com/api/v1/payment/pay_2f3c1b7e/fix-price",
timeout=30,
)
res.raise_for_status()
data = res.json()
package main
import (
"encoding/json"
"fmt"
"net/http"
)
func main() {
req, err := http.NewRequest("POST", "https://dualkeep.com/api/v1/payment/pay_2f3c1b7e/fix-price", nil)
if err != nil {
panic(err)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var data map[string]any
if err := json.NewDecoder(res.Body).Decode(&data); err != nil {
panic(err)
}
fmt.Println(res.StatusCode, data)
}
Returns the deposit details the payment page renders: the address, two QR codes (base64 PNG data URIs - one of the bare address, one of the payment URI with the amount), the payment URI, and the timers. For XRP the response carries destination_tag; for XLM memo / memo_type (always id).
Public: no API key; rate-limited by client IP.
Path parameters
Name
Type
Description
payment_idrequired
string
The DUALKEEP payment id (e.g. pay_2f3c1b7e).
Errors
Status
Code
When
404
not_found
The requested resource was not found.
409
invalid_payment_state
The payment is not in a state that permits this operation.
429
rate_limited
Too many requests for this API key.
500
internal_error
Unexpected server error.
curl -X GET "https://dualkeep.com/api/v1/payment/pay_2f3c1b7e/wallet"
const res = await fetch("https://dualkeep.com/api/v1/payment/pay_2f3c1b7e/wallet", {
method: "GET",
});
if (!res.ok) throw new Error(`DUALKEEP ${res.status}: ${await res.text()}`);
const data = await res.json();
import os, requests
res = requests.get(
"https://dualkeep.com/api/v1/payment/pay_2f3c1b7e/wallet",
timeout=30,
)
res.raise_for_status()
data = res.json()
package main
import (
"encoding/json"
"fmt"
"net/http"
)
func main() {
req, err := http.NewRequest("GET", "https://dualkeep.com/api/v1/payment/pay_2f3c1b7e/wallet", nil)
if err != nil {
panic(err)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var data map[string]any
if err := json.NewDecoder(res.Body).Decode(&data); err != nil {
panic(err)
}
fmt.Println(res.StatusCode, data)
}
Returns the session view the hosted checkout page boots from - the same shape as the initiate response (order details plus supported assets with current USD rates). The gateway also serves a ready-made checkout page at GET /checkout/{payment_id} driving these endpoints.
Path parameters
Name
Type
Description
payment_idrequired
string
The DUALKEEP payment id (e.g. pay_2f3c1b7e).
Errors
Status
Code
When
404
not_found
The requested resource was not found.
429
rate_limited
Too many requests for this API key.
curl -X GET "https://dualkeep.com/api/v1/payment/pay_2f3c1b7e/session"
const res = await fetch("https://dualkeep.com/api/v1/payment/pay_2f3c1b7e/session", {
method: "GET",
});
if (!res.ok) throw new Error(`DUALKEEP ${res.status}: ${await res.text()}`);
const data = await res.json();
import os, requests
res = requests.get(
"https://dualkeep.com/api/v1/payment/pay_2f3c1b7e/session",
timeout=30,
)
res.raise_for_status()
data = res.json()
package main
import (
"encoding/json"
"fmt"
"net/http"
)
func main() {
req, err := http.NewRequest("GET", "https://dualkeep.com/api/v1/payment/pay_2f3c1b7e/session", nil)
if err != nil {
panic(err)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var data map[string]any
if err := json.NewDecoder(res.Body).Decode(&data); err != nil {
panic(err)
}
fmt.Println(res.StatusCode, data)
}
The status the checkout frontend polls, in the buyer-facing vocabulary (waiting | processing | success | expired | failed). asset, network, and crypto_amount are empty until the customer selects an asset.
Public: no API key; rate-limited by client IP.
Path parameters
Name
Type
Description
payment_idrequired
string
The DUALKEEP payment id (e.g. pay_2f3c1b7e).
Errors
Status
Code
When
404
not_found
The requested resource was not found.
429
rate_limited
Too many requests for this API key.
500
internal_error
Unexpected server error.
curl -X GET "https://dualkeep.com/api/v1/payment/pay_2f3c1b7e/status"
const res = await fetch("https://dualkeep.com/api/v1/payment/pay_2f3c1b7e/status", {
method: "GET",
});
if (!res.ok) throw new Error(`DUALKEEP ${res.status}: ${await res.text()}`);
const data = await res.json();
import os, requests
res = requests.get(
"https://dualkeep.com/api/v1/payment/pay_2f3c1b7e/status",
timeout=30,
)
res.raise_for_status()
data = res.json()
package main
import (
"encoding/json"
"fmt"
"net/http"
)
func main() {
req, err := http.NewRequest("GET", "https://dualkeep.com/api/v1/payment/pay_2f3c1b7e/status", nil)
if err != nil {
panic(err)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var data map[string]any
if err := json.NewDecoder(res.Body).Decode(&data); err != nil {
panic(err)
}
fmt.Println(res.StatusCode, data)
}
The merchant's USD ledger balance, the portion locked by in-flight withdrawals (withdrawal_pending_usd), the plan, and the fee rate deducted from each confirmed payment. Every confirmed payment credits (usd value - fee) to the balance immediately.
available_usd is the balance net of the platform fee: the largest amount a withdrawal can be requested for, and the figure the dashboard shows as "available balance". Withdraw against that, not balance_usd.
Errors
Status
Code
When
401
unauthorized
Missing or invalid API key.
429
rate_limited
Too many requests for this API key.
500
internal_error
Unexpected server error.
curl -X GET "https://dualkeep.com/api/v1/merchant/balance" \
-H "X-API-Key: $DUALKEEP_API_KEY"
const res = await fetch("https://dualkeep.com/api/v1/merchant/balance", {
method: "GET",
headers: {
"X-API-Key": process.env.DUALKEEP_API_KEY,
},
});
if (!res.ok) throw new Error(`DUALKEEP ${res.status}: ${await res.text()}`);
const data = await res.json();
import os, requests
res = requests.get(
"https://dualkeep.com/api/v1/merchant/balance",
headers={"X-API-Key": os.environ["DUALKEEP_API_KEY"]},
timeout=30,
)
res.raise_for_status()
data = res.json()
package main
import (
"encoding/json"
"fmt"
"net/http"
"os"
)
func main() {
req, err := http.NewRequest("GET", "https://dualkeep.com/api/v1/merchant/balance", nil)
if err != nil {
panic(err)
}
req.Header.Set("X-API-Key", os.Getenv("DUALKEEP_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var data map[string]any
if err := json.NewDecoder(res.Body).Decode(&data); err != nil {
panic(err)
}
fmt.Println(res.StatusCode, data)
}
Creates a deposit request: a unique deposit address (or the shared hot account plus tag/memo matching on XRP, XLM and TON), the exact crypto amount at the live rate, a QR code, and a payment URI. The monitor detects the deposit and the balance is credited automatically once it confirms. Every supported asset is accepted.
Body parameters
Name
Type
Description
amount_usdrequired
string
USD amount to add to the balance.
assetrequired
string
A supported currency code, e.g. BTC, ETH, USDT_TRC20, USDC_SPL. Call GET /api/v1/currencies for the live list, including network and confirmation policy per asset.
Signed balance history with a running balance after every entry. The type is one of payment, topup, fee, adjustment, withdrawal, withdrawal_refund, and ref links the entry back to the payment, top-up or withdrawal that caused it.
Query parameters
Name
Type
Description
limit
integer
Page size (default 20, max 100). Defaults to 20.
offset
integer
Number of records to skip. Defaults to 0.
Errors
Status
Code
When
401
unauthorized
Missing or invalid API key.
429
rate_limited
Too many requests for this API key.
curl -X GET "https://dualkeep.com/api/v1/merchant/ledger?limit=20&offset=0" \
-H "X-API-Key: $DUALKEEP_API_KEY"
const res = await fetch("https://dualkeep.com/api/v1/merchant/ledger?limit=20&offset=0", {
method: "GET",
headers: {
"X-API-Key": process.env.DUALKEEP_API_KEY,
},
});
if (!res.ok) throw new Error(`DUALKEEP ${res.status}: ${await res.text()}`);
const data = await res.json();
import os, requests
res = requests.get(
"https://dualkeep.com/api/v1/merchant/ledger?limit=20&offset=0",
headers={"X-API-Key": os.environ["DUALKEEP_API_KEY"]},
timeout=30,
)
res.raise_for_status()
data = res.json()
package main
import (
"encoding/json"
"fmt"
"net/http"
"os"
)
func main() {
req, err := http.NewRequest("GET", "https://dualkeep.com/api/v1/merchant/ledger?limit=20&offset=0", nil)
if err != nil {
panic(err)
}
req.Header.Set("X-API-Key", os.Getenv("DUALKEEP_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var data map[string]any
if err := json.NewDecoder(res.Body).Decode(&data); err != nil {
panic(err)
}
fmt.Println(res.StatusCode, data)
}
The credit history in customer terms: one entry per confirmed payment, with the crypto amount, its USD value at credit time, the service fee deducted, and the on-chain tx hash.
Query parameters
Name
Type
Description
limit
integer
Page size (default 20, max 100). Defaults to 20.
offset
integer
Number of records to skip. Defaults to 0.
Errors
Status
Code
When
401
unauthorized
Missing or invalid API key.
429
rate_limited
Too many requests for this API key.
curl -X GET "https://dualkeep.com/api/v1/merchant/ledger-transactions?limit=20&offset=0" \
-H "X-API-Key: $DUALKEEP_API_KEY"
const res = await fetch("https://dualkeep.com/api/v1/merchant/ledger-transactions?limit=20&offset=0", {
method: "GET",
headers: {
"X-API-Key": process.env.DUALKEEP_API_KEY,
},
});
if (!res.ok) throw new Error(`DUALKEEP ${res.status}: ${await res.text()}`);
const data = await res.json();
import os, requests
res = requests.get(
"https://dualkeep.com/api/v1/merchant/ledger-transactions?limit=20&offset=0",
headers={"X-API-Key": os.environ["DUALKEEP_API_KEY"]},
timeout=30,
)
res.raise_for_status()
data = res.json()
package main
import (
"encoding/json"
"fmt"
"net/http"
"os"
)
func main() {
req, err := http.NewRequest("GET", "https://dualkeep.com/api/v1/merchant/ledger-transactions?limit=20&offset=0", nil)
if err != nil {
panic(err)
}
req.Header.Set("X-API-Key", os.Getenv("DUALKEEP_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var data map[string]any
if err := json.NewDecoder(res.Body).Decode(&data); err != nil {
panic(err)
}
fmt.Println(res.StatusCode, data)
}
Queues a payout of amount_usd, debited from the balance immediately (402 insufficient_balance otherwise), in the chosen asset to the given destination address. The estimated network fee (network_fee_usd) is withheld from the amount. Payouts are processed in periodic batches; the USD amount converts to crypto at the live rate when the batch runs, so estimated_crypto at request time is indicative and crypto_amount is what actually went out.
The destination must be an active whitelisted address (dashboard: Balance -> Withdraw -> Address book). Send idempotency_key, or the Idempotency-Key header, so a retried request returns the original withdrawal instead of opening a second payout.
Body parameters
Name
Type
Description
amount_usdrequired
string
Gross USD amount; the network fee is withheld from it.
assetrequired
string
A supported currency code, e.g. BTC, ETH, USDT_TRC20, USDC_SPL. Call GET /api/v1/currencies for the live list, including network and confirmation policy per asset.
addressrequired
string
Whitelisted destination address.
destination_tag
string
The memo the payout must carry when the destination identifies accounts by one (XRP destination tag, Stellar memo, TON comment). Exchange deposit addresses are shared between customers, so a payout to one without this arrives unattributed. Ignored by chains that have no memo.
idempotency_key
string
Max 64 chars. The Idempotency-Key header wins if both are sent.
Errors
Status
Code
When
400
validation_error
The request was malformed or failed validation.
401
unauthorized
Missing or invalid API key.
402
insufficient_balance
The available balance does not cover the amount plus fees.
const res = await fetch("https://dualkeep.com/health", {
method: "GET",
});
if (!res.ok) throw new Error(`DUALKEEP ${res.status}: ${await res.text()}`);
const data = await res.json();
import os, requests
res = requests.get(
"https://dualkeep.com/health",
timeout=30,
)
res.raise_for_status()
data = res.json()
package main
import (
"encoding/json"
"fmt"
"net/http"
)
func main() {
req, err := http.NewRequest("GET", "https://dualkeep.com/health", nil)
if err != nil {
panic(err)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var data map[string]any
if err := json.NewDecoder(res.Body).Decode(&data); err != nil {
panic(err)
}
fmt.Println(res.StatusCode, data)
}
200 OK
{
"status": "ok"
}
Need a hand?
If something here does not match what the server does, that is a bug in the contract and
we want to hear about it: [email protected], or
the contact page.