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 `VexPay-Signature`, `VexPay-Event-Id`, and the legacy `X-Webhook-Event` / `X-Webhook-Signature` headers.
Event catalog
| Event | When | Key fields in data |
|---|---|---|
| payment.pending | A payment was created and awaits completion | paymentId, method, externalRef |
| payment.completed | The payment was confirmed and credited to your balance | paymentId, method, vesAmount / amountCop / amountUsdt, fee |
| payment.failed | The charge failed or expired | paymentId, failureCode |
| payment.canceled | An intent was canceled (superseded, expired or by you) | paymentId, cancelReason |
| payment.reversed | A completed payment was reversed: a C2P reversal, a COP refund, or a full card chargeback | paymentId, reversalType (reversal | chargeback), reversalRef |
| payment.chargeback | The bank charged back a card (VPOS) payment; your VES balance was debited | chargebackId, paymentId, amountVes, feeVes, reason, ledgerEntryIds |
| payment.chargeback_closed | A chargeback was decided: won (money returned) or lost | chargebackId, paymentId, outcome, ledgerEntryIds |
| payout.completed / payout.failed | A payout was paid or failed (failed payouts return the money) | payoutId, currency, failureReason |
| conversion.created | A VES or COP→USDT conversion started (including auto-convert of a COP payment) | conversionId |
| conversion.completed / conversion.canceled | A VES or COP→USDT conversion finished or was canceled | conversionId |
| merchant.* | Merchant lifecycle and balances (marketplaces) | merchantId, externalRef |
| tenant.* | Your account status, live activation and API keys | — |
| notification.test | POST /v1/notifications/test | — |
Every movement of your VES balance — including chargebacks and adjustments that are not a normal payment or payout — is listed in `GET /v1/balance/transactions`. See Reconcile your balance.
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 signature
Prefer the `VexPay-Signature` header (`t=<unix>,v1=<hex>`). The `v1` value is HMAC-SHA256 of `<t>.<rawBody>` with your endpoint secret. Reject deliveries whose `t` is outside your tolerance (official SDKs default to 300 seconds). Use `VexPay-Event-Id` to deduplicate.
import VexPay from '@vexpay/node';
const vexpay = new VexPay(process.env.VEXPAY_API_KEY);
app.post('/webhooks/vexpay', express.raw({ type: 'application/json' }), (req, res) => {
const event = vexpay.webhooks.constructEvent(
req.body,
req.headers['vexpay-signature'],
process.env.VEXPAY_WEBHOOK_SECRET,
);
// fulfill from event.data — then res.sendStatus(200)
});from vexpay import Webhook
event = Webhook.construct_event(
request.body, # raw bytes
request.headers["VexPay-Signature"],
os.environ["VEXPAY_WEBHOOK_SECRET"],
)