Ir al contenido
CoinsSendDesarrolladores
Documentación de Payments

Authentication

Ver MarkdownConfigurar agente

Esta página aún no está disponible en tu idioma.

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:

HeaderRequiredDescription
MerchantYesMerchant ID
SignYesLowercase hexadecimal request signature
TimestampWithdrawals: yes; other merchant endpoints: optionalUnix 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.

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.

<?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)

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 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
$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 base64
import 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()

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 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:

Merchant: 12345678-1234-1234-1234-123456789012
Timestamp: 1716200000
Sign: 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 base64
import hashlib
import 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.

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
})

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 failures return a compact body such as:

{
"error": "Invalid sign"
}
HTTP statusMessageMeaning
400Missing headersMerchant or Sign is missing
400Missing timestampWithdrawal timestamp is missing
401Invalid merchantMerchant or API key was not found
401Invalid signSignature verification failed
401Timestamp expiredTimestamp is invalid or outside the 300-second window
403Merchant account is suspendedMerchant 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.

  • 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.