# CoinsSend Payments documentation
---
Source: https://docs.coinssend.com/README.md
# CoinsSend API Documentation
This documentation covers the verified public merchant integration and checkout
contract. Start with the machine-readable [OpenAPI 3.1 document](https://docs.coinssend.com/openapi.json)
for agent/tool integration and use the narrative guides for operational detail.
## Guides
- [Supported Coins & Networks](https://docs.coinssend.com/supported-coins.md)
- [Getting Started](https://docs.coinssend.com/getting-started.md)
- [Authentication and signature vectors](https://docs.coinssend.com/authentication.md)
- [API Reference](https://docs.coinssend.com/api-reference.md)
- [Invoices](https://docs.coinssend.com/invoices.md)
- [Withdrawals](https://docs.coinssend.com/withdrawals.md)
- [Static Wallets](https://docs.coinssend.com/static-wallets.md)
- [Merchant balances and fees](https://docs.coinssend.com/merchants.md)
- [Coin rates and live catalog](https://docs.coinssend.com/coin-rates.md)
- [Webhooks](https://docs.coinssend.com/webhooks.md)
- [Error Handling](https://docs.coinssend.com/error-handling.md)
- [Rate Limits and Safe Retries](https://docs.coinssend.com/rate-limits.md)
- [Server-side examples](https://docs.coinssend.com/examples/README.md)
- [AI integration prompts](https://docs.coinssend.com/ai-integration-prompts.md)
## Public Allowlist
Authenticated merchant operations:
- `POST /v1/invoices`
- `POST /v1/withdrawals`
- `GET /v1/merchants/balances`
- `GET /v1/merchants/fees`
- `POST /v1/wallet-address`
Public discovery and checkout operations:
- `GET /v1/get-coin-rate`
- `GET /v1/coins-and-fee`
- `GET /v1/invoices/{invoiceCode}`
- `POST /v1/invoices/{invoiceCode}/provider-card-orders`
- `GET /v1/wallet-addresses/{walletAddress}/qr`
Only the operations listed above are part of the supported public integration
contract.
## Authentication Summary
Merchant operations require `Merchant` and `Sign`. Withdrawal submission also
requires a fresh `Timestamp` (`X-Timestamp` is accepted as an alias).
The request signature uses the server-canonical JSON body, not arbitrary raw
JSON formatting:
```text
canonical_body = PHP-compatible canonical_json(request_object)
legacy Sign = md5(base64(canonical_body) + API_KEY)
withdrawal Sign = hmac_sha256(base64(canonical_body) + "." + Timestamp, API_KEY)
```
Generate one canonical string, sign it, and send it as the request body. Verify
client implementations with
[`signature-test-vectors.json`](https://docs.coinssend.com/signature-test-vectors.json). Keep API keys in
trusted server-side secrets.
Webhook signing is deliberately different: verify `X-Signature` as
HMAC-SHA256 over the exact raw webhook body before parsing it.
## Live Asset Data
See [Supported Coins & Networks](https://docs.coinssend.com/supported-coins.md) for the coin/network
tables, API identifiers, and guidance on displaying payment options.
Do not copy a static coin/network enum from examples. Use
`GET /v1/coins-and-fee` for discovery and handle validation from the target
write endpoint as authoritative. Availability, limits, decimals, fees, rates,
and provider-card capability can change.
## Response Shapes
Most successful responses use:
```json
{
"data": {},
"status": "success"
}
```
Endpoint errors usually use `status` and `message`; validation errors add
`errors`; authentication errors return `{"error":"Reason"}`. Symbolic
error labels in integration code are client-side categories, not guaranteed
wire fields.
## Idempotency and Retries
The public write API does not accept a general `Idempotency-Key` header.
- Invoice `order_id` uniqueness returns `409` and does not replay the original
response.
- Withdrawals are not idempotent; timestamp freshness only limits stale replay.
- Static-wallet creation is not idempotent.
- Provider-card creation only reuses an already-active order.
- Withdrawal webhook deliveries currently include a stable
`X-Idempotency-Key`; other webhook families do not guarantee it.
Do not blindly retry a write after an unknown outcome. See
[Rate Limits and Safe Retries](https://docs.coinssend.com/rate-limits.md).
## Rate-Limit Scope
The published API contract guarantees one specific limit: 100 successful wallet
creations per merchant in 60 seconds. Additional limits may apply, so clients
must also handle any HTTP `429` response and honor `Retry-After` when present.
---
Source: https://docs.coinssend.com/supported-coins.md
# Supported Coins & Networks
Use this page to see which cryptocurrencies and blockchain networks CoinsSend
supports, choose labels for your interface, and find the codes to send in API
requests.
## Supported assets at a glance
The public catalog was checked on **September 17, 2026 (UTC)**: **9 coins,
7 networks, and 18 coin/network pairs**. This is a dated reference; load the
[live catalog](https://api.coinssend.com/v1/coins-and-fee) to build your current
payment or withdrawal choices.
| Network display name | API `network` code | Coins in the catalog |
| ------------------------ | ------------------ | ------------------------------------ |
| Bitcoin | `bitcoin` | BTC |
| Ethereum (ERC-20) | `eth` | ETH, USDT, USDC |
| TRON (TRC-20) | `tron` | TRX, USDT |
| BNB Smart Chain (BEP-20) | `bsc` | BNB, USDT, USDC |
| Polygon PoS | `polygon` | POL, USDT, USDC |
| TON (Jetton) | `ton` | TON, USDT |
| Arbitrum One | `arbitrum` | ARB, ETH (deposits only), USDT, USDC |
At that check, deposits and withdrawals were enabled for all listed pairs
except **ETH on Arbitrum One**, where deposits were enabled and withdrawals
were disabled. Check the current operation flags before enabling a choice.
The coin codes for requests are `btc`, `eth`, `trx`, `bnb`, `pol`, `ton`,
`arb`, `usdt`, and `usdc`. Keep these values dynamic in your integration;
new assets may be added and existing pairs may become unavailable.
## Coin codes, network codes, and display labels
A coin and its network form one payment option. **USDT on TRON and USDT on
Ethereum are different options**: always display both the coin and the network
when asking a customer where to send funds.
| Purpose | Value to use | Example |
| ---------------------------- | -------------------------------------- | --------------- |
| Display the coin | Catalog `coin.label` | `USDT` |
| Display the network | Catalog `networks[].title` | `TRON (TRC-20)` |
| Send `coin` in a request | Lowercase catalog `coin.symbol` | `usdt` |
| Send `network` in a request | Lowercase catalog `networks[].network` | `tron` |
| Identify a choice in your UI | Both normalized codes | `usdt:tron` |
Use `eth` for the Ethereum network and `bitcoin` for the Bitcoin network.
Display names and token-standard labels such as `ERC-20`, `TRC-20`, and
`BEP-20` are not API network codes. The native Polygon coin code is `pol`.
Do not combine every coin with every network or infer the network from an
address alone.
## Load the live catalog
Both discovery endpoints are public and require no API key, `Merchant`, or
`Sign` header:
```bash
curl --fail --show-error https://api.coinssend.com/v1/coins-and-fee
curl --fail --show-error https://api.coinssend.com/v1/get-coin-rate
```
| Endpoint | How to use it |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET /v1/coins-and-fee` | Read `data.coins` for the available coin/network pairs, display labels, network fees, service-fee percentages, and minimum withdrawals. |
| `GET /v1/get-coin-rate` | Match `data[].coin.symbol` and `networks[].network` for `deposit_enabled`, `withdraw_enabled`, amount limits, decimals, and exchange rates. |
The catalog includes pairs enabled for deposits, withdrawals, **or both**;
it does not return the individual operation flags. A `min_withdrawal` or
`network_fee` value by itself does not mean withdrawals are enabled.
Use the catalog as the starting list, then match the rate metadata by the
lowercase **coin/network pair**. For receiving payments, require
`deposit_enabled === true`; for withdrawals, require
`withdraw_enabled === true`.
The rates response can contain coins with no available networks and can omit
assets without rate data. For example, at the check above SOL had rate data
but no networks and was absent from the catalog. A price or quote alone is
not evidence that an asset is available for payments. If metadata for a
catalog pair is missing, show its availability as unknown and refresh before
enabling the operation.
### JavaScript: build coin and network choices
This example only reads public data. It keeps catalog entries with missing
metadata visible as unavailable choices, without assuming that they support
either operation.
```javascript
const api = 'https://api.coinssend.com';
async function getData(path) {
const response = await fetch(`${api}${path}`);
if (!response.ok) throw new Error(`Discovery failed: ${response.status}`);
const body = await response.json();
if (body.status !== 'success') throw new Error('Discovery failed');
return body.data;
}
const [catalog, rates] = await Promise.all([
getData('/v1/coins-and-fee'),
getData('/v1/get-coin-rate'),
]);
const metadata = new Map(rates.flatMap((entry) =>
entry.networks.map((network) => [
`${entry.coin.symbol.toLowerCase()}:${network.network.toLowerCase()}`,
network,
])
));
const choices = catalog.coins.flatMap((entry) =>
entry.networks.map((network) => {
const coin = entry.coin.symbol.toLowerCase();
const code = network.network.toLowerCase();
const id = `${coin}:${code}`;
const details = metadata.get(id);
return {
id,
coin,
network: code,
label: `${entry.coin.label} — ${network.title}`,
availabilityKnown: details !== undefined,
depositEnabled: details?.deposit_enabled === true,
withdrawEnabled: details?.withdraw_enabled === true,
};
})
);
const depositChoices = choices.filter((choice) => choice.depositEnabled);
const withdrawalChoices = choices.filter((choice) => choice.withdrawEnabled);
```
If discovery fails, show a loading/error state with a retry option. Do not
replace a failed response with a hardcoded list of enabled payment methods.
The catalog advertises `Cache-Control: public, max-age=900` (15 minutes);
respect that cache window and handle changes between discovery and submission.
## Use a selected pair in requests
| Operation | Fields |
| -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Create an invoice](https://docs.coinssend.com/invoices.md#create-invoice) | Use lowercase `coin` and `network` to preselect a pair. `allowed_coins` is an array of lowercase coin codes, not network codes or `coin:network` identifiers. |
| [Create a static wallet](https://docs.coinssend.com/static-wallets.md#create-wallet-address) | Send the selected lowercase `coin` and `network` with the other required wallet fields. |
| [Create a withdrawal](https://docs.coinssend.com/withdrawals.md#initiate-withdrawal) | Send the selected lowercase `coin` and `network`; check withdrawal availability and limits for that pair. |
For example, a selected USDT payment on TRON supplies these fields (this is
only the pair, not a complete request):
```json
{
"coin": "usdt",
"network": "tron"
}
```
Discovery is a UI aid. The target endpoint makes the final decision on pair
and amount validation; handle HTTP `422`, refresh the choices, and explain the
validation message to the customer. Do not silently switch the network or
retry a write automatically.
## Fees, limits, and precision
Read current values from the discovery responses; do not copy the examples
into permanent fee or limit tables. Keep monetary values as decimal strings,
and use the selected pair's precision rather than assuming one decimal count
for a coin across every network.
The public catalog's service fee is not a merchant-specific fee quote. For
your merchant's settings, use [Merchant Fees](https://docs.coinssend.com/merchants.md#get-merchant-fees).
See [Coin Rates API](https://docs.coinssend.com/coin-rates.md) for the complete discovery response fields.
---
Source: https://docs.coinssend.com/getting-started.md
# Getting Started with CoinsSend API
Welcome to the CoinsSend API documentation. This guide will help you integrate cryptocurrency payments, wallets, and withdrawals into your application with our simple yet powerful API. Follow these steps to get started quickly.
## Integration Overview
1
Create a Merchant Account
Sign up for a merchant account to get your unique merchant ID and API credentials. Our simple onboarding process helps you get started in minutes.
Create API keys with specific permissions for invoice creation, wallet management and withdrawals. Our granular permissions system ensures you have the right security controls.
Choose between invoices for one-time payments or static wallets for recurring transactions. Both options provide seamless integration with your existing systems.
Configure webhooks to receive real-time payment notifications and transaction updates. Our robust event system keeps your application in sync with invoice status changes and static wallet transactions.
## Account Setup
### Create Merchant Account
1. Go to [CoinsSend Sign Up](https://app.coinssend.com/sign-up)
2. Complete the registration form with your business information (no KYC required)
3. Verify your email address
### Generate API Credentials
After setting up your merchant account:
1. Log in to your merchant dashboard
2. Create merchant
3. Navigate to Merchant Settings
4. Note your **Merchant ID** (you'll need this for all API requests)
5. Find your API key (generated automatically when the merchant is created) - withdrawal permissions are configured in merchant settings
Security Best Practice: Always store your API keys securely in environment variables or a secure key vault. Never expose API keys in client-side code, public repositories, or include them directly in your application's source code.
## Payment Integration Options
Start with [Supported Coins & Networks](https://docs.coinssend.com/supported-coins.md) to see the
available assets, network identifiers, and how to keep payment options current.
CoinsSend offers two primary methods for accepting cryptocurrency payments:
Invoices
Generate single-use payment links for specific order amounts. Ideal for e-commerce and one-time payments.
Fixed payment amounts in USD with automatic crypto conversion
Stablecoin support including USDT and other stablecoins
## Code Example: Create an Invoice
Here's a quick server-side example. Each language generates the canonical JSON
body from [Authentication](https://docs.coinssend.com/authentication.md), signs it, and sends the same
string. Never put the merchant API key in browser code.
```php
'test_order_' . time(),
'amount' => '100.50',
'is_customer_fee' => true
];
// Generate signature
$jsonData = json_encode($data, JSON_THROW_ON_ERROR);
$base64Data = base64_encode($jsonData);
$signature = md5($base64Data . $apiKey);
// Create HTTP client
$ch = curl_init('https://api.coinssend.com/v1/invoices');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonData);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Merchant: ' . $merchantId,
'Sign: ' . $signature
]);
// Execute request
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// Process response
$result = json_decode($response, true);
// Handle result
if ($httpCode >= 200 && $httpCode < 300 && $result['status'] === 'success') {
echo "Invoice created! Payment URL: {$result['data']['url']}\n";
echo "Invoice code: {$result['data']['code']}\n";
} else {
echo "Error: " . json_encode($result) . "\n";
}
```
```javascript
// Replace with your actual credentials
const merchantId = 'your_merchant_id';
const apiKey = 'your_api_key';
// Create request data
const data = {
order_id: `test_order_${Date.now()}`,
amount: '100.50',
is_customer_fee: true
};
// Generate signature
const jsonData = JSON.stringify(data)
.replace(/\//g, '\\/')
.replace(/[\u0080-\uFFFF]/g, character =>
`\\u${character.charCodeAt(0).toString(16).padStart(4, '0')}`
);
const base64Data = Buffer.from(jsonData, 'utf8').toString('base64');
const crypto = require('node:crypto');
const signature = crypto.createHash('md5').update(base64Data + apiKey).digest('hex');
// Make the API request
fetch('https://api.coinssend.com/v1/invoices', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Merchant': merchantId,
'Sign': signature
},
body: jsonData
})
.then(response => response.json())
.then(result => {
if (result.status === 'success') {
console.log(`Invoice created! Payment URL: ${result.data.url}`);
console.log(`Invoice code: ${result.data.code}`);
} else {
console.error('Error:', result);
}
})
.catch(error => console.error('API Request failed:', error));
```
```python
import requests
import json
import base64
import hashlib
import time
# Replace with your actual credentials
merchant_id = 'your_merchant_id'
api_key = 'your_api_key'
# Create request data
data = {
'order_id': f'test_order_{int(time.time())}',
'amount': '100.50',
'is_customer_fee': True
}
def canonical_json(payload):
encoded = json.dumps(
payload, ensure_ascii=False, separators=(',', ':'), allow_nan=False
)
escaped = []
for character in encoded:
codepoint = ord(character)
if character == '/':
escaped.append(r'\/')
elif codepoint < 0x80:
escaped.append(character)
elif codepoint <= 0xFFFF:
escaped.append(f'\\u{codepoint:04x}')
else:
codepoint -= 0x10000
escaped.append(f'\\u{0xD800 + (codepoint >> 10):04x}')
escaped.append(f'\\u{0xDC00 + (codepoint & 0x3FF):04x}')
return ''.join(escaped)
# Generate signature from the exact body that will be sent
json_data = canonical_json(data)
base64_data = base64.b64encode(json_data.encode()).decode()
signature = hashlib.md5((base64_data + api_key).encode()).hexdigest()
# Make the API request
response = requests.post(
'https://api.coinssend.com/v1/invoices',
headers={
'Content-Type': 'application/json',
'Merchant': merchant_id,
'Sign': signature
},
data=json_data
)
# Process response
result = response.json()
# Handle result
if response.status_code >= 200 and response.status_code < 300 and result['status'] == 'success':
print(f"Invoice created! Payment URL: {result['data']['url']}")
print(f"Invoice code: {result['data']['code']}")
else:
print(f"Error: {result}")
```
## Implementing Webhooks
To receive real-time payment notifications, set up a webhook endpoint:
1. Create an HTTP endpoint in your application to receive webhook events
2. Configure the merchant callback URL in your dashboard; static-wallet creation may also specify its own `webhook_url`
3. Implement signature verification to validate incoming webhooks
4. Process webhook events asynchronously
Developer Tip: During development, use a webhook testing tool like webhook.site or ngrok to inspect and debug incoming webhook payloads. These tools provide temporary endpoints that you can use to test your webhook integration before setting up your production endpoint.
See the [Webhooks documentation](https://docs.coinssend.com/webhooks.md) for details on webhook formats and best practices.
## Next Steps
Now that you have the basics set up, you can:
## Support
If you need help with your integration:
- Check our [API reference documentation](https://docs.coinssend.com/api-reference.md)
- Review common [HTTP errors and troubleshooting](https://docs.coinssend.com/error-handling.md)
- Contact our support team at
---
Source: https://docs.coinssend.com/QUICKSTART.md
# API Quickstart: invoice → webhook
This quickstart walks through a minimal, end-to-end integration path:
1. Prepare signature-based API key authentication
2. Create an invoice
3. Redirect buyer to the payment page
4. Verify invoice webhooks
***
## Prerequisites
- Base URL: `https://api.coinssend.com`
- A merchant ID from your CoinsSend dashboard
- A merchant API key for signature auth (`Merchant` + `Sign` headers; withdrawals also require `Timestamp`)
References:
- [Authentication](https://docs.coinssend.com/authentication.md)
- [OpenAPI contract](https://docs.coinssend.com/openapi.json)
- [Signature test vectors](https://docs.coinssend.com/signature-test-vectors.json)
- [Invoices](https://docs.coinssend.com/invoices.md)
- [Webhooks](https://docs.coinssend.com/webhooks.md)
> Want a coding assistant to build this flow? Use the full checkout prompt in [AI Integration Prompts](https://docs.coinssend.com/ai-integration-prompts.md) after reviewing these steps.
***
## Step 1) Prepare API key signature auth for merchant endpoints
Get your merchant ID and API key from the CoinsSend dashboard before making public API requests.
For signed endpoints (for example `POST /v1/invoices`), generate:
```text
canonical_body = json_encode(json_decode(request_body)) using PHP defaults
Sign = md5(base64(canonical_body) + API_KEY)
```
Withdrawal requests use a stricter timestamped signature instead:
```text
Timestamp = current Unix timestamp in seconds
Sign = hmac_sha256(base64(canonical_body) + "." + Timestamp, API_KEY)
```
Send `canonical_body` as the request body. PHP's default JSON encoding escapes
slashes and non-ASCII characters, so plain `JSON.stringify()` output is not
always equivalent.
Example in Node.js:
```javascript
import { createHash } from 'node:crypto';
function canonicalJson(value) {
return JSON.stringify(value)
.replace(/\//g, '\\/')
.replace(/[\u0080-\uFFFF]/g, (character) =>
`\\u${character.charCodeAt(0).toString(16).padStart(4, '0')}`
);
}
const body = canonicalJson({
order_id: 'order_10001',
amount: '100.50',
is_customer_fee: true,
allow_card_payments: true,
success_url: 'https://merchant.example/pay/success',
cancel_url: 'https://merchant.example/pay/cancel'
});
const sign = createHash('md5')
.update(Buffer.from(body).toString('base64') + process.env.COINSSEND_API_KEY)
.digest('hex');
```
Required headers for signed merchant API calls:
- `Merchant: `
- `Sign: `
- `Content-Type: application/json`
For `POST /v1/withdrawals`, send `Timestamp: ` and set `Sign` to the HMAC-SHA256 value.
***
## Step 2) Create an invoice (API key auth)
```bash
BODY='{"order_id":"order_10001","amount":"100.50","is_customer_fee":true,"allow_card_payments":true,"success_url":"https:\/\/merchant.example\/pay\/success","cancel_url":"https:\/\/merchant.example\/pay\/cancel"}'
# BODY is canonical PHP-compatible JSON. Compute SIGN from these exact bytes.
SIGN=''
curl -X POST "https://api.coinssend.com/v1/invoices" \
-H "Content-Type: application/json" \
-H "Merchant: $MERCHANT_ID" \
-H "Sign: $SIGN" \
-d "$BODY"
```
Example response:
```json
{
"status": "success",
"data": {
"id": "4e7d786f-a4a6-4a42-b0e5-6f2bb8a5f0f2",
"order_id": "order_10001",
"url": "https://pay.coinssend.com/invoice_code",
"code": "invoice_code",
"amount": "100.50",
"payer_amount": "100.50",
"expired_at": "2026-04-03T12:00:00+00:00",
"created_at": "2026-04-02T12:00:00+00:00"
}
}
```
***
## Step 3) Send buyer to payment URL
Use `data.url` from invoice creation response as the checkout URL.
- Typical shape: `https://pay.coinssend.com/{invoice_code}`
- `data.code` can also be used with public invoice lookup: `GET /v1/invoices/{invoiceCode}`
- Direct crypto payment is always available. Card checkout is shown only when `allow_card_payments` is `true` and the invoice amount or remaining amount is at least `$15.00` / `1500` cents.
- `allow_card_payments` and `provider_card` can appear in invoice lookup and payment-page invoice payloads, but they are not part of the `POST /v1/invoices` create response.
- Redirects from success or cancel URLs, provider redirects, provider webhooks, and provider status polling are only UX signals or provider hints. Confirm payment from invoice status changes after chain confirmation, usually through signed webhooks.
***
## Step 4) Handle webhook events
When invoice/payment state changes, CoinsSend sends signed webhook callbacks to your webhook URL.
Common events for this flow:
- `invoice.paid`
- `invoice.expired`
- `aml.rejected.invoice` (when AML blocks invoice deposit)
Verify `X-Signature` using your API key and the **raw** request body bytes:
```php
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
$expected = hash_hmac('sha256', $payload, $apiKey);
if (!hash_equals($expected, $signature)) {
http_response_code(401);
exit('invalid signature');
}
```
## Next steps
- Expand invoice validation and error handling: [Invoices](https://docs.coinssend.com/invoices.md)
- Add duplicate-safe processing in your webhook consumer: [Webhooks](https://docs.coinssend.com/webhooks.md)
- Do not blindly retry invoice creation: the endpoint has no idempotency-key contract. Reconcile by `order_id` before deciding whether to create again.
- Review request signing details: [Authentication](https://docs.coinssend.com/authentication.md)
---
Source: https://docs.coinssend.com/ai-integration-prompts.md
# AI Integration Prompts
Use these copy-paste prompts with your AI coding assistant when you want help integrating the CoinsSend API into a full checkout flow. Each prompt tells the assistant to follow the canonical docs, keep merchant secrets server-side, and verify the work inside your own project.
## Before you ask an AI assistant
Read these source-of-truth docs first, then paste the relevant prompt into your coding assistant:
- [Getting Started](https://docs.coinssend.com/getting-started.md)
- [OpenAPI 3.1](https://docs.coinssend.com/openapi.json)
- [Signature Test Vectors](https://docs.coinssend.com/signature-test-vectors.json)
- [API Quickstart](https://docs.coinssend.com/QUICKSTART.md)
- [Authentication](https://docs.coinssend.com/authentication.md)
- [Invoices](https://docs.coinssend.com/invoices.md)
- [Webhooks](https://docs.coinssend.com/webhooks.md)
- [Error Handling](https://docs.coinssend.com/error-handling.md)
- [Rate Limits](https://docs.coinssend.com/rate-limits.md)
## Full checkout integration prompt
```markdown
You are helping me add a CoinsSend full checkout integration to my application.
Use the canonical CoinsSend docs as the source of truth:
- getting-started.md
- QUICKSTART.md
- authentication.md
- invoices.md
- webhooks.md
- error-handling.md
- rate-limits.md
- openapi.json
- signature-test-vectors.json
Implement a server-side checkout flow using these values only after I replace them:
- Base API URL: {BASE_URL}
- Merchant ID: {MERCHANT_ID}
- API key environment variable name: {API_KEY_ENV}
- Order ID: {ORDER_ID}
- Amount: {AMOUNT}
- Success redirect URL: {SUCCESS_URL}
- Cancel redirect URL: {CANCEL_URL}
- Webhook URL: {WEBHOOK_URL}
Requirements:
1. Keep the merchant API key in environment variables or server secrets only. Never put it in frontend code, public code, logs, build output, or browser storage.
2. Create the invoice from server-side code. The frontend may ask my server to start checkout, but it must not sign merchant API requests.
3. Generate the PHP-compatible canonical JSON body from authentication.md, sign exactly md5(base64(canonical_body) + API_KEY), and send that same canonical string as the HTTP body. Verify the implementation against signature-test-vectors.json.
4. Send the required merchant authentication headers from the server request.
5. Include the order ID, amount, success URL, and cancel URL according to the invoice docs. Configure the merchant webhook callback in merchant settings; webhook_url is not an invoice-creation field.
6. Return or redirect the buyer to the invoice payment URL from the invoice creation response.
7. Treat success and cancel redirect URLs as UX signals only. Do not mark the order paid from a redirect. Payment confirmation must come from verified webhooks and persisted invoice status.
8. Persist the local order, CoinsSend invoice ID or code, payment URL, current status, and timestamps needed for reconciliation.
9. Handle invoice.paid as terminal success. Handle invoice.expired and aml.rejected.invoice as terminal failure states. Handle partial as not complete and keep the order awaiting full payment unless my business rules say otherwise.
10. Add a local uniqueness guard for repeated checkout starts. The API returns 409 for a duplicate order_id but does not replay the original response. Separately make webhook processing idempotent.
11. Add user-project tests and checks for signing, invoice creation, redirect URL behavior, status persistence, partial payment handling, expired handling, AML rejection handling, and the rule that frontend code never sees merchant secrets.
12. Run the relevant lint, type, unit, integration, and build checks available in my project, then report the exact commands and results.
Do not duplicate whole endpoint references in my code comments or docs. Link back to the canonical CoinsSend docs instead.
```
## Webhook receiver prompt
Use this webhook assistant prompt whenever you need a webhook implementation checklist before code.
```markdown
You are helping me implement a CoinsSend webhook receiver.
Use the canonical CoinsSend webhooks and authentication docs as the source of truth. Build the receiver in my existing server framework and match my project's routing, persistence, logging, and test patterns.
Requirements:
1. Preserve the raw request body before JSON parsing.
2. Read the X-Signature header from the request.
3. Verify the webhook with HMAC-SHA256 using the raw request body and the merchant API key from server secrets.
4. Use a constant-time comparison for signatures.
5. Reject missing or invalid signatures before parsing or trusting the payload.
6. Parse JSON only after signature verification passes.
7. Handle invoice.paid, invoice.expired, and aml.rejected.invoice events.
8. Persist terminal status changes in my database. invoice.paid is terminal success. invoice.expired and aml.rejected.invoice are terminal failure states.
9. Treat partial as not fully paid. Persist the partial state or payment progress without releasing goods or marking the order paid.
10. Make webhook handling idempotent so duplicate deliveries don't double-apply business effects. Use X-Idempotency-Key when present; it is currently guaranteed only for withdrawal events, so use an event/entity deduplication key otherwise.
11. Return a 2xx response only after the event is accepted for processing. Return a clear non-2xx response for invalid signatures or malformed payloads.
12. Add user-project tests and checks for raw body preservation, X-Signature verification, invalid signature rejection, duplicate delivery handling, invoice.paid, invoice.expired, aml.rejected.invoice, partial, and expired status persistence.
13. Run the relevant lint, type, unit, integration, and build checks available in my project, then report the exact commands and results.
Do not verify after parsing JSON. The raw request body must be captured and verified first.
```
## Production hardening review prompt
```markdown
You are reviewing my CoinsSend checkout integration for production readiness.
Use the canonical CoinsSend getting started, quickstart, authentication, invoices, webhooks, error handling, and rate limit docs as the source of truth. Inspect my existing implementation, tests, configuration, and runtime assumptions.
Review checklist:
1. Confirm merchant API keys are stored only in environment variables or server secrets and never exposed to frontend code, logs, build output, public repositories, or browser storage.
2. Confirm invoice creation happens server-side and merchant API requests are signed only on the server.
3. Confirm request signing generates the PHP-compatible canonical JSON body, signs md5(base64(canonical_body) + API_KEY) for legacy endpoints or timestamped HMAC-SHA256 for withdrawals, sends the same canonical body, and passes signature-test-vectors.json.
4. Confirm success and cancel redirect URLs are treated only as UX signals, never as payment confirmation.
5. Confirm verified webhooks or persisted status checks are the only source of payment confirmation.
6. Confirm webhook verification reads X-Signature, uses HMAC-SHA256, and verifies the raw request body before JSON parsing.
7. Confirm terminal state persistence covers invoice.paid as success and invoice.expired plus aml.rejected.invoice as failure states.
8. Confirm partial and expired statuses don't release goods or mark orders paid.
9. Confirm duplicate webhook deliveries are idempotent and repeated checkout starts use a local uniqueness guard without assuming the CoinsSend API replays a duplicate invoice response.
10. Confirm failures are handled clearly, including authentication and endpoint error shapes, invalid signatures, validation errors, rate limits, unknown write outcomes, expired invoices, partial payments, and AML rejection.
11. Confirm observability doesn't leak merchant secrets or signed payload material.
12. Confirm my user-project tests and checks cover signing, invoice creation, redirect behavior, webhook verification, terminal persistence, failure states, partial, expired, and secret handling.
13. Run the relevant lint, type, unit, integration, and build checks available in my project, then report the exact commands and results.
Do not add new SDK-specific setup instructions or copy full endpoint reference sections. If docs are needed, link back to the canonical CoinsSend docs.
```
## Required guardrails
- Merchant API keys stay in environment variables or server secrets only.
- Frontend code must never sign merchant API requests or store merchant API keys.
- Invoice creation for checkout must happen on the server.
- Invoice and static-wallet request signing must use exactly `md5(base64(canonical_body) + API_KEY)`.
- Withdrawal request signing must send `Timestamp` and use exactly `hmac_sha256(base64(canonical_body) + "." + Timestamp, API_KEY)`.
- Generate one PHP-compatible canonical request body, sign it, send that same string, and pass the published signature vectors.
- Webhook verification is a separate contract: verify the exact raw received body bytes before JSON parsing.
- Webhook receivers must read `X-Signature`, verify with HMAC-SHA256, and preserve the raw request body before JSON parsing.
- Redirect success and cancel URLs are UX signals only, not payment confirmation.
- Payment confirmation must come from verified webhooks and terminal status persistence.
- Handle `invoice.paid`, `invoice.expired`, `aml.rejected.invoice`, `partial`, and `expired` explicitly.
- Persist terminal outcomes so `invoice.paid` closes the order as paid, while `invoice.expired` and `aml.rejected.invoice` close it as failed or rejected.
- Treat `partial` as not fully paid unless a separate business rule is intentionally implemented.
- Add user-project tests and checks for signing, webhook verification, redirects, persistence, failure states, duplicate deliveries, and secret handling.
- Link back to the canonical docs pages instead of duplicating endpoint references.
## What not to ask the assistant to do
- Don't ask it to put merchant API keys in frontend code, mobile clients, public repositories, logs, browser storage, or build output.
- Don't ask it to sign CoinsSend merchant requests in the browser.
- Don't ask it to mark an order paid after a success redirect.
- Don't ask it to trust a cancel redirect as the final invoice state.
- Don't ask it to verify webhooks after parsing JSON. Preserve and verify the raw request body first.
- Don't ask it to skip `X-Signature` validation or replace HMAC-SHA256 with another webhook signing method.
- Don't ask it to ignore `partial`, `expired`, `invoice.expired`, or `aml.rejected.invoice` states.
- Don't ask it to blindly retry a withdrawal, static-wallet creation, or another write after an unknown outcome.
- Don't ask it to invent a universal `Idempotency-Key`, global rate limit, symbolic wire error code, or static asset enum.
- Don't ask it to hardcode credentials, merchant IDs, signatures, or production-looking secrets.
- Don't ask it to duplicate whole endpoint reference sections when a link to the canonical docs is enough.
---
Source: https://docs.coinssend.com/authentication.md
# Authentication
CoinsSend merchant API endpoints use request signatures. Keep signing code and the API key on a trusted server; never put a merchant API key in browser or mobile code.
Authenticated endpoints require:
| Header | Required | Description |
| ----------- | ---------------------------------------------------- | ----------------------------------------------------------------- |
| `Merchant` | Yes | Merchant ID |
| `Sign` | Yes | Lowercase hexadecimal request signature |
| `Timestamp` | Withdrawals: yes; other merchant endpoints: optional | Unix timestamp in seconds. `X-Timestamp` is accepted as an alias. |
Public catalog and checkout lookup endpoints do not use these headers. See the verified allowlist in [API Reference](https://docs.coinssend.com/api-reference.md) or [OpenAPI](https://docs.coinssend.com/openapi.json).
## Canonical JSON Body
Request signing does **not** use arbitrary raw JSON formatting. For a JSON object or array, the API parses the body and re-encodes it with PHP's default `json_encode` behavior before verifying the signature. That canonical form:
- has no insignificant whitespace;
- preserves object member order;
- escapes `/` as `\/`;
- escapes non-ASCII characters as lowercase `\uXXXX` sequences;
- uses an empty string for a request with no body.
Generate this canonical JSON once, sign it, and send those same bytes as the HTTP body. Keep money values as strings and avoid numeric object keys or language-specific floating-point values.
Interoperability fixtures, including whitespace, URL, Unicode, withdrawal, and empty-GET cases, are in [`signature-test-vectors.json`](https://docs.coinssend.com/signature-test-vectors.json).
### Canonical JSON Helpers
## Legacy MD5 Signature
Invoice creation, wallet creation, balances, and fees accept the legacy signature when no timestamp header is supplied:
```text
canonical_body = canonical_json(request_object) or "" for a bodyless request
payload_base64 = base64_utf8(canonical_body)
Sign = md5(payload_base64 + API_KEY)
```
MD5 here is part of the current compatibility protocol, not a password-hashing recommendation. Always use HTTPS and keep the key server-side.
```php
## Timestamped HMAC Signature
`POST /v1/withdrawals` requires a Unix timestamp within 300 seconds of server time. Other authenticated endpoints also accept this mode when `Timestamp` or `X-Timestamp` is supplied.
```text
canonical_body = canonical_json(request_object) or "" for a bodyless request
payload_base64 = base64_utf8(canonical_body)
message = payload_base64 + "." + Timestamp
Sign = hmac_sha256(message, API_KEY)
```
Send exactly the timestamp string used in the signature:
```text
Merchant: 12345678-1234-1234-1234-123456789012
Timestamp: 1716200000
Sign: 64-character-lowercase-hmac-sha256-hex
```
```php
The timestamp prevents stale replay but does not make a write idempotent. In particular, it is unsafe to automatically repeat a withdrawal after an unknown outcome.
## Complete Invoice Example
```javascript
const crypto = require('node:crypto')
function canonicalJson(payload) {
return JSON.stringify(payload)
.replace(/\//g, '\\/')
.replace(/[\u0080-\uFFFF]/g, character =>
`\\u${character.charCodeAt(0).toString(16).padStart(4, '0')}`
)
}
const payload = {
order_id: 'order_12345',
amount: '100.50',
success_url: 'https://merchant.example/success'
}
const body = canonicalJson(payload)
const payloadBase64 = Buffer.from(body, 'utf8').toString('base64')
const sign = crypto.createHash('md5').update(payloadBase64 + process.env.COINSSEND_API_KEY).digest('hex')
const response = await fetch('https://api.coinssend.com/v1/invoices', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Merchant: process.env.COINSSEND_MERCHANT_ID,
Sign: sign
},
body
})
```
## GET Requests
For an authenticated GET with no body, the canonical body and Base64 payload are both empty. The legacy signature is therefore:
```text
Sign = md5("" + API_KEY)
```
Do not add merchant signature headers to public `GET /v1/invoices/{invoiceCode}`, `GET /v1/get-coin-rate`, or `GET /v1/coins-and-fee` calls.
## Authentication Errors
Authentication failures return a compact body such as:
```json
{
"error": "Invalid sign"
}
```
| HTTP status | Message | Meaning |
| ----------- | ------------------------------- | ----------------------------------------------------- |
| `400` | `Missing headers` | `Merchant` or `Sign` is missing |
| `400` | `Missing timestamp` | Withdrawal timestamp is missing |
| `401` | `Invalid merchant` | Merchant or API key was not found |
| `401` | `Invalid sign` | Signature verification failed |
| `401` | `Timestamp expired` | Timestamp is invalid or outside the 300-second window |
| `403` | `Merchant account is suspended` | Merchant is suspended |
These messages are not a separate `code` property. Endpoint and validation
errors use different documented shapes; always branch on the HTTP status and
then parse the applicable schema.
## Operational Guidance
- Keep the API key in a secrets manager or server environment variable.
- Generate and send the body as one immutable string.
- Use a clock synchronized with NTP for withdrawal timestamps.
- Compare webhook HMAC values in constant time; webhook signing is a separate raw-body contract described in [Webhooks](https://docs.coinssend.com/webhooks.md).
- Use the published test vectors in automated client tests before making live calls.
---
Source: https://docs.coinssend.com/api-reference.md
# API Reference
This page lists the supported public integration operations. The
machine-readable source is [OpenAPI 3.1](https://docs.coinssend.com/openapi.json).
## Base URL
```
https://api.coinssend.com/v1/
```
## Authentication
For detailed authentication information, see [Authentication](https://docs.coinssend.com/authentication.md).
## Headers
| Header | Description |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `Content-Type` | Must be `application/json` for all requests |
| `Merchant` | Your merchant ID (required for authenticated endpoints) |
| `Sign` | Request signature (required for authenticated endpoints) |
| `Timestamp` | Unix timestamp in seconds. Required for `POST /v1/withdrawals`; optional only when using timestamped HMAC on other merchant endpoints. |
## Endpoints Summary
| Method | Path | Description | Authentication |
| ------ | ------------------------------------------------- | ---------------------------------------------------------------- | --------------------------- |
| `POST` | `/v1/invoices` | Create a new invoice | Merchant + Sign |
| `POST` | `/v1/withdrawals` | Initiate a withdrawal | Merchant + Timestamp + Sign |
| `GET` | `/v1/merchants/balances` | Get merchant balances | Merchant + Sign |
| `GET` | `/v1/merchants/fees` | Get merchant fees | Merchant + Sign |
| `POST` | `/v1/wallet-address` | Create static wallet address | Merchant + Sign |
| `GET` | `/v1/get-coin-rate` | Get current USD exchange rates for supported assets | None |
| `GET` | `/v1/coins-and-fee` | Get the current supported coin/network catalog with fee metadata | None |
| `GET` | `/v1/invoices/{invoiceCode}` | Get public invoice checkout state | None |
| `POST` | `/v1/invoices/{invoiceCode}/provider-card-orders` | Start or reuse active card checkout | None |
| `GET` | `/v1/wallet-addresses/{walletId}/qr` | Render the QR URL returned by wallet/invoice responses | None |
## Detailed API Documentation
### Invoices
- [Create Invoice](https://docs.coinssend.com/invoices.md#create-invoice)
- [Get Invoice](https://docs.coinssend.com/invoices.md#retrieve-invoice)
- [Create Provider-Card Order](https://docs.coinssend.com/invoices.md#create-provider-card-order)
### Withdrawals
- [Initiate Withdrawal](https://docs.coinssend.com/withdrawals.md#initiate-withdrawal)
### Merchants
- [Get Merchant Balances](https://docs.coinssend.com/merchants.md#get-merchant-balances)
- [Get Merchant Fees](https://docs.coinssend.com/merchants.md#get-merchant-fees)
### Static Wallets
- [Create Wallet Address](https://docs.coinssend.com/static-wallets.md#create-wallet-address)
### Supported Coins, Networks & Rates {#coin-rates}
- [Supported Coins & Networks](https://docs.coinssend.com/supported-coins.md)
- [Get Coin Rates](https://docs.coinssend.com/coin-rates.md#get-coin-rates)
- [Get Supported Coins & Fee](https://docs.coinssend.com/coin-rates.md#get-supported-coins-and-fee)
## Supported Coins & Networks
Browse the [coin and network tables](https://docs.coinssend.com/supported-coins.md) for display names,
API identifiers, and guidance on building payment selectors.
Use `GET /v1/coins-and-fee` for live discovery instead of copying a static
coin or network list into an integration. Availability, limits, fees, and
precision can change without an API schema change.
Individual write endpoints remain the final authority for accepting a pair;
the catalog is not an offline validation guarantee. See [Coin Rates](https://docs.coinssend.com/coin-rates.md).
### Webhooks
- [Webhook Events](https://docs.coinssend.com/webhooks.md#webhook-events)
- [Webhook Authentication](https://docs.coinssend.com/webhooks.md#authentication)
## Common Response Formats
Most operations use the following success envelope:
### Success Response
```json
{
"status": "success",
"data": {
// Response data specific to the endpoint
}
}
```
### Endpoint Error Response
```json
{
"status": "error",
"message": "Error description"
}
```
Some errors include additional fields (for example, fee breakdowns) alongside
`status` and `message`. Authentication errors may instead return
`{"error": "Reason"}` without the `status` field. Always rely on the HTTP
status code first.
Validation failures use HTTP `422` with `errors`, `status`, and `message`.
Symbolic labels shown in prose are not guaranteed response fields.
## Response Data Types
The following data types are used throughout API responses:
| Type | Description | Example |
| --------- | -------------------------------------- | -------------------- |
| `string` | Text values | `"example"` |
| `integer` | Whole numbers | `42` |
| `float` | Numbers with decimals | `10.5` |
| `boolean` | `true` or `false` values | `true` |
| `array` | Ordered list of values | `[1, 2, 3]` |
| `object` | JSON object containing key/value pairs | `{ "key": "value" }` |
### Amount Precision
All monetary amounts, percentage values, and currency rates in API responses are
returned as **strings**. Using strings preserves accuracy for values with many
decimals. Request parameters should also be sent as strings containing numeric
values.
## Common HTTP Error Responses
| HTTP Code | Description |
| --------- | ----------------------------------------------------------------------------- |
| 400 | Bad Request - The request was invalid or cannot be served |
| 401 | Unauthorized - Authentication credentials are missing or invalid |
| 403 | Forbidden - The request is understood, but it has been refused |
| 404 | Not Found - The requested resource does not exist |
| 422 | Unprocessable Entity - The request was well-formed but could not be processed |
| 429 | Too Many Requests - Rate limit exceeded |
| 500 | Server Error - Something went wrong on our end |
## Rate Limits
The only merchant-public write limit verified in this application is the
wallet-creation limit: 100 successful creations per merchant in a 60-second
window. Additional limits may apply. See [Rate Limits](https://docs.coinssend.com/rate-limits.md).
## Versioning
The API is versioned in the URL path (`/v1/`). When breaking changes are introduced, a new version number will be used.
---
Source: https://docs.coinssend.com/invoices.md
# Invoices API
The Invoices API allows you to create and manage cryptocurrency payment invoices for your customers. Each invoice generates a unique payment page where customers can select their preferred cryptocurrency and complete the payment. Invoices are ideal for e-commerce checkout flows and single-payment scenarios.
## Overview
An invoice is a payment request with a specific amount that customers can pay using various cryptocurrencies. When an invoice is created, customers receive a payment URL where they can choose their preferred cryptocurrency and complete the payment.
### Invoice Lifecycle
1
Create Invoice
Generate a new invoice with a specified amount using the POST /v1/invoices endpoint.
2
Customer Payment
Customer visits the payment URL, selects direct crypto payment, or uses card checkout when the invoice allows cards and the remaining amount meets the minimum.
3
Payment Confirmation
Once payment is confirmed on the blockchain, the invoice status is updated to "paid". Partial payments are supported - if the customer pays less than the full amount, they can complete the payment with a second transaction for the remaining balance.
4
Webhook Notification
A webhook notification is sent to your server with payment details.
## API Endpoints
### Create Invoice
Creates a new invoice for direct cryptocurrency payment, with optional card checkout.
POST/v1/invoices
This endpoint creates a new invoice with the specified amount and options. The invoice will generate a unique payment page URL that you can redirect your customers to for completing the payment.
Authentication required
#### Request Headers
| Header | Required | Description |
| -------------- | -------- | -------------------------------------------------------------------------------------- |
| `Content-Type` | Yes | Must be `application/json` |
| `Merchant` | Yes | Your merchant ID |
| `Sign` | Yes | Request signature (see [Authentication](https://docs.coinssend.com/authentication.md)) |
| `Timestamp` | No | Include only when using timestamped HMAC; `X-Timestamp` is accepted as an alias |
Build the canonical JSON body described in [Authentication](https://docs.coinssend.com/authentication.md),
sign it, and send that same string as the request body.
#### Request Parameters
| Parameter | Type | Required | Default | Description |
| ------------------------- | ------- | -------- | ------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `order_id` | string | Yes | - | Your order identifier (must be unique per merchant) |
| `amount` | string | Yes | - | Invoice amount in USD (minimum `3.00`, e.g., "10.50") |
| `is_customer_fee` | boolean | No | false | If true, the customer pays the service fee (CoinsSend commission) |
| `is_customer_network_fee` | boolean | No | false | If true, the customer pays the blockchain transaction fee; if false, the merchant covers it |
| `allow_card_payments` | boolean | No | false | If true, card checkout may be shown when the invoice amount or remaining amount is at least `$15.00` / `1500` cents |
| `allowed_coins` | array | No | all supported coins | List of currently supported cryptocurrencies to allow for this invoice |
| `coin` | string | No | - | Preselect a currently supported coin for the payment page |
| `network` | string | No | - | Preselect a network that matches the selected coin |
| `success_url` | string | No | - | URL to redirect the customer after successful payment |
| `cancel_url` | string | No | - | URL to redirect the customer if they cancel payment |
> **Important:** `coin` and `network` must be a currently supported combination.
> Fetch the live catalog from `GET /v1/coins-and-fee`. Requests containing an
> unsupported pair (for example `eth` on `tron`) or unsupported
> `allowed_coins` entries fail validation with HTTP `422`.
See [Supported Coins & Networks](https://docs.coinssend.com/supported-coins.md) for the coin/network
tables and guidance on selecting invoice payment options.
> **Card checkout:** `allow_card_payments` defaults to `false`. Card checkout is not shown or available unless you set `allow_card_payments` to `true` for that invoice. Even when cards are allowed, the invoice amount or remaining amount must be at least `$15.00` / `1500` cents. Direct crypto payment remains available when cards are disabled or the remaining amount is below the card minimum.
#### Request Example
```json
{
"order_id": "order_12345",
"amount": "100.50",
"is_customer_fee": true,
"is_customer_network_fee": false,
"allow_card_payments": true,
"allowed_coins": ["usdt", "eth"],
"coin": "usdt",
"network": "tron",
"success_url": "https://example.com/payment-success",
"cancel_url": "https://example.com/payment-canceled"
}
```
#### Success Response
```json
{
"status": "success",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"order_id": "order_12345",
"url": "https://pay.coinssend.com/invoice_code",
"code": "invoice_code",
"amount": "100.50",
"payer_amount": "100.50",
"expired_at": "2024-01-02T12:00:00+00:00",
"created_at": "2024-01-01T12:00:00+00:00"
}
}
```
#### Response Field Descriptions
| Field | Type | Description |
| -------------- | ------ | --------------------------------------------------------------------- |
| `id` | string | Unique invoice ID (UUID) |
| `order_id` | string | Unique order ID in your system |
| `url` | string | Payment URL for the invoice |
| `code` | string | Unique invoice code |
| `amount` | string | Invoice amount |
| `payer_amount` | string | Total amount the payer needs to pay including fees |
| `expired_at` | string | ISO 8601 timestamp when the invoice expires (24 hours after creation) |
| `created_at` | string | ISO 8601 timestamp when the invoice was created |
`allow_card_payments` and `provider_card` are not returned by `POST /v1/invoices`. They can appear in invoice lookup and payment-page invoice payloads, where `provider_card` explains whether card checkout is currently available and why it may be disabled.
#### Card Payment Availability
Direct crypto payment is always available on the payment page. Card checkout is optional per invoice and requires both of these conditions:
1. `allow_card_payments` is `true` on the invoice.
2. The invoice amount, or remaining amount after a partial payment, is at least `$15.00` / `1500` cents.
If either condition is not met, the payment page does not show card checkout and customers can still pay directly with crypto. In invoice lookup and payment-page invoice payloads, the `provider_card` object explains the current card state with `enabled`, `disabled_reason`, `minimum_amount_cents`, and `remaining_amount_cents`.
Provider redirects, KYC success, provider webhooks, and provider status polling are not final payment confirmation. Chain confirmation remains the source of truth for moving an invoice to `paid`.
### Create Provider-Card Order
Creates a card checkout order for an existing invoice when `allow_card_payments` is enabled and the remaining invoice amount is at least `$15.00` / `1500` cents.
#### Request Parameters
| Parameter | Type | Required | Default | Description |
| --------- | ------ | -------- | ------- | ------------------------------------------------ |
| `email` | string | No | - | Optional customer email to store on the invoice. |
Do not send `provider`, `coin`, or `network` fields for card checkout. CoinsSend
selects these values. If a client includes them, they are ignored and cannot
override the selected checkout route.
#### Request Example
```json
{
"email": "customer@example.com"
}
```
#### Success Response
```json
{
"status": "success",
"data": {
"provider_card_order": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"provider": "simpleswap",
"local_status": "redirect_pending",
"expires_at": "2026-05-11T12:30:00+00:00",
"redirect_url": "https://provider.example/checkout/session"
},
"reused": false
}
}
```
Repeating this operation while the same active order is usable returns that
order with `reused: true`. This narrow active-order reuse is not a general
`Idempotency-Key` contract.
### Retrieve Invoice {#retrieve-invoice}
Returns the current public checkout state for an invoice.
```text
GET /v1/invoices/{invoiceCode}
```
No merchant signature is required. The response uses the standard success
envelope with `data.invoice` and current `data.network_fees`. The invoice
object includes amounts, payment selection, status, allowed payment options,
transaction hashes, QR URL, and `provider_card` capability. See the exact
machine-readable schema in [OpenAPI](https://docs.coinssend.com/openapi.json).
Treat the invoice code as a public checkout identifier. Do not use it as
authorization for merchant balances, fees, wallet creation, or withdrawals.
> **AI assistant prompt:** Build a server-side invoice creation and buyer redirect flow.
> Create `POST /v1/invoices`, store the invoice ID, code, status, and URL, then send the buyer to `data.url`.
> Treat success/cancel redirect URLs as UX signals only, not payment confirmation.
> Confirm payment through [Webhooks](https://docs.coinssend.com/webhooks.md), handle `partial` as unpaid progress, and handle `expired` as a terminal failure state.
> For the full checkout prompt, see [AI Integration Prompts](https://docs.coinssend.com/ai-integration-prompts.md).
#### Error Responses
| HTTP status | Response shape | Description |
| ----------- | ------------------------------------------------------------ | --------------------------------------------------------------------------- |
| `400` | `{"error":"Missing headers"}` | Merchant or Sign header is missing |
| `401` | `{"error":"Invalid merchant"}` or `{"error":"Invalid sign"}` | Authentication failed |
| `403` | authentication or endpoint error | Merchant status blocks invoice creation |
| `409` | `{"status":"error","message":"Order id already exists"}` | The merchant order ID already exists; the original response is not replayed |
| `422` | validation error with `errors`, `status`, and `message` | Request parameters are invalid |
### Invoice Fees
When an invoice is paid, the amount your customer must send is shown in the
`payer_amount` field. This value includes the blockchain network fee and may also
include the service fee depending on the `is_customer_fee` setting.
1. **Service Fee (CoinsSend commission)** – Calculated as `amount × service_fee_percent`.
It is added to `payer_amount` only when `is_customer_fee` is `true`. Otherwise
this fee is deducted from the merchant balance.
2. **Network/Transaction Fee** – Specified in the same coin as the invoice payment.
Network fee values can be obtained from the `network_fees` list in the
[Merchant API](https://docs.coinssend.com/merchants.md). It is added to `payer_amount` only when
`is_customer_network_fee` is `true`. When the merchant covers it (`false`),
the customer sees no network surcharge and the fee is deducted from the merchant
proceeds.
The final crypto amount to pay is calculated as:
```text
amount_coin = (amount + (service_fee if is_customer_fee else 0)) / rate
total_coin = amount_coin + (network_fee if is_customer_network_fee else 0)
```
#### Fee Calculation Examples
**Customer Pays Service Fee**
```
Invoice amount: 100 USD
Network fee: 1 USD
Service fee percent: 0.45%
Service fee: 100 × 0.45% = 0.45 USD
Total amount to pay: 100 + 1 + 0.45 = 101.45 USD
```
**Merchant Pays Service Fee**
```
Invoice amount: 100 USD
Network fee: 1 USD
Service fee percent: 0.45%
Service fee: 100 × 0.45% = 0.45 USD
Total amount to pay: 100 + 1 = 101 USD
Service fee deducted from merchant balance: 0.45 USD
```
**Merchant Pays Network Fee**
```
Invoice amount: 100 USD
Network fee: 1 USD
Service fee percent: 0.45%
Network fee covered by merchant ➜ payer sends: 100.45 USD if customer pays service fee
or 100 USD if merchant also covers service fee.
```
**ETH USDC Example**
Using the ETH network with USDC, the exchange rate on 2025‑07‑09 is
`1 USDC = 1.0000363274333 USD`.
```
Invoice amount: 100 USD
Network fee: 1 USDC
Service fee percent: 0.45%
Service fee: 100 × 0.45% = 0.45 USD
Amount before network fee: 100 + 0.45 = 100.45 USD
Amount in USDC: 100.45 ÷ 1.000036327 ≈ 100.4463293 USDC
Total to send: 100.4463293 + 1 ≈ 101.4463293 USDC
```
The service fee is rounded down to 8 decimal places when converting to
cryptocurrency because invoice amounts are stored in cents. The formula is:
```text
fee_coin = floor(amount_coin × service_fee_percent × 100000000) / 100000000
```
Where `amount_coin` is the invoice amount in the selected coin before the network fee is added.
#### Code Examples
## Invoice Statuses
Invoices transition through the following states:
| Status | Description |
| -------------- | -------------------------------------------------------------- |
| `new` | Invoice created; no payment method selected. |
| `waiting` | Payment method locked in; awaiting blockchain payment. |
| `partial` | Partial payment detected; remaining balance still due. |
| `paid` | Full payment confirmed. |
| `expired` | Invoice expired (24 hours after creation) before full payment. |
| `aml_rejected` | Payment was rejected by AML processing. |
Note: Invoice webhook events are currently emitted for invoice.paid, invoice.expired, and AML rejection. Do not assume every intermediate status has an event. See the Webhooks documentation for details.
## Best Practices
- **Store Invoice Data**: Always store the invoice code and status in your database for tracking and reconciliation.
- **Handle Webhooks**: Implement webhook handling to receive real-time notifications of payment status changes.
- **Support Partial Payments**: Implement UI and business logic to handle partially paid invoices, allowing customers to complete payments in multiple transactions.
- **Track Payment Progress**: Use `payed_usd`, `payed_crypto`, and payer amount fields from invoice lookup to show payment progress.
- **Use Card Checkout Deliberately**: Set `allow_card_payments` only when you want card checkout available. Keep direct crypto payment available for invoices that do not allow cards or fall below the `$15.00` card minimum after partial payment.
- **Verify Webhooks**: Always verify webhook signatures to ensure they come from CoinsSend.
- **Check Expiration**: Invoices expire after 24 hours. Consider this when implementing payment flows.
## Related Resources
- [Webhooks](https://docs.coinssend.com/webhooks.md) - Receive real-time notifications for invoice status changes
- [Authentication](https://docs.coinssend.com/authentication.md) - Learn about API authentication and signatures
- [Error Handling](https://docs.coinssend.com/error-handling.md) - Understand error responses and how to handle them
## Code Examples
For complete code examples in various languages, see:
- [PHP Examples](https://docs.coinssend.com/examples/php-invoice.md)
- [JavaScript Examples](https://docs.coinssend.com/examples/js-invoice.md)
- [Python Examples](https://docs.coinssend.com/examples/python-invoice.md)
---
Source: https://docs.coinssend.com/static-wallets.md
# Static Wallets API
Static wallets are reusable cryptocurrency addresses. Create them with the
merchant-signed API, then process deposits through signed webhooks.
## Create a wallet address {#create-wallet-address}
```http
POST /v1/wallet-address
```
### Headers
| Name | Required | Description |
| -------------- | -------: | -------------------------------------------------------------------- |
| `Content-Type` | Yes | `application/json` |
| `Merchant` | Yes | Merchant ID |
| `Sign` | Yes | Signature of the canonical request body |
| `Timestamp` | No | Unix seconds. If present, `Sign` must use the timestamped HMAC form. |
See [Authentication](https://docs.coinssend.com/authentication.md) and the executable
[signature test vectors](https://docs.coinssend.com/signature-test-vectors.json). The signature is made
from PHP-compatible canonical JSON, and those exact canonical bytes must be sent
as the HTTP body. Do not sign plain `JSON.stringify()` or default
`json.dumps()` output.
### Request body
| Field | Type | Required | Description |
| ------------- | -------------- | -------: | ------------------------------------------------------ |
| `network` | string | Yes | Network identifier. Input is trimmed and lowercased. |
| `coin` | string | Yes | Coin identifier. Input is trimmed and lowercased. |
| `type` | string | No | `static` (default) or `merchant`. |
| `label` | string or null | No | Merchant-defined label, at most 255 characters. |
| `webhook_url` | URI or null | No | Address-specific callback URL, at most 255 characters. |
```json
{
"network": "tron",
"coin": "usdt",
"type": "static",
"label": "Checkout deposits",
"webhook_url": "https:\/\/merchant.example\/coinssend\/webhook"
}
```
The network/coin example is illustrative. Asset availability can change. Use
`GET /v1/coins-and-fee` for discovery, but treat the create request's validation
response as final because wallet creation may not be available for every listed
pair at every moment.
See [Supported Coins & Networks](https://docs.coinssend.com/supported-coins.md) for the coin/network
tables, API identifiers, and wallet selection guidance.
Multiple `static` wallets can be created for the same pair. A merchant can have
only one `merchant` wallet for a pair. A `merchant` wallet deposit is credited
without the static-wallet fee or 24-hour hold; those are different product
semantics, not an idempotency guarantee.
### Success response
```json
{
"status": "success",
"data": {
"address": "TXHXiQ2aXYqtz3y9gEGiVjzxXxXechdLwE",
"qr_code_url": "https://api.coinssend.com/v1/wallet-addresses/87654321-4321-4321-4321-210987654321/qr"
}
}
```
`qr_code_url` is a public binary-image endpoint. Its path parameter is the
wallet-address record ID, not the blockchain address.
Complete server-side examples:
- [PHP](https://docs.coinssend.com/examples/php-static-wallet.md)
- [Node.js](https://docs.coinssend.com/examples/js-static-wallet.md)
- [Python](https://docs.coinssend.com/examples/python-static-wallet.md)
Keep the API key on a trusted server. Browser-side signing exposes the key.
## Errors
There is no symbolic `code` field in the current wire contract. Authentication
errors return a simple `error` string, endpoint errors return
`status`/`message`, and validation failures return `message` plus `errors`.
| HTTP status | Current condition | Body shape |
| ----------: | -------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| `400` | Missing `Merchant` or `Sign` | `{"error":"Missing headers"}` |
| `400` | Duplicate `merchant` wallet or another request error | `{"status":"error","message":"..."}` |
| `401` | Invalid merchant, signature, or timestamp | `{"error":"..."}` |
| `403` | Merchant status blocks wallet creation | `{"status":"error","message":"..."}` or authentication `error` shape |
| `409` | Concurrent creation already in progress for the merchant | `{"status":"error","message":"Wallet creation already in progress for this merchant."}` |
| `422` | Invalid fields or unsupported pair | `{"message":"...","errors":{"field":["..."]}}` |
| `429` | Wallet-creation limit reached | `{"status":"error","message":"Too many requests. Please try again later."}` |
The application records at most 100 successful wallet creations per merchant
in a 60-second window. Infrastructure in front of the application may enforce
additional limits, so clients must also honor `429` and `Retry-After` when it is
present.
Wallet creation has no request idempotency-key contract. A timed-out `POST` may
have succeeded, and retrying a `static` request can create another address. Do
not retry automatically; reconcile the outcome through merchant tooling or
support before issuing another creation request.
## Current fees and balances
Do not hard-code fee values. Fetch merchant-specific percentages and configured
network fees from signed `GET /v1/merchants/fees`, and use
`GET /v1/coins-and-fee` for the current public asset/fee catalog. Values and
enabled pairs can change.
For `static` wallets, CoinsSend deducts the configured merchant and network
fees before crediting the net amount. A static-wallet deposit is held for 24
hours before becoming available. For `merchant` wallets, those fees are not
deducted and the deposit is immediately available. Signed webhook amounts are
the authoritative transaction snapshot for your reconciliation record.
## Deposit webhook
The public payload event for a successful wallet deposit is
`wallet.transaction`. Use only the event values documented here and in
[Webhooks](https://docs.coinssend.com/webhooks.md).
```json
{
"event": "wallet.transaction",
"timestamp": 1746984120,
"data": {
"wallet": {
"address": "TXHXiQ2aXYqtz3y9gEGiVjzxXxXechdLwE",
"network": "tron",
"coin": "usdt",
"label": "Checkout deposits",
"rate": "1.0001"
},
"transaction": {
"id": "87654321-4321-4321-4321-210987654321",
"hash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"block_number": 53214321,
"amounts": {
"gross": {"amount": "10.05", "currency": "usd", "crypto": "10.05"},
"net": {"amount": "9.849", "currency": "usd", "crypto": "9.849"}
},
"fees": {
"merchant": {"amount": "0.201", "currency": "usd", "crypto": "0.201"},
"network": {"amount": "0", "currency": "usd", "crypto": "0"},
"total": {"amount": "0.201", "currency": "usd", "crypto": "0.201"}
},
"from_address": "TMDKznuDWaZwfZHcM61FYFRx9dAVPndpNv",
"to_address": "TXHXiQ2aXYqtz3y9gEGiVjzxXxXechdLwE",
"status": "success",
"timestamp": 1746984100
}
}
}
```
Callbacks go to the address-specific `webhook_url` when set, otherwise to the
merchant's default callback URL. Verify `X-Signature` against the exact raw body
bytes before parsing JSON. `X-Idempotency-Key` is included only when the
delivery path supplies one, so consumers should also deduplicate by stable
business identifiers such as `data.transaction.id`.
AML rejection uses the separate external event
`aml.rejected.static_wallet`. See [Webhooks](https://docs.coinssend.com/webhooks.md) for payloads,
signature verification, delivery behavior, and the complete event list.
---
Source: https://docs.coinssend.com/withdrawals.md
# Withdrawals API
This document describes the endpoints for initiating cryptocurrency withdrawals.
Important: To use this API, you must first enable the "Allow Withdrawals" setting in your merchant dashboard. Withdrawal requests will be rejected if this setting is not enabled.
## Initiate Withdrawal
Initiates a withdrawal of funds from your merchant account to an external wallet address.
### Request
```
POST /v1/withdrawals
```
### Headers
| Name | Required | Description |
| -------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `Content-Type` | Yes | Must be `application/json` |
| `Merchant` | Yes | Your merchant ID |
| `Timestamp` | Yes | Unix timestamp in seconds. Must be within 5 minutes of server time. `X-Timestamp` is also accepted. |
| `Sign` | Yes | HMAC-SHA256 withdrawal signature (see [Authentication](https://docs.coinssend.com/authentication.md#timestamped-hmac-signature)) |
Generate the canonical JSON body described in [Authentication](https://docs.coinssend.com/authentication.md),
sign it, and send that same string as the request body:
```text
Sign = hmac_sha256(base64(canonical_json_body) + "." + Timestamp, API_KEY)
```
### Request Parameters
| Parameter | Type | Required | Description |
| ------------ | ------ | -------- | ----------------------------------------------------------------------------------------------------------------------- |
| `network` | string | Yes | Blockchain network (e.g., "tron", "bsc") |
| `coin` | string | Yes | Cryptocurrency (e.g., "usdt", "usdc") |
| `amount` | string | Yes | Amount to withdraw in actual value (the minimum depends on the selected asset/network pair, e.g., "50.5" for 50.5 USDT) |
| `to_address` | string | Yes | Destination wallet address |
> **Asset catalog:** Use `GET /v1/coins-and-fee` to fetch the currently enabled
> coin/network pairs, network fees, and minimum withdrawal values. These values
> can change. The withdrawal endpoint remains the final validation
> authority for the pair and amount.
See [Supported Coins & Networks](https://docs.coinssend.com/supported-coins.md) for the coin/network
tables and guidance on displaying withdrawal options.
### Example Request
```json
{
"network": "tron",
"coin": "usdt",
"amount": "50.75",
"to_address": "TXHXiQ2aXYqtz3y9gEGiVjzxXxXechdLwE"
}
```
Note: Minimum and maximum withdrawal amounts depend on the current asset/network settings. Do not embed example thresholds as permanent constants.
### Example Response
```json
{
"data": {
"success": true,
"withdrawal_id": "5118a064-d5b9-4fab-ad14-8cf42f0020de",
"message": "Withdrawal request submitted successfully",
"status": "pending",
"amounts": {
"requested": {
"crypto": "100.00",
"usd": "100.00"
},
"net": {
"crypto": "98.15",
"usd": "98.15"
}
},
"fees": {
"merchant": {
"crypto": "0.45",
"usd": "0.45"
},
"network": {
"crypto": "1.40",
"usd": "1.40"
},
"total": {
"crypto": "1.85",
"usd": "1.85"
}
},
"fee_payer": {
"merchant_fee": false,
"network_fee": false
}
},
"status": "success"
}
```
### Response Field Descriptions
| Field | Type | Description |
| ------------------------------- | ------- | ------------------------------------------------------------------ |
| `data` | object | Response data container |
| `data.success` | boolean | Whether the withdrawal request succeeded |
| `data.withdrawal_id` | string | Unique withdrawal identifier |
| `data.message` | string | Human-readable message |
| `data.status` | string | Current withdrawal status (pending, processing, completed, failed) |
| `data.amounts` | object | Requested and net withdrawal amounts |
| `data.amounts.requested.crypto` | string | Requested amount in cryptocurrency |
| `data.amounts.requested.usd` | string | Requested amount in USD |
| `data.amounts.net.crypto` | string | Net amount after fees in cryptocurrency |
| `data.amounts.net.usd` | string | Net amount after fees in USD |
| `data.fees` | object | Fee breakdown |
| `data.fees.merchant.crypto` | string | Merchant fee in cryptocurrency |
| `data.fees.merchant.usd` | string | Merchant fee in USD |
| `data.fees.network.crypto` | string | Network fee in cryptocurrency |
| `data.fees.network.usd` | string | Network fee in USD |
| `data.fees.total.crypto` | string | Total fee in cryptocurrency |
| `data.fees.total.usd` | string | Total fee in USD |
| `data.fee_payer.merchant_fee` | boolean | Whether merchant pays the merchant fee |
| `data.fee_payer.network_fee` | boolean | Whether merchant pays the network fee |
| `status` | string | Overall response status ("success" or "error") |
If your merchant account has the "Merchant Pays" settings enabled, the fields in `fee_payer` would be `true` and the `amounts.net.crypto` would equal the requested `amounts.requested.crypto`.
### Error Responses
| HTTP status | Response shape | Description |
| ----------- | ---------------------------------------------------------------- | --------------------------------------------------------------- |
| `400` | `{"error":"Missing headers"}` or `{"error":"Missing timestamp"}` | Required authentication input is missing |
| `400` | endpoint error with `status` and `message` | Includes withdrawal-permission denial and balance/fee rejection |
| `401` | authentication `{"error":"..."}` | Invalid merchant, signature, or expired timestamp |
| `403` | authentication or endpoint error | Merchant status blocks withdrawals |
| `422` | validation error with `errors`, `status`, and `message` | Pair, amount, or destination validation failed |
### Example Error Response
```json
{
"status": "error",
"message": "Insufficient available balance"
}
```
### Withdrawal Limits
Each asset/network pair has current minimum and maximum amounts. Fetch the
current discovery catalog immediately before presenting withdrawal choices and
handle `422` as the authoritative answer if configuration changes between
discovery and submission. The public schema intentionally does not publish a
closed asset enum or static limit table.
### Merchant Settings for Withdrawals
To use the Withdrawals API, you must first enable withdrawals in your merchant settings by checking the "**Allow Withdrawals**" checkbox. The description in settings reads: "Enable this to allow withdrawals via API". This setting is required before any withdrawal API calls will work.
Additionally, there are two important settings that control how withdrawal fees are handled:
1. **"Pay Withdrawal Network Fee"** - The description in settings reads: "Enable this if merchant will pay withdrawal network fee". When enabled, the network fee is deducted from the merchant's balance instead of the withdrawal amount.
2. **"Pay Withdrawal Service Fee"** - The description in settings reads: "Enable this if merchant will pay withdrawal service fee". When enabled, the merchant fee is deducted from the merchant's balance instead of the withdrawal amount.
If both checkboxes are enabled, the recipient will receive exactly the amount specified in the API request, as all fees will be deducted from your merchant balance instead of the withdrawal amount.
### Withdrawal Fees
When initiating withdrawals, two types of fees apply:
1. **Network Fee**: A fixed fee for processing transactions on the blockchain
2. **Merchant Fee**: A percentage-based fee applied to the withdrawal amount
#### Fee Discovery
Use `GET /v1/merchants/fees` for merchant-specific percentages and fee-payer
settings, and `GET /v1/coins-and-fee` for current network-fee discovery. The
accepted withdrawal response is the authoritative fee snapshot for that
withdrawal.
#### Fee Calculation Examples
The following arithmetic is illustrative only; substitute live fee values.
**Scenario 1: Recipient Amount Is Reduced by Fees**
When both "Pay Withdrawal Service Fee" and "Pay Withdrawal Network Fee" checkboxes are disabled:
```
Withdrawal amount: 100 USDT
Network fee: 1.4 USDT (fixed)
Merchant fee: 100 × 0.45% = 0.45 USDT
Total fees: 1.4 + 0.45 = 1.85 USDT
Amount sent to destination: 100 - 1.85 = 98.15 USDT
Deducted from merchant balance: 0 USDT
```
**Scenario 2: Merchant Pays All Fees**
When both "Pay Withdrawal Service Fee" and "Pay Withdrawal Network Fee" checkboxes are enabled:
```
Withdrawal amount: 100 USDT
Network fee: 1.4 USDT (fixed)
Merchant fee: 100 × 0.45% = 0.45 USDT
Total fees: 1.4 + 0.45 = 1.85 USDT
Amount sent to destination: 100 USDT (full amount)
Deducted from merchant balance: 1.85 USDT (all fees)
```
The response includes the fields `fee_payer.merchant_fee` and `fee_payer.network_fee` to indicate which fee payment options are active for your merchant account. These correspond directly to the "Pay Withdrawal Service Fee" and "Pay Withdrawal Network Fee" settings.
### Additional Notes
- Withdrawals are processed asynchronously
- The HTTP `200` response contains the current projected status, commonly `pending` or `processing`; it is not final settlement
- You will receive `withdrawal.in_progress`, `withdrawal.success`, or `withdrawal.failed` webhooks
- The `to_address` must be valid for the selected network
- Always double-check the destination address before initiating a withdrawal
- There is no request `Idempotency-Key`. If the outcome is unknown, reconcile by the returned/stored withdrawal ID or contact support instead of automatically submitting the same withdrawal again.
## Code Examples
Generate withdrawal signatures only in trusted server-side code. Do not expose merchant API keys in browsers, mobile apps, or public frontend bundles.
For more details on working with withdrawals, see [the Node.js withdrawal example](https://docs.coinssend.com/examples/node-withdrawal.md).
---
Source: https://docs.coinssend.com/webhooks.md
# Webhooks
CoinsSend uses webhooks to notify your application about events that happen in your merchant account. Webhooks are HTTP callbacks that receive notification payloads when events occur.
## Setting Up Webhooks
You can set up a webhook URL in your merchant settings. This URL will receive POST requests when specific events occur.
## Authentication
For security, all webhook requests include an HMAC signature in the `X-Signature` header. You should validate this signature to ensure the webhook was sent by CoinsSend.
The signature is generated using HMAC-SHA256, with your API key as the secret:
```php
// PHP example for validating a webhook signature
function isValidSignature($payload, $signature, $secretKey) {
$computedSignature = hash_hmac('sha256', $payload, $secretKey);
return hash_equals($computedSignature, $signature);
}
// Usage
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_SIGNATURE'];
$secretKey = 'your_api_key';
if (!isValidSignature($payload, $signature, $secretKey)) {
http_response_code(401);
echo json_encode(['error' => 'Invalid signature']);
exit;
}
```
Important: calculate the HMAC from the exact raw request body bytes that were received. Do not decode the JSON and re-encode it before verification, because key order, spacing, and escaping may change the payload and produce a different signature.
This raw-body rule is specific to **receiving webhooks**. Merchant API request
signing uses the separate canonical-JSON contract in
[Authentication](https://docs.coinssend.com/authentication.md).
> **AI assistant prompt:** Implement a CoinsSend webhook receiver that preserves the raw request body before JSON parsing.
> Read `X-Signature`, verify it with HMAC-SHA256 and the merchant API key, then parse the payload only after the signature passes.
> Add idempotent processing for duplicate deliveries and persist outcomes for `invoice.paid`, `invoice.expired`, and `aml.rejected.invoice`.
> Use [AI Integration Prompts](https://docs.coinssend.com/ai-integration-prompts.md) for the full webhook receiver prompt.
## Webhook Events
The externally emitted `event` values are:
- `invoice.paid`
- `invoice.expired`
- `wallet.transaction`
- `aml.rejected.invoice`
- `aml.rejected.static_wallet`
- `withdrawal.in_progress`
- `withdrawal.success`
- `withdrawal.failed`
- `test`
Use only the public event values listed above when routing webhook payloads.
### Fee Payer Information
For invoice webhooks (`invoice.paid` and `invoice.expired`), the `fee_payer` object indicates who pays each fee:
- `merchant_fee`: `true` if customer pays, `false` if merchant pays
- `network_fee`: `true` if customer pays, `false` if merchant pays
For withdrawal webhooks the boolean direction is different: `true` means the
merchant pays that withdrawal fee. Treat invoice and withdrawal payloads as
event-discriminated schemas rather than sharing one ambiguous `fee_payer` type.
### `invoice.paid`
Sent when an invoice is paid.
```json
{
"event": "invoice.paid",
"timestamp": 1746984000,
"data": {
"invoice_id": "12345678-1234-1234-1234-123456789012",
"order_id": "order_12345",
"code": "invoice_code",
"status": "paid",
"amounts": {
"requested": {
"amount": "100.50",
"currency": "usd",
"crypto": "100.5"
},
"paid": {
"amount": "100.50",
"currency": "usd",
"crypto": "100.5"
}
},
"fees": {
"merchant": {
"amount": "2.0100",
"currency": "usd",
"crypto": "2.0100"
},
"network": {
"amount": "1.4000",
"currency": "usd",
"crypto": "1.4000"
},
"total": {
"amount": "3.4100",
"currency": "usd",
"crypto": "3.4100"
}
},
"payment": {
"coin": "usdt",
"network": "tron",
"address": "TXHXiQ2aXYqtz3y9gEGiVjzxXxXechdLwE",
"rate": "1.0001"
},
"fee_payer": {
"merchant_fee": true,
"network_fee": true
},
"created_at": "2025-01-01T11:30:00Z",
"paid_at": "2025-01-01T12:00:00Z"
}
}
```
### `invoice.expired`
Sent when an invoice expires without being paid (after 24 hours).
Important: The payment field is optional and will only be present if a payment method was selected before the invoice expired.
```json
{
"event": "invoice.expired",
"timestamp": 1746984000,
"data": {
"invoice_id": "12345678-1234-1234-1234-123456789012",
"order_id": "order_12345",
"code": "invoice_code",
"status": "expired",
"amounts": {
"requested": {
"amount": "100.50",
"currency": "usd",
"crypto": "100.5"
},
"paid": {
"amount": "0",
"currency": "usd",
"crypto": "0"
}
},
"fees": {
"merchant": {
"amount": "0",
"currency": "usd",
"crypto": "0"
},
"network": {
"amount": "0",
"currency": "usd",
"crypto": "0"
},
"total": {
"amount": "0",
"currency": "usd",
"crypto": "0"
}
},
"payment": {
"coin": "usdt",
"network": "tron",
"address": "TXHXiQ2aXYqtz3y9gEGiVjzxXxXechdLwE",
"rate": "1.0001"
},
"fee_payer": {
"merchant_fee": true,
"network_fee": true
},
"created_at": "2025-01-01T11:30:00Z",
"expired_at": "2025-01-02T11:30:00Z"
}
}
```
### `wallet.transaction`
Sent when a static wallet receives a deposit.
```json
{
"event": "wallet.transaction",
"timestamp": 1746984120,
"data": {
"wallet": {
"address": "TXHXiQ2aXYqtz3y9gEGiVjzxXxXechdLwE",
"network": "tron",
"coin": "usdt",
"label": "Main wallet",
"rate": "1.0001"
},
"transaction": {
"id": "87654321-4321-4321-4321-210987654321",
"hash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
"block_number": 53214321,
"amounts": {
"gross": {
"amount": "10.05",
"currency": "usd",
"crypto": "10.05"
},
"net": {
"amount": "9.849",
"currency": "usd",
"crypto": "9.849"
}
},
"fees": {
"merchant": {
"amount": "0.201",
"currency": "usd",
"crypto": "0.201"
},
"network": {
"amount": "0",
"currency": "usd",
"crypto": "0"
},
"total": {
"amount": "0.201",
"currency": "usd",
"crypto": "0.201"
}
},
"from_address": "TMDKznuDWaZwfZHcM61FYFRx9dAVPndpNv",
"to_address": "TXHXiQ2aXYqtz3y9gEGiVjzxXxXechdLwE",
"status": "success",
"timestamp": 1746984100
}
}
}
```
### `aml.rejected.invoice`
Sent when AML screening blocks an invoice deposit. Snapshot details supplied by
the AML flow are carried inside `risk_details`.
```json
{
"event": "aml.rejected.invoice",
"timestamp": 1765472000,
"data": {
"wallet_address": "TXHXiQ2aXYqtz3y9gEGiVjzxXxXechdLwE",
"coin": "usdt",
"network": "tron",
"amounts": {
"amount": "100.00",
"currency": "usd",
"crypto": "1"
},
"invoice": {
"id": "5118a064-d5b9-4fab-ad14-8cf42f0020de",
"code": "INV123",
"order_id": "ORDER-1"
},
"risk_score": 95,
"risk_level": "severe",
"risk_details": {
"amount_smallest": "1000000",
"usd_cents": "10000",
"balance_snapshot": "1000000",
"source": "misttrack"
}
}
}
```
### `aml.rejected.static_wallet`
Sent when AML blocks a static-wallet deposit. Snapshot details supplied by the
AML flow are carried inside `risk_details`.
```json
{
"event": "aml.rejected.static_wallet",
"timestamp": 1765472000,
"data": {
"wallet_address": "TXHXiQ2aXYqtz3y9gEGiVjzxXxXechdLwE",
"coin": "usdt",
"network": "tron",
"amounts": {
"amount": "250.00",
"currency": "usd",
"crypto": "2.5"
},
"static_wallet": {
"id": "87654321-4321-4321-4321-210987654321",
"label": "USDT Payment Address"
},
"risk_score": 87,
"risk_level": "high",
"risk_details": {
"amount_smallest": "2500000",
"usd_cents": "25000",
"balance_snapshot": "2500000",
"source": "misttrack"
}
}
}
```
### `withdrawal.in_progress`
Sent when a withdrawal request is being processed.
```json
{
"event": "withdrawal.in_progress",
"timestamp": 1746984100,
"data": {
"withdrawal_id": "5118a064-d5b9-4fab-ad14-8cf42f0020de",
"status": "processing",
"amounts": {
"requested": {
"amount": "5.0000",
"currency": "usd",
"crypto": "5"
},
"net": {
"amount": "5.0000",
"currency": "usd",
"crypto": "5"
}
},
"fees": {
"merchant": {
"amount": "0.0225",
"currency": "usd",
"crypto": "0.0225"
},
"network": {
"amount": "1.4000",
"currency": "usd",
"crypto": "1.4"
},
"total": {
"amount": "1.4225",
"currency": "usd",
"crypto": "1.4225"
}
},
"payment": {
"coin": "usdt",
"network": "tron",
"to_address": "TLbqtn4EPerCj5a3TmbSffadYgDZ3UEmoe",
"rate": "1.0001"
},
"created_at": "2025-05-11T17:20:50+00:00",
"updated_at": "2025-05-11T17:21:40+00:00",
"transaction_hash": "22255482649487ca9f6f70a618c5b2e5e93399c09fd85ab589eff20cb0996e34",
"fee_payer": {
"merchant_fee": false,
"network_fee": false
}
}
}
```
### `withdrawal.success`
Sent when a withdrawal has been successfully completed.
```json
{
"event": "withdrawal.success",
"timestamp": 1746984108,
"data": {
"withdrawal_id": "5118a064-d5b9-4fab-ad14-8cf42f0020de",
"status": "completed",
"amounts": {
"requested": {
"amount": "5.0000",
"currency": "usd",
"crypto": "5"
},
"net": {
"amount": "5.0000",
"currency": "usd",
"crypto": "5"
}
},
"fees": {
"merchant": {
"amount": "0.0225",
"currency": "usd",
"crypto": "0.0225"
},
"network": {
"amount": "1.4000",
"currency": "usd",
"crypto": "1.4"
},
"total": {
"amount": "1.4225",
"currency": "usd",
"crypto": "1.4225"
}
},
"payment": {
"coin": "usdt",
"network": "tron",
"to_address": "TLbqtn4EPerCj5a3TmbSffadYgDZ3UEmoe",
"rate": "1.0001"
},
"created_at": "2025-05-11T17:20:50+00:00",
"transaction_hash": "934cc03cfac6a1fe99be9156fde1807cfbe74e1ed7fe5e184596b932b7c9f30a",
"updated_at": "2025-05-11T17:21:48+00:00",
"fee_payer": {
"merchant_fee": false,
"network_fee": false
}
}
}
```
### `withdrawal.failed`
Sent when a withdrawal has failed.
```json
{
"event": "withdrawal.failed",
"timestamp": 1746984110,
"data": {
"withdrawal_id": "5118a064-d5b9-4fab-ad14-8cf42f0020de",
"status": "failed",
"amounts": {
"requested": {
"amount": "5.0000",
"currency": "usd",
"crypto": "5"
},
"net": {
"amount": "5.0000",
"currency": "usd",
"crypto": "5"
}
},
"fees": {
"merchant": {
"amount": "0.0225",
"currency": "usd",
"crypto": "0.0225"
},
"network": {
"amount": "1.4000",
"currency": "usd",
"crypto": "1.4"
},
"total": {
"amount": "1.4225",
"currency": "usd",
"crypto": "1.4225"
}
},
"payment": {
"coin": "usdt",
"network": "tron",
"to_address": "TLbqtn4EPerCj5a3TmbSffadYgDZ3UEmoe",
"rate": "1.0001"
},
"created_at": "2025-05-11T17:20:50+00:00",
"updated_at": "2025-05-11T17:21:50+00:00",
"error_message": "Insufficient energy on network",
"fee_payer": {
"merchant_fee": false,
"network_fee": false
}
}
}
```
## Responding to Webhooks
Your server should respond with a 2xx HTTP status to acknowledge receipt. A
non-2xx response or transport failure can result in another delivery. The exact
attempt count and schedule are not guaranteed, so do not depend on a fixed
retry count.
### Delivery Idempotency
Withdrawal events currently include a stable header:
```text
X-Idempotency-Key: withdrawal:{withdrawal_id}:{event}
```
The same key is reused across withdrawal delivery attempts and manual resends.
Invoice, wallet, AML, and test events do not currently guarantee this header.
For those events, store a receiver-side deduplication key built from the event
and its stable entity identifier where available. Make processing idempotent
even when the header is absent.
## Testing Webhooks
You can test your webhook integration from your merchant dashboard using the "Test Webhook" feature. This will send a test webhook to your configured webhook URL with sample data.
### Test Webhook Payload
The dashboard sends a simple payload when you trigger a test:
```json
{
"event": "test",
"timestamp": 1746984000,
"data": {
"merchant_id": "12345678-1234-1234-1234-123456789012",
"test": true,
"message": "This is a test webhook to verify your integration / - ё"
}
}
```
## Webhook Delivery
- Webhooks are delivered in real-time as events occur
- Delivery can be attempted more than once; the exact retry policy is not a public constant
- Delivery outcomes are logged for operational review
- Failed webhooks can be manually resent from the merchant dashboard
## Best Practices
1. **Respond Quickly**: Your webhook endpoint should respond as quickly as possible, ideally within a few seconds.
2. **Process Asynchronously**: To ensure quick responses, process the webhook data asynchronously after acknowledging receipt.
3. **Verify Signatures**: Always verify the webhook signature to ensure the request is legitimate.
4. **Handle Duplicate Events**: Use `X-Idempotency-Key` when present and an event/entity deduplication key otherwise.
5. **Monitor Webhook Failures**: Regularly check your merchant dashboard for failed webhook deliveries.
6. **Test Your Integration**: Use the test webhook feature to ensure your endpoint properly handles and processes webhook data.
---
Source: https://docs.coinssend.com/merchants.md
# Merchant API
This document describes endpoints related to merchant account management, balances, and statistics.
## Get Merchant Balances
Retrieves the current cryptocurrency balances for a merchant, including available and frozen amounts with USD conversions.
### Request
```
GET /v1/merchants/balances
```
### Headers
| Name | Required | Description |
| -------------- | -------- | -------------------------------------------------------------------------------------- |
| `Content-Type` | Yes | Must be `application/json` |
| `Merchant` | Yes | Your merchant ID |
| `Sign` | Yes | Request signature (see [Authentication](https://docs.coinssend.com/authentication.md)) |
### Example Response
```json
{
"status": "success",
"data": {
"total_balance_usd": "62.2798",
"total_frozen_balance_usd": "0.0000",
"total_available_balance_usd": "62.2798",
"balances": [
{
"network": "tron",
"coin": "usdt",
"balance": "7.85938000",
"available_balance": "7.85938000",
"frozen_balance": "0.00000000",
"coin_exchange_frozen_balance": "0.00000000",
"usd_rate": "1.0003677165865",
"usd_value": "7.8623",
"frozen_usd_value": "0.0000",
"coin_exchange_frozen_usd_value": "0.0000",
"available_usd_value": "7.8623"
},
{
"network": "bsc",
"coin": "usdt",
"balance": "4.45500000",
"available_balance": "4.45500000",
"frozen_balance": "0.00000000",
"coin_exchange_frozen_balance": "0.00000000",
"usd_rate": "1.0003677165865",
"usd_value": "4.4566",
"frozen_usd_value": "0.0000",
"coin_exchange_frozen_usd_value": "0.0000",
"available_usd_value": "4.4566"
}
]
}
}
```
### Response Field Descriptions
| Field | Type | Description |
| ------------------------------------------- | ------ | --------------------------------------------------------------------------------------- |
| `total_balance_usd` | string | Total balance across all coins and networks converted to USD |
| `total_frozen_balance_usd` | string | Total frozen balance across all coins and networks converted to USD |
| `total_available_balance_usd` | string | Total available balance (total - frozen) across all coins and networks converted to USD |
| `balances` | array | Array of balance objects for each coin/network combination |
| `balances[].network` | string | Blockchain network (e.g., "tron", "bsc") |
| `balances[].coin` | string | Cryptocurrency symbol (e.g., "usdt", "usdc") |
| `balances[].balance` | string | Total balance in human-readable format |
| `balances[].available_balance` | string | Available balance in human-readable format (total - frozen) |
| `balances[].frozen_balance` | string | Frozen balance in human-readable format (funds that cannot be withdrawn yet) |
| `balances[].coin_exchange_frozen_balance` | string | Amount reserved for in-progress exchanges |
| `balances[].usd_rate` | string | Current USD exchange rate for this coin |
| `balances[].usd_value` | string | Total balance converted to USD |
| `balances[].frozen_usd_value` | string | Frozen balance converted to USD |
| `balances[].coin_exchange_frozen_usd_value` | string | Exchange-reserved balance converted to USD |
| `balances[].available_usd_value` | string | Available balance converted to USD |
### Notes
- Frozen balances represent funds from recently received deposits that are on hold for 24 hours. This hold period is necessary for Anti-Money Laundering (AML) compliance checks and to prevent fraudulent transactions.
- `coin_exchange_frozen_balance` reflects funds currently locked by automatic exchange jobs. They are excluded from `available_balance` until processing completes.
- Available balance is the portion that can be withdrawn immediately (`balance - frozen_balance - coin_exchange_frozen_balance`).
- USD values are calculated using current exchange rates.
- The response may include zeroed entries for supported coins/networks even when no historical balance exists, allowing frontends to render consistent tables.
## Get Merchant Fees
Returns the current fee settings for your merchant account.
### Request
```
GET /v1/merchants/fees
```
### Headers
| Name | Required | Description |
| -------------- | -------- | -------------------------------------------------------------------------------------- |
| `Content-Type` | Yes | Must be `application/json` |
| `Merchant` | Yes | Your merchant ID |
| `Sign` | Yes | Request signature (see [Authentication](https://docs.coinssend.com/authentication.md)) |
### Example Response
```json
{
"status": "success",
"data": {
"service_fee_percent": "2.5",
"static_wallet_fee_percent": "1.0",
"withdrawal_fee_percent": "1.5",
"pay_withdrawal_service_fee": true,
"pay_withdrawal_network_fee": true,
"is_auto_exchange": false,
"auto_exchange_coin": null,
"auto_exchange_network": null,
"auto_exchange_min_usd_amount": null,
"invoice_underpayment_percent": "1.0000",
"invoice_underpayment_max_amount": null,
"invoice_underpayment_max_currency": null,
"network_fees": [
{
"network": "tron",
"coin": "usdt",
"network_fee": "1"
}
]
}
}
```
### Response Field Descriptions
| Field | Type | Description | |
| ----------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
| `service_fee_percent` | string | Fee percentage applied to invoice payments | |
| `static_wallet_fee_percent` | string | Fee percentage for static wallet deposits | |
| `withdrawal_fee_percent` | string | Fee percentage for withdrawals | |
| `pay_withdrawal_service_fee` | boolean | Whether merchant pays the service fee when withdrawing | |
| `pay_withdrawal_network_fee` | boolean | Whether merchant pays the network fee when withdrawing | |
| `is_auto_exchange` | boolean | Whether automatic exchange is enabled for new deposits | |
| `auto_exchange_coin` | string | null | Target coin used for automatic exchanges when enabled |
| `auto_exchange_network` | string | null | Target network for automatic exchanges when enabled |
| `auto_exchange_min_usd_amount` | string | null | Minimum USD amount required to trigger an automatic exchange |
| `invoice_underpayment_percent` | string | Allowed underpayment percentage between 0 and 90 as a decimal. Both `12.2` and `12,2` mean `12.2%` | |
| `invoice_underpayment_max_amount` | string | Maximum underpayment amount in USD (e.g., `1.1` or `1,1` means `1.1 USD`) | |
| `invoice_underpayment_max_currency` | string | Currency of the maximum underpayment amount. Only `USD` is allowed and it defaults to this value when omitted. | |
| `network_fees` | array | List of supported coins and their network fees | |
`invoice_underpayment_max_amount` may only be provided when `invoice_underpayment_percent` is included in the request. The currency is always `USD` and defaults to this value if omitted.
---
Source: https://docs.coinssend.com/coin-rates.md
# Coin Rates API
For a readable coin/network catalog, API identifiers, and payment selector
guidance, start with [Supported Coins & Networks](https://docs.coinssend.com/supported-coins.md).
Retrieves current exchange rates and asset/network metadata. USD is the fiat
baseline used by this response, but consumers must locate entries by
`coin.symbol` rather than depending on array order. Assets without usable rate
data may be absent. Rates, quotes, networks, limits, fees, and precision can
change without a schema version change.
## Get Coin Rates {#get-coin-rates}
### Request
```
GET /v1/get-coin-rate
```
### Example Response
This is an illustrative response shape, not a static price or availability
contract.
```json
{
"status": "success",
"data": [
{
"coin": {
"symbol": "usd",
"name": "US Dollar",
"type": "fiat"
},
"quotes": [
{
"currency_to": "usdt",
"price": "1",
"type": "crypto"
},
{
"currency_to": "usdc",
"price": "1",
"type": "crypto"
},
{
"currency_to": "trx",
"price": "8.333333333333",
"type": "crypto"
},
{
"currency_to": "bnb",
"price": "0.001723692148",
"type": "crypto"
},
{
"currency_to": "eth",
"price": "0.000320466599",
"type": "crypto"
}
],
"networks": []
},
{
"coin": {
"symbol": "usdt",
"name": "Tether",
"type": "crypto"
},
"quotes": [
{
"currency_to": "usd",
"price": "1",
"type": "fiat"
},
{
"currency_to": "trx",
"price": "8.3167",
"type": "crypto"
},
{
"currency_to": "bnb",
"price": "0.00172",
"type": "crypto"
},
{
"currency_to": "usdc",
"price": "1",
"type": "crypto"
}
],
"networks": [
{
"network": "tron",
"name": "TRON (TRC-20)",
"type": "crypto",
"deposit_enabled": true,
"withdraw_enabled": true,
"min_deposit": "5",
"min_withdrawal": "5",
"max_withdrawal": "1000",
"network_fee": "1.40",
"decimals": 6
}
]
},
{
"coin": {
"symbol": "eth",
"name": "Ethereum",
"type": "crypto"
},
"quotes": [
{
"currency_to": "usd",
"price": "3120.45",
"type": "fiat"
},
{
"currency_to": "bnb",
"price": "10.215",
"type": "crypto"
},
{
"currency_to": "usdt",
"price": "3120.45",
"type": "crypto"
}
],
"networks": [
{
"network": "eth",
"name": "Ethereum (ERC-20)",
"type": "crypto",
"deposit_enabled": true,
"withdraw_enabled": true,
"min_deposit": "0.001",
"min_withdrawal": "0.003",
"max_withdrawal": "10",
"network_fee": "0.0002",
"decimals": 18
}
]
}
]
}
```
### Response Field Descriptions
| Field | Type | Description | |
| ----------------------------- | ------- | -------------------------------------------------------------------- | ---------------------------------------------- |
| `coin.symbol` | string | Coin ticker symbol | |
| `coin.name` | string | Human readable name | |
| `coin.type` | string | `crypto` or `fiat`, describing the asset class | |
| `quotes[].currency_to` | string | Currency code the rate is quoted against (e.g. `usd`, `usdt`, `trx`) | |
| `quotes[].price` | string | Price quoted against `currency_to` | |
| `quotes[].type` | string | Indicates whether the quote currency is `fiat` or `crypto` | |
| `networks[].network` | string | Network identifier (e.g. `tron`, `eth`, `bsc`) | |
| `networks[].name` | string | Human-readable network name and token standard | |
| `networks[].type` | string | Asset type on the network (`crypto` or `fiat`) | |
| `networks[].deposit_enabled` | boolean | Indicates if deposits are currently allowed | |
| `networks[].withdraw_enabled` | boolean | Indicates if withdrawals are currently allowed | |
| `networks[].min_deposit` | string | null | Minimum deposit amount accepted on the network |
| `networks[].min_withdrawal` | string | null | Minimum withdrawal amount allowed |
| `networks[].max_withdrawal` | string | null | Maximum withdrawal amount allowed |
| `networks[].network_fee` | string | null | Network fee charged in coin units |
| `networks[].decimals` | integer | null | Number of decimals supported on-chain |
## Get Supported Coins & Fee {#get-supported-coins-and-fee}
Use this public endpoint for live asset discovery. It excludes hidden pairs
and pairs where both deposits and withdrawals are disabled. Individual write
endpoints still own final pair/amount validation, so handle a later `422` even
after successful discovery.
### Request
```
GET /v1/coins-and-fee
```
### Example Response
Values below are examples only. Do not copy them into a closed enum or fee
table.
```json
{
"status": "success",
"data": {
"coins": [
{
"coin": {
"symbol": "USDT",
"label": "USDT"
},
"networks": [
{
"network": "TRON",
"title": "TRON (TRC-20)",
"network_fee": "1.4",
"service_fee": "0.45",
"min_withdrawal": "5"
},
{
"network": "ETH",
"title": "Ethereum (ERC-20)",
"network_fee": "1",
"service_fee": "0.45",
"min_withdrawal": "5"
}
]
},
{
"coin": {
"symbol": "BTC",
"label": "BTC"
},
"networks": [
{
"network": "BITCOIN",
"title": "Bitcoin",
"network_fee": "0.000016",
"service_fee": "0.45",
"min_withdrawal": "0.0001"
}
]
}
]
}
}
```
### Response Field Descriptions
| Field | Type | Description | |
| ---------------------------------------- | ------ | ------------------------------------------- | ------------------------------------------------------- |
| `data.coins` | array | Supported assets grouped by coin symbol | |
| `data.coins[].coin.symbol` | string | Uppercase coin symbol | |
| `data.coins[].coin.label` | string | Display label for the coin | |
| `data.coins[].networks` | array | Enabled networks for the coin | |
| `data.coins[].networks[].network` | string | Uppercase network code | |
| `data.coins[].networks[].title` | string | Human-readable network name | |
| `data.coins[].networks[].network_fee` | string | null | Network fee charged in coin units |
| `data.coins[].networks[].service_fee` | string | Service fee percentage applied to the asset | |
| `data.coins[].networks[].min_withdrawal` | string | null | Minimum withdrawal amount for the asset on that network |
The route advertises a public cache lifetime of 900 seconds. Respect that cache
window, refresh before presenting a write flow when practical, and let the
write endpoint remain authoritative.
---
Source: https://docs.coinssend.com/error-handling.md
# Error Handling
CoinsSend does not use one universal error envelope and does not guarantee a
symbolic `code` field. Branch on HTTP status first, then parse the applicable
documented shape.
## Success Envelope
Most successful operations return:
```json
{
"data": {},
"status": "success"
}
```
Successful write operations currently return HTTP `200`, including asynchronous
withdrawal submission.
## Error Shapes
### Authentication Errors
Header authentication failures return only `error`:
```json
{
"error": "Invalid sign"
}
```
Verified messages include `Missing headers`, `Missing timestamp`,
`Invalid merchant`, `Invalid sign`, `Timestamp expired`, and
`Merchant account is suspended`.
### Endpoint Errors
Endpoint failures normally return:
```json
{
"status": "error",
"message": "Order id already exists"
}
```
Some endpoints add fields at the top level. Provider-card failures add
`failure_code`; withdrawal failures can add amounts and fee details. Treat
human-readable message text as descriptive rather than a stable machine code.
### Validation
Validation failures use HTTP `422`:
```json
{
"errors": {
"amount": [
"The amount field is required."
]
},
"status": "error",
"message": "The given data was invalid."
}
```
### Rate Limiting
The wallet-creation limit returns `status` and `message`. Other HTTP `429`
responses may use a different shape or include `retry_after` and a
`Retry-After` header. Treat rate-limit metadata as optional; see
[Rate Limits and Safe Retries](https://docs.coinssend.com/rate-limits.md).
## HTTP Statuses
| Status | Meaning in the public contract |
| ------ | ----------------------------------------------------------------------------------------------------- |
| `400` | Missing auth input or an endpoint rejection. Withdrawal permission denial currently uses this status. |
| `401` | Invalid merchant/signature or expired timestamp. |
| `403` | Suspended/blocked merchant state or authorization failure. |
| `404` | Public invoice or QR resource not found. |
| `409` | Invoice `order_id` already exists, or wallet creation is already in progress. |
| `422` | Request validation or provider-card capability/state failure. |
| `429` | A rate limit was exceeded. |
| `500` | Unexpected application failure. |
| `502` | Provider-card checkout could not obtain a usable provider response. |
## Endpoint-Specific Cases
The labels in this table are suggested **client-side categories**. They are not
wire fields returned by CoinsSend.
| Client category | HTTP status | Endpoint / condition | Wire shape |
| ------------------------------ | ------------- | ----------------------------------------------------- | ----------------------------------------- |
| `missing_headers` | `400` | Authenticated endpoint without Merchant or Sign | `{"error":"Missing headers"}` |
| `missing_timestamp` | `400` | Withdrawal without Timestamp/X-Timestamp | `{"error":"Missing timestamp"}` |
| `invalid_signature` | `401` | Signature mismatch | `{"error":"Invalid sign"}` |
| `timestamp_expired` | `401` | Invalid or stale timestamp | `{"error":"Timestamp expired"}` |
| `invoice_order_conflict` | `409` | Duplicate merchant `order_id` | endpoint error |
| `validation_failed` | `422` | Invalid invoice, wallet, or withdrawal input | validation error |
| `withdrawal_permission_denied` | `400` | API key lacks withdrawal permission | endpoint error |
| `withdrawal_rejected` | `400` | Balance/fee/service rejection | endpoint error with possible extra fields |
| `provider_card_unavailable` | usually `422` | Current availability or invoice state blocks checkout | endpoint error with `failure_code` |
| `wallet_creation_in_progress` | `409` | Merchant-scoped wallet lock conflict | endpoint error |
## Robust Parsing
```php
## Retry Safety
Automatically retry only verified read operations with bounded exponential
backoff and jitter. A timeout on a write does not prove the server rejected it.
- Duplicate invoice `order_id` returns `409`; it does not replay the first
response.
- Withdrawals have no request idempotency key.
- Static-wallet creation can create another address when repeated.
- Provider-card creation only reuses an already-active order; this is not a
general idempotency contract.
Reconcile an unknown write outcome instead of blindly resubmitting it. Full
guidance is in [Rate Limits and Safe Retries](https://docs.coinssend.com/rate-limits.md).
## Troubleshooting Invalid Signatures
- Generate the PHP-compatible canonical JSON described in
[Authentication](https://docs.coinssend.com/authentication.md).
- Sign and send the same immutable canonical string.
- Use strings for monetary amounts.
- For withdrawals, recompute a fresh timestamp and HMAC.
- Run the client against
[`signature-test-vectors.json`](https://docs.coinssend.com/signature-test-vectors.json), including the
URL/Unicode vector.
- Remember that webhook verification is different: it uses the exact raw
received body bytes.
The exact machine-readable response schemas are in
[OpenAPI](https://docs.coinssend.com/openapi.json).
---
Source: https://docs.coinssend.com/rate-limits.md
# Rate Limits and Safe Retries
The public contract does not promise universal `X-RateLimit-*` headers or one
fixed global request count. Clients must handle HTTP `429` on any operation.
## Documented Wallet-Creation Limit
`POST /v1/wallet-address` has this merchant limit:
- threshold: 100 successful wallet creations;
- window: 60 seconds;
- key: current merchant;
- failed creation attempts are not counted;
- rejection: HTTP `429` with `status` and `message`.
Example:
```json
{
"status": "error",
"message": "Too many requests. Please try again later."
}
```
This response does not currently guarantee
`X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`,
`Retry-After`, or `retry_after`.
## Additional Limits
Additional limits may apply. Their response bodies and headers are not part of
a universal contract. Clients should tolerate HTTP `429` whether or not
`Retry-After` is present.
## Retry Decision Table
| Operation | Automatic retry after `429` or transport failure? | Reason |
| ------------------------------------------------------ | ------------------------------------------------- | -------------------------------------------------------------------------------------- |
| `GET /v1/get-coin-rate` | Yes, with bounded backoff | Read-only public discovery |
| `GET /v1/coins-and-fee` | Yes, with bounded backoff and caching | Read-only public discovery |
| `GET /v1/merchants/balances` | Yes, with a fresh signature/timestamp when used | Read-only merchant state |
| `GET /v1/merchants/fees` | Yes, with a fresh signature/timestamp when used | Read-only merchant state |
| `GET /v1/invoices/{invoiceCode}` | Yes | Read-only checkout state |
| `GET /v1/wallet-addresses/{walletAddress}/qr` | Yes | Read-only binary response |
| `POST /v1/invoices` | No blind retry after an unknown outcome | `order_id` uniqueness returns `409`; it does not replay the original response |
| `POST /v1/withdrawals` | No | No request idempotency key; timestamp freshness is not idempotency |
| `POST /v1/wallet-address` | No for static wallets | A successful retry can create another address |
| `POST /v1/invoices/{invoiceCode}/provider-card-orders` | Do not generalize | Only an already-active order is reused; there is no general `Idempotency-Key` contract |
An HTTP response proves whether a request was rejected. A client-side timeout
does not: the server may already have accepted the write.
## Backoff for Safe Reads
For read operations, use bounded exponential backoff with jitter:
```javascript
async function retrySafeRead(makeRequest, maxAttempts = 4) {
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
const response = await makeRequest()
if (response.status !== 429 || attempt === maxAttempts) {
return response
}
const retryAfterHeader = response.headers.get('Retry-After')
const retryAfter = retryAfterHeader === null ? Number.NaN : Number(retryAfterHeader)
const baseDelayMs = Number.isFinite(retryAfter)
? retryAfter * 1000
: Math.min(500 * 2 ** (attempt - 1), 8000)
const jitterMs = Math.floor(Math.random() * 250)
await new Promise(resolve => setTimeout(resolve, baseDelayMs + jitterMs))
}
}
```
For timestamp-signed reads, recompute `Timestamp` and `Sign` for each attempt.
Never sleep and reuse a timestamp that can age beyond the 300-second signature
window.
## Handling an Unknown Write Outcome
1. Persist the client order/reference and request intent before sending.
2. If a complete success response arrives, persist the returned CoinsSend ID.
3. If the result is unknown, do not issue a blind duplicate.
4. Reconcile using a known returned/public identifier where one exists, or
investigate through merchant operations/support.
5. For withdrawals, wait for the signed withdrawal webhook when an ID was
obtained; otherwise escalate the unknown outcome instead of resubmitting.
## Monitoring
- Record endpoint, HTTP status, request correlation data, and `Retry-After` when
present; never log the API key or full `Sign` header.
- Separate application `429` responses from edge `429` responses.
- Alert on sustained rate limiting and unknown outcomes for write operations.
- Cache the catalog according to its advertised 900-second cache lifetime.
For request-signature freshness and canonicalization, see
[Authentication](https://docs.coinssend.com/authentication.md). For exact operation metadata, see
[OpenAPI](https://docs.coinssend.com/openapi.json).
---
Source: https://docs.coinssend.com/mcp.md
# Documentation MCP Server
CoinsSend exposes a read-only [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server for AI coding agents and developer tools. It serves the same reviewed public documentation and OpenAPI contract as this site. It cannot create invoices, wallets, withdrawals, or call any other business endpoint.
## Connect remotely
Use this Streamable HTTP endpoint in an MCP client that supports protocol revision `2025-11-25` or an older revision negotiated by the server:
```text
https://api.coinssend.com/v1/docs/mcp
```
No API key is required because the server exposes public documentation only. Requests are rate-limited. A client that requires only the newer stateless `2026-07-28` protocol revision will need a future server upgrade.
For Codex, register the remote server with:
```bash
codex mcp add coinssend-docs --url https://api.coinssend.com/v1/docs/mcp
```
MCP client configuration formats differ. In clients that accept a URL-based server entry, the equivalent shape is:
```json
{
"mcpServers": {
"coinssend-docs": {
"url": "https://api.coinssend.com/v1/docs/mcp"
}
}
}
```
## Resources
| URI | Media type | Contents |
| ------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------ |
| `docs://manifest` | `application/json` | Ordered allowlist of public pages and machine-readable resources |
| `docs://openapi` | `application/vnd.oai.openapi+json;version=3.1` | Canonical public OpenAPI 3.1 contract |
| `docs://signature-test-vectors` | `application/json` | Executable fixtures for PHP-compatible request canonicalization and signatures |
The manifest contains only the public pages and resources available through the
documentation server.
## Tools
| Tool | Purpose |
| ----------------------- | ---------------------------------------------------------------------- |
| `search_docs` | Deterministic lexical search with at most eight bounded results |
| `get_docs_page` | Read one exact public Markdown path returned by the manifest or search |
| `get_openapi_operation` | Retrieve an operation by its stable `operationId` |
| `get_openapi_schema` | Retrieve a named schema from `components.schemas` |
All tools are annotated read-only, idempotent, and closed-world. They serve only
the public documentation corpus and do not call the CoinsSend business API.
## Agent safety guidance
- Use OpenAPI operations and schemas for request and response structure; use prose pages for workflow and operational guidance.
- Treat MCP output as documentation, not as approval to move funds or perform a production action.
- Do not infer idempotency. In particular, invoice creation, static-wallet creation, and withdrawal creation do not accept an `Idempotency-Key` contract.
- Validate the current live coin and network catalog before constructing a transaction; supported assets and fees can change.
- Keep merchant API keys outside prompts, logs, source control, and MCP arguments.
For clients without MCP support, use the generated [`llms.txt`](https://docs.coinssend.com/llms.txt) index or the canonical [`openapi.json`](https://docs.coinssend.com/openapi.json) directly.
---
Source: https://docs.coinssend.com/examples/README.md
# CoinsSend API Examples
These examples implement the verified public contract from
[`openapi.json`](https://docs.coinssend.com/openapi.json).
## Examples
- [PHP invoice](https://docs.coinssend.com/examples/php-invoice.md)
- [Node.js invoice](https://docs.coinssend.com/examples/js-invoice.md)
- [Python invoice](https://docs.coinssend.com/examples/python-invoice.md)
- [PHP static wallet](https://docs.coinssend.com/examples/php-static-wallet.md)
- [Node.js static wallet](https://docs.coinssend.com/examples/js-static-wallet.md)
- [Python static wallet](https://docs.coinssend.com/examples/python-static-wallet.md)
- [Node.js withdrawal and webhook receiver](https://docs.coinssend.com/examples/node-withdrawal.md)
## Shared Signing Rules
1. Build the request object using strings for monetary values.
2. Encode it with the PHP-compatible canonical JSON rules documented in
[Authentication](https://docs.coinssend.com/authentication.md).
3. Sign `base64(canonical_body)`.
4. Send the same canonical string as the HTTP request body.
5. Use legacy MD5 only when no timestamp header is sent.
6. For withdrawals, send `Timestamp` and use timestamped HMAC-SHA256.
Run client implementations against
[`signature-test-vectors.json`](https://docs.coinssend.com/signature-test-vectors.json). Keep merchant
API keys in backend secrets; none of these examples belongs in browser code.
## Write Safety
The public API does not accept a general `Idempotency-Key` request header.
Invoice `order_id` uniqueness rejects a duplicate with `409`, but does not
replay the first response. Withdrawals and static-wallet creation must not be
blindly retried after an unknown outcome. Provider-card creation only reuses an
already-active order.
Error bodies are not universal: authentication errors return `{"error":"..."}`;
endpoint errors normally return `status` and `message`; validation returns `errors`,
`status`, and `message`.
---
Source: https://docs.coinssend.com/examples/php-invoice.md
# PHP Example: Creating an Invoice
```php
'order_'.time(),
'amount' => '100.50',
'is_customer_fee' => false,
'is_customer_network_fee' => false,
'allow_card_payments' => true,
'success_url' => 'https://merchant.example/payment-success',
'cancel_url' => 'https://merchant.example/payment-cancelled',
];
// PHP default json_encode flags are the API's canonical request-body format.
$body = json_encode($payload, JSON_THROW_ON_ERROR);
$sign = md5(base64_encode($body).$apiKey);
$handle = curl_init('https://api.coinssend.com/v1/invoices');
curl_setopt_array($handle, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $body,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Merchant: '.$merchantId,
'Sign: '.$sign,
],
]);
$rawResponse = curl_exec($handle);
if ($rawResponse === false) {
throw new RuntimeException(curl_error($handle));
}
$httpStatus = curl_getinfo($handle, CURLINFO_HTTP_CODE);
curl_close($handle);
$response = json_decode($rawResponse, true, flags: JSON_THROW_ON_ERROR);
if ($httpStatus < 200 || $httpStatus >= 300) {
throw new RuntimeException($response['error'] ?? $response['message'] ?? "HTTP {$httpStatus}");
}
printf("Invoice %s: %s\n", $response['data']['code'], $response['data']['url']);
```
Keep the API key server-side. Persist the returned invoice identifiers. A
duplicate `order_id` returns `409` and does not replay the original response, so
do not blindly retry an unknown outcome with a new order ID.
---
Source: https://docs.coinssend.com/examples/js-invoice.md
# Node.js Example: Creating an Invoice
Run this code on a trusted server. Never expose the merchant API key in browser
JavaScript.
```javascript
const crypto = require('node:crypto')
function canonicalJson(payload) {
return JSON.stringify(payload)
.replace(/\//g, '\\/')
.replace(/[\u0080-\uFFFF]/g, character =>
`\\u${character.charCodeAt(0).toString(16).padStart(4, '0')}`
)
}
function legacySign(body, apiKey) {
const payloadBase64 = Buffer.from(body, 'utf8').toString('base64')
return crypto.createHash('md5').update(payloadBase64 + apiKey).digest('hex')
}
async function createInvoice({ merchantId, apiKey, orderId, amount }) {
const payload = {
order_id: orderId,
amount,
is_customer_fee: false,
is_customer_network_fee: false,
allow_card_payments: true,
success_url: 'https://merchant.example/payment-success',
cancel_url: 'https://merchant.example/payment-cancelled'
}
const body = canonicalJson(payload)
const response = await fetch('https://api.coinssend.com/v1/invoices', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Merchant: merchantId,
Sign: legacySign(body, apiKey)
},
body
})
const result = await response.json()
if (!response.ok) {
throw new Error(result.error || result.message || `HTTP ${response.status}`)
}
return result.data
}
const invoice = await createInvoice({
merchantId: process.env.COINSSEND_MERCHANT_ID,
apiKey: process.env.COINSSEND_API_KEY,
orderId: `order_${Date.now()}`,
amount: '100.50'
})
console.log(invoice.code, invoice.url)
```
Persist `id`, `order_id`, `code`, and `url` before redirecting the buyer. If the
request outcome is unknown, do not generate a new order ID and retry blindly.
A repeated accepted `order_id` returns `409` without replaying the original
invoice response.
---
Source: https://docs.coinssend.com/examples/python-invoice.md
# Python Example: Creating an Invoice
```python
import base64
import hashlib
import json
import os
import time
import requests
def canonical_json(payload):
encoded = json.dumps(
payload,
ensure_ascii=False,
separators=(',', ':'),
allow_nan=False,
)
escaped = []
for character in encoded:
codepoint = ord(character)
if character == '/':
escaped.append(r'\/')
elif codepoint < 0x80:
escaped.append(character)
elif codepoint <= 0xFFFF:
escaped.append(f'\\u{codepoint:04x}')
else:
codepoint -= 0x10000
escaped.append(f'\\u{0xD800 + (codepoint >> 10):04x}')
escaped.append(f'\\u{0xDC00 + (codepoint & 0x3FF):04x}')
return ''.join(escaped)
def legacy_sign(body, api_key):
payload_base64 = base64.b64encode(body.encode('utf-8')).decode('ascii')
return hashlib.md5((payload_base64 + api_key).encode('utf-8')).hexdigest()
merchant_id = os.environ['COINSSEND_MERCHANT_ID']
api_key = os.environ['COINSSEND_API_KEY']
payload = {
'order_id': f'order_{int(time.time())}',
'amount': '100.50',
'is_customer_fee': False,
'is_customer_network_fee': False,
'allow_card_payments': True,
'success_url': 'https://merchant.example/payment-success',
'cancel_url': 'https://merchant.example/payment-cancelled',
}
body = canonical_json(payload)
response = requests.post(
'https://api.coinssend.com/v1/invoices',
headers={
'Content-Type': 'application/json',
'Merchant': merchant_id,
'Sign': legacy_sign(body, api_key),
},
data=body,
timeout=15,
)
result = response.json()
if not response.ok:
raise RuntimeError(result.get('error') or result.get('message') or f'HTTP {response.status_code}')
print(result['data']['code'], result['data']['url'])
```
Use `data=body`, not `json=payload`: the canonical string is both the signed
payload and the sent body. Persist the returned identifiers. A duplicate
`order_id` returns `409` without replaying the original response.
---
Source: https://docs.coinssend.com/examples/php-static-wallet.md
# PHP Example: Creating a Static Wallet
```php
'tron',
'coin' => 'usdt',
'type' => 'static',
'label' => 'Checkout deposits',
'webhook_url' => 'https://merchant.example/coinssend/webhook',
];
$body = json_encode($payload, JSON_THROW_ON_ERROR);
$sign = md5(base64_encode($body).$apiKey);
$handle = curl_init('https://api.coinssend.com/v1/wallet-address');
curl_setopt_array($handle, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $body,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Merchant: '.$merchantId,
'Sign: '.$sign,
],
]);
$rawResponse = curl_exec($handle);
if ($rawResponse === false) {
throw new RuntimeException(curl_error($handle));
}
$httpStatus = curl_getinfo($handle, CURLINFO_HTTP_CODE);
curl_close($handle);
$response = json_decode($rawResponse, true, flags: JSON_THROW_ON_ERROR);
if ($httpStatus < 200 || $httpStatus >= 300) {
throw new RuntimeException($response['error'] ?? $response['message'] ?? "HTTP {$httpStatus}");
}
printf("Address: %s\nQR: %s\n", $response['data']['address'], $response['data']['qr_code_url']);
```
Static-wallet creation has no request idempotency contract. A timeout does not
prove that creation failed, so do not automatically repeat this POST. Fetch the
live catalog instead of treating the example asset pair as permanent.
---
Source: https://docs.coinssend.com/examples/js-static-wallet.md
# Node.js Example: Creating a Static Wallet
Run this code on a trusted server. Static-wallet creation has no request
idempotency contract, so do not automatically retry an unknown outcome.
```javascript
const crypto = require('node:crypto')
function canonicalJson(payload) {
return JSON.stringify(payload)
.replace(/\//g, '\\/')
.replace(/[\u0080-\uFFFF]/g, character =>
`\\u${character.charCodeAt(0).toString(16).padStart(4, '0')}`
)
}
function legacySign(body, apiKey) {
const payloadBase64 = Buffer.from(body, 'utf8').toString('base64')
return crypto.createHash('md5').update(payloadBase64 + apiKey).digest('hex')
}
async function createStaticWallet({ merchantId, apiKey, network, coin }) {
const payload = {
network,
coin,
type: 'static',
label: 'Checkout deposits',
webhook_url: 'https://merchant.example/coinssend/webhook'
}
const body = canonicalJson(payload)
const response = await fetch('https://api.coinssend.com/v1/wallet-address', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Merchant: merchantId,
Sign: legacySign(body, apiKey)
},
body
})
const result = await response.json()
if (!response.ok) {
throw new Error(result.error || result.message || `HTTP ${response.status}`)
}
return result.data
}
const wallet = await createStaticWallet({
merchantId: process.env.COINSSEND_MERCHANT_ID,
apiKey: process.env.COINSSEND_API_KEY,
network: 'tron',
coin: 'usdt'
})
console.log(wallet.address, wallet.qr_code_url)
```
Discover the current pair catalog at `GET /v1/coins-and-fee`; do not treat the
example pair as a permanent availability guarantee.
---
Source: https://docs.coinssend.com/examples/python-static-wallet.md
# Python Example: Creating a Static Wallet
```python
import base64
import hashlib
import json
import os
import requests
def canonical_json(payload):
encoded = json.dumps(
payload,
ensure_ascii=False,
separators=(',', ':'),
allow_nan=False,
)
escaped = []
for character in encoded:
codepoint = ord(character)
if character == '/':
escaped.append(r'\/')
elif codepoint < 0x80:
escaped.append(character)
elif codepoint <= 0xFFFF:
escaped.append(f'\\u{codepoint:04x}')
else:
codepoint -= 0x10000
escaped.append(f'\\u{0xD800 + (codepoint >> 10):04x}')
escaped.append(f'\\u{0xDC00 + (codepoint & 0x3FF):04x}')
return ''.join(escaped)
def legacy_sign(body, api_key):
payload_base64 = base64.b64encode(body.encode('utf-8')).decode('ascii')
return hashlib.md5((payload_base64 + api_key).encode('utf-8')).hexdigest()
merchant_id = os.environ['COINSSEND_MERCHANT_ID']
api_key = os.environ['COINSSEND_API_KEY']
payload = {
'network': 'tron',
'coin': 'usdt',
'type': 'static',
'label': 'Checkout deposits',
'webhook_url': 'https://merchant.example/coinssend/webhook',
}
body = canonical_json(payload)
response = requests.post(
'https://api.coinssend.com/v1/wallet-address',
headers={
'Content-Type': 'application/json',
'Merchant': merchant_id,
'Sign': legacy_sign(body, api_key),
},
data=body,
timeout=15,
)
result = response.json()
if not response.ok:
raise RuntimeError(result.get('error') or result.get('message') or f'HTTP {response.status_code}')
print(result['data']['address'], result['data']['qr_code_url'])
```
Static-wallet creation has no request idempotency contract. Do not retry this
POST automatically after a timeout. The asset pair is an example only; discover
current values through `GET /v1/coins-and-fee`.
---
Source: https://docs.coinssend.com/examples/node-withdrawal.md
# Node.js Example: Withdrawal and Webhook Receiver
Withdrawals require timestamped HMAC signing and must run on a trusted server.
There is no request idempotency key: do not automatically resubmit after a
timeout or another unknown outcome.
## Submit a Withdrawal
```javascript
const crypto = require('node:crypto')
function canonicalJson(payload) {
return JSON.stringify(payload)
.replace(/\//g, '\\/')
.replace(/[\u0080-\uFFFF]/g, character =>
`\\u${character.charCodeAt(0).toString(16).padStart(4, '0')}`
)
}
function withdrawalSign(body, timestamp, apiKey) {
const payloadBase64 = Buffer.from(body, 'utf8').toString('base64')
return crypto
.createHmac('sha256', apiKey)
.update(`${payloadBase64}.${timestamp}`)
.digest('hex')
}
async function initiateWithdrawal({ merchantId, apiKey, network, coin, amount, toAddress }) {
const payload = {
network,
coin,
amount,
to_address: toAddress
}
const body = canonicalJson(payload)
const timestamp = Math.floor(Date.now() / 1000).toString()
const response = await fetch('https://api.coinssend.com/v1/withdrawals', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Merchant: merchantId,
Timestamp: timestamp,
Sign: withdrawalSign(body, timestamp, apiKey)
},
body
})
const result = await response.json()
if (!response.ok) {
throw new Error(result.error || result.message || `HTTP ${response.status}`)
}
return result.data
}
const withdrawal = await initiateWithdrawal({
merchantId: process.env.COINSSEND_MERCHANT_ID,
apiKey: process.env.COINSSEND_API_KEY,
network: 'tron',
coin: 'usdt',
amount: '50.75',
toAddress: process.env.WITHDRAWAL_DESTINATION
})
console.log(withdrawal.withdrawal_id, withdrawal.status)
```
HTTP `200` means the command was accepted; `data.status` is only the current
projected state. Persist `withdrawal_id` and wait for signed status webhooks.
Timestamp freshness limits replay but does not make the POST idempotent.
## Verify Withdrawal Webhooks
Webhook signing is different from request signing: verify HMAC-SHA256 over the
exact raw body bytes before parsing JSON.
```javascript
const crypto = require('node:crypto')
const express = require('express')
const app = express()
function validWebhookSignature(rawBody, suppliedHex, apiKey) {
if (typeof suppliedHex !== 'string' || !/^[a-f0-9]{64}$/.test(suppliedHex)) {
return false
}
const expected = crypto.createHmac('sha256', apiKey).update(rawBody).digest()
const supplied = Buffer.from(suppliedHex, 'hex')
return supplied.length === expected.length && crypto.timingSafeEqual(supplied, expected)
}
app.post('/coinssend/webhook', express.raw({ type: 'application/json' }), async (request, response) => {
const rawBody = request.body
const signature = request.header('X-Signature')
if (!validWebhookSignature(rawBody, signature, process.env.COINSSEND_API_KEY)) {
return response.status(401).json({ error: 'Invalid signature' })
}
const event = JSON.parse(rawBody.toString('utf8'))
const deliveryKey = request.header('X-Idempotency-Key')
|| `${event.event}:${event.data.withdrawal_id}`
// Insert deliveryKey into a table with a unique constraint, enqueue new
// events, and acknowledge duplicates without applying side effects twice.
await enqueueIfNew(deliveryKey, event)
return response.status(204).end()
})
```
Withdrawal deliveries currently use
`withdrawal:{withdrawal_id}:{event}` as `X-Idempotency-Key`. The header is not a
universal guarantee for other webhook families.