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.
The Vipter API follows the conventions of Stripe's API wherever there is an equivalent: field names, error format, cursor pagination, idempotency and date-based versions. Anyone who has integrated Stripe will recognize everything. This page gathers what applies to every endpoint; each endpoint is in the reference.
Base URL
https://api.vipter.com/v1The same service answers at https://app.vipter.com/api/v1, with the same paths. Use api.vipter.com in new integrations; the other address exists for networks where only the dashboard's domain is allowed.
Every call is HTTPS. A URL outside /v1 or a path that does not exist gets 404 resource_missing, in the same format as the other errors.
Version
The API has date-based versions. The current version is 2026-11-01, the only one that exists today.
Each API key is born pinned to the current version on the day it was created, and requests with it use that version without saying anything. To ask for another version on a call, send the header:
Vipter-Version: 2026-11-01An unknown version gets 400 invalid_api_version, with the list of accepted versions in the message. Every authenticated response carries the Vipter-Version header with the version used.
Within a version, the API only changes in compatible ways: new fields may appear in any object at any time, and new values may appear in text fields such as status and payment_method. Write your code to ignore fields it does not know. An incompatible change, such as renaming or removing a field, becomes a new version, and existing keys stay on the old one.
Request format
Query parameters go in the query string. Request bodies, on the endpoints that take one, may be JSON or a form:
Content-Type | Example |
|---|---|
application/json | {"metadata": {"plan": "pro"}, "tags": ["a", "b"]} |
application/x-www-form-urlencoded | metadata[plan]=pro&tags[]=a&tags[]=b |
The form format uses Stripe's bracket notation: a[b]=1 becomes an object, a[0]=x&a[1]=y or a[]=x&a[]=y become a list. A curl copied from Stripe's documentation works as is. In a form every value arrives as text; the API converts numbers and booleans where it expects them.
The query string accepts the same bracket notation. A body with another Content-Type, or JSON that is not an object, gets 400 parameter_invalid.
Response format
Every response is JSON in UTF-8, with Cache-Control: no-store. Retrieving an object returns the object; a listing returns a list object (see Pagination); an error returns an error object (see Errors).
Headers present on every response:
| Header | Content |
|---|---|
Request-Id | Unique identifier of the request, req_ followed by 20 hexadecimal characters. See Request-Id. |
Vipter-Version | The API version used. Only on authenticated responses. |
X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset | The state of your rate limit. |
Retry-After | Only on 429: seconds until you may try again. |
Idempotent-Replayed | Only when the response is the replay of an earlier request with the same Idempotency-Key. See Idempotency. |
Data types
| What | How it comes | Example |
|---|---|---|
| Money | Integer in the currency's minor unit. Never a decimal. | 9900 is R$ 99.00; 1250 is US$ 12.50. |
| Currency | Lowercase ISO 4217 code. | "brl", "usd" |
| Date and time | Integer in Unix seconds, UTC. null when it does not apply. | 1790790000 |
| IDs | Text with a prefix per object type. Treat as opaque: do not depend on the length or the format. | cust_…, sub_…, ord_…, ofr_…, prd_…, cs_…, bps_…, ak_…, req_… |
object | On every object: the type's name. | "customer", "subscription", "order", "offer", "product", "checkout.session", "billing_portal.session", "account", "list" |
livemode | On every object. On orders, false when the payment went through a provider's test connection. On the other objects, true in production. | true |
created | On every object: when it was created, in Unix seconds. | 1790790000 |
| Absence | Field present with null, not an omitted field. Empty lists come as [], empty maps as {}. | "phone": null |
| Country | Uppercase ISO 3166-1 alpha-2 code. | "BR" |
| Documents | Only the type and the last four digits. The API never returns a full CPF or CNPJ (Brazilian tax IDs). | {"type": "cpf", "number_masked": "*******1234"} |
The metadata, client_reference_id and checkout_session fields of orders and subscriptions come filled in when the sale was born from a checkout session created through the API; on other sales, they come {} or null.
Errors
An error is a response with a 4xx or 5xx HTTP status and this body:
{
"error": {
"type": "invalid_request_error",
"code": "parameter_invalid",
"message": "Invalid query parameter 'limit': Number must be less than or equal to 100",
"param": "limit",
"doc_url": "https://docs.vipter.com/pt-br/developers/api-conventions#erros"
}
}| Field | Content |
|---|---|
type | The error family. Decides how your code should react. |
code | The exact reason, stable across versions. Use it to handle specific cases. |
message | English text for whoever is debugging. It may change; do not compare against it. |
param | The parameter or header that caused the error, when there is one. In nested bodies it uses dots and brackets: lines[0].amount. |
doc_url | This section. |
Error types
| HTTP | type | When |
|---|---|---|
| 400 | invalid_request_error | Missing or invalid parameter, malformed body, unknown version. |
| 401 | authentication_error | Key missing, invalid, revoked or expired. |
| 403 | permission_error | Key without the required scope, API switched off for the store, inactive store. |
| 404 | invalid_request_error | The object or the path does not exist. code is resource_missing. |
| 400 or 409 | idempotency_error | Problem with the Idempotency-Key. |
| 429 | rate_limit_error | Rate limit reached. |
| 402 | card_error | Reserved for charge refusals on the payment endpoints, coming soon. |
| 500 | api_error | Failure on Vipter's side. Keep the Request-Id and try again. |
Codes
code | HTTP | Meaning |
|---|---|---|
parameter_missing | 400 | A required parameter was not sent. param says which. |
parameter_invalid | 400 | A parameter came with the wrong value, type or format. param says which. |
invalid_api_version | 400 | The Vipter-Version header has an unknown version. |
api_key_missing, invalid_api_key, api_key_revoked, api_key_expired | 401 | See Authentication errors. |
api_not_enabled, project_inactive, insufficient_scope | 403 | See Authentication errors. |
resource_missing | 404 | There is no object with that ID in the store, or the path does not exist. param is id, or starting_after/ending_before when the cursor does not exist. |
idempotency_key_too_long | 400 | The Idempotency-Key is longer than 255 characters. |
idempotency_key_reused | 400 | The same Idempotency-Key was used with another method, path or body. |
idempotency_key_in_use | 409 | The first request with that Idempotency-Key is still being processed. |
rate_limit | 429 | See Rate limits. |
internal_error | 500 | Failure on Vipter's side. |
An object that exists but belongs to another store is a 404, the same as a nonexistent ID. The codes specific to one endpoint, such as offer_unavailable or portal_disabled, are in the reference, next to the endpoint.
Idempotency
Repeating a request because of a network timeout must not charge twice. For that, every POST and DELETE accepts the Idempotency-Key header, with a unique value you generate, such as a UUID v4:
curl -X POST https://api.vipter.com/v1/… \
-H "Authorization: Bearer vk_live_…" \
-H "Idempotency-Key: 5f0b2c8e-3a1d-4e7f-9b6c-2d4a8e1f0c3b" \
…The rules, the same as Stripe's:
- The key is valid for 24 hours within the store. Up to 255 characters.
- The same key with the same method, path and body returns the stored response from the first time, with the same HTTP status, without executing anything again. The replayed response carries
Idempotent-Replayed: trueandOriginal-Request-Idwith the first request'sRequest-Id. - The same key with a different body gets
400 idempotency_key_reused. - While the first request is still running, a second one with the same key gets
409 idempotency_key_in_use. Wait a moment and repeat with the same key. 4xxresponses are stored and replayed too.5xxresponses are not: you may retry with the same key.
GET endpoints are idempotent by nature and ignore the header. On today's POST endpoints (checkout sessions, customers, portal sessions) the key is optional and recommended; on the charge endpoints that come next, it will be mandatory.
Pagination
Every listing returns a list object:
{
"object": "list",
"url": "https://api.vipter.com/v1/orders",
"has_more": true,
"data": [
{ "id": "ord_7b3e9f1c2a8d4e6f", "object": "order", "created": 1790790412 },
{ "id": "ord_5c1e8a2b9d4f4e7a", "object": "order", "created": 1790704011 }
]
}Items come newest first. Parameters, the same as Stripe's:
| Parameter | Content |
|---|---|
limit | How many items per page, from 1 to 100. Default 10. |
starting_after | The id of the last item on the current page. Returns the items older than it: the next page. |
ending_before | The id of the first item on the current page. Returns the items newer than it: the previous page. |
Do not send both cursors in the same call (400 parameter_invalid). A cursor that does not exist in the store gets 404 resource_missing, with param saying which. The endpoint's filters, such as customer or status, keep applying with the cursor.
To walk through everything, repeat while has_more is true, passing the last item's id in starting_after:
curl "https://api.vipter.com/v1/orders?limit=100" \
-H "Authorization: Bearer vk_live_…"
# has_more: true, last id: ord_5c1e8a2b9d4f4e7a
curl "https://api.vipter.com/v1/orders?limit=100&starting_after=ord_5c1e8a2b9d4f4e7a" \
-H "Authorization: Bearer vk_live_…"Offers come in alphabetical order by name, not by date, because they are a small catalog. The cursors work the same way.
Rate limits
Each API key may make 100 requests every 2 seconds, which is 50 per second with room for bursts. Every response says where you stand:
| Header | Content |
|---|---|
X-RateLimit-Limit | The window size: 100. |
X-RateLimit-Remaining | How many requests still fit in the current window. |
X-RateLimit-Reset | When the window reopens, in Unix seconds. |
Past the limit, the answer is 429 rate_limit with Retry-After in seconds. Wait that long and repeat. In an integration that sweeps a lot of data, use limit=100 and a small pause between pages instead of parallelizing calls.
Requests without a valid key have a separate limit, per IP address: 10 per minute.
Request-Id
Every response, including errors and 429, carries a unique Request-Id:
Request-Id: req_8c2f4e6a1b3d5f7e9a0cRecord this value in your logs next to the call. When talking to Vipter support about a request, give the Request-Id: with it, support finds the exact call, with its status, duration and error. The same value appears in the dashboard's request logs, which are kept for 30 days.
Differences from Stripe
The API is not a clone: the goal is for a Stripe integration to be adapted by mapping, not for Stripe's SDK to work pointed at Vipter. What differs:
| Topic | Stripe | Vipter |
|---|---|---|
| Key | sk_live_… and sk_test_… | Only vk_live_…. There is no test mode; test with a test connection and use livemode per object. |
| Version | Stripe-Version | Vipter-Version, in the same date format. |
| Request body | Form only | Form or JSON. |
| Invoice | invoice | order, with billing_reason to say whether it is a one-off purchase, a first charge, a renewal, a manual charge or usage. |
| Price | price, one per product and currency | offer, with a prices[] list, one per currency, and a ready checkout_url. |
| Subscription | items[] with several prices | One offer per subscription: offer and product are one object each. |
| Checkout session | line_items[] with several prices | One offer per session: offer (or line_items[0][price]), with pack for the quantity. Coupons go in discounts[0][coupon], as in Stripe. |
| Subscription status | incomplete, unpaid | Do not exist. past_due is the subscription in payment recovery; the details come in past_due_details. |
| Order with manual status | Does not exist | status_source says whether the status came from the provider or was set in the dashboard. |
expand[] | Expands related objects | Does not exist. Related objects come as an id, or as {id, name} when the name helps. |
| Webhooks | Stripe-Signature | Vipter-Signature, with the same algorithm. See Verify the signature. |
What to do next
- See every endpoint and every object in the API reference.
- Create and protect keys in Authentication and API keys.
Authentication and API keys
How to create an API key in the dashboard, the vk_live_… format, the read and write scopes, how to send the key on every request, how to revoke, how to test without a test mode and what each 401 and 403 error means.
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.