1 Overview

Introduction to the Webhook event system.

CRYPTOMENTS sends real-time notifications to your configured Webhook URL when key events occur — such as deposit confirmation or withdrawal completion. This lets partners keep their systems in sync with CRYPTOMENTS.

Key Features

  • Real-time notifications: Sent immediately when an event occurs.
  • HMAC-SHA256 signing: Every Webhook request is signed with the partner's API Secret for verification.
  • Automatic retry: If there's no response within 5 seconds or an error occurs, the request is retried automatically with exponential backoff.
  • Idempotency guaranteed: Duplicate processing is prevented based on transactionHash.

Request Model

CRYPTOMENTS Webhook requests follow this structure:

  • Method: HTTP POST
  • Content-Type: application/json
  • Timeout: 5 seconds
  • Success response: HTTP 200 OK
ℹ️
Tip: We recommend responding to the Webhook with HTTP 200 immediately and processing the event in a background job. This way you make the most of CRYPTOMENTS' retry mechanism.

2 Configuration

How to configure and manage your Webhook URL.

Webhook URL Setup

You can configure your Webhook from the Partner Console.

1
Open the Partner Console

Sign in to the Partner Console.

2
Go to Settings

Navigate to Settings Integrations.

3
Select the Webhook tab

Enter your callback URL on the Webhook tab.

4
Send a test event

Click the "Send test" button to verify that your endpoint receives the event correctly.

⚠️
Important: Webhook URLs must use HTTPS. HTTP is not supported for security reasons.

3 Authentication

How to verify the signature of a Webhook request.

Signature Verification (HMAC-SHA256)

CRYPTOMENTS signs every Webhook request with HMAC-SHA256. Your receiver must verify the signature to confirm the request's origin and integrity.

Signature Data Composition

The signature is generated by joining the core fields with a pipe (|) delimiter:

partnerId|transactionHash|amount|timestamp

Signature Formula

signature = Hex(HMAC-SHA256(api_secret, "partnerId|txHash|amount|timestamp"))
Benefits:
  • Simple, clear structure
  • Easy for partners to implement
  • Easy to debug
  • timestamp prevents replay attacks

Webhook Request Format

Item Description Example
Content-Type header Content type application/json
signature (body field) HMAC-SHA256 signature (hex-encoded) a1b2c3d4e5f6...
timestamp (body field) Unix timestamp (seconds) 1710508200
transactionHash (body field) Transaction hash (idempotency key) 0xabc123def456...
ℹ️
Note: The signing key is your partner-specific API Secret. You can find it in the Partner Console under Integrations.

Verification Implementation (JavaScript)

JavaScript
const crypto = require('crypto');

function verifySignature(body, apiSecret) {
  const { partnerId, transactionHash, amount, timestamp, signature } = body;

  // Build signature data (delimiter |)
  const signatureData = `${partnerId}|${transactionHash}|${amount}|${timestamp}`;

  const computed = crypto
    .createHmac('sha256', apiSecret)
    .update(signatureData)
    .digest('hex');

  return computed === signature;
}

app.post('/webhook', (req, res) => {
  const apiSecret = process.env.API_SECRET;

  if (!verifySignature(req.body, apiSecret)) {
    return res.status(401).json({ error: 'Signature mismatch' });
  }

  res.status(200).json({ success: true });
});

Verification Implementation (Python)

Python
import hmac
import hashlib
from flask import Flask, request

app = Flask(__name__)
API_SECRET = 'your_api_secret'

@app.route('/webhook', methods=['POST'])
def webhook():
    body = request.get_json()

    # Build signature data (delimiter |)
    signature_data = f"{body['partnerId']}|{body['transactionHash']}|{body['amount']}|{body['timestamp']}"

    computed = hmac.new(
        API_SECRET.encode(),
        signature_data.encode(),
        hashlib.sha256
    ).hexdigest()

    # Verify signature
    if computed != body.get('signature'):
        return {'error': 'Signature mismatch'}, 401

    return {'success': True}, 200

Verification Implementation (Java)

Java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Map;

@PostMapping("/webhook")
public ResponseEntity<?> receiveWebhook(@RequestBody Map<String, Object> body) {
  try {
    String apiSecret = System.getenv("API_SECRET");

    // Build signature data (delimiter |)
    String signatureData = body.get("partnerId") + "|"
      + body.get("transactionHash") + "|"
      + body.get("amount") + "|"
      + body.get("timestamp");

    Mac mac = Mac.getInstance("HmacSHA256");
    mac.init(new SecretKeySpec(
      apiSecret.getBytes(StandardCharsets.UTF_8),
      "HmacSHA256"
    ));
    byte[] hash = mac.doFinal(signatureData.getBytes(StandardCharsets.UTF_8));

    // hex encode
    StringBuilder hex = new StringBuilder();
    for (byte b : hash) {
      hex.append(String.format("%02x", b));
    }

    if (!hex.toString().equals(body.get("signature"))) {
      return ResponseEntity.status(401).build();
    }

    return ResponseEntity.ok().build();
  } catch (Exception e) {
    return ResponseEntity.status(500).build();
  }
}
🔒
Security: Store your Webhook Secret in environment variables and never hardcode it in source code. Rotate the Secret immediately if it's exposed.

4 Event Types

Event types sent via Webhook.

Click an event card below to inspect its JSON payload.

Deposit DEPOSIT_CONFIRMED
Deposit confirmed on-chain.
JSON payload:
{
  "eventType": "DEPOSIT_CONFIRMED",
  "transactionId": 245,
  "partnerId": "7",
  "userId": "user_001",
  "transactionHash": "0xabc123def456...",
  "fromAddress": "0x123456789...",
  "toAddress": "0x987654321...",
  "amount": "1000.000000",
  "currencyType": "USDT",
  "chainType": "BSC",
  "depositMethod": "HD_WALLET",
  "status": "CONFIRMED",
  "confirmedAt": "2026-03-15T14:30:00",
  "tokenKrwPrice": "1350.0",
  "tokenUsdPrice": "1.0",
  "krwAmount": "1350000",
  "usdAmount": "1000.00",
  "timestamp": "1710508200",
  "signature": "a1b2c3d4e5f6789..."
}
Field descriptions:
  • eventType — Event type (DEPOSIT_CONFIRMED / WITHDRAWAL_CONFIRMED / WITHDRAWAL_FAILED)
  • transactionId — Internal transaction ID (Long). Used in deposit/withdrawal lookup APIs
  • partnerId — Partner ID
  • userId — Partner-side user ID
  • depositMethod — Deposit method (see table below)
  • krwAmount / usdAmount — Deposit amount converted to KRW/USD
  • signature — HMAC-SHA256 signature (for verification)
depositMethod values:
  • HD_WALLET — Deposit to an HD-derived wallet address
  • EXTERNAL_WALLET — Deposit from a linked external wallet (e.g. Axim)
  • DECIMAL_MATCH — Decimal-tail matching deposit
  • DIRECT — Direct top-up by the partner
  • MANUAL — Manual entry by an administrator
Withdrawal WITHDRAWAL_CONFIRMED
Withdrawal request confirmed on-chain.
JSON payload:
{
  "eventType": "WITHDRAWAL_CONFIRMED",
  "transactionId": 127,
  "partnerId": "7",
  "userId": "user_002",
  "transactionHash": "0xdef789abc123...",
  "fromAddress": null,
  "toAddress": "0x789012345...",
  "amount": "500.000000",
  "currencyType": "USDC",
  "chainType": "POLYGON",
  "status": "CONFIRMED",
  "confirmedAt": "2026-03-15T15:00:00",
  "tokenKrwPrice": "1350.0",
  "tokenUsdPrice": "1.0",
  "krwAmount": "675000",
  "usdAmount": "500.00",
  "feeAmount": "0",
  "timestamp": "1710508200",
  "signature": "a1b2c3d4e5f6789..."
}
Field descriptions:
  • feeAmount — Fee deducted from the transfer
  • status — Always "CONFIRMED"
  • fromAddress — null for withdrawals (sent from the MASTER wallet)
Failed WITHDRAWAL_FAILED
Withdrawal request failed.
JSON payload:
{
  "eventType": "WITHDRAWAL_FAILED",
  "transactionId": 128,
  "partnerId": "7",
  "userId": "user_003",
  "transactionHash": null,
  "fromAddress": null,
  "toAddress": "T9yD14Nj9j7xAB4dbGeiX9h8...",
  "amount": "200.000000",
  "currencyType": "USDT",
  "chainType": "TRON",
  "status": "FAILED",
  "confirmedAt": null,
  "tokenKrwPrice": "1350.0",
  "tokenUsdPrice": "1.0",
  "krwAmount": "270000",
  "usdAmount": "200.00",
  "feeAmount": "0",
  "timestamp": "1710508200",
  "signature": "a1b2c3d4e5f6789..."
}
Field descriptions:
  • status — "FAILED" (withdrawal failed)
  • transactionHash — May be null on failure (failed before TX broadcast)
  • confirmedAt — null on failure
  • Payload structure is the same as WITHDRAWAL_CONFIRMED; distinguished by the status value

📋 Payload Field Reference

Fields common to every Webhook event.

Field Type Required Description
eventType String Event type. One of DEPOSIT_CONFIRMED, WITHDRAWAL_CONFIRMED, WITHDRAWAL_FAILED
transactionId Long Internal transaction ID. Used by deposit/withdrawal lookup APIs
partnerId String Partner unique ID
userId String Partner-registered user ID
transactionHash String On-chain transaction hash. Used as the idempotency key. May be null on withdrawal failure
fromAddress String Sender address. Sender wallet for deposits, null for withdrawals (sent from MASTER wallet)
toAddress String Recipient address. Partner wallet for deposits, withdrawal target for withdrawals
amount String Transaction amount (token units, decimal string)
currencyType String Token symbol. USDT, USDC, etc.
chainType String Blockchain network. BSC, ETHEREUM, POLYGON, TRON, etc.
depositMethod String Deposit method. Included only in deposit events (see table below)
status String Transaction status (see table below)
confirmedAt String Block confirmation time (yyyy-MM-dd'T'HH:mm:ss). null if unconfirmed
tokenKrwPrice String KRW price per token
tokenUsdPrice String USD price per token
krwAmount String KRW value of the amount (amount × tokenKrwPrice, integer)
usdAmount String USD value of the amount (amount × tokenUsdPrice, 2 decimal places)
feeAmount String Fee amount. Included only in withdrawal events (currently defaults to "0")
timestamp String Event timestamp (Unix seconds). Used for signature verification
signature String HMAC-SHA256 signature (hex-encoded). See Authentication for verification
ℹ️
Note: If price lookup fails, tokenKrwPrice, tokenUsdPrice, krwAmount, and usdAmount are all sent as "0".

📡 eventType Values

ValueDescriptionWhen triggered
DEPOSIT_CONFIRMED Deposit confirmed When a deposit transaction is confirmed on-chain
WITHDRAWAL_CONFIRMED Withdrawal complete When a withdrawal transaction is confirmed on-chain
WITHDRAWAL_FAILED Withdrawal failed When an error occurs during withdrawal (insufficient gas, balance, chain error, etc.)

🔄 status Values

Deposits and withdrawals follow separate status flows. The status sent in the Webhook is the current status of that transaction.

Deposit Status

ValueDescriptionWebhook sent
DETECTED Deposit transaction detected on-chain (before block confirmation)
CONFIRMING Block confirmation in progress (waiting for required confirmations)
CONFIRMED Confirmed on-chain. Deposit is valid DEPOSIT_CONFIRMED
NOTIFIED Webhook successfully delivered to the partner ❌ (internal)
COLLECTING Sweeping funds from HOT/POOL wallet to MASTER wallet ❌ (internal)
SETTLED Final state. Sweep finalized and included in settlement ❌ (internal)
FAILED Deposit processing failed (chain error, confirmation failure, etc.)
ℹ️
Note: A deposit Webhook is sent only once, when the status is CONFIRMED. Subsequent state changes (NOTIFIED → COLLECTING → SETTLED) do not trigger additional Webhooks.

Withdrawal Status

ValueDescriptionWebhook sent
REQUESTED Withdrawal request received
PENDING_APPROVAL Waiting for admin approval
REJECTED Rejected by an administrator
APPROVED Approved by an administrator
PROCESSING Withdrawal processing (transaction creation & signing)
BROADCASTING Broadcasting transaction to the blockchain
CONFIRMED Withdrawal transaction confirmed on-chain WITHDRAWAL_CONFIRMED
FAILED Error during withdrawal (insufficient gas, balance, chain error, etc.) WITHDRAWAL_FAILED
CANCELLED Withdrawal cancelled (by user or system)
STALE Unconfirmed for more than 1 hour. Flagged by the safety-net scheduler for manual review
⚠️
Important: Withdrawal Webhooks are only sent on CONFIRMED (success) or FAILED (failure). Intermediate states (REQUESTED → APPROVED → PROCESSING, etc.) do not trigger Webhooks — check the Partner Console for those.

💰 depositMethod Values

A field included only in DEPOSIT_CONFIRMED events, indicating how the deposit was made.

ValueDescription
HD_WALLET Deposit via an HD-derived wallet address. Each user gets a unique address; the depositor is identified by the TO address.
EXTERNAL_WALLET Deposit from an externally linked wallet (e.g. Axim). The depositor is identified by the FROM address.
DECIMAL_MATCH Decimal-tail matching deposit. A unique 4-digit decimal tail is assigned per depositor on a shared address.
DIRECT Direct top-up by the partner. The partner sends tokens directly to the MASTER wallet.
MANUAL Manual entry by an administrator. Used when an inbound deposit can't be processed automatically.
PHONEPAY PhonePay (BanqPipe) KRW deposit. An indirect deposit via KRW payment.

⛓️ chainType / currencyType Values

Supported blockchain networks and tokens.

chainType (blockchain network)

ValueNetworkNotes
BSCBNB Smart Chain (BEP-20)BNB gas, low fees
ETHEREUMEthereum (ERC-20)ETH gas
POLYGONPolygon PoS (ERC-20)MATIC gas, low fees
TRONTRON (TRC-20)Energy/bandwidth based, can be gasless

currencyType (token)

ValueTokenSupported chains
USDTTether USDBSC, ETHEREUM, POLYGON, TRON
USDCUSD CoinBSC, ETHEREUM, POLYGON

5 Retry Policy

How automatic retries work when Webhook delivery fails.

If your server does not respond with HTTP 200 within 5 seconds, CRYPTOMENTS retries up to 5 times. Each retry interval grows with exponential backoff.

Retry Schedule

1
Immediate
2
30 sec
3
1 min
4
5 min
5
15 min
6
1 hour

After All Retries Fail

If all retries fail, the event is marked as FAILED. Partners can view it in the "Failed Webhooks" section of the console and trigger a manual re-send.

💡
Recommended: Plan Webhook processing time around your server maintenance and deploy windows. If you notify the CRYPTOMENTS support team of scheduled infrastructure maintenance, retries can be paused during that period.

6 Telegram Alerts

Receive real-time notifications via a Telegram bot alongside Webhooks.

In addition to Webhook events, CRYPTOMENTS supports two-channel notifications via a Telegram bot. Webhooks are for system integration; Telegram is for operators to monitor status in real time.

Supported Notification Types

NotificationDescriptionExample fields
Deposit detectedDeposit TX detected on-chain (awaiting confirmation)Deposit code, amount, network, TX hash
Deposit confirmedDeposit confirmed on-chainDeposit code, amount, network, TX hash
Large depositDeposit exceeding the alert thresholdDeposit code, amount, threshold, network
Withdrawal requestedNew withdrawal request receivedWithdrawal code, amount, recipient, status
Withdrawal approvedWithdrawal request approvedWithdrawal code, amount, approver
Withdrawal rejectedWithdrawal rejected by an administratorWithdrawal code, amount, reason
Withdrawal completedWithdrawal confirmed on-chainWithdrawal code, amount, recipient, TX hash
Withdrawal failedError during withdrawal processingWithdrawal code, amount, network, reason
Withdrawal cancelledWithdrawal request cancelledWithdrawal code, amount
💬
Setup: Connect the bot from Partner Console > Settings > Integrations > Telegram tab. Search for the bot ID @CryptomentsBot in Telegram, then authenticate with the /start or /verify command. See Partner Guide > Integrations for details.

7 Idempotency

How to prevent duplicate processing.

Network delays and retries can cause the same event to be delivered multiple times. To prevent this, every event payload includes a transactionHash that you use as an idempotency key.

Idempotency Implementation

Your receiver should guarantee idempotency as follows:

1
Store transaction hashes

Save the transactionHash of every processed event in your database.

2
Check for duplicates

On receiving a Webhook, first check whether that transactionHash has already been processed.

3
Respond on duplicate

If it's a duplicate, return HTTP 200 immediately (not an error).

Idempotency Example (JavaScript)

JavaScript
app.post('/webhook', async (req, res) => {
  const txHash = req.body.transactionHash;

  try {
    // 1. Check for duplicate transaction hash
    const existing = await db.webhookEvents.findOne({ tx_hash: txHash });
    if (existing) {
      return res.status(200).json({
        success: true,
        message: 'Event already processed'
      });
    }

    // 2. Verify signature (omitted)

    // 3. Handle event
    await processEvent(req.body);

    // 4. Store transaction hash
    await db.webhookEvents.insertOne({
      tx_hash: txHash,
      event_type: req.body.eventType,
      processed_at: new Date()
    });

    res.status(200).json({ success: true });
  } catch (error) {
    console.error(error);
    res.status(500).json({ error: 'Internal error' });
  }
});
Recommended Database Index:
SQL
CREATE UNIQUE INDEX idx_tx_hash ON webhook_events(tx_hash);

8 Receiver Implementation

Guide to implementing a Webhook receiver server.

Best Practices

  • Respond fast: Reply with HTTP 200 immediately on receiving the Webhook, and handle actual processing in a background job (queue, message broker).
  • Verify signature: Verify the signature on every request to confirm the origin.
  • Idempotency: Use transactionHash to prevent duplicate processing.
  • Logging: Log every Webhook received and its processing outcome.
  • Error handling: Design failures to be retryable.

Node.js (Express) Example

JavaScript
const express = require('express');
const crypto = require('crypto');
const Bull = require('bull');

const app = express();
const webhookQueue = new Bull('webhooks');
const API_SECRET = process.env.API_SECRET;

app.use(express.json());

// Signature verification helper
function verifySignature(body) {
  const { partnerId, transactionHash, amount, timestamp, signature } = body;
  const signatureData = `${partnerId}|${transactionHash}|${amount}|${timestamp}`;
  const computed = crypto
    .createHmac('sha256', API_SECRET)
    .update(signatureData)
    .digest('hex');
  return computed === signature;
}

// Webhook handler
app.post('/webhook', async (req, res) => {
  // 1. Verify signature
  if (!verifySignature(req.body)) {
    return res.status(401).json({ error: 'Unauthorized' });
  }

  // 2. Check for duplicates (by transactionHash)
  const txHash = req.body.transactionHash;
  const exists = await db.webhookEvents.findOne({ tx_hash: txHash });
  if (exists) {
    return res.status(200).json({ success: true });
  }

  // 3. Enqueue and respond immediately
  await webhookQueue.add(req.body);
  res.status(200).json({ success: true });
});

// Background job
webhookQueue.process(async (job) => {
  const data = job.data;
  try {
    switch (data.eventType) {
      case 'DEPOSIT_CONFIRMED':
        await handleDepositConfirmed(data);
        break;
      case 'WITHDRAWAL_CONFIRMED':
        await handleWithdrawalConfirmed(data);
        break;
      case 'WITHDRAWAL_FAILED':
        await handleWithdrawalFailed(data);
        break;
    }
    await db.webhookEvents.insertOne({
      tx_hash: data.transactionHash,
      event_type: data.eventType,
      processed_at: new Date()
    });
  } catch (error) {
    console.error('Webhook processing error:', error);
    throw error;
  }
});

app.listen(3000);

Python (Flask) Example

Python
from flask import Flask, request, jsonify
from celery import Celery
import hmac
import hashlib

app = Flask(__name__)
celery = Celery(app.name, broker='redis://localhost:6379')

API_SECRET = 'your_api_secret'

def verify_signature(body):
    signature_data = f"{body['partnerId']}|{body['transactionHash']}|{body['amount']}|{body['timestamp']}"
    computed = hmac.new(
        API_SECRET.encode(),
        signature_data.encode(),
        hashlib.sha256
    ).hexdigest()
    return computed == body.get('signature')

@app.route('/webhook', methods=['POST'])
def receive_webhook():
    body = request.get_json()

    # 1. Verify signature
    if not verify_signature(body):
        return jsonify({'error': 'Unauthorized'}), 401

    # 2. Check for duplicates
    tx_hash = body.get('transactionHash')
    if db.webhook_events.find_one({'tx_hash': tx_hash}):
        return jsonify({'success': True}), 200

    # 3. Process async and respond immediately
    process_webhook.delay(body)
    return jsonify({'success': True}), 200

@celery.task
def process_webhook(data):
    event_type = data['eventType']
    try:
        if event_type == 'DEPOSIT_CONFIRMED':
            handle_deposit_confirmed(data)
        elif event_type == 'WITHDRAWAL_CONFIRMED':
            handle_withdrawal_confirmed(data)
        elif event_type == 'WITHDRAWAL_FAILED':
            handle_withdrawal_failed(data)

        db.webhook_events.insert_one({
            'tx_hash': data['transactionHash'],
            'event_type': event_type,
            'processed_at': datetime.utcnow()
        })
    except Exception as e:
        print(f'Error processing webhook: {e}')
        raise

if __name__ == '__main__':
    app.run()

9 Troubleshooting

Common issues and how to fix them.

Common Issues

Issue Cause Resolution
Webhook not received Misconfigured URL, network firewall 1. Confirm the URL is HTTPS
2. Use "Send test" in the console to verify connectivity
3. Review firewall settings
Signature verification fails Wrong API Secret, mismatched signature data 1. Re-check API Secret
2. Verify the order: partnerId|txHash|amount|timestamp
3. Confirm hex encoding
Timeout (over 5 seconds) Slow server response, heavy processing 1. Respond with HTTP 200 immediately, then process async
2. Remove synchronous processing
3. Optimize DB queries
SSL/TLS error Expired or self-signed certificate 1. Check certificate validity
2. Renew the certificate
3. Use a trusted CA
Duplicate processing Idempotency not implemented 1. Add transactionHash persistence
2. Implement duplicate-hash checks
3. Use a UNIQUE index in the DB
Console shows failure All 5 retries failed 1. Check server health
2. Click "Manual re-send" in the console
3. Inspect logs

Debugging Tips

🔍
Signature debugging: Log the received partnerId, transactionHash, amount, and timestamp, then regenerate the signature from the partnerId|txHash|amount|timestamp string and compare.

Monitoring

For Webhook reliability, monitor the following:

  • Response time: Investigate if average response time exceeds 5 seconds
  • Error rate: Track HTTP 5xx error increases
  • Retry rate: A high retry ratio is a sign to inspect your server
  • Event lag: Monitor the delay between event creation and processing
⚠️
Important: Notify the CRYPTOMENTS support team in advance about scheduled server maintenance or deploys so retry timing can be adjusted.

Need help? Contact support.

CRYPTOMENTS Webhook v2.0 © 2026