Webhooks and signature verification
Register endpoints, verify signatures, retries, pause and secret rotation.
WireBloom notifies your HTTPS endpoint when something happens, so you do not have to poll. The payload schema of every event is in the event catalogue.
Register an endpoint
POST /v1/integrator-webhooks (scope webhooks:manage)
{ "url": "https://erp.example.com/wirebloom/webhooks",
"events": ["payment.status_changed", "incoming_payment.received"],
"description": "ERP payments" }The response of createIntegratorWebhook contains secret (whsec_…, shown once) for verifying signatures. Endpoint rules: https:// only, a public host name or address (no private, loopback, link-local, internal names, no credentials in the URL); every delivery re-checks the resolved address. Up to 10 endpoints per customer. testIntegratorWebhook sends a signed test event right away (data.test: true, header WireBloom-Test: true) and returns the attempt.
Events
| Event | When |
|---|---|
payment.created | a payment was submitted (left draft) |
payment.status_changed | any later status change (previousStatus → status) |
payment.completed / payment.failed / payment.returned | the corresponding terminal change (also sent as payment.status_changed) |
transfer.completed | an internal transfer between own balances completed |
incoming_payment.received | money arrived on a balance |
exchange.completed | a currency exchange completed |
recipient.status_changed | a recipient was added or its approval status changed |
approval.requested / approval.decided | a payment or recipient needs / got an approval decision |
funds_request.status_changed | a funds request changed status |
recipient.verification_completed, statement.available, balance.low | defined; delivery arrives with later releases (the sandbox can simulate them) |
Payload and headers
Payloads are thin: ids, statuses and amounts. Fetch the full resource with the API (GET /payments/{id} …) when you need details.
POST /wirebloom/webhooks HTTP/1.1
Content-Type: application/json
User-Agent: WireBloom-Webhooks/1.0
WireBloom-Event-Id: 0192a6f3-0000-7000-8000-0000000e0001
WireBloom-Event-Type: payment.status_changed
WireBloom-Timestamp: 1790161320
WireBloom-Signature: v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
{"id":"0192a6f3-0000-7000-8000-0000000e0001","type":"payment.status_changed",
"createdAt":"2026-09-23T11:02:00Z","apiVersion":"v1","tenantId":"0192a6f0-0000-7000-8000-0000000000c1",
"data":{"object":"payment","id":"0192a6f1-3333-7000-8000-00000000c001","status":"completed",
"previousStatus":"processing","amount":{"amountMinor":"100000","currency":"EUR"}}}Verify every delivery
WireBloom-Signature is v1= + hex HMAC-SHA256 of WireBloom-Timestamp + "." + rawBody with your endpoint secret. During a secret rotation it carries two entries (v1=…, v1=…); accept either.
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verify(
rawBody: Buffer,
timestamp: string,
signatures: string,
secret: string,
): boolean {
if (!/^\d+$/.test(timestamp) || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300)
return false;
const expected = createHmac('sha256', secret).update(`${timestamp}.`).update(rawBody).digest();
return signatures
.split(',')
.map((s) => s.trim())
.filter((s) => s.startsWith('v1='))
.some((s) => {
const candidate = Buffer.from(s.slice(3), 'hex');
return candidate.length === expected.length && timingSafeEqual(candidate, expected);
});
}import hashlib, hmac, time
def verify(raw_body: bytes, timestamp: str, signatures: str, secret: str) -> bool:
if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > 300:
return False
expected = hmac.new(secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(s.strip()[3:], expected)
for s in signatures.split(",") if s.strip().startswith("v1="))Receiver rules:
- Verify against the raw body bytes, before JSON parsing; compare in constant time.
- Reject timestamps more than 5 minutes from your clock (replay protection).
- De-duplicate on
WireBloom-Event-Id(deliveries are at-least-once; an event id is the same for retries, redeliveries and every endpoint that receives it). - Answer
2xxwithin 10 seconds, then process asynchronously. Redirects are not followed. - Do not rely on ordering: use
status/previousStatusand re-read the resource.
Retries, pause and recovery
Any non-2xx, timeout or connection error is retried after 1 min, 5 min, 30 min, 2 h, 6 h, then every 12 h, for up to 72 h after the event. After 20 consecutive failed attempts the endpoint is paused; after 72 h without a successful delivery it is disabled. Both notify the customer's admins. Paused or disabled endpoints receive no new events.
To recover: fix the receiver, updateIntegratorWebhook with "enabled": true (send If-Match), then list listWebhookDeliveries with filter[succeeded]=false and redeliver what you missed (redeliverWebhook, same event id). You can also disable an endpoint yourself with "enabled": false.
Secret rotation
rotateIntegratorWebhookSecret returns a new secret; for 24 h deliveries are signed with both the new and the old secret. Deploy the new secret within that window.