Перейти до вмісту
CoinsSendРозробникам
Документація Payments

Node.js Example: Withdrawal and Webhook Receiver

Відкрити MarkdownПідключити агента

Цей контент ще не доступний вашою мовою.

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.

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.

Webhook signing is different from request signing: verify HMAC-SHA256 over the exact raw body bytes before parsing JSON.

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.