# Webhooks

Get a signed event on your server when something happens in your business, covering adding an endpoint, all 43 events by product, the envelope and payloads, checking the signature, replies and retries, the automatic switch-off, the delivery log, catching up on missed events and changing the signing secret.

Source: https://vezmo.com/docs/developers/webhooks

## Overview

A webhook tells your server what happened, as it happens: a payment succeeded, a refund went through, a dispute opened,
an invoice was paid. You add an https endpoint once, pick the events it should get, and Vezmo sends each one as a signed
JSON `POST`.

Webhooks are how you ship orders: a redirect or a browser event can be faked, a signed event can't.

## Add an endpoint

You need the webhooks permissions; the **Manager** role has them.

1. **Open Webhooks**
Go to **Payments › VezmoPay**, then **Developers**, and open the **Webhooks** tab. Select **Add endpoint**.

2. **Enter the endpoint URL**
The **Endpoint URL** must start with `https://`, in test and in live. Addresses that point to a private network are
refused, and we never follow redirects. To receive events on your own computer, use an https tunnel.

3. **Select the events**
Under **Select Events**, tick each event you want. Search narrows the list, and **Select all** ticks every event. An
endpoint only gets the events ticked here: `payment.success` doesn't bring `payment.failed` with it.

(Screenshot: Add New Webhook: Endpoint URL https://shop.example.com/webhooks/vezmo, Select Events with 2 selected, the search narrowed to the two ticked events payment.failed and payment.success with their descriptions, and the Create Webhook button.)

4. **Confirm with the emailed code**
Select **Create Webhook**. We email you a 6-digit code: type it in. Adding an endpoint, or changing its URL or events,
always needs one.

5. **Save the signing secret**
The **Signing secret** (`whsec_` and 64 characters) is shown once. Store it on your server, then select
**I've saved it — done**. You need it to check every event.

Endpoints belong to your business, not to the teammate who added them: they keep working when that person leaves, and
everyone with webhook access sees and manages them. One endpoint gets both test and live events.

(Screenshot: Developers, Webhooks tab: the Endpoints card with one endpoint, https://shop.example.com/webhooks/vezmo, Enabled, 43 events, created Nov 09, 2026, with the Deliveries, edit and delete buttons, and the Add endpoint button.)

## Events

There are 43 events. Payloads that aren't shown here are in the
[webhooks reference](https://user.vezmo.com/docs/v1/webhooks) and on each product's reference page.

### Payments

| Event | When it is sent |
| --- | --- |
| `payment.success` | A payment succeeded: a card or wallet payment at once, a bank payment when it settles. `method` says how it was paid. |
| `payment.processing` | A payment is on its way, like a US bank payment that is settling. `payment.success` follows when it settles, usually a few business days later. |
| `payment.awaiting_verification` | A bank payment entered by hand waits for the customer to confirm small test deposits. It carries a `verification` block with the link to share. |
| `payment.failed` | A payment was declined or failed, or a bank payment was returned. It carries the decline details. |
| `payment.canceled` | A bank payment still on its way was canceled before the debit went out. No money moved. |
| `payment.refunded` | A refund went through, in full or in part. Each refund is its own event. |
| `payment.refund_failed` | A refund couldn't be completed. The amount is back in your balance; pay the customer back another way. |
| `payment_method.saved` | A customer saved their card at checkout for later payments with you. |

### Disputes

| Event | When it is sent |
| --- | --- |
| `dispute.created` | A customer disputed a payment (a chargeback or an inquiry). It carries the reason and `evidenceDueBy`. |
| `dispute.updated` | The dispute's status changed, for example when evidence was submitted. Each change is its own event. |
| `dispute.closed` | The dispute reached its final status: won, lost or closed. |

### Payouts

| Event | When it is sent |
| --- | --- |
| `payout.paid` | A payout reached your bank. |
| `payout.failed` | A payout failed. A bank can return a payout days after it was paid: the same payout then sends `payout.failed` after `payout.paid`. Keep the latest. |

### Invoices

| Event | When it is sent |
| --- | --- |
| `invoice.created` | An invoice was created. |
| `invoice.sent` | An invoice was sent to the client. |
| `invoice.paid` | An invoice was paid: by the client at checkout, or when a bank payment settled. |
| `invoice.cancelled` | You canceled an invoice. |

### Proposals

| Event | When it is sent |
| --- | --- |
| `proposal.sent` | A proposal was sent. |
| `proposal.accepted` | A proposal was accepted: by the client from the link (`acceptedBy: "client"`), or recorded by you with `POST /merchant/proposal/:id/mark-accepted` (`"business"`). |
| `proposal.rejected` | A proposal was rejected: by the client, or recorded by you with `POST /merchant/proposal/:id/mark-rejected` (`rejectedBy`). |

### Subscriptions

| Event | When it is sent |
| --- | --- |
| `subscription.trial_started` | A free trial started: the customer saved their card. Nothing is charged yet. |
| `subscription.activated` | The first payment succeeded. |
| `subscription.renewed` | A renewal was paid. |
| `subscription.payment_failed` | A subscription charge failed. Each failed attempt is its own event. |
| `subscription.updated` | The quantity or the plan changed. |
| `subscription.paused` | The subscription was paused. |
| `subscription.resumed` | A paused subscription started again. |
| `subscription.cancel_scheduled` | A cancel was booked for the end of the period, by you or the customer. It stays active until then. |
| `subscription.canceled` | The subscription ended: canceled now, at the end of the period, or because its first payment was never made. |
| `subscription.refunded` | A subscription payment was refunded, in full or in part. |
| `subscription.disputed` | A subscription payment was disputed, or its dispute was won or lost. |

### Agreements

Agreements have no test mode: their events always carry `livemode: true`.

| Event | When it is sent |
| --- | --- |
| `agreement.sent` | An agreement was sent for signature. |
| `agreement.viewed` | A recipient opened it for the first time. Once per recipient. |
| `agreement.signed` | A recipient signed. Once per recipient. |
| `agreement.completed` | Everyone signed and the signed PDF is ready. |
| `agreement.declined` | A recipient declined to sign. |
| `agreement.voided` | The agreement was voided. |
| `agreement.expired` | Its deadline passed with someone still to sign. |
| `agreement.paid` | The payment attached to it was paid. |
| `agreement.identity_checked` | A signer's ID check passed, needs your review, was approved or rejected, or failed for the last time. |
| `agreement.awaiting_approval` | Everyone signed, and it waits for you to approve the signers' IDs. |

### Identity checks

| Event | When it is sent |
| --- | --- |
| `kyc.verification.submitted` | A teammate's identity check went into review. |
| `kyc.verification.decided` | A teammate's identity check was approved or rejected. |

These two carry only the check's `id`, `userId`, `status`, `documentType`, `submittedAt` and `decidedAt` (and your
`merchantId`): never a score or a reason.

## What we send

Every event is a `POST` with a JSON body and these headers:

| Header | What it holds |
| --- | --- |
| `X-Webhook-Signature-V2` | The signature to check: see [Check the signature](#check-the-signature). |
| `X-Webhook-Timestamp` | When this attempt was sent, in Unix seconds. It is new on every retry. |
| `X-Webhook-Event` | The event name, like `payment.success`. |
| `X-Webhook-Signature` | An older signature over the body only, kept for integrations that already check it. New code checks V2. |
| `Content-Type` | `application/json` |

The body is always the same envelope, with three fields. A full example of every event is in the
[Webhooks reference](https://user.vezmo.com/docs/v1/webhooks#events).

- **id**:The event's id. The same on every retry of this event, and different for every other event: use it to spot repeats. Don't parse it.
- **event**:The event name, as in the header.
- **data**:The object the event is about. In `payment.success` and `payment.failed`, `paymentId` is the id you got when you created the checkout session, beside `amount`, `currency`, `status`, `method` and `livemode`. A `payment.success` example is in [the quickstart](/docs/developers/quickstart#6-receive-paymentsuccess).

- Amounts are JSON numbers in major units: `49.99` is $49.99.
- `livemode` is `false` for test events. Never ship a real order on one.
- `merchantId` is your VezmoPay merchant id, once your business is approved.
- When a customer paid in their own currency, `amount` and `currency` are your price, and `charged` holds what they
  paid. See [Local currency](https://user.vezmo.com/docs/v1/local-currency).

## Check the signature

Check every event before you trust it. The `X-Webhook-Signature-V2` header is the HMAC-SHA256 of
`timestamp + "." + raw body`, keyed with your signing secret, as lowercase hex. `timestamp` is the
`X-Webhook-Timestamp` header, exactly as sent.

1. Read the **raw body** bytes. Parsing the JSON and turning it back into text changes it, and the signature won't
   match.
2. Compute the HMAC and compare it to the header in constant time.
3. Refuse a timestamp more than 5 minutes from your clock, so an old event can't be replayed.

Answer `400` to a bad signature and `200` to a good one. A working example, with the raw body, the 5-minute check and
the constant-time comparison, is in [Verify signatures](https://user.vezmo.com/docs/v1/webhooks#verify) in the API
reference.

## Replies and retries

Answer with any `2xx` status **within 5 seconds**: the 5 seconds cover connecting and your reply. We read only the first
2 KB of what you send back. Anything else (another status, a timeout, a redirect) is a failed delivery.

A failed event is tried again, 4 attempts in all: at once, then 6 hours after the first failure, 12 hours after the
second and 24 hours after the third. Each attempt has a new timestamp and signature, and the same `id`. A retry goes to
the endpoint's URL at that moment, signed with its secret at that moment, so once you fix your endpoint the pending
retries land.

To stay inside 5 seconds, save the event and answer first, then do the slow work.

### When an endpoint keeps failing

If every delivery to an endpoint fails for at least 3 days and at least 50 attempts, we switch it off: it shows as
**Disabled**, and your team gets an email, "We paused one of your webhook endpoints". Its pending retries are dropped, and
events that happen while it is off aren't kept for it.

The dashboard has no switch to turn a paused endpoint back on. Fix your server, delete the endpoint and add it again
(you get a new signing secret), then [catch up](#catch-up-on-missed-events) on what you missed.

## Handle events well

- **Expect repeats.** The same event can arrive more than once. Keep the `id`s you've handled and skip one you've seen.
  Each refund, each dispute status, each failed subscription charge and each agreement signer has its own `id`.
- **Don't count on the order.** A retry can arrive hours after a later event. Before you act on an old one, read the
  current state from the API, for example `GET /merchant/payment/:id`.
- **Check `livemode`.** Test and live events reach the same endpoint.
- **Match the money.** Before you ship, check that `data.amount` and `data.currency` are what the order expects.

## The delivery log

**Developers › Webhooks › Deliveries** lists every attempt: the event, your server's answer code, how long it took and
when. It shows one endpoint at a time: choose it in the list at the top, or select **Deliveries** on its row. Open a row
to see the request we sent and what your server replied.

(Screenshot: The Deliveries card for https://shop.example.com/webhooks/vezmo: rows for payment.success, payment.failed, dispute.created, invoice.sent, proposal.accepted and proposal.sent, each with a 200 answer, the time it took and how long ago; the newest payment.success row is open on Request we sent, showing the event's JSON data.)

- A row with `4xx` or `5xx`: we delivered and your server refused it. A bad signature check is the usual cause.
- **No reply**: your server didn't answer within 5 seconds, or couldn't be reached.
- No row at all: the endpoint isn't subscribed to that event, or the endpoint is off.

## Catch up on missed events

There is no button to resend an event, no test event, and no API to list past events. To catch up after an outage, read
the current state from the API ([Payments reference](https://user.vezmo.com/docs/v1/payments)):

1. **Orders waiting for payment.** For each one, read the payment id you saved with `GET /merchant/payment/:id`, and
   act on its `state`.
2. **Payments you have no record of.** List the days you missed with `GET /merchant/payment`, using `range=custom`,
   `startDate` and `endDate`, and follow `data.pagination.nextPage` to the end.

   Bank payments still settling aren't in that list: list them with `pending=true`, and those waiting for the
   customer's deposit check with `state=awaiting_verification`. These two lists page with `page=`.
3. **Disputes.** `GET /merchant/dispute?from=2026-11-08&to=2026-11-09`
   ([reference](https://user.vezmo.com/docs/v1/disputes)).
4. **Invoices and subscriptions.** `GET /merchant/invoice` and `GET /merchant/subscription`
   ([invoices](https://user.vezmo.com/docs/v1/invoices), [subscriptions](https://user.vezmo.com/docs/v1/subscriptions)).

## Change the signing secret

A secret can't be changed in place: editing an endpoint keeps it. To get a new one, add a new endpoint and delete the old
one.

- **Delete, then add** the same URL: the simplest, but events in between are missed. Catch up as above.
- **Add, then delete**: no gap. While both endpoints exist, each event goes to both, with a different `id` and each
  signed with its own endpoint's secret. For that time your handler has to accept both secrets, and spot repeats by the
  event name and the object in `data` (like `data.paymentId`), not by `id`.

If a secret leaks, do it at once.

## Questions

### How do I send a test event?

Make a test payment with a test key: see [Take your first test payment](/docs/developers/quickstart). Its events reach
your endpoint with `livemode: false`.

### Can I add endpoints over the API?

No. Endpoints, like API keys, are managed in the dashboard only.

### I get events for things a teammate did

Endpoints belong to the business, so they get events from the whole business, whoever caused them.

## Related

- [Take your first test payment](/docs/developers/quickstart)
- [Hosted checkout](/docs/developers/hosted-checkout)
- [Go-live checklist](/docs/developers/go-live)
- [Webhooks in the API reference](https://user.vezmo.com/docs/v1/webhooks)
