Help Center Developer Docs Webhooks

Webhooks

A signed POST to your server when a payment confirms on the network. It is the only notification Brykto sends, and the only thing that proves a payment happened.


Setup

  1. Open Dashboard > Developer Tools.
  2. Enter your webhook URL and save.
  3. Copy your webhook secret.

Your URL is checked at delivery time and must be:

  • https, not http
  • Publicly resolvable. Private, loopback, link-local, reserved and multicast addresses are all refused
  • Reachable without a login

Local development

A tunnelling tool that gives you a public HTTPS address works. localhost never will, because the check happens on Brykto's side.


The order.paid event

order.paid is the only event. The payload nests everything under data.

{
  "event": "order.paid",
  "timestamp": "2026-08-17T12:04:11.882431+00:00",
  "data": {
    "request_id": "3f9a1c2e-7b4d-4f81-9c22-1a5e6d8b0f33",
    "amount": "49.9",
    "currency": "USD",
    "receive_asset": "USDC",
    "status": "completed",
    "description": "Order 1042",
    "order_id": "1042",
    "customer_id": "cust_88",
    "customer_name": "Jane Smith",
    "rail": "stellar",
    "tx_hash": "a1b2c3d4e5f6...",
    "caller_wallet": "GBXY...",
    "amount_received": "49.9",
    "paid_at": "2026-08-17T12:04:09+00:00",
    "expires_at": "2026-08-17T12:30:00+00:00"
  }
}

The payload is nested

Read event["data"]["tx_hash"], not event["tx_hash"].

Field Notes
amount What was invoiced. A string, not a number
amount_received What actually arrived, read from the network. Also a string
caller_wallet The payer's address, read from the network
tx_hash The on-chain transaction. Unique, and your replay key
order_id Whatever you passed at creation

amount and amount_received can differ. Brykto accepts a payment within 0.1% for USDC, USDT0 and RLUSD, and within 2% for XLM and XRP.


Headers

Header Contents
X-Brykto-Signature sha256= followed by the hex digest
X-Brykto-Timestamp Unix timestamp, and part of the signed string

Verifying the signature

HMAC-SHA256 over {timestamp}.{body}, keyed with your webhook secret.

import hashlib
import hmac


def verify(body: bytes, signature: str, timestamp: str, secret: str) -> bool:
    signed   = timestamp.encode("utf-8") + b"." + body
    expected = hmac.new(secret.encode("utf-8"), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(f"sha256={expected}", signature)
import crypto from "crypto";

export function verify(body, signature, timestamp, secret) {
  const expected = "sha256=" + crypto
    .createHmac("sha256", secret)
    .update(Buffer.concat([Buffer.from(timestamp + "."), body]))
    .digest("hex");

  const a = Buffer.from(expected);
  const b = Buffer.from(signature || "");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
<?php
function brykto_verify(string $body, string $signature,
                       string $timestamp, string $secret): bool {
    $expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $body, $secret);
    return hash_equals($expected, $signature);
}

Three things that break verification:

Sign the raw body

If your framework parses the JSON and you re-serialise it before hashing, key order and whitespace change and the signature will not match. Take the bytes exactly as received. In Express that means express.raw({ type: "application/json" }).

  • The timestamp is the one in the header, not the timestamp inside the payload.
  • The header value includes the sha256= prefix. Compare the whole value, or strip it from both sides.

Retries

Three attempts, 30 seconds apart. Your endpoint has 10 seconds to return a 2xx.

Reply first and do the slow work afterwards. A handler that sends email before responding will time out and be retried.

webhook_attempts, webhook_delivered_at and webhook_last_error are recorded against the payment.


Replay protection

tx_hash is unique per payment. Store the ones you have handled and ignore repeats.

if db.already_handled(data["tx_hash"]):
    return "", 200

db.mark_handled(data["tx_hash"])
fulfil(data["order_id"])

Without this, a retry after a slow response ships the same order twice.


Security checklist

  • Verify the signature on every call, and reject anything that fails
  • Use the raw request body
  • Ignore any tx_hash you have already handled
  • Compare amount against your own record before fulfilling
  • Return 2xx quickly, then do the work
  • Never fulfil from a browser redirect

Next steps