CRM Open API DocumentationCRM Open API Documentation
REST API
Webhooks
PSP Open API
External Exchange Rates
REST API
Webhooks
PSP Open API
External Exchange Rates
  • Webhook Introduction
  • Securing your Webhooks
  • Events - Approval

    • Withdrawal

      • Approve Withdrawal Requests
      • Reject Withdrawal Requests
    • Prop Trading

      • Approve Prop Trading Requests
      • Reject Prop Trading Live Requests
    • Prop Trading Live

      • Approve Prop Trading Live Requests
      • Reject Prop Trading Live Requests

Setting your secret token

Validating payloads from CRM

When your secret token is set, CRM uses it to create a hash signature with each JSON payload. This hash signature is included with the headers of each request as X-Crm-Signature.

You should calculate a hash using your SECRET_TOKEN, and ensure that the result matches the hash from CRM. CRM uses an HMAC hex digest to compute the hash.

Note:

Webhook payloads can contain unicode characters. If your language and server implementation specifies a character encoding, ensure that you handle the payload as UTF-8.

Your language and server implementations may differ from the following examples

Test your Signature

Regardless of the programming language that you use to implement HMAC verification in your code, you can use the following secret and payload values to verify that your implementation is correct.

  • secret: "It's a Secret to Everybody"
  • payload: "Hello, World!"

If your implementation is correct and uses the SHA-256 algorithm, the signatures that you generate should match the following signature values:

  • signature: 757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17
  • X-Crm-Signature: 757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17

Code Example

Ruby
def verify_signature(payload_body)
  signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), ENV['SECRET_TOKEN'], payload_body)
  return halt 500, "Signatures didn't match!" unless Rack::Utils.secure_compare(signature, request.env['HTTP_X_HUB_SIGNATURE_256'])
end

post '/payload' do
  request.body.rewind
  payload_body = request.body.read
  verify_signature(payload_body)
  push = JSON.parse(payload_body)
  "I got some JSON: #{push.inspect}"
end
PHP
<?php
    use Psr\Http\Message\RequestInterface;
    use Psr\Http\Message\ResponseInterface;
    
    function verify_signature(RequestInterface $req) {
        $WEBHOOK_SECRET = getenv('WEBHOOK_SECRET');
        $signature = hash_hmac('sha256', json_encode($req->getBody()), $WEBHOOK_SECRET);
        $trusted = $signature;
        $untrusted = $req->getHeaderLine('X-Crm-Signature');
        return hash_equals($trusted, $untrusted);
    }
    
    function handleWebhook(RequestInterface $req, ResponseInterface $res) {
        if (!verify_signature($req)) {
            $res->getBody()->write('Unauthorized');
            return $res->withStatus(401);
        }
        // The rest of your logic here
    }
?>
Python
import hashlib
import hmac
def verify_signature(payload_body, secret_token, signature_header):
    if not signature_header:
        raise HTTPException(status_code=403, detail="X-Crm-Signature header is missing!")
    hash_object = hmac.new(secret_token.encode('utf-8'), msg=payload_body, digestmod=hashlib.sha256)
    expected_signature = hash_object.hexdigest()
    if not hmac.compare_digest(expected_signature, signature_header):
        raise HTTPException(status_code=403, detail="Request signatures didn't match!")
Javascript
let encoder = new TextEncoder();

async function verifySignature(secret, header, payload) {
    let sigHex = header;

    let algorithm = { name: "HMAC", hash: { name: 'SHA-256' } };

    let keyBytes = encoder.encode(secret);
    let extractable = false;
    let key = await crypto.subtle.importKey(
        "raw",
        keyBytes,
        algorithm,
        extractable,
        [ "sign", "verify" ],
    );

    let sigBytes = hexToBytes(sigHex);
    let dataBytes = encoder.encode(payload);
    let equal = await crypto.subtle.verify(
        algorithm.name,
        key,
        sigBytes,
        dataBytes,
    );

    return equal;
}

function hexToBytes(hex) {
    let len = hex.length / 2;
    let bytes = new Uint8Array(len);

    let index = 0;
    for (let i = 0; i < hex.length; i += 2) {
        let c = hex.slice(i, i + 2);
        let b = parseInt(c, 16);
        bytes[index] = b;
        index += 1;
    }

    return bytes;
}
Typescript
import * as crypto from "crypto";

const WEBHOOK_SECRET: string = process.env.WEBHOOK_SECRET;

const verify_signature = (req: Request) => {
  const signature = crypto
    .createHmac("sha256", WEBHOOK_SECRET)
    .update(JSON.stringify(req.body))
    .digest("hex");
  let trusted = Buffer.from(signature, 'ascii');
  let untrusted =  Buffer.from(req.headers.get("X-Crm-Signature"), 'ascii');
  return crypto.timingSafeEqual(trusted, untrusted);
};

const handleWebhook = (req: Request, res: Response) => {
  if (!verify_signature(req)) {
    res.status(401).send("Unauthorized");
    return;
  }
  // The rest of your logic here
};
Last Updated:: 5/14/25, 2:47 AM
Prev
Webhook Introduction