API reference (v1)
Every endpoint of the Vipter API with its parameters, an example call and response, and the field table of each object: account, customer, subscription and its actions, subscription charge, metered usage (meters, usage events, items and periods), order, offer, product, checkout session, customer portal session, events and webhook endpoints.
All endpoints live under https://api.vipter.com/v1, require an API key and follow the conventions: money as integers in the minor unit, dates in Unix seconds, lowercase currency, cursor-paginated lists. GET endpoints require the read scope; POST and DELETE endpoints require the write scope and accept an Idempotency-Key, which the subscription charge requires.
The machine-readable description, in OpenAPI 3.1, is at GET https://api.vipter.com/v1/openapi.json. It is meant for generating clients. A Postman collection generated from it, with one folder per resource and example bodies, is at GET https://api.vipter.com/v1/postman.json; it imports into Postman, Bruno and Insomnia. For AI agents, see Integrating with AI agents.
The examples use fictitious IDs and amounts in Brazilian reais. Authorization is shortened to vk_live_….
Account
Retrieve the account
GET /v1/accountReturns the store and the key that made the call. It is the first call of a new integration: if it answers 200, the key is right and the API is enabled.
curl https://api.vipter.com/v1/account \
-H "Authorization: Bearer vk_live_…"{
"id": "a3f1c2d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"object": "account",
"name": "Loja Demo",
"slug": "loja-demo",
"country": "BR",
"currency": "brl",
"timezone": "America/Sao_Paulo",
"api_key": {
"id": "ak_4f8e2c1a9b7d6e5f3a2b1c0d",
"name": "ERP",
"scopes": ["read"],
"default_version": "2026-11-01"
},
"api_version": "2026-11-01",
"livemode": true
}| Field | Type | Content |
|---|---|---|
id | text | The store's ID in Vipter. |
name, slug | text | The store's name and slug. slug is null if the store has none. |
country | text | The store's country, ISO 3166-1 alpha-2. |
currency | text | The store's default currency. |
timezone | text | The store's time zone, in IANA format. |
api_key | object | The key used: id, name, scopes (read, write) and default_version, the API version the key uses when the call sends no Vipter-Version. |
api_version | text | The version used on this call. |
livemode | boolean | true in production. |
Customers
A customer is someone who bought or subscribed in the store. Customers are created by the checkout, by the team in the dashboard or through POST /v1/customers.
List customers
GET /v1/customers| Parameter | Type | Content |
|---|---|---|
email | text | Only customers with this email, case-insensitive. |
limit, starting_after, ending_before | Pagination. |
curl "https://api.vipter.com/v1/customers?email=ana@example.com" \
-H "Authorization: Bearer vk_live_…"{
"object": "list",
"url": "https://api.vipter.com/v1/customers",
"has_more": false,
"data": [
{
"id": "cust_9d2e4f6a8b1c3d5e",
"object": "customer",
"email": "ana@example.com",
"name": "Ana Souza",
"phone": "+5511999990000",
"document": { "type": "cpf", "number_masked": "*******1234" },
"address": {
"line1": "Rua das Flores, 100",
"line2": "Apto 42",
"city": "São Paulo",
"state": "SP",
"postal_code": "01310-100",
"country": "BR"
},
"country": "BR",
"locale": "pt-BR",
"metadata": {},
"livemode": true,
"created": 1790186400
}
]
}Retrieve a customer
GET /v1/customers/{id}curl https://api.vipter.com/v1/customers/cust_9d2e4f6a8b1c3d5e \
-H "Authorization: Bearer vk_live_…"Returns the customer object. An ID that does not exist in the store gets 404 resource_missing.
Create a customer
POST /v1/customersCreates the customer in the store before the first purchase, to open a checkout session with customer or to store metadata. Requires the write scope. The payment provider requires a full name, a phone and a document, so they are mandatory here.
| Parameter | Type | Content |
|---|---|---|
email | text | Required. Stored in lowercase. |
name | text | Required. Full name, 3 to 120 characters. |
phone | text | Required. In international format (+5511999990000), or a national number of the store's country. A number that is not valid for the country gets 400 parameter_invalid with param phone. |
document[type], document[number] | text | Required. type is cpf, cnpj, passport or tax_id; number may come with dots and dashes, which are removed. |
address | object | Billing address: line1, city, state, postal_code and country (ISO 3166-1 alpha-2) are required inside the object; line2, number and district are optional. |
metadata | object | Free data, with the same limits as the metadata of checkout sessions. |
curl -X POST https://api.vipter.com/v1/customers \
-H "Authorization: Bearer vk_live_…" \
-H "Idempotency-Key: 2a7c0e4b-9d1f-4b3a-8e6c-5f2d7a1b0c9e" \
-H "Content-Type: application/json" \
-d '{
"email": "ana@example.com",
"name": "Ana Souza",
"phone": "+5511999990000",
"document": { "type": "cpf", "number": "123.456.789-09" },
"metadata": { "user_id": "user_8213" }
}'The response is the customer object, with 201 when the customer was created. If a customer with that email already exists in the store, the response is 200 with the existing customer, without changing any field: to change the data, use Update a customer. A new customer generates the customer.created event.
code | HTTP | Meaning |
|---|---|---|
parameter_missing, parameter_invalid | 400 | A required field is missing or has the wrong format. param says which. |
customer_rejected | 400 | The payment provider refused the registration. message carries the reason it gave, such as an invalid document. |
Update a customer
POST /v1/customers/{id}Accepts the same fields as Create a customer, all optional. Only what is sent is changed, with one exception: metadata replaces the whole map, as in Stripe. To delete a key, send the map without it. An ID that does not exist gets 404 resource_missing before any change.
curl -X POST https://api.vipter.com/v1/customers/cust_9d2e4f6a8b1c3d5e \
-H "Authorization: Bearer vk_live_…" \
-H "Content-Type: application/json" \
-d '{ "metadata": { "user_id": "user_8213", "plan": "pro" } }'Returns the updated customer object and generates the customer.updated event. The errors are the same as on creation.
The customer object
| Field | Type | Content |
|---|---|---|
id | text | cust_… |
object | text | "customer" |
email | text | The customer's email. It is what identifies the person in the checkout and in the customer portal. |
name | text or null | Name given at checkout. |
phone | text or null | Phone in international format, with + and the country code. |
document | object or null | type (such as cpf or cnpj) and number_masked, with only the last four digits. The API never returns the full document. |
address | object or null | Billing address: line1, line2, city, state, postal_code, country. Each field may be null. |
country | text or null | The customer's country, ISO 3166-1 alpha-2. |
locale | text or null | The customer's language, such as pt-BR, en or es. |
metadata | object | The data stored by POST /v1/customers or POST /v1/customers/{id}. {} on customers created by the checkout or the dashboard. A checkout session's metadata goes to the order and the subscription, not to the customer. |
livemode | boolean | true in production. |
created | integer | When the customer was created. |
Subscriptions
List subscriptions
GET /v1/subscriptions| Parameter | Type | Content |
|---|---|---|
customer | text | Only this customer's subscriptions (cust_…). |
status | text | One of trialing, active, past_due, paused, canceled, expired. Another value gets 400 parameter_invalid. |
limit, starting_after, ending_before | Pagination. |
curl "https://api.vipter.com/v1/subscriptions?status=active&limit=1" \
-H "Authorization: Bearer vk_live_…"{
"object": "list",
"url": "https://api.vipter.com/v1/subscriptions",
"has_more": true,
"data": [
{
"id": "sub_3c7a9e1f5b2d8c4e",
"object": "subscription",
"status": "active",
"customer": "cust_9d2e4f6a8b1c3d5e",
"customer_email": "ana@example.com",
"offer": { "id": "ofr_6e2b8d4f1a9c3e7b", "name": "Plano Pro mensal" },
"product": { "id": "prd_1a5c9e3b7d2f6a8c", "name": "Plano Pro" },
"billing_cycle": "monthly",
"custom_billing_days": null,
"currency": "brl",
"amount": 9900,
"current_period_start": 1790186400,
"current_period_end": 1792778400,
"next_billing_at": 1792778400,
"trial_start": null,
"trial_end": null,
"cycles_completed": 1,
"cycle_limit": null,
"cancel_at_period_end": false,
"canceled_at": null,
"ended_at": null,
"cancellation_details": null,
"default_payment_method": { "id": "pm_8f3d1c7e2a5b9d4f", "type": "card" },
"installments": null,
"past_due_details": null,
"checkout_session": "cs_4b7e2d9a1c3f5e8b0d2a6c4e1f3b",
"client_reference_id": "user_8213",
"metadata": { "plan": "pro" },
"livemode": true,
"created": 1790186400
}
]
}Retrieve a subscription
GET /v1/subscriptions/{id}curl https://api.vipter.com/v1/subscriptions/sub_3c7a9e1f5b2d8c4e \
-H "Authorization: Bearer vk_live_…"Returns the subscription object.
The subscription object
| Field | Type | Content |
|---|---|---|
id | text | sub_… |
object | text | "subscription" |
status | text | trialing (in a free trial), active, past_due (the last charge failed and Vipter is retrying), paused, canceled, expired (reached the cycle limit or the end without renewing). |
customer | text or null | The subscriber's cust_…. |
customer_email | text or null | The subscriber's email, so you do not need another call. |
offer | object or null | The current offer: id (ofr_…) and name. |
product | object or null | The product: id (prd_…) and name. |
billing_cycle | text or null | The billing interval: daily, biweekly, monthly, quarterly, half_yearly, yearly or custom. |
custom_billing_days | integer or null | With billing_cycle custom, the interval in days. |
currency | text or null | The subscription's currency. |
amount | integer or null | The amount of each charge, in the minor unit. |
current_period_start, current_period_end | integer or null | The period already paid. |
next_billing_at | integer or null | When the next charge is scheduled. null when there is no next one. |
trial_start, trial_end | integer or null | The free trial period, if there was one. |
cycles_completed | integer | How many charges were already made. |
cycle_limit | integer or null | How many charges the subscription makes in total, on offers with a fixed number of cycles. null means no limit. |
cancel_at_period_end | boolean | true when the cancellation was scheduled for the end of the paid period. status stays active until then. |
canceled_at | integer or null | When the cancellation was requested. |
ended_at | integer or null | When the subscription stopped being valid. |
cancellation_details | object or null | reason (reason code), comment (free text) and source: who canceled, such as the dashboard, the customer portal or the provider. |
default_payment_method | object or null | The saved card that pays the renewals: id and type (card). null when the subscription pays by PIX or another method without a saved card. |
installments | integer or null | In how many installments each charge is made, when the offer allows it. |
past_due_details | object or null | Only with status past_due: attempts (how many attempts failed so far), next_retry_at and since (when the first one failed). |
checkout_session | text or null | cs_… of the checkout session that created the subscription. null on subscriptions that came from a regular link or from the dashboard. |
client_reference_id | text or null | Your identifier, copied from the checkout session's client_reference_id or stored through POST /v1/subscriptions/{id}. |
metadata | object | The checkout session's subscription_data[metadata] or, when it was not sent, the session's metadata; or what you stored through POST /v1/subscriptions/{id}. {} on other subscriptions. |
livemode | boolean | true in production. |
created | integer | When the subscription started. |
Subscription actions
The same actions the team performs on the subscription page in the dashboard, and the subscriber performs in the customer portal. All of them require the write scope, accept an Idempotency-Key and return the already updated subscription object: read status, cancel_at_period_end, offer and next_billing_at from the response instead of waiting for the webhook. The matching event goes out exactly as it does for an action in the dashboard; see the event catalog.
An ID that does not exist in the store gets 404 resource_missing. An action that does not fit the subscription's current state, such as pausing an already paused subscription or resuming one that is not paused, gets 400 provider_error, with the reason in message.
Update a subscription
POST /v1/subscriptions/{id}Stores your reference and metadata on the subscription, or schedules the cancellation for the end of the paid period, like Stripe's cancel_at_period_end.
| Parameter | Type | Content |
|---|---|---|
client_reference_id | text or null | Your identifier, up to 200 characters. null clears it. |
metadata | object | Replaces the whole map, as in Stripe. To remove a key, send the map without it. Same limits as the metadata of checkout sessions. |
cancel_at_period_end | boolean | true schedules the cancellation: status stays active until current_period_end, and the subscription does not renew. false is refused with 400 parameter_invalid: to undo a scheduled cancellation, call POST …/reactivate. |
cancellation_details[reason] | text | With cancel_at_period_end: true: the reason, one of too_expensive, not_using, missing_features, switching, temporary, other. Any other value is ignored. |
cancellation_details[comment] | text | With cancel_at_period_end: true: a free comment, up to 500 characters. |
curl -X POST https://api.vipter.com/v1/subscriptions/sub_3c7a9e1f5b2d8c4e \
-H "Authorization: Bearer vk_live_…" \
-H "Content-Type: application/json" \
-d '{
"cancel_at_period_end": true,
"cancellation_details": { "reason": "switching", "comment": "Moving to the yearly plan in January." }
}'Returns the subscription object with cancel_at_period_end: true and cancellation_details carrying the reason and the comment. Changing client_reference_id or metadata emits no event, and neither does scheduling the cancellation: the object shows up with cancel_at_period_end: true in the subscription's next event and, when the period ends, customer.subscription.deleted goes out. When the call carries both the reference and the scheduling, the reference is stored first; if the scheduling is refused, it has already been saved.
Cancel now
DELETE /v1/subscriptions/{id}Cancels right away, without waiting for the end of the paid period, like Stripe's DELETE. The reason goes in the query string, with the same values as in Update a subscription.
curl -X DELETE "https://api.vipter.com/v1/subscriptions/sub_3c7a9e1f5b2d8c4e?cancellation_details[reason]=not_using" \
-H "Authorization: Bearer vk_live_…"Returns the subscription object with status: "canceled" and emits customer.subscription.deleted (subscription.cancelled in the original catalog). In cancellation_details, the source of a cancellation through the API comes as dashboard, the same value as a cancellation by the team. The subscriber gets the same notices as for a cancellation in the dashboard.
Pause and resume
POST /v1/subscriptions/{id}/pause
POST /v1/subscriptions/{id}/resumePausing stops the renewals without canceling; resuming starts charging again. The body of pause accepts reason, free text up to 200 characters kept in the subscription's history; resume has no body.
curl -X POST https://api.vipter.com/v1/subscriptions/sub_3c7a9e1f5b2d8c4e/pause \
-H "Authorization: Bearer vk_live_…" \
-H "Content-Type: application/json" \
-d '{ "reason": "Customer asked for a two-month pause." }'They return the subscription object with status: "paused" and, later, "active", and emit customer.subscription.paused and customer.subscription.resumed. A paused subscription does not accept one-off charges.
Reactivate
POST /v1/subscriptions/{id}/reactivateUndoes a scheduled cancellation (cancel_at_period_end goes back to false and the subscription renews normally) or reactivates a canceled or expired subscription, when the subscription provider allows it; a reactivation it refuses gets 400 provider_error. No body.
curl -X POST https://api.vipter.com/v1/subscriptions/sub_3c7a9e1f5b2d8c4e/reactivate \
-H "Authorization: Bearer vk_live_…"Returns the subscription object and emits customer.subscription.updated (subscription.reactivated in the original catalog).
Change the offer
POST /v1/subscriptions/{id}/change_offerMoves the subscription to another offer, such as an upgrade from the monthly plan to the yearly one.
| Parameter | Type | Content |
|---|---|---|
offer | text | Required. The ofr_… of the new offer. Only the ID; the slug is not accepted. An offer that does not exist in the store gets 404 resource_missing with param offer. |
The new offer must belong to the same product, or the same product family, as the current one; otherwise the change is refused with 400 provider_error.
curl -X POST https://api.vipter.com/v1/subscriptions/sub_3c7a9e1f5b2d8c4e/change_offer \
-H "Authorization: Bearer vk_live_…" \
-H "Content-Type: application/json" \
-d '{ "offer": "ofr_9a4c2e8b6d1f3a7c" }'Returns the subscription object with offer, amount, billing_cycle and next_billing_at already from the new offer, and emits customer.subscription.updated (subscription.upgraded or subscription.downgraded in the original catalog, depending on whether the new amount is greater than or equal to, or less than, the previous one).
Subscription charges
A subscription charge is a one-off amount charged now on a subscription's saved card, outside the cycle: a usage overage, an add-on, an extra service. The next renewal date and the recurring amount do not change. It is the same charge the team makes in the dashboard through the Charge extra amount… button of the subscription (One-off charge); both show up in the same list, and the source field says where each one came from (API or Dashboard on the subscription page).
The charge asks nothing of the subscriber: Vipter charges the saved card without the customer present, through the same provider that took the subscription's first charge. Every approved charge becomes a paid order and counts as one order in the store's Vipter plan quota. The guide for billing usage in a SaaS is in Additional usage charges.
Charge the saved card
POST /v1/subscriptions/{id}/chargesRequires the write scope and requires an Idempotency-Key: without it, the call gets 400 idempotency_key_required before anything else. Use a value that identifies the charge you intend to make, such as the usage period's ID in your system. The same key never charges twice: it returns the same charge, with 200 and the Idempotent-Replayed: true header. Unlike the other calls, a charge's key does not expire after 24 hours: it stays tied to the charge forever, and repeating it months later still returns the same charge.
| Parameter | Type | Content |
|---|---|---|
amount | integer | Required. The total to charge, in the currency's minor unit (4990 is R$ 49.90). The minimum is one unit of the currency (100 in brl or usd), otherwise 400 amount_too_small. |
currency | text | Optional. Must be the subscription's currency, otherwise 400 currency_mismatch. Without it, the subscription's currency is used. |
description | text | Up to 140 characters. It is the charge description sent to the provider and, without lines, the name of the order's single item. Without description, the first line's description applies. |
lines[] | list | 1 to 50 items explaining the amount; they become the order's items. Each item: description (required, up to 140 characters), quantity (integer, default 1) and unit_amount (integer, in the minor unit). The sum of quantity × unit_amount must equal amount, otherwise 400 lines_total_mismatch. Without lines, the order has a single item, with description and amount. |
metadata | object | Free data, with the same limits as the metadata of checkout sessions. It stays on the charge and comes back in the subscription_charge.* events. |
curl -X POST https://api.vipter.com/v1/subscriptions/sub_3c7a9e1f5b2d8c4e/charges \
-H "Authorization: Bearer vk_live_…" \
-H "Idempotency-Key: usage:user_8213:2026-09" \
-H "Content-Type: application/json" \
-d '{
"amount": 5750,
"currency": "brl",
"description": "Additional usage for September",
"lines": [
{ "description": "Calls beyond the allowance", "quantity": 2300, "unit_amount": 2 },
{ "description": "Additional storage (GB)", "quantity": 23, "unit_amount": 50 }
],
"metadata": { "user_id": "user_8213", "period": "2026-09" }
}'The response is 201 with the subscription_charge object:
{
"id": "sch_7e2a9c4b1d3f5a6e8b0c2d4f",
"object": "subscription_charge",
"status": "succeeded",
"subscription": "sub_3c7a9e1f5b2d8c4e",
"customer": "cust_9d2e4f6a8b1c3d5e",
"amount": 5750,
"currency": "brl",
"description": "Additional usage for September",
"lines": [
{ "description": "Calls beyond the allowance", "quantity": 2300, "unit_amount": 2, "amount": 4600 },
{ "description": "Additional storage (GB)", "quantity": 23, "unit_amount": 50, "amount": 1150 }
],
"order": "ord_2f8c4e6a1b3d5f7e",
"transaction": "tx_9b1d3f5a7c2e4a6c",
"failure_code": null,
"failure_message": null,
"source": "api",
"metadata": { "user_id": "user_8213", "period": "2026-09" },
"livemode": true,
"created": 1790790400,
"settled_at": 1790790403
}What happens with an approved charge:
- It becomes an order with
billing_reason: "manual",order_type: "api",recurrence: "unscheduled",subscriptionfilled in and thelinesas items (kind: "charge"). The charge'ssch_…is in the order'sexternal_order_id. - The events
invoice.paid(with the order) andsubscription_charge.succeeded(with the charge) go out in the 2026-11-01 catalog, andorder.paidin the original one. - The subscriber receives the store's purchase confirmation e-mail, when it is active, and sees the order in the customer portal.
A charge the provider leaves under review comes back with 201 and status: "pending", without settled_at. It is settled later, when the provider notifies, and then becomes succeeded or failed and emits the events. Look it up with GET /v1/subscription_charges/{id} or wait for subscription_charge.succeeded / subscription_charge.failed.
Card declined. The response is 402 with type card_error, as in Stripe. The code is the decline code reported by the provider, or declined when it reports none; codes vary by provider, so do not depend on a fixed list. The declined charge comes whole in error.subscription_charge, with status: "failed", failure_code and failure_message:
{
"error": {
"type": "card_error",
"code": "insufficient_funds",
"message": "Insufficient funds.",
"doc_url": "https://docs.vipter.com/pt-br/developers/api-conventions#erros",
"subscription_charge": {
"id": "sch_7e2a9c4b1d3f5a6e8b0c2d4f",
"object": "subscription_charge",
"status": "failed",
"subscription": "sub_3c7a9e1f5b2d8c4e",
"customer": "cust_9d2e4f6a8b1c3d5e",
"amount": 5750,
"currency": "brl",
"description": "Additional usage for September",
"lines": [
{ "description": "Calls beyond the allowance", "quantity": 2300, "unit_amount": 2, "amount": 4600 },
{ "description": "Additional storage (GB)", "quantity": 23, "unit_amount": 50, "amount": 1150 }
],
"order": "ord_2f8c4e6a1b3d5f7e",
"transaction": "tx_9b1d3f5a7c2e4a6c",
"failure_code": "insufficient_funds",
"failure_message": "Insufficient funds.",
"source": "api",
"metadata": { "user_id": "user_8213", "period": "2026-09" },
"livemode": true,
"created": 1790790400,
"settled_at": 1790790402
}
}
}A decline emits subscription_charge.failed and, when the provider recorded an order for the attempt, invoice.payment_failed with that order (status: "failed"). Vipter does not retry on its own: the charge stays failed, and repeating the same Idempotency-Key returns the same 402. To charge again, after the subscriber changes the card in the customer portal, for example, make a new call with another key.
Errors of this call, besides the general errors:
code | HTTP | Meaning |
|---|---|---|
idempotency_key_required | 400 | The call came without an Idempotency-Key. The type is idempotency_error. |
resource_missing | 404 | The subscription does not exist in the store. |
subscription_not_chargeable | 400 | The subscription is paused, canceled or expired. Only active, trialing and past_due accept a charge. |
no_payment_method | 400 | The subscription has no saved card: it pays by PIX or another method without a card. |
payment_method_not_chargeable | 400 | The card is stored at a provider that does not accept charges without the customer present. Today, Mercado Pago. |
currency_mismatch | 400 | currency differs from the subscription's currency. message says which it is. |
amount_too_small | 400 | amount is less than one unit of the currency. |
lines_total_mismatch | 400 | The lines do not add up to amount. message carries both values. |
provider_error | 400 | The provider refused the charge request before it reached the card. message carries the reason. The charge is recorded as failed under that key; use another one to try again. |
project_inactive | 403 | The store cannot charge because its Vipter subscription is past due. The type is permission_error. |
List a subscription's charges
GET /v1/subscriptions/{id}/charges| Parameter | Type | Content |
|---|---|---|
status | text | pending, succeeded or failed. |
limit, starting_after, ending_before | Pagination. |
curl "https://api.vipter.com/v1/subscriptions/sub_3c7a9e1f5b2d8c4e/charges?status=succeeded" \
-H "Authorization: Bearer vk_live_…"Returns a list object of subscription_charge, newest first, with the charges made through the API and those made in the dashboard.
Retrieve a charge
GET /v1/subscription_charges/{id}curl https://api.vipter.com/v1/subscription_charges/sch_7e2a9c4b1d3f5a6e8b0c2d4f \
-H "Authorization: Bearer vk_live_…"Returns the subscription_charge object. It is the call for following a charge that stayed pending.
The subscription charge object
| Field | Type | Content |
|---|---|---|
id | text | sch_… |
object | text | "subscription_charge" |
status | text | pending (under review at the provider), succeeded (paid) or failed (declined, or the charge request was refused). |
subscription | text | sub_… of the charged subscription. |
customer | text or null | cust_… of the subscriber. |
amount | integer | The total charged, in the minor unit. |
currency | text | The currency, lowercase: always the subscription's. |
description | text or null | The description sent, or the first line's. |
lines | list | The lines sent: description, quantity, unit_amount and amount (unit_amount × quantity). [] when the charge came without lines. |
order | text or null | ord_… of the order the charge generated. null when the provider recorded no order. |
transaction | text or null | The transaction's ID at the payment provider. |
failure_code | text or null | With status failed: the provider's decline code, declined when it reported none, or the error code when the charge request was refused. |
failure_message | text or null | With status failed: the reason, in the provider's words. |
source | text | Where the charge came from: api (this API), dashboard (the dashboard button) or usage (metered usage billing, made by Vipter when a period closes). |
metadata | object | What you sent. {} on charges made in the dashboard. |
livemode | boolean | true in production. |
created | integer | When the charge was requested. |
settled_at | integer or null | When it became succeeded or failed. null while pending. |
Metered usage
Consumption billing with the count on Vipter's side, in the shape of Stripe's Billing Meters: a meter per kind of usage, usage events per customer, a usage item that prices the meter on the subscription, and a period per subscription cycle, which closes and becomes a subscription charge with source: "usage". The guide, with pricing examples and what happens at the end of the cycle, is in Metered usage (Meters). GET calls need the read scope; POST and DELETE, the write scope.
Create a meter
POST /v1/billing/meters| Parameter | Type | Content |
|---|---|---|
display_name | string | Required. The meter's name, 1 to 250 characters. Shown on the buyer's order line. |
event_name | string | Required. The name events use: a-z, 0-9, _, . and - only, up to 100 characters, unique in the store. It does not change afterwards. |
default_aggregation[formula] | string | sum (adds the values, default), count (counts the events, ignores the value) or last (keeps the period's latest value). |
customer_mapping[event_payload_key] | string | The payload key that carries the customer's cust_…. Default customer_id. customer_mapping[type] accepts only by_id. |
value_settings[event_payload_key] | string | The payload key that carries the value. Default value. |
curl -X POST https://api.vipter.com/v1/billing/meters \
-H "Authorization: Bearer vk_live_…" \
-H "Content-Type: application/json" \
-d '{ "display_name": "API calls", "event_name": "api_calls", "default_aggregation": { "formula": "sum" } }'The response is 201 with the billing.meter object. An event_name that already exists in the store gets 400 event_name_taken.
List meters
GET /v1/billing/meters| Parameter | Type | Content |
|---|---|---|
status | string | active or inactive. |
limit | integer | Page size. |
Returns a list object of billing.meter, newest first.
Retrieve and update a meter
GET /v1/billing/meters/{id}
POST /v1/billing/meters/{id}The POST accepts only display_name; the meter's other fields do not change. An ID that does not exist in the store gets 404 resource_missing.
Deactivate a meter
POST /v1/billing/meters/{id}/deactivateNo body. The meter becomes inactive with status_transitions.deactivated_at filled in, and new events with its event_name get 400 meter_inactive. Open periods keep showing the meter's quantity, priced at zero. There is no reactivation through the API.
The billing.meter object
{
"id": "mtr_4f8e2c1a9b7d6e5f3a2b1c0d",
"object": "billing.meter",
"display_name": "API calls",
"event_name": "api_calls",
"default_aggregation": { "formula": "sum" },
"customer_mapping": { "type": "by_id", "event_payload_key": "customer_id" },
"value_settings": { "event_payload_key": "value" },
"status": "active",
"status_transitions": { "deactivated_at": null },
"livemode": true,
"created": 1791100800,
"updated": 1791100800
}| Field | Type | Content |
|---|---|---|
id | string | mtr_… |
object | string | "billing.meter" |
display_name | string | The meter's name. |
event_name | string | The name events use. |
default_aggregation | object | formula: sum, count or last. |
customer_mapping | object | type (by_id) and event_payload_key, the payload key with the customer. |
value_settings | object | event_payload_key, the payload key with the value. |
status | string | active or inactive. |
status_transitions | object | deactivated_at: when the meter was deactivated, or null. |
livemode | boolean | true in production. |
created, updated | integer | Creation and last change. |
Report a usage event
POST /v1/billing/meter_events| Parameter | Type | Content |
|---|---|---|
event_name | string | Required. The meter's event_name. |
payload | object | Required. Up to 20 keys with string, number or boolean values. It must carry the customer under the meter's key (customer_id by default; stripe_customer_id is accepted as an alias) and, except on count meters, the numeric value under the value key (value by default). subscription_id picks the subscription when the customer has more than one priced for the meter. |
identifier | string | Up to 100 characters. Idempotency per meter: the same identifier returns the event already stored, with 200. Without it, Vipter generates one. |
timestamp | integer | When the usage happened, in Unix seconds: up to 35 days in the past and up to 5 minutes ahead. Default: now. |
curl -X POST https://api.vipter.com/v1/billing/meter_events \
-H "Authorization: Bearer vk_live_…" \
-H "Content-Type: application/json" \
-d '{ "event_name": "api_calls", "identifier": "req_01J9X3K7M2", "payload": { "customer_id": "cust_9d2e4f6a8b1c3d5e", "value": 1 } }'The response is 201 with the billing.meter_event object, or 200 with the stored event when the identifier repeats. On arrival, the event is attached to the customer's active, trialing or past_due subscription that has a usage item for the meter (the most recent, if there is more than one). An unattached event is stored with subscription: null, is not charged, and triggers billing.meter.error_report_triggered, at most once per meter and per hour. See Unattached events.
code | HTTP | Meaning |
|---|---|---|
no_meter_found | 400 | No meter has that event_name. |
meter_inactive | 400 | The meter was deactivated. |
invalid_payload | 400 | The customer is missing, or the value is not a number. param names the key (payload.customer_id, payload.value). |
timestamp_out_of_range | 400 | timestamp outside the window of 35 days back to 5 minutes ahead. |
resource_missing | 404 | The payload's customer does not exist in the store. param is the customer key. |
Report events in batch
POST /v1/billing/meter_events/batch| Parameter | Type | Content |
|---|---|---|
events[] | list | Required. 1 to 100 events, each with the fields of Report a usage event. |
curl -X POST https://api.vipter.com/v1/billing/meter_events/batch \
-H "Authorization: Bearer vk_live_…" \
-H "Content-Type: application/json" \
-d '{
"events": [
{ "event_name": "api_calls", "identifier": "req_01J9X3K7M2", "payload": { "customer_id": "cust_9d2e4f6a8b1c3d5e", "value": 1 } },
{ "event_name": "api_calls", "identifier": "req_01J9X3K7M3", "payload": { "customer_id": "cust_0000000000000000", "value": 1 } }
]
}'{
"object": "billing.meter_event_batch",
"accepted": 1,
"duplicates": 0,
"errors": 1,
"results": [
{ "status": "accepted", "event": { "id": "mev_1a5c9e3b7d2f6a8c0e4b2d6f", "object": "billing.meter_event", "event_name": "api_calls", "identifier": "req_01J9X3K7M2", "payload": { "customer_id": "cust_9d2e4f6a8b1c3d5e", "value": 1 }, "customer": "cust_9d2e4f6a8b1c3d5e", "subscription": "sub_3c7a9e1f5b2d8c4e", "value": 1, "timestamp": 1791101000, "livemode": true, "created": 1791101000 } },
{ "status": "error", "error": { "code": "customer_not_found", "message": "No such customer: 'cust_0000000000000000'", "param": "payload.customer_id" } }
]
}Each event is accepted or refused on its own, in the order sent: results[i] answers events[i] with status accepted (stored now), duplicate (the identifier already existed; event is the stored one) or error (error with code, message and param, the same codes as the single call, with customer_not_found in place of resource_missing). The response is 200 whenever at least one event was accepted or repeated, and 400 only when none got in. A batch with an invalid field in the body (such as an empty events) gets 400 parameter_invalid without storing anything.
The billing.meter_event object
| Field | Type | Content |
|---|---|---|
id | string | mev_… |
object | string | "billing.meter_event" |
event_name | string | The meter. |
identifier | string | Your identifier, or the one Vipter generated. |
payload | object | The payload sent. |
customer | string | The customer's cust_…. |
subscription | string or null | The subscription the event will be charged on. null when no active subscription of the customer prices the meter. |
value | number | The event's value. 1 on count meters. |
timestamp | integer | When the usage happened. Decides which period the event falls into. |
livemode | boolean | true in production. |
created | integer | When the event arrived. |
Aggregate usage of a customer
GET /v1/billing/meters/{id}/event_summaries| Parameter | Type | Content |
|---|---|---|
customer | string | Required. The cust_…. |
start_time, end_time | integer | Required. The window, in Unix seconds: start_time inclusive, end_time exclusive. end_time must be later, and the window may span up to one year, otherwise 400 parameter_invalid. |
value_grouping_window | string | hour or day: one summary per hour or per day (in UTC), only for the windows that have events. Without it, a single summary. |
curl "https://api.vipter.com/v1/billing/meters/mtr_4f8e2c1a9b7d6e5f3a2b1c0d/event_summaries?customer=cust_9d2e4f6a8b1c3d5e&start_time=1790186400&end_time=1792778400&value_grouping_window=day" \
-H "Authorization: Bearer vk_live_…"{
"object": "list",
"url": "https://api.vipter.com/v1/billing/meters/mtr_4f8e2c1a9b7d6e5f3a2b1c0d/event_summaries",
"has_more": false,
"data": [
{ "object": "billing.meter_event_summary", "meter": "mtr_4f8e2c1a9b7d6e5f3a2b1c0d", "customer": "cust_9d2e4f6a8b1c3d5e", "start_time": 1790186400, "end_time": 1790208000, "aggregated_value": 412, "livemode": true },
{ "object": "billing.meter_event_summary", "meter": "mtr_4f8e2c1a9b7d6e5f3a2b1c0d", "customer": "cust_9d2e4f6a8b1c3d5e", "start_time": 1790208000, "end_time": 1790294400, "aggregated_value": 1180, "livemode": true }
]
}aggregated_value applies the meter's formula (sum, count or last) to the customer's events in the window, attached to a subscription or not. The list is not paginated.
Usage items of a subscription
GET /v1/subscriptions/{id}/usage_items
POST /v1/subscriptions/{id}/usage_items
DELETE /v1/subscriptions/{id}/usage_items/{itemId}A usage item prices a meter on a subscription: one item per meter. It comes from the offer (configured in the dashboard, on the offer's page) on the first metered-usage run after the subscription is created, within 10 minutes, with source: "offer", or is set here, with source: "api". An item set through the API is never overwritten by the offer's inheritance. A POST for the same meter replaces the item.
| Parameter | Type | Content |
|---|---|---|
meter | string | Required. The mtr_…. A meter that does not exist gets 404 resource_missing. |
currency | string | Optional. Must be the subscription's currency, otherwise 400 currency_mismatch. |
unit_amount | number | Required. Price of one unit, in the currency's minor unit, fractions allowed: 0.4 is R$ 0.004. 0 or more. With tiers, it is not part of the calculation. |
included_units | number | Allowance: units of the period that are not charged. Default 0. |
tiers[] | list | 1 to 20 graduated tiers over the units beyond the allowance, each with up_to (upper bound, inclusive; null on the last one), unit_amount (per unit in the tier) and flat_amount (optional, charged once when the tier is used). up_to must be ascending and the last one null, otherwise 400 parameter_invalid with param tiers. |
rounding | string | up (round up, default) or nearest (closest), applied to the line's total, in whole cents. |
billing_threshold | integer | In cents. The period closes and charges before the end of the cycle when the accumulated total reaches it. |
label | string | Up to 120 characters, for your own records. |
curl -X POST https://api.vipter.com/v1/subscriptions/sub_3c7a9e1f5b2d8c4e/usage_items \
-H "Authorization: Bearer vk_live_…" \
-H "Content-Type: application/json" \
-d '{ "meter": "mtr_4f8e2c1a9b7d6e5f3a2b1c0d", "unit_amount": 0.4, "included_units": 10000, "billing_threshold": 20000 }'The POST answers 201 with the usage_item object; the GET returns a list object of usage_item, without pagination; the DELETE returns { "id": "usi_…", "object": "usage_item", "deleted": true }, or 404 resource_missing if the item does not belong to that subscription.
The usage_item object
{
"id": "usi_9d2e4f6a8b1c3d5e7f0a2b4c",
"object": "usage_item",
"meter": "mtr_4f8e2c1a9b7d6e5f3a2b1c0d",
"subscription": "sub_3c7a9e1f5b2d8c4e",
"offer": null,
"currency": "brl",
"unit_amount": 0.4,
"included_units": 10000,
"tiers": null,
"rounding": "up",
"billing_threshold": 20000,
"label": null,
"source": "api",
"livemode": true,
"created": 1791100900
}| Field | Type | Content |
|---|---|---|
id | string | usi_… |
object | string | "usage_item" |
meter | string | The meter's mtr_…. |
subscription | string | The subscription's sub_…. |
offer | null | Reserved. On a subscription's items it is always null. |
currency | string | The currency, lowercase: the subscription's. |
unit_amount, included_units, tiers, rounding, billing_threshold, label | As sent. tiers and billing_threshold come null when absent. | |
source | string | offer (inherited from the offer), api or dashboard. |
livemode | boolean | true in production. |
created | integer | When the item was created. |
Retrieve the usage periods
GET /v1/subscriptions/{id}/usagecurl https://api.vipter.com/v1/subscriptions/sub_3c7a9e1f5b2d8c4e/usage \
-H "Authorization: Bearer vk_live_…"Returns a list object with the subscription's 12 most recent periods, newest first, without pagination. The open period is computed at call time, from the events; closed ones come as they were at the close. A subscription without usage items returns an empty list.
The usage_period object
{
"id": "usp_7b3e9f1c2a8d4e6f0b2d4f6a",
"object": "usage_period",
"subscription": "sub_3c7a9e1f5b2d8c4e",
"status": "closed",
"close_reason": "period_end",
"period_start": 1790186400,
"period_end": 1792778400,
"currency": "brl",
"lines": [
{ "meter": "mtr_4f8e2c1a9b7d6e5f3a2b1c0d", "event_name": "api_calls", "quantity": 12300, "included": 10000, "billable": 2300, "unit_amount": 0.4, "amount": 920 }
],
"amount_total": 920,
"charge": "sch_7e2a9c4b1d3f5a6e8b0c2d4f",
"computed_at": 1792778700,
"closed_at": 1792778700,
"livemode": true,
"created": 1790187000
}| Field | Type | Content |
|---|---|---|
id | string | usp_… |
object | string | "usage_period" |
subscription | string | The subscription's sub_…. |
status | string | open (accumulating), closing (being charged) or closed. |
close_reason | string or null | period_end (the cycle ended), threshold (the billing_threshold was reached) or subscription_ended (the subscription stopped being chargeable). null while open. |
period_start, period_end | integer | The period's window: the subscription's cycle, or the rest of it after a threshold close. |
currency | string | The currency, lowercase. |
lines | list | One per meter with an item: meter, event_name, quantity (the aggregate), included (the allowance), billable (quantity minus included), unit_amount and amount (in cents). |
amount_total | integer | The sum of the lines, in cents. |
charge | string or null | The sch_… of the period's charge, approved or declined. null while open, when the total was zero, or when the charge was refused before reaching the card. |
computed_at | integer or null | When the lines were computed. On the open period, the time of the call. |
closed_at | integer or null | When the period closed. |
livemode | boolean | true in production. |
created | integer | When the period opened. |
Metered usage errors
Besides the general errors:
code | HTTP | Where | Meaning |
|---|---|---|---|
event_name_taken | 400 | Create a meter | A meter with that event_name already exists. |
no_meter_found | 400 | Events | No meter has that event_name. |
meter_inactive | 400 | Events | The meter was deactivated. |
invalid_payload | 400 | Events | The customer is missing from the payload, or the value is not a number. |
timestamp_out_of_range | 400 | Events | timestamp outside the window of 35 days back to 5 minutes ahead. |
customer_not_found | Batch | Only inside results[]: the customer does not exist. On the single call it is 404 resource_missing. | |
currency_mismatch | 400 | Usage items | currency differs from the subscription's currency. |
parameter_invalid | 400 | Usage items, summaries | tiers out of order or without the last null tier; end_time before start_time or a window longer than one year. |
resource_missing | 404 | All | Meter, subscription, item or customer that does not exist in the store. |
Orders
An order is a charge: a one-off purchase, the first charge of a subscription, a renewal, a one-off charge on a subscription made in the dashboard or through the API. It is what Stripe calls an invoice; the name follows what the store owner sees in the dashboard.
List orders
GET /v1/orders| Parameter | Type | Content |
|---|---|---|
customer | text | Only this customer's orders (cust_…). |
subscription | text | Only this subscription's orders (sub_…). |
status | text | One of pending, pre_authorized, authorized, failed, canceled, refund_pending, partially_refunded, refunded, charged_back. |
limit, starting_after, ending_before | Pagination. |
curl "https://api.vipter.com/v1/orders?subscription=sub_3c7a9e1f5b2d8c4e&limit=1" \
-H "Authorization: Bearer vk_live_…"{
"object": "list",
"url": "https://api.vipter.com/v1/orders",
"has_more": true,
"data": [
{
"id": "ord_7b3e9f1c2a8d4e6f",
"object": "order",
"status": "authorized",
"paid": true,
"status_source": "provider",
"billing_reason": "subscription_cycle",
"customer": "cust_9d2e4f6a8b1c3d5e",
"customer_email": "ana@example.com",
"subscription": "sub_3c7a9e1f5b2d8c4e",
"offer": "ofr_6e2b8d4f1a9c3e7b",
"order_type": "renewal",
"recurrence": "subsequent",
"currency": "brl",
"amount_total": 9900,
"amount_refunded": 0,
"amount_discount": 0,
"amount_shipping": 0,
"amount_interest": 0,
"installments": 1,
"payment_method": "credit_card",
"provider": "pagarme",
"coupon_codes": [],
"lines": [
{
"description": "Plano Pro mensal",
"quantity": 1,
"unit_amount": 9900,
"amount": 9900,
"offer": "ofr_6e2b8d4f1a9c3e7b",
"product": "prd_1a5c9e3b7d2f6a8c",
"kind": "main"
}
],
"shipping": null,
"external_order_id": null,
"checkout_session": null,
"client_reference_id": null,
"metadata": {},
"paid_at": 1790790412,
"livemode": true,
"created": 1790790400
}
]
}Retrieve an order
GET /v1/orders/{id}curl https://api.vipter.com/v1/orders/ord_7b3e9f1c2a8d4e6f \
-H "Authorization: Bearer vk_live_…"Returns the order object.
The order object
| Field | Type | Content |
|---|---|---|
id | text | ord_… |
object | text | "order" |
status | text | See Order status. |
paid | boolean | true when the money came in, even if it was later refunded in whole or in part. Use status for the detail. |
status_source | text | provider when the status came from the payment provider; manual when someone set the status in the dashboard. |
billing_reason | text | Why the order exists: purchase (one-off purchase), subscription_create (first charge of a subscription), subscription_cycle (renewal), manual (one-off charge on a subscription, through the dashboard or the API), usage (metered usage charge, at the close of a period). |
customer, customer_email | text or null | The buyer. |
subscription | text or null | sub_… when the order belongs to a subscription. |
offer | text or null | ofr_… of the order's main offer. |
order_type | text or null | How the order was born: checkout, renewal, api (charge started by the store owner), trial_setup or card_setup (card registration without a charge). May gain new values. |
recurrence | text or null | initial, subsequent or unscheduled on subscription orders; null on one-off purchases. |
currency | text | The order's currency. |
amount_total | integer | The total charged, in the minor unit, with discount, shipping and interest already applied. |
amount_refunded | integer | How much was refunded so far. |
amount_discount | integer | The coupon discount. |
amount_shipping | integer | Shipping, on physical products. |
amount_interest | integer | Installment interest passed on to the buyer. |
installments | integer or null | In how many installments it was paid. |
payment_method | text or null | credit_card, debit_card, pix, boleto or wallet. |
provider | text or null | The provider that processed it, such as pagarme, stripe, mercadopago or asaas. |
coupon_codes | list of text | The coupons applied. |
lines | list | The order's items. See Order lines. |
shipping | object or null | Only on orders with delivery: delivery status, carrier, tracking_code, tracking_url and address, in the same format as the customer's address. |
external_order_id | text or null | An identifier of yours, when the order came with one. On the orders of a subscription charge, the charge's sch_…. |
checkout_session | text or null | cs_… of the checkout session that produced the order. Only on the order paid through the session: the subscription's renewals come with null, and you link them to your system through subscription. |
client_reference_id | text or null | Your identifier, copied from the checkout session's client_reference_id. |
metadata | object | The checkout session's metadata. {} on other orders. |
paid_at | integer or null | When the payment was confirmed. |
livemode | boolean | false when the payment went through a provider's test connection. |
created | integer | When the order was created. |
Order status
status | paid | Meaning |
|---|---|---|
pending | false | Awaiting payment: PIX generated and unpaid, boleto issued, card under review. |
pre_authorized | false | Amount reserved on the card, not yet captured. |
authorized | true | Paid. |
failed | false | The payment was refused or expired. |
canceled | false | Canceled before being paid. |
refund_pending | true | Refund requested and not yet confirmed by the provider. |
partially_refunded | true | Part of the amount was returned. amount_refunded says how much. |
refunded | true | The whole amount was returned. |
charged_back | true | The buyer disputed the charge with their bank. |
Order lines
Each item in lines:
| Field | Type | Content |
|---|---|---|
description | text or null | The item's name as it appeared in the checkout. |
quantity | integer | Quantity. |
unit_amount | integer or null | Unit price, in the minor unit. |
amount | integer or null | unit_amount times quantity. |
offer, product | text or null | The item's ofr_… and prd_…. |
kind | text | The item's role in the order: main (the main item), bump (an add-on offer ticked at checkout), composition (a line the team composed on a quick link) or charge (a line of a subscription charge). |
Offers
An offer is what the buyer can pay for: a product with a price, a billing cycle and conditions. It is the equivalent of Stripe's price. Each offer has a ready checkout link.
List offers
GET /v1/offers| Parameter | Type | Content |
|---|---|---|
product | text | Only this product's offers (prd_…). |
active | true or false | Only active offers, or only the ones that are not active. |
type | text | one_time or recurring. |
limit, starting_after, ending_before | Pagination. Offers come in alphabetical order by name. |
curl "https://api.vipter.com/v1/offers?active=true&type=recurring" \
-H "Authorization: Bearer vk_live_…"{
"object": "list",
"url": "https://api.vipter.com/v1/offers",
"has_more": false,
"data": [
{
"id": "ofr_6e2b8d4f1a9c3e7b",
"object": "offer",
"name": "Plano Pro mensal",
"slug": "pro-mensal",
"product": { "id": "prd_1a5c9e3b7d2f6a8c", "name": "Plano Pro" },
"type": "recurring",
"billing_cycle": "monthly",
"custom_billing_days": null,
"cycle_limit": null,
"trial_days": 7,
"setup_charge": false,
"status": "active",
"active": true,
"prices": [
{ "id": "prc_2d8f4a6c1e9b3d7f", "currency": "brl", "unit_amount": 9900, "first_charge_amount": null, "default": true },
{ "id": "prc_7a1c3e5b9d2f4a6c", "currency": "usd", "unit_amount": 1900, "first_charge_amount": null, "default": false }
],
"checkout_url": "https://pay.vipter.com/pro-mensal",
"livemode": true,
"created": 1788300000
}
]
}Retrieve an offer
GET /v1/offers/{id}curl https://api.vipter.com/v1/offers/ofr_6e2b8d4f1a9c3e7b \
-H "Authorization: Bearer vk_live_…"Returns the offer object.
The offer object
| Field | Type | Content |
|---|---|---|
id | text | ofr_… |
object | text | "offer" |
name | text | The offer's name. |
slug | text or null | The checkout link's slug, when the offer has one. |
product | object | The product's id (prd_…) and name. |
type | text | one_time (one-off purchase) or recurring (subscription). |
billing_cycle | text or null | The billing interval on recurring offers: daily, biweekly, monthly, quarterly, half_yearly, yearly or custom. null on one-off ones. |
custom_billing_days | integer or null | With billing_cycle custom, the interval in days. |
cycle_limit | integer or null | How many charges the subscription makes in total. null means no limit. |
trial_days | integer or null | Free trial days. null when the offer has no trial. |
setup_charge | boolean | true when the first charge has a different amount from the rest (first_charge_amount in prices). |
status | text | The status in the catalog, such as active or inactive. |
active | boolean | true when status is active. Only active offers accept purchases. |
prices | list | One price per currency: id, currency, unit_amount, first_charge_amount (the first charge's amount when it differs, else null) and default (the currency the checkout uses when the buyer does not choose). |
checkout_url | text or null | The offer's checkout link, on the store's custom domain when there is one. null when the link is turned off in the offer's settings. Accepts the URL parameters. |
livemode | boolean | true in production. |
created | integer or null | When the offer was created. |
Products
A product groups offers: "Pro plan" is the product, "Pro plan monthly" and "Pro plan yearly" are its offers.
List products
GET /v1/products| Parameter | Type | Content |
|---|---|---|
active | true or false | Only active products, or only the ones that are not active. |
limit, starting_after, ending_before | Pagination. |
curl "https://api.vipter.com/v1/products?active=true" \
-H "Authorization: Bearer vk_live_…"{
"object": "list",
"url": "https://api.vipter.com/v1/products",
"has_more": false,
"data": [
{
"id": "prd_1a5c9e3b7d2f6a8c",
"object": "product",
"name": "Plano Pro",
"description": "Acesso completo à plataforma.",
"type": "digital",
"status": "active",
"active": true,
"product_family": null,
"metadata": {},
"livemode": true,
"created": 1788200000
}
]
}Retrieve a product
GET /v1/products/{id}curl https://api.vipter.com/v1/products/prd_1a5c9e3b7d2f6a8c \
-H "Authorization: Bearer vk_live_…"Returns the product object.
The product object
| Field | Type | Content |
|---|---|---|
id | text | prd_… |
object | text | "product" |
name | text | The product's name. |
description | text or null | Description. |
type | text or null | The product type, such as digital or physical. May gain new values. |
status | text | The status in the catalog, such as active or inactive. |
active | boolean | true when the product is active and was not deleted. |
product_family | text or null | The product family's ID, when the product belongs to one. |
metadata | object | Free data stored on the product. |
livemode | boolean | true in production. |
created | integer or null | When the product was created. |
Checkout sessions
A checkout session is a checkout opened by your system for one offer, with the buyer, your reference and the destination after payment already set. The response carries the url where you send the buyer. When they pay, the session starts pointing to customer, order and subscription, and the checkout.session.completed event goes out in the 2026-11-01 catalog. It is the equivalent of Stripe's Checkout Session. The step by step with code is in SaaS: from sign-up to dashboard.
The session fixes the offer, the pack, the currency and the coupon: the buyer cannot change any of them on the page. With customer or customer_email, the email field comes filled in and locked. Creating the session creates nothing at the payment provider; that happens when the buyer fills in the form, as on a regular checkout link.
Create a session
POST /v1/checkout/sessionsRequires the write scope. Send an Idempotency-Key so you can repeat the call after a network error without creating two sessions.
| Parameter | Type | Content |
|---|---|---|
offer | text | Required, unless line_items is sent. The offer: ofr_… or the checkout link's slug. An offer that does not exist or belongs to another store gets 404 resource_missing with param offer. |
line_items[0][price], line_items[0][quantity] | text, integer | Alias in Stripe's format: price is the offer and quantity is the pack. One item only. When offer or pack is also sent, they take precedence. |
pack | integer | The pack, in units (1 to 999). The offer must have a pack with that quantity, otherwise 400 pack_unavailable. |
currency | text | The session's currency, ISO 4217 (brl, usd). It must exist in the offer's prices, otherwise 400 currency_unsupported, with the available currencies in message. Without it, the checkout opens in the offer's default currency and the buyer can switch, as on a regular link. |
customer | text | cust_… of a store customer. Their email comes filled in and locked; name, phone and document come filled in. An ID that does not exist gets 404 resource_missing with param customer. |
customer_email | text | The buyer's email, when they are not a customer yet. The field comes filled in and locked: the checkout only accepts paying with that email. Stored in lowercase. |
customer_name | text | Name to fill in the form, 2 to 120 characters. The buyer can change it. |
client_reference_id | text | Your identifier, up to 200 characters: the user's ID in your system. It is copied to the order and to the subscription, and filters the session list. |
metadata | object | Up to 50 keys; keys up to 40 characters, values up to 500. Numbers and booleans are stored as text; null removes the key. It stays on the session and is copied to the order. |
subscription_data[metadata] | object | The metadata of the subscription the session creates, on recurring offers. Without it, the subscription receives the session's metadata. Same limits. |
discounts[0][coupon] | text | The code of an active coupon of the store, case-insensitive. It comes applied at checkout. A non-existent or inactive coupon gets 400 coupon_invalid. One coupon only. |
success_url | text | Where the buyer goes after paying. https://, up to 2000 characters (http://localhost is accepted in development). The text {CHECKOUT_SESSION_ID} is replaced with the session's id. See After the payment. |
cancel_url | text | Becomes the back link at the top of the checkout. It is also where the buyer goes if they open the session after it expired. Same format rules. |
redirect_delay | integer | Seconds the Vipter thank-you page stays on screen before going to success_url: 0 to 30, default 5. |
expires_at | integer | When the session expires, in Unix seconds: between 30 minutes and 24 hours from now, otherwise 400 parameter_invalid. Default: 24 hours. |
locale | text | The buyer's language, stored on the session and returned in the object: en, pt, es, fr, de, it, ja, ko, ru or zh. |
mode | text | payment or subscription. Optional: Vipter derives it from the offer's type and returns it in the object. A value different from the offer's type gets 400 mode_mismatch. |
allow_promotion_codes | boolean | Accepted for Stripe compatibility and ignored for now. |
curl -X POST https://api.vipter.com/v1/checkout/sessions \
-H "Authorization: Bearer vk_live_…" \
-H "Idempotency-Key: 9c1f0a52-7e4b-4d3a-9b8e-2f6c1d0a7e45" \
-H "Content-Type: application/json" \
-d '{
"offer": "ofr_6e2b8d4f1a9c3e7b",
"customer_email": "ana@example.com",
"client_reference_id": "user_8213",
"metadata": { "plan": "pro" },
"success_url": "https://app.example.com/billing/success?session_id={CHECKOUT_SESSION_ID}",
"cancel_url": "https://app.example.com/billing"
}'The response is 201 with the checkout.session object:
{
"id": "cs_4b7e2d9a1c3f5e8b0d2a6c4e1f3b",
"object": "checkout.session",
"status": "open",
"payment_status": "unpaid",
"url": "https://pay.vipter.com/c/cs_4b7e2d9a1c3f5e8b0d2a6c4e1f3b",
"mode": "subscription",
"offer": { "id": "ofr_6e2b8d4f1a9c3e7b", "name": "Plano Pro mensal" },
"pack": null,
"currency": null,
"amount_total": 9900,
"customer": null,
"customer_email": "ana@example.com",
"customer_name": null,
"client_reference_id": "user_8213",
"metadata": { "plan": "pro" },
"subscription_data": { "metadata": {} },
"discounts": [],
"success_url": "https://app.example.com/billing/success?session_id={CHECKOUT_SESSION_ID}",
"cancel_url": "https://app.example.com/billing",
"redirect_delay": 5,
"locale": null,
"order": null,
"subscription": null,
"expires_at": 1790272800,
"completed_at": null,
"livemode": true,
"created": 1790186400
}Send the buyer to url. The address lives at https://pay.vipter.com/c/{id} or, when the store has an active custom domain, on that domain.
Errors of this call, besides the general errors:
code | HTTP | Meaning |
|---|---|---|
resource_missing | 404 | The offer (param offer) or the customer (param customer) does not exist in the store. |
offer_unavailable | 400 | The offer exists but cannot be sold right now: checkout link turned off, archived offer, no price, or the store is not in a condition to sell. message says why. |
pack_unavailable | 400 | The offer has no pack with the quantity requested in pack. |
currency_unsupported | 400 | The offer has no price in the requested currency. |
mode_mismatch | 400 | mode does not match the offer's type. |
coupon_invalid | 400 | The coupon does not exist or is inactive. param is discounts[0].coupon. |
selling_blocked | 403 | The store cannot sell because its Vipter subscription is past due. The type is permission_error. |
Retrieve a session
GET /v1/checkout/sessions/{id}curl https://api.vipter.com/v1/checkout/sessions/cs_4b7e2d9a1c3f5e8b0d2a6c4e1f3b \
-H "Authorization: Bearer vk_live_…"Returns the checkout.session object. It is the call your success page makes with the id that arrived in the success_url: check status and payment_status instead of trusting the redirect alone. After payment, customer, order and, on recurring offers, subscription come filled in. subscription may arrive a few seconds after order, when the provider's notice is processed; if it still comes null, query again or wait for the webhook.
List sessions
GET /v1/checkout/sessions| Parameter | Type | Content |
|---|---|---|
customer | text | Only this customer's sessions (cust_…), including the ones that gained the customer when paid. |
client_reference_id | text | Only sessions created with this client_reference_id, exact match. |
status | text | open, complete or expired. |
payment_status | text | unpaid, paid or pending. |
limit, starting_after, ending_before | Pagination. |
curl "https://api.vipter.com/v1/checkout/sessions?client_reference_id=user_8213&status=complete" \
-H "Authorization: Bearer vk_live_…"Returns a list object of checkout.session, newest first.
Expire a session
POST /v1/checkout/sessions/{id}/expireCloses an open session before its deadline: a buyer who opens the url afterwards goes to cancel_url, or sees an unavailable-link page. Useful when the user gave up in your system or chose another plan. Requires the write scope.
curl -X POST https://api.vipter.com/v1/checkout/sessions/cs_4b7e2d9a1c3f5e8b0d2a6c4e1f3b/expire \
-H "Authorization: Bearer vk_live_…"Returns the object with status: "expired" and generates the checkout.session.expired event. A session that is already complete or expired gets 400 checkout_session_not_open, with the current status in message.
The checkout.session object
| Field | Type | Content |
|---|---|---|
id | text | cs_… |
object | text | "checkout.session" |
status | text | open, complete or expired. See Session status. |
payment_status | text | unpaid, paid or pending. |
url | text or null | The checkout address. Only while status is open; afterwards it comes null. |
mode | text or null | payment on a one-off offer, subscription on a recurring one. |
offer | object | The offer's id (ofr_…) and name. |
pack | integer or null | The fixed pack, in units. |
currency | text or null | The currency fixed at creation. null when the session let the buyer choose. |
amount_total | integer or null | The offer's price in the session's currency (or the default currency), in the minor unit: the first charge's amount, when it differs. It comes before coupon, shipping and add-ons, and null on sessions with a pack. The amount actually charged is in the order's amount_total. |
customer | text or null | cust_…: the one you passed, or the customer created when the buyer paid. |
customer_email, customer_name | text or null | What you passed; with customer, the customer's email and name. |
client_reference_id | text or null | Your identifier. |
metadata | object | What you sent. |
subscription_data | object | metadata: what you sent in subscription_data[metadata], or {}. |
discounts | list | [{ "coupon": "CODE" }] when the session has a coupon; otherwise []. |
success_url, cancel_url | text or null | As you sent them, with {CHECKOUT_SESSION_ID} still to be replaced. |
redirect_delay | integer | Seconds before the redirect, 0 to 30. |
locale | text or null | The language sent. |
order | text or null | ord_… of the order the session produced. Filled in when the buyer pays or generates a PIX. |
subscription | text or null | sub_… of the subscription created, on recurring offers. |
expires_at | integer | When the session expires. |
completed_at | integer or null | When the session became complete. |
livemode | boolean | true in production. |
created | integer | When the session was created. |
Session status
status | payment_status | Meaning |
|---|---|---|
open | unpaid | The buyer has not paid yet. A declined card leaves the session open: they can try again on the same page. |
complete | paid | Paid: approved card, or paid PIX. order is filled in. |
complete | pending | The buyer generated a PIX, or the card went under review, and the money has not arrived yet. order is filled in with status: "pending". The session stays like this until the payment is confirmed (paid) or fails (unpaid). |
complete | unpaid | The pending payment did not happen: the PIX expired or the charge was refused after review. The session does not reopen; create another one. |
expired | unpaid | The deadline passed without payment, or someone called POST …/expire. |
Each change generates an event of the 2026-11-01 catalog: checkout.session.completed when the session becomes complete (with paid or pending), checkout.session.async_payment_succeeded when a pending payment is confirmed, checkout.session.async_payment_failed when it fails, and checkout.session.expired. A session completes only once.
After the payment
- The buyer pays on the Vipter page and sees the store's thank-you page.
- Once the payment is confirmed, the page counts
redirect_delayseconds and goes tosuccess_url, with{CHECKOUT_SESSION_ID}replaced with the session'sid. While a PIX is unpaid, the page keeps waiting and does not redirect. - The session's
success_urltakes precedence over the success URL of the quick link, the offer's and the store's default. Withoutsuccess_url, the next one on the list applies.
When the product has downloadable files, the page does not redirect on its own: it shows the files and a button to continue to the success_url.
A buyer who opens the url of an already paid session is taken to the order's thank-you page. An expired session leads to cancel_url or, without it, to an unavailable-link page. Overdue sessions are marked expired by a periodic routine, and also right away if someone opens the link.
Customer portal
Create a portal session
POST /v1/billing_portal/sessionsGenerates a direct sign-in link to the store's customer portal for one customer, without the e-mail code step. It serves the "manage subscription" button of your system: the customer changes the card, cancels or sees the charges in the Vipter portal. Requires the write scope.
| Parameter | Type | Content |
|---|---|---|
customer | text | Required. The customer's cust_…. |
return_url | text | Accepted for Stripe compatibility and returned in the object. The portal does not have a button back to it yet. |
curl -X POST https://api.vipter.com/v1/billing_portal/sessions \
-H "Authorization: Bearer vk_live_…" \
-H "Content-Type: application/json" \
-d '{ "customer": "cust_9d2e4f6a8b1c3d5e" }'{
"id": "bps_3e9a1c7f5b2d4e6a8c0b",
"object": "billing_portal.session",
"url": "https://portal.vipter.com/loja-demo/verify?token=EXAMPLE-TOKEN",
"customer": "cust_9d2e4f6a8b1c3d5e",
"return_url": null,
"expires_at": 1790187300,
"livemode": true,
"created": 1790186400
}| Field | Type | Content |
|---|---|---|
id | text | bps_…. The session cannot be retrieved later. |
object | text | "billing_portal.session" |
url | text | The sign-in link. It is valid for 15 minutes and serves one sign-in: when opened, it creates the portal session in the customer's browser and stops working. Generate a new link on every click, at the time of the click, and redirect the customer to it; do not store it or send it by e-mail. |
customer | text | The customer. |
return_url | text or null | What you sent. |
expires_at | integer | When the link stops being valid. |
livemode | boolean | true in production. |
created | integer | When the session was created. |
The link opens at the store's portal address: the custom domain, when there is an active one, or portal.vipter.com/{slug}.
code | HTTP | Meaning |
|---|---|---|
resource_missing | 404 | The customer does not exist in the store. |
portal_disabled | 400 | The customer portal is turned off in the store's settings. See Customer portal. |
portal_unavailable | 400 | The store has no portal address yet: it needs a slug or an active custom domain. |
Events
Every event Vipter generates is stored and can be queried through the API, in both catalogs: to check what your endpoint received, to recover what it missed while it was disabled, and to request a resend in code. The body of each event is the same envelope that reaches the endpoint, and api_version says which catalog it belongs to. Test events are not listed.
List events
GET /v1/events| Parameter | Type | Content |
|---|---|---|
type | string | An exact type, such as invoice.paid, or a pattern with *, such as invoice.* or customer.subscription.*. |
created[gte], created[gt], created[lte], created[lt] | integer | Only events created at or after, after, at or before, or before that moment, in Unix seconds. |
limit, starting_after, ending_before | Pagination. The cursor is an event id. |
curl "https://api.vipter.com/v1/events?type=invoice.*&created[gte]=1790726400" \
-H "Authorization: Bearer vk_live_…"{
"object": "list",
"url": "https://api.vipter.com/v1/events",
"has_more": false,
"data": [
{
"id": "evt_5a7c9e1b3d5f7a9c1e3b5d7f",
"object": "event",
"type": "invoice.paid",
"api_version": "2026-11-01",
"created": 1790790413,
"livemode": true,
"pending_webhooks": 0,
"request": { "id": null, "idempotency_key": null },
"data": {
"object": { "id": "ord_7b3e9f1c2a8d4e6f", "object": "order", "status": "authorized", "paid": true, "…": "…" }
}
}
]
}The list comes newest first and merges both catalogs: the same fact appears twice when the store has endpoints on both versions, once under each name. An event of version 2026-09-01 comes without pending_webhooks and request, as in that version's envelope.
Retrieve an event
GET /v1/events/{id}curl https://api.vipter.com/v1/events/evt_5a7c9e1b3d5f7a9c1e3b5d7f \
-H "Authorization: Bearer vk_live_…"Returns the event object, the same JSON the endpoint received in the POST. In version 2026-11-01, pending_webhooks is computed at query time: how many deliveries of the event have not succeeded yet. A test event can be retrieved by the id the test call returns. An id that does not exist in the store gets 404 resource_missing.
Resend an event
POST /v1/events/{id}/resendQueues the event for delivery again, with the same id and the same created, and sends it right away. Requires the write scope.
| Parameter | Type | Content |
|---|---|---|
webhook_endpoint | string | The id of an endpoint. With it, only that endpoint receives the event: its delivery goes back to pending with the attempts reset, or is created if the endpoint never had one, such as an endpoint created after the event or disabled when it went out. Without it, every delivery the event already has is resent, including the ones that had succeeded. |
curl -X POST https://api.vipter.com/v1/events/evt_5a7c9e1b3d5f7a9c1e3b5d7f/resend \
-H "Authorization: Bearer vk_live_…" \
-H "Content-Type: application/json" \
-d '{ "webhook_endpoint": "0b7e2c4a-9d1f-4e3b-8a6c-5f2d1e0c9b8a" }'{
"object": "event_resend",
"event": "evt_5a7c9e1b3d5f7a9c1e3b5d7f",
"webhook_endpoint": "0b7e2c4a-9d1f-4e3b-8a6c-5f2d1e0c9b8a",
"deliveries": 1
}deliveries is how many deliveries were queued. Without webhook_endpoint, it is the number of endpoints that already had a delivery of the event; 0 when none had. The endpoint must receive the event's catalog: an invoice.paid event (2026-11-01) cannot be sent to a 2026-09-01 endpoint. Resending to a disabled endpoint closes the delivery with endpoint disabled; enable it first. If the delivery fails, it follows the retry schedule from the start. Your idempotency check treats the resend as a repeat.
code | HTTP | Meaning |
|---|---|---|
resource_missing | 404 | The event, or the endpoint in webhook_endpoint, does not exist in the store. |
invalid_event_type | 400 | The endpoint receives another catalog. The message says which one the event belongs to and which one the endpoint receives. |
Webhook endpoints
The same endpoints as the GeneralIntegrationsAutomationsWebhooks page of the dashboard, created and managed in code. An endpoint created through the API shows up in the dashboard like any other, and the rules are those of Receive events in your system: an https:// URL, one catalog per endpoint, retries and automatic disabling. An endpoint's id is a UUID; treat it as an opaque string.
List endpoints
GET /v1/webhook_endpointscurl https://api.vipter.com/v1/webhook_endpoints \
-H "Authorization: Bearer vk_live_…"Returns a list object with every endpoint of the store, oldest first, with no filters. The secret is not in the list.
Create an endpoint
POST /v1/webhook_endpointsRequires the write scope. The response is 201 and carries the secret (whsec_…) only once: store it on your server to verify the signature. Afterwards, only rotating the secret produces another.
| Parameter | Type | Content |
|---|---|---|
url | string | Required. The https:// address of your server, up to 2000 characters, on a public host. A host on the internal network (localhost, 10.x, 192.168.x, .internal or .local names) gets 400 private_host; an address without https:// gets 400 invalid_url. |
description | string | A reminder for the team, up to 200 characters. |
enabled_events | list | The types the endpoint receives, up to 100: names from the version's catalog, such as invoice.paid, or patterns with *, such as customer.subscription.*. Omitted, empty or ["*"], the endpoint receives every event of the catalog, including the ones created in the future. A name that does not exist in the version gets 400 invalid_event_type. |
api_version | string | The catalog the endpoint receives: 2026-11-01 (default) or 2026-09-01. It does not change after creation. |
curl -X POST https://api.vipter.com/v1/webhook_endpoints \
-H "Authorization: Bearer vk_live_…" \
-H "Content-Type: application/json" \
-d '{
"url": "https://app.example.com/webhooks/vipter",
"description": "SaaS billing",
"enabled_events": ["checkout.session.*", "customer.subscription.*", "invoice.paid"]
}'{
"id": "0b7e2c4a-9d1f-4e3b-8a6c-5f2d1e0c9b8a",
"object": "webhook_endpoint",
"url": "https://app.example.com/webhooks/vipter",
"description": "SaaS billing",
"enabled_events": ["checkout.session.*", "customer.subscription.*", "invoice.paid"],
"api_version": "2026-11-01",
"status": "enabled",
"disabled_reason": null,
"consecutive_failures": 0,
"created_via": "api",
"secret": "whsec_EXAMPLE0000000000000000000000000000",
"livemode": true,
"created": 1790186400
}The endpoint is born enabled and starts receiving the events created from then on; earlier events do not reach it, except through a resend with webhook_endpoint.
Retrieve an endpoint
GET /v1/webhook_endpoints/{id}curl https://api.vipter.com/v1/webhook_endpoints/0b7e2c4a-9d1f-4e3b-8a6c-5f2d1e0c9b8a \
-H "Authorization: Bearer vk_live_…"Returns the webhook_endpoint object, without secret. An id that does not exist in the store gets 404 resource_missing.
Update an endpoint
POST /v1/webhook_endpoints/{id}Requires the write scope. Only the fields sent change.
| Parameter | Type | Content |
|---|---|---|
url | string | The new address, with the same rules as on creation. The following deliveries already go to it. |
description | string or null | null clears the description. |
enabled_events | list | Replaces the whole list. ["*"] goes back to receiving every event of the catalog. In the dashboard this list cannot be changed; through the API, it can. |
status | string | enabled or disabled. It is the dashboard's Active switch: enabled resets consecutive_failures and clears disabled_reason, which turns an automatically disabled endpoint back on. While it is disabled, new events are not stored for it. |
An api_version in the body is ignored: the version does not change. To move to the other catalog, create a new endpoint and remove the old one.
curl -X POST https://api.vipter.com/v1/webhook_endpoints/0b7e2c4a-9d1f-4e3b-8a6c-5f2d1e0c9b8a \
-H "Authorization: Bearer vk_live_…" \
-H "Content-Type: application/json" \
-d '{ "enabled_events": ["*"], "status": "enabled" }'Returns the updated webhook_endpoint object.
Delete an endpoint
DELETE /v1/webhook_endpoints/{id}Deletes the endpoint, its secret and its delivery history; pending deliveries stop. Requires the write scope.
{ "id": "0b7e2c4a-9d1f-4e3b-8a6c-5f2d1e0c9b8a", "object": "webhook_endpoint", "deleted": true }Rotate the secret
POST /v1/webhook_endpoints/{id}/rotate_secretGenerates a new secret and returns the webhook_endpoint object with it in secret, only once. The previous secret keeps signing for 24 hours: during that period, each delivery carries two v1= values in the Vipter-Signature header, and your server can switch the environment variable without losing events. See Rotate the secret. Requires the write scope.
curl -X POST https://api.vipter.com/v1/webhook_endpoints/0b7e2c4a-9d1f-4e3b-8a6c-5f2d1e0c9b8a/rotate_secret \
-H "Authorization: Bearer vk_live_…"Send a test event
POST /v1/webhook_endpoints/{id}/testCreates a test customer.created event and delivers it to this endpoint only, in its catalog: a customer object in the format of the endpoint's version, with an extra "test": true and livemode: false. It is the same as the Send test event button on the endpoint's card in the dashboard. The event does not go through automations, e-mails, invoices, member areas or pixels, does not reach the other endpoints and does not appear in GET /v1/events. The format is in the catalog. Requires the write scope.
curl -X POST https://api.vipter.com/v1/webhook_endpoints/0b7e2c4a-9d1f-4e3b-8a6c-5f2d1e0c9b8a/test \
-H "Authorization: Bearer vk_live_…"{
"object": "webhook_test",
"webhook_endpoint": "0b7e2c4a-9d1f-4e3b-8a6c-5f2d1e0c9b8a",
"event": "evt_1c3e5a7b9d0f2e4c6a8b0d2f"
}The delivery goes out right away. Follow it in GET /v1/webhook_endpoints/{id}/deliveries or under Recent deliveries in the dashboard. If your server fails, it follows the retry schedule of a real event and counts toward automatic disabling. With the endpoint disabled, the delivery ends right away with endpoint disabled.
List an endpoint's deliveries
GET /v1/webhook_endpoints/{id}/deliveries| Parameter | Type | Content |
|---|---|---|
status | string | pending, delivering, succeeded, failed or exhausted. |
limit, starting_after, ending_before | Pagination. The cursor is a delivery id. |
curl "https://api.vipter.com/v1/webhook_endpoints/0b7e2c4a-9d1f-4e3b-8a6c-5f2d1e0c9b8a/deliveries?status=failed" \
-H "Authorization: Bearer vk_live_…"{
"object": "list",
"url": "https://api.vipter.com/v1/webhook_endpoints/0b7e2c4a-9d1f-4e3b-8a6c-5f2d1e0c9b8a/deliveries",
"has_more": false,
"data": [
{
"id": "6f1d2c3b-4a5e-4f6a-9b8c-7d6e5f4a3b2c",
"object": "webhook_delivery",
"event": "evt_5a7c9e1b3d5f7a9c1e3b5d7f",
"webhook_endpoint": "0b7e2c4a-9d1f-4e3b-8a6c-5f2d1e0c9b8a",
"status": "failed",
"attempts": 2,
"next_attempt_at": 1790790780,
"last_attempt_at": 1790790480,
"delivered_at": null,
"last_response_status": 500,
"last_error": "HTTP 500",
"last_duration_ms": 184,
"created": 1790790413
}
]
}Returns a list object of webhook_delivery, newest first: one delivery per event the endpoint received. What each status means is in Retries, disabling and resending.
The endpoint object
| Field | Type | Content |
|---|---|---|
id | string | The endpoint's UUID. |
object | string | "webhook_endpoint" |
url | string | The address that receives the events. |
description | string or null | The description. |
enabled_events | list | The types and patterns the endpoint receives. ["*"] when it receives every event of the catalog. |
api_version | string | 2026-11-01 or 2026-09-01: the catalog the endpoint receives. |
status | string | enabled or disabled. |
disabled_reason | string or null | The reason, when Vipter disabled the endpoint on its own: auto-disabled after 20 exhausted deliveries. null otherwise, including when the team turned the switch off. |
consecutive_failures | integer | Consecutive deliveries that exhausted their attempts. At 20, the endpoint is disabled; a succeeded delivery resets it. |
created_via | string | dashboard or api: where the endpoint was created. |
secret | string | The signing secret, whsec_…. Only in the response of create and rotate the secret. |
livemode | boolean | true in production. |
created | integer | When the endpoint was created. |
The delivery object
| Field | Type | Content |
|---|---|---|
id | string | The delivery's UUID. |
object | string | "webhook_delivery" |
event | string | evt_… of the delivered event. Retrieve it with GET /v1/events/{id}. |
webhook_endpoint | string | The endpoint. |
status | string | pending (queued), delivering (being sent), succeeded (your server answered 2xx), failed (the last attempt failed and another is scheduled) or exhausted (the attempts ran out, or the endpoint was disabled). |
attempts | integer | How many attempts were made, out of up to 8. Goes back to 0 on a resend. |
next_attempt_at | integer or null | When the next attempt is scheduled. Only with status pending or failed. |
last_attempt_at | integer or null | When the last attempt was. |
delivered_at | integer or null | When your server answered 2xx. |
last_response_status | integer or null | The HTTP code of the last response. null when there was no response. |
last_error | string or null | The error of the last attempt: HTTP 500, timeout, endpoint disabled or the network message. null when it succeeded. |
last_duration_ms | integer or null | How long the last attempt took, in milliseconds. |
created | integer | When the delivery was created. |
Coming soon
What does not exist in the API yet: a client_reference_id filter on GET /v1/subscriptions and on GET /v1/customers. To find one of your users' subscription, use the subscription field of the checkout session that created it, or store the sub_… that arrives in the webhook. Metered usage, previously listed here, is live: see Metered usage.
What to do next
- Follow the guide SaaS: from sign-up to dashboard, with code to create the session, receive the webhook and bill usage at the end of the month.
- Read the API conventions before writing the client: errors, pagination, limits.
- To bill by consumption without keeping the count on your side, follow Metered usage (Meters).
- To know about a sale right away, instead of polling the API, receive webhooks. Create the endpoint in code under Webhook endpoints and check what arrived under Events.
API conventions
The base URL, the version header, the request and response format, how money, dates and IDs are represented, the error table, idempotency, pagination, rate limits, the Request-Id header and what differs from Stripe.
SaaS: from sign-up to dashboard
A guide with code to charge a user of your SaaS through Vipter: create the checkout session with the user's ID, redirect, confirm the payment through the API or the checkout.session.completed webhook, handle PIX, idempotency, bill the month's usage on the saved card, cancel and change plans, what to store and how to coexist with Stripe.