Dieser Inhalt ist noch nicht in deiner Sprache verfügbar.
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 or OpenAPI.
Canonical JSON Body
Section titled “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
\uXXXXsequences; - 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.
Canonical JSON Helpers
Section titled “Canonical JSON Helpers”<?php
function canonicalJson(array $payload): string{ return json_encode($payload, JSON_THROW_ON_ERROR);}// Node.js. Object insertion order must match the body you intend to send.function canonicalJson(payload) { return JSON.stringify(payload) .replace(/\//g, '\\/') .replace(/[\u0080-\uFFFF]/g, character => `\\u${character.charCodeAt(0).toString(16).padStart(4, '0')}` )}import json
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)Legacy MD5 Signature
Section titled “Legacy MD5 Signature”Invoice creation, wallet creation, balances, and fees accept the legacy signature when no timestamp header is supplied:
canonical_body = canonical_json(request_object) or "" for a bodyless requestpayload_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
$body = json_encode($payload, JSON_THROW_ON_ERROR);$sign = md5(base64_encode($body).$apiKey);const crypto = require('node:crypto')
const body = canonicalJson(payload)const payloadBase64 = Buffer.from(body, 'utf8').toString('base64')const sign = crypto.createHash('md5').update(payloadBase64 + apiKey).digest('hex')import base64import hashlib
body = canonical_json(payload)payload_base64 = base64.b64encode(body.encode('utf-8')).decode('ascii')sign = hashlib.md5((payload_base64 + api_key).encode('utf-8')).hexdigest()Timestamped HMAC Signature
Section titled “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.
canonical_body = canonical_json(request_object) or "" for a bodyless requestpayload_base64 = base64_utf8(canonical_body)message = payload_base64 + "." + TimestampSign = hmac_sha256(message, API_KEY)Send exactly the timestamp string used in the signature:
Merchant: 12345678-1234-1234-1234-123456789012Timestamp: 1716200000Sign: 64-character-lowercase-hmac-sha256-hex<?php
$body = json_encode($payload, JSON_THROW_ON_ERROR);$payloadBase64 = base64_encode($body);$sign = hash_hmac('sha256', $payloadBase64.'.'.$timestamp, $apiKey);const crypto = require('node:crypto')
const body = canonicalJson(payload)const payloadBase64 = Buffer.from(body, 'utf8').toString('base64')const sign = crypto .createHmac('sha256', apiKey) .update(`${payloadBase64}.${timestamp}`) .digest('hex')import base64import hashlibimport hmac
body = canonical_json(payload)payload_base64 = base64.b64encode(body.encode('utf-8')).decode('ascii')message = f'{payload_base64}.{timestamp}'sign = hmac.new(api_key.encode('utf-8'), message.encode('utf-8'), hashlib.sha256).hexdigest()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
Section titled “Complete Invoice Example”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
Section titled “GET Requests”For an authenticated GET with no body, the canonical body and Base64 payload are both empty. The legacy signature is therefore:
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
Section titled “Authentication Errors”Authentication failures return a compact body such as:
{ "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
Section titled “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.
- Use the published test vectors in automated client tests before making live calls.