Receive signed webhooks
Verify HMAC signatures before applying payment state changes.
Register an endpoint
curl --request POST "${API_URL}/v1/webhooks" \
--header "content-type: application/json" \
--header "x-api-key: ${VEXPAY_API_KEY}" \
--data '{
"url": "https://merchant.example.com/webhooks/vexpay",
"events": ["payment.pending", "payment.completed", "payment.failed", "merchant.verified", "merchant.rejected", "merchant.deactivated", "merchant.reactivated", "payout.completed", "payout.failed"]
}'{
"event": "payout.completed",
"data": {
"payoutId": "00000000-0000-4000-8000-000000000000",
"externalRef": "po_a1",
"reference": "16142941",
"livemode": true
},
"timestamp": "2026-07-20T15:00:00.000Z"
}{
"event": "merchant.rejected",
"data": {
"merchantId": "292d478b-a1fb-4d27-ab5c-8701ed14da88",
"externalRef": "seller_47",
"payoutMethodId": "0064456b-7a6a-4b7e-b0b5-d9b27b0dfa5c",
"failureCode": "BE01",
"reason": "Datos del cliente no corresponden a la cuenta",
"action": "payout_method_deleted",
"livemode": true
},
"timestamp": "2026-07-23T18:12:05.000Z"
}Update subscribed events later with PATCH /v1/webhooks/:id — sending events replaces the full list. You can also change url or isActive; the signing secret cannot be rotated via PATCH.
curl --request PATCH "${API_URL}/v1/webhooks/ENDPOINT_ID" \
--header "content-type: application/json" \
--header "x-api-key: ${VEXPAY_API_KEY}" \
--data '{
"events": ["payment.completed", "payment.failed", "payout.completed", "payout.failed"]
}'Use POST /v1/notifications/test after registering an endpoint. Deliveries include X-Webhook-Event and X-Webhook-Signature headers.
Delivery behavior
- Each event is attempted once. Failed deliveries are not retried.
- Your endpoint has 10 seconds to respond.
- Event ordering is not guaranteed; compare payment status instead of arrival order.
- Return a 2xx response only after the event is accepted for durable processing.
- Reconcile missed or uncertain events through the payment lookup endpoints.
Verify the HMAC signature
Compute an HMAC-SHA256 digest over the exact raw request body with your endpoint secret. Compare the received and expected signatures with a timing-safe operation before parsing or acting on the event.
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyWebhook(rawBody, signature, secret) {
if (!/^sha256=[a-f0-9]{64}$/i.test(signature)) return false;
const expected = createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
const received = Buffer.from(signature.slice('sha256='.length), 'hex');
const calculated = Buffer.from(expected, 'hex');
return timingSafeEqual(received, calculated);
}