01 / Build the connection

Verify every request

Authenticate wallet callbacks using the original request bytes.

Only process a wallet callback after its RSA-SHA256 signature has been verified. Use the public key supplied during onboarding and the original request body; changing those bytes can invalidate the signature.

How It Works

  1. Wildvolt serializes the request body to a compact JSON string (UTF-8, no extra whitespace)
  2. Signs it with RSA-SHA256 (PKCS1v15 padding, SHA-256 digest)
  3. Encodes the signature as base64url with no padding (RFC 4648 section 5, no = characters)
  4. Sends it in the signature header

Verifying Signatures

You receive Wildvolt's public key (PEM format) during onboarding. Store it as an environment variable or in secure config. Use it to verify every incoming webhook request.

The critical rule: verify the signature against the raw request body bytes, not a re-serialized version. If you parse the JSON first and then re-serialize it, key ordering or whitespace differences will break verification.

Node.js (Express)

const express = require('express');
const crypto = require('node:crypto');
const app = express();
const publicKey = crypto.createPublicKey(process.env.PROVIDER_PUBLIC_KEY);
const webhookPaths = [
  '/withdraw', '/deposit', '/deposit-batch', '/rollback', '/player-balance'
];

// Use the SAME paths for raw-body capture and signature verification.
// Register this before any other body parser or wallet handler.
app.use(webhookPaths, express.json({
  limit: '1mb',
  verify: (req, res, buf) => { req.rawBody = Buffer.from(buf); }
}), verifySignature);

function verifySignature(req, res, next) {
  const signature = req.get('signature');
  if (!signature || !/^[A-Za-z0-9_-]+$/.test(signature) ||
      !Buffer.isBuffer(req.rawBody)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  try {
    const valid = crypto.verify(
      'RSA-SHA256', req.rawBody, publicKey,
      Buffer.from(signature, 'base64url')
    );
    if (!valid) return res.status(401).json({ error: 'Invalid signature' });
  } catch {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  next();
}

// Register your actual wallet handlers after this middleware.
// This example verifies signatures; it does not implement wallet operations.

Python (FastAPI)

import base64
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
from fastapi import Request, HTTPException

PROVIDER_PUBLIC_KEY = serialization.load_pem_public_key(
    open("provider_public_key.pem", "rb").read()
)

async def verify_signature(request: Request):
    signature = request.headers.get("signature")
    if not signature:
        raise HTTPException(401, "Invalid signature")

    raw_body = await request.body()

    try:
        padded = signature + "=" * (-len(signature) % 4)
        sig_bytes = base64.urlsafe_b64decode(padded)
        PROVIDER_PUBLIC_KEY.verify(sig_bytes, raw_body, padding.PKCS1v15(), hashes.SHA256())
    except Exception:
        raise HTTPException(401, "Invalid signature")

Java (Spring Boot)

import java.security.*;
import java.util.Base64;

public boolean verifySignature(byte[] rawBody, String signatureHeader, PublicKey publicKey)
    throws Exception {
    byte[] sigBytes = Base64.getUrlDecoder().decode(signatureHeader);
    Signature sig = Signature.getInstance("SHA256withRSA");
    sig.initVerify(publicKey);
    sig.update(rawBody);
    return sig.verify(sigBytes);
}

Common Mistakes

Re-serializing the body before verification. This is the #1 cause of signature failures. Different JSON serializers produce different output (key order, spacing). Always verify against the raw bytes you received.

Using standard base64 instead of base64url. The signature uses URL-safe base64 (- and _ instead of + and /) with no = padding. Most languages have a urlsafe or URL_SAFE variant in their base64 library.

Not rejecting unsigned requests. Every endpoint must reject requests without a valid signature - including /player-balance and /rollback, not just /withdraw.

On this page