Envelope format
The request Vipter sends to your endpoint, field by field, with the headers, the response deadline, how to handle repeated events and what Vipter guarantees about ordering.
Each event arrives as a POST to the endpoint URL. The body is a JSON object, the envelope, which is the same for every type. What changes is the object inside data.object, described in the event catalog.
The request
POST /webhooks/vipter HTTP/1.1
Host: erp.example.com
Content-Type: application/json
User-Agent: Vipter-Webhooks/1.0
Vipter-Signature: t=1790604192,v1=5d41402abc4b2a76b9719d911017c592ae2e6b7c0f1d3e5a7b9c2d4f6a8b0c1e
Vipter-Event-Id: evt_3f9a1c7e5b2d4f6a8c0e1b3d
Vipter-Event-Type: order.paid
{"id":"evt_3f9a1c7e5b2d4f6a8c0e1b3d","object":"event","type":"order.paid","created":1790604191,"livemode":true,"api_version":"2026-09-01","data":{"object":{"object":"order","id":"ord_5c1e8a2b9d4f4e7a8b3c6d1e2f7a9b0c", "…": "…"}}}The body comes as compact JSON, with no spaces or line breaks, in UTF-8. Don't reformat the body before you verify the signature.
Headers
| Header | Content |
|---|---|
Content-Type | application/json |
User-Agent | Vipter-Webhooks/1.0 |
Vipter-Signature | t=<Unix seconds>,v1=<HMAC in hexadecimal>. During a secret rotation, a second v1= is included. See Verify the signature. |
Vipter-Event-Id | The same value as the id field in the body. You can discard a repeat before reading the body. |
Vipter-Event-Type | The same value as the type field in the body. You can route the event without reading the body. |
In most frameworks, header names are case-insensitive: vipter-signature in Node.js, $_SERVER['HTTP_VIPTER_SIGNATURE'] in PHP.
Envelope fields
| Field | Type | Content |
|---|---|---|
id | string | Unique event ID: evt_ followed by 24 hexadecimal characters. It is the same across all attempts and resends. |
object | string | Always "event". |
type | string | Event type, such as order.paid. See the catalog. |
created | integer | When the event was created, in Unix seconds. It doesn't change between attempts. |
livemode | boolean | The Vipter environment that generated the event. It doesn't tell you whether the payment provider was in test mode: a test purchase with a provider in sandbox arrives with the same value as real sales. |
api_version | string | Format version. Today every event goes out with "2026-09-01". |
data.object | object | The order, subscription, customer or abandoned checkout, in the state it was in when the event was created. |
created and the signature's t are different things. created is the moment of the event and stays fixed. t is the moment that attempt was sent and changes with each attempt.
The response
The delivery succeeds when your server answers with any 2xx code within 10 seconds. The response body is ignored.
These count as a failure and trigger a retry:
- Any code outside 2xx, including 3xx. Vipter does not follow redirects: if the URL changed, update the endpoint.
- No response within 10 seconds. The delivery gets the error
timeout. - A network, DNS or TLS error. The URL must be
https://, with a valid certificate.
Also answer 2xx for the event types your system ignores. An error response does not discard the event: it comes back in the following attempts and counts toward automatic disabling.
Answer before processing
Verify the signature, write the event to a queue or table and answer 200. Do the heavy work, such as calling other systems, after answering.
Idempotency
Delivery is "at least once". The same event, with the same id, can arrive more than once when:
- your server processed the event but the response didn't reach Vipter within 10 seconds, and the next attempt sends it again;
- someone clicked Resend on a delivery;
- sending was interrupted on Vipter's side before the result was recorded. The delivery goes back to the queue after 5 minutes and is sent again.
To handle this, store the id (or the Vipter-Event-Id header) in a column with a unique constraint and ignore the event if it already exists:
create table vipter_events (
id text primary key, -- evt_…
type text not null,
received_at timestamptz not null default now()
);
-- On receipt: if the row already exists, the event is a repeat.
insert into vipter_events (id, type) values ($1, $2) on conflict (id) do nothing;There is a second case, with a different id: the same fact in two events. A change made in the dashboard or in the customer portal, such as cancelling a subscription, generates the event right away, and the provider's confirmation of the same change can generate another event of the same type. So, besides discarding a repeated id, make your processing depend on the state and not on the event: "mark the subscription as cancelled" can run twice without harm, while "send the cancellation e-mail" must check whether it was already sent.
Event order
Vipter does not guarantee the order of arrival. Deliveries run in parallel, and a delivery that failed comes back hours later, when newer events have already arrived. A subscription.renewed can arrive before the order.paid of the renewal, and an order.refunded can arrive before the order.paid of the same order, if the order.paid failed on the first attempt.
To avoid overwriting a new state with an old one:
- Compare
data.object.updated_atwith what you already have stored and ignore the event if it is older. - Or, when you receive any event for an order or subscription, treat the object's
statusas the truth at that moment, and not the event type.
What to do next
- Verify the signature with ready-made code in Node.js, PHP and Python.
- See the retry schedule and what happens when the endpoint is down.
Event catalog
The 20 event types Vipter sends by webhook, when each one fires and what comes in data.object, with a full example of an order, a subscription, a customer and an abandoned checkout.
Verify the signature
How Vipter signs each webhook, tested code in Node.js, PHP and Python to check the signature, how to rotate the secret without losing events and the mistakes that most often make verification fail.