POST /v1/deposit-addresses
Allocate a deposit address to receive an inbound transfer. The gateway either reserves a fresh address for the (user, currency, network) tuple or — for user_identifier-tagged calls — returns the same sticky address you've used before for that identifier.
| Method & path | POST /v1/deposit-addresses |
| Required permission | request_deposit_address |
| Auth | HMAC — see Authentication |
Request body
{
"currency": "USDT",
"network": "ETH",
"user_identifier": "user_8721",
"idempotency_key": "alloc_2026_05_24_abc123"
}
| Field | Type | Required | Description |
|---|---|---|---|
currency | string | required | Currency symbol (1–32 chars). The currency must be enabled for deposits on this account. |
network | string | required | Network code (1–32 chars). |
user_identifier | string | optional | Free-form string up to 256 chars. When set, the address is sticky — every call with the same user_identifier returns the same address forever. Omit to draw from the recyclable pool instead. |
idempotency_key | string | optional | Up to 128 chars. See "Idempotency" below. |
Response
{
"success": true,
"message": "OK",
"data": {
"request_id": 42,
"address": "0xabc...def",
"network": "ETH",
"currency": "USDT",
"contract_address": "0xdAC17F958D2ee523a2206206994597C13D831ec7",
"user_identifier": "user_8721",
"display_expires_at": "2026-05-16T22:00:00+00:00",
"created_at": "2026-05-16T19:00:00+00:00"
}
}
| Field | Type | Description |
|---|---|---|
request_id | integer | Unique id for this allocation. Use it with GET /v1/deposit-requests/:request_id to poll for incoming transactions. |
address | string | The on-chain address to show the payer. |
network | string | Echoes the request. |
currency | string | Echoes the request. |
contract_address | string | null | The token contract address for tokens (e.g. ERC-20). null for native currency. |
user_identifier | string | null | Echoes the request (or null if you didn't supply one). |
display_expires_at | ISO-8601 UTC | Stop showing this address to your user after this timestamp (default: 3 hours). The gateway keeps monitoring the address after this, so late deposits still credit. |
created_at | ISO-8601 UTC | When the request was allocated. |
Sticky vs pool addresses
The decision tree the gateway runs on every call:
You supplied user_identifier? | Existing sticky address for this (account, network, user_identifier)? | Result |
|---|---|---|
| Yes | Yes | Return the existing sticky address. The same identifier always maps to the same address. |
| Yes | No | Allocate a fresh address, mark it sticky, and store the identifier. |
| No | — | Reuse an idle pooled address belonging to your account (one past its monitoring window). If none is available, allocate a fresh one from the pool. |
When to use a sticky identifier: any time you want a stable mapping (per-user deposit address, per-invoice address, per-order address). Sticky addresses are never recycled.
When to skip the identifier: one-off payments where the address only needs to live for a few hours. Pool addresses become reusable after 7 days, so the same address can serve many short-lived payments over time.
Idempotency
A network blip between client and server can leave you unsure whether your POST succeeded. Setting idempotency_key makes retries safe:
- On the first call with a given
(api_key, idempotency_key)pair, the gateway processes the request and remembers the result. - On any retry with the same
(api_key, idempotency_key), the gateway returns the original allocation instead of allocating a new address.
// Initial POST — succeeds, returns request_id=42
{ "currency": "USDT", "network": "ETH", "idempotency_key": "order_99_attempt_1" }
// Retried after a timeout — same body — returns request_id=42 again
{ "currency": "USDT", "network": "ETH", "idempotency_key": "order_99_attempt_1" }
Keys are scoped per API key only — not per (currency, network). Two consequences:
- The same
idempotency_keyunder a different API key allocates a new address. - The same
idempotency_keyunder the same API key returns the original allocation, even if you changecurrencyornetworkon the retry. The new request'scurrencyandnetworkare ignored. Make eachidempotency_keyrepresent one specific allocation attempt — don't reuse it for a different deposit.
Display & monitoring windows
Every allocation comes with these windows (all configurable; defaults shown):
| Window | Default | What it controls |
|---|---|---|
| Display | 3 h | How long you should show the address in your UI. After this, status flips to expired_for_display. |
| Monitoring | 7 d | The gateway watches the address for inbound transfers. Deposits that arrive after the display window still confirm and credit. |
| Reuse | 7 d | Pool addresses become eligible for reuse for this same account after this point. Sticky addresses are never reused. |
If a deposit arrives even after the display window, it still confirms and credits — the gateway keeps watching the address through the reuse window.
Errors
| Status | Cause |
|---|---|
422 | Schema validation failed — missing/empty currency or network, user_identifier over 256 chars, idempotency_key over 128 chars. message lists the offending field(s). |
401 / 403 / 429 | Standard middleware — see Errors page. |
403 Deposits for this currency are not enabled for this account | The currency exists and is active, but you haven't toggled deposits on for it via the dashboard's Currencies page. |
404 Network not found | network is not a known network code, or it's inactive. |
404 Currency not found | currency is not a known currency symbol on the given network, or it's inactive. |
Code samples
Examples assume sign_request(...) from the Authentication page.
- curl + bash
- Python
- Node.js
- PHP
METHOD="POST"
ENDPOINT="/v1/deposit-addresses"
QUERY=""
BODY='{"currency":"USDT","network":"ETH","user_identifier":"user_8721","idempotency_key":"alloc_2026_05_24_abc123"}'
# ... sign as on the Authentication page; remember the body hash is sha256($BODY) ...
curl -sS "${BASE_URL}${ENDPOINT}" \
-X POST \
-H "Content-Type: application/json" \
-H "X-CPG-Key: ${API_KEY}" \
-H "X-CPG-Timestamp: ${TS}" \
-H "X-CPG-Signature: ${SIG}" \
--data-raw "$BODY"
import json
body_dict = {
"currency": "USDT",
"network": "ETH",
"user_identifier": "user_8721",
"idempotency_key": "alloc_2026_05_24_abc123",
}
body_bytes = json.dumps(body_dict).encode()
headers = sign_request("POST", "/v1/deposit-addresses", body=body_bytes)
headers["Content-Type"] = "application/json"
resp = requests.post(f"{BASE_URL}/v1/deposit-addresses", data=body_bytes, headers=headers, timeout=10)
resp.raise_for_status()
print(resp.json())
Critical: sign the exact bytes you'll send. Don't pass a Python dict to requests and let it serialise — the resulting bytes might differ in whitespace or key order from what you hashed.
const bodyDict = {
currency: 'USDT',
network: 'ETH',
user_identifier: 'user_8721',
idempotency_key: 'alloc_2026_05_24_abc123',
};
const bodyStr = JSON.stringify(bodyDict);
const headers = {
...signRequest('POST', '/v1/deposit-addresses', bodyStr),
'Content-Type': 'application/json',
};
const resp = await fetch(`${BASE_URL}/v1/deposit-addresses`, {
method: 'POST',
headers,
body: bodyStr,
});
if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${await resp.text()}`);
console.log(await resp.json());
$body = json_encode([
'currency' => 'USDT',
'network' => 'ETH',
'user_identifier' => 'user_8721',
'idempotency_key' => 'alloc_2026_05_24_abc123',
]);
$headers = sign_request('POST', '/v1/deposit-addresses', $body);
$headers[] = 'Content-Type: application/json';
$ch = curl_init("$baseUrl/v1/deposit-addresses");
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $body,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
echo curl_exec($ch);
curl_close($ch);
Notes
- Addresses are a finite resource. Each fresh allocation consumes a new address. Sticky-by-identifier and idempotency-key reuse exist specifically to avoid allocating more than you need — set
idempotency_keyfor every allocation you might retry, and pass auser_identifierwhen you want a stable per-user address. contract_addressis informational. You don't need it to receive funds (the address handles that), but it's useful for showing wallet-app QR codes that bake the token contract into the URI (e.g. EIP-681).- Cross-network reuse is not supported. An address allocated on
ETHwon't receive funds sent onBSC(even though the byte representation matches). You need separate allocations per network.