VipterHelp Center
Developers

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

HeaderContent
Content-Typeapplication/json
User-AgentVipter-Webhooks/1.0
Vipter-Signaturet=<Unix seconds>,v1=<HMAC in hexadecimal>. During a secret rotation, a second v1= is included. See Verify the signature.
Vipter-Event-IdThe same value as the id field in the body. You can discard a repeat before reading the body.
Vipter-Event-TypeThe 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

FieldTypeContent
idstringUnique event ID: evt_ followed by 24 hexadecimal characters. It is the same across all attempts and resends.
objectstringAlways "event".
typestringEvent type, such as order.paid. See the catalog.
createdintegerWhen the event was created, in Unix seconds. It doesn't change between attempts.
livemodebooleanThe 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_versionstringFormat version. Today every event goes out with "2026-09-01".
data.objectobjectThe 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_at with 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 status as the truth at that moment, and not the event type.

What to do next

On this page