Entwickler
Webhooks
Webhooks informieren Ihren Server, wenn etwas passiert, zum Beispiel wenn eine Rechnung bezahlt wird, sodass Sie nicht ständig die API abfragen müssen.
Endpunkt hinzufügen
Öffnen Sie im Portal Entwickler und fügen Sie die HTTPS-Adresse auf Ihrem Server hinzu, die Ereignisse empfangen soll. Jeder Endpunkt erhält sein eigenes Signaturgeheimnis, das mit whsec_ beginnt. Ein Endpunkt kann alle Ereignisse empfangen, einschließlich später hinzugefügter, oder nur die Ereignisse, die Sie auswählen. Prüfen Sie mit Testereignis senden, ob Ihr Endpunkt funktioniert.
Ereignisse
invoice.created- Eine Rechnung wurde erstellt.
invoice.confirming- Eine Zahlung wurde on-chain erkannt und wartet auf Bestätigungen.
invoice.paid- Die Rechnung ist bezahlt und bestätigt. Liefern Sie Bestellungen bei diesem Ereignis aus.
invoice.underpaid- Bei der bestätigten Zahlung fehlt mehr als Ihre Toleranz.
invoice.expired- Die Zeit ist abgelaufen, bevor eine Zahlung erkannt wurde.
invoice.cancelled- Die Rechnung wurde storniert.
invoice.refunded- Coins gingen an den Kunden zurück: eine Erstattung bei einer unterbezahlten Rechnung, oder ein von Ihnen erstellter Erstattungslink wurde eingelöst. Wird bei jeder Einlösung gesendet, also auch bei einer Teilerstattung.
deposit.pending- Eine Einzahlung auf die feste Wallet eines Ihrer Kunden wurde on-chain erkannt und wartet auf Bestätigungen.
deposit.confirmed- Eine Einzahlung auf eine feste Wallet wurde bestätigt und Ihrem Guthaben abzüglich der Gebühr gutgeschrieben: Schreiben Sie sie jetzt diesem Kunden gut.
deposit.dropped- Eine erkannte Einzahlung auf eine feste Wallet wurde nie bestätigt (fehlgeschlagen oder aus der Chain entfernt).
payout.sent- Eine Auszahlung wurde an Ihre Wallet gesendet, oder ein Partneranteil wurde an die Wallet des Partners gesendet.
payout.failed- Eine Auszahlung konnte nicht gesendet werden.
swap.completed- Ein Tausch in Ihren Ziel-Coin wurde abgeschlossen.
Payload
Jedes Ereignis wird als HTTP-POST mit einem JSON-Body gesendet:
{
"id": "evt_…",
"type": "invoice.paid",
"created": "2026-10-28T09:41:07.000Z",
"data": {
"id": "inv_…",
"object": "invoice",
"status": "paid",
…
}
}id ist für das Ereignis eindeutig, type ist eines der oben genannten Ereignisse, und created gibt an, wann es eingetreten ist. Bei Rechnungsereignissen ist data das Rechnungsobjekt. Bei Auszahlungsereignissen ist es das Auszahlungsobjekt. Bei Tauschereignissen beschreibt data den Tausch.
Header
Content-Type- Immer
application/json. CM-Signature- Zeitstempel und Signatur:
t=die Unix-Zeit in Sekunden, dannv1=die hexadezimale HMAC-SHA256-Signatur. CM-Event-Id- Die Ereignis-ID, identisch mit
idim Body. User-Agent- Immer
CryptoMerchant-Webhooks/1.
POST /webhooks/crypto-merchant HTTP/1.1
Content-Type: application/json
User-Agent: CryptoMerchant-Webhooks/1
CM-Signature: t=1792834867,v1=5f2c…
CM-Event-Id: evt_…Signatur prüfen
Prüfen Sie immer die Signatur, bevor Sie einem Ereignis vertrauen. Jeder kann eine Anfrage an Ihren Endpunkt senden, aber nur Crypto Merchant kennt Ihr Geheimnis.
- 1
Lesen Sie den rohen Request-Body genau so, wie er angekommen ist, bevor Sie das JSON parsen.
- 2
Teilen Sie den
CM-Signature-Header an den Kommas auf und lesen Sie die Wertetundv1. - 3
Berechnen Sie den HMAC-SHA256 aus dem Zeitstempel, einem Punkt und dem rohen Body, mit Ihrem Endpunkt-Geheimnis als Schlüssel, als Hex in Kleinbuchstaben.
- 4
Vergleichen Sie ihn mit
v1mithilfe eines zeitkonstanten Vergleichs. - 5
Lehnen Sie Ereignisse ab, deren Zeitstempel älter als fünf Minuten ist, damit alte Anfragen nicht erneut eingespielt werden können.
import crypto from "node:crypto";
/**
* Checks a Crypto Merchant webhook and returns the event.
* rawBody: the request body exactly as it arrived (string or Buffer).
*/
export function verifyWebhook(rawBody, signatureHeader, secret, toleranceSec = 300) {
const parts = Object.fromEntries(
String(signatureHeader ?? "")
.split(",")
.map((part) => part.trim().split("=")),
);
const t = parts.t;
const v1 = parts.v1 ?? "";
if (!t || !v1) throw new Error("Missing signature");
const body = Buffer.isBuffer(rawBody) ? rawBody.toString("utf8") : rawBody;
const expected = crypto
.createHmac("sha256", secret) // your endpoint secret, whsec_…
.update(`${t}.${body}`)
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(v1);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
throw new Error("Bad signature");
}
if (Math.abs(Date.now() / 1000 - Number(t)) > toleranceSec) {
throw new Error("Timestamp too old");
}
return JSON.parse(body);
}import express from "express";
import { verifyWebhook } from "./verify-webhook.js";
import { db } from "./db.js"; // your database, e.g. a pg Pool
// CREATE TABLE webhook_events (
// id text PRIMARY KEY, type text NOT NULL, payload jsonb NOT NULL,
// received_at timestamptz NOT NULL DEFAULT now(), processed_at timestamptz
// );
const app = express();
// Keep the raw body: re-serialised JSON would not match the signature.
app.post(
"/webhooks/crypto-merchant",
express.raw({ type: "application/json" }),
async (req, res) => {
let event;
try {
event = verifyWebhook(req.body, req.get("CM-Signature"), process.env.CM_WEBHOOK_SECRET);
} catch {
return res.sendStatus(400);
}
try {
// 1. Save the event before replying. A repeat delivery has the same id and is ignored.
await db.query(
"INSERT INTO webhook_events (id, type, payload) VALUES ($1, $2, $3) ON CONFLICT (id) DO NOTHING",
[event.id, event.type, event],
);
} catch {
return res.sendStatus(500); // not saved: we send it again later
}
// 2. It's stored, so confirm it.
res.sendStatus(200);
// 3. Do the work from what you saved, here or in a background job.
// If your server stops now, the event is still waiting in the table.
processEvents().catch(console.error);
},
);
async function processEvents() {
const { rows } = await db.query(
"SELECT payload FROM webhook_events WHERE processed_at IS NULL ORDER BY received_at",
);
for (const { payload: event } of rows) {
if (event.type === "invoice.paid") {
// Mark the order in event.data.order_id as paid. Doing it twice must change nothing.
}
await db.query("UPDATE webhook_events SET processed_at = now() WHERE id = $1", [event.id]);
}
}import hashlib, hmac, json, time
def verify_webhook(raw_body: bytes, signature_header: str, secret: str, tolerance_sec: int = 300) -> dict:
"""Checks a Crypto Merchant webhook and returns the event. raw_body: the request body exactly as it arrived."""
parts = dict(part.strip().split("=", 1) for part in (signature_header or "").split(",") if "=" in part)
t, v1 = parts.get("t"), parts.get("v1")
if not t or not v1:
raise ValueError("Missing signature")
signed = t.encode() + b"." + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest() # your endpoint secret, whsec_…
if not hmac.compare_digest(expected, v1):
raise ValueError("Bad signature")
if abs(time.time() - int(t)) > tolerance_sec:
raise ValueError("Timestamp too old")
return json.loads(raw_body)<?php
// Checks a Crypto Merchant webhook and returns the event.
// Read the raw body with file_get_contents('php://input') and the header with $_SERVER['HTTP_CM_SIGNATURE'].
function verify_webhook(string $rawBody, string $signatureHeader, string $secret, int $toleranceSec = 300): array {
$parts = [];
foreach (explode(',', $signatureHeader) as $part) {
[$key, $value] = array_pad(explode('=', trim($part), 2), 2, '');
$parts[$key] = $value;
}
$t = $parts['t'] ?? '';
$v1 = $parts['v1'] ?? '';
if ($t === '' || $v1 === '') {
throw new RuntimeException('Missing signature');
}
$expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret); // your endpoint secret, whsec_…
if (!hash_equals($expected, $v1)) {
throw new RuntimeException('Bad signature');
}
if (abs(time() - (int) $t) > $toleranceSec) {
throw new RuntimeException('Timestamp too old');
}
return json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);
}Antworten und Wiederholungen
Antworten Sie innerhalb von 10 Sekunden mit einem beliebigen 2xx-Status, um den Empfang des Ereignisses zu bestätigen. Jede andere Antwort oder keine Antwort gilt als Fehlschlag, und wir versuchen es erneut nach:
- 1 Minute
- 5 Minuten
- 15 Minuten
- 1 Stunde
- 3 Stunden
- 6 Stunden
- 12 Stunden
- 24 Stunden
Das sind bis zu acht Wiederholungen über etwa zwei Tage. Jeder Versuch wird mit einem neuen Zeitstempel erneut signiert.
Bewährte Praktiken
- Speichern Sie jedes Ereignis, bevor Sie antworten, und erledigen Sie die Arbeit danach anhand des Gespeicherten, zum Beispiel in einem Hintergrundjob. Schlägt das Speichern fehl, antworten Sie mit einem Fehler, und wir senden das Ereignis erneut.
- Ereignisse können mehr als einmal und nicht immer in der richtigen Reihenfolge ankommen. Verwenden Sie die Ereignis-
id, um bereits verarbeitete Ereignisse zu überspringen. - Bevor Sie eine wertvolle Bestellung versenden, können Sie die Rechnung über die API abrufen, um ihren Status noch einmal zu prüfen.
- Bewahren Sie Ihr
whsec_-Geheimnis wie einen API-Schlüssel auf Ihrem Server auf.