Versioning & rate limits¶
Use these contracts to keep integrations stable as the API evolves. This page covers the versioning and deprecation policy, which guarantees backward compatibility within a major version, and the rate limits, which protect tenant isolation and platform stability with a predictable, standards-based 429 contract.
API versioning¶
The API is versioned by major version in the path. Tenant data operations live
under /v1, product configuration and ingestion under /api/v1, and partner
operations under /api/partner/v1. All three roots follow the same
additive-within-a-major lifecycle. The OpenAPI 3.1 document is the single source
of truth for the contract, published alongside a machine-readable changelog and
the SDKs.
The backward-compatibility guarantee¶
Within a major version, the contract only grows; it never breaks. Specifically,
within v1:
- No field is ever removed, renamed, retyped, or narrowed.
- No closed enum loses a member, including the output states, the edge taxonomy, and the error-code catalog.
- No required input becomes more restrictive.
- Confidence semantics are never versioned away:
coperstays capped at 0.95, and the 0.80 chargeback floor stays the single floor.
Additive changes are always allowed: new optional fields, new routes, new optional parameters, and new members on explicitly open enums. Design your clients to tolerate unknown response fields: that is what lets you adopt additive changes without redeploying.
A v1.0 client keeps working
A client written against v1.0 continues to function unchanged against any
later v1.x. This is enforced: a schema-diff gate compares each candidate
contract against the last released document for the same major version and
blocks any breaking change in CI. Only additive diffs ship within a major
version.
The same discipline governs the webhook event catalog: within a
major event-schema version, no event payload field is removed, renamed, retyped,
or narrowed, and no event type is removed. New event types and optional payload
fields are additive.
Breaking changes and deprecation¶
A genuinely breaking change ships as a new major version (for example /v2,
/api/v2, or /api/partner/v2), served in parallel with the current one.
The old version remains available through a deprecation window of at least 12
months, so you migrate on your schedule.
| Stage | What it means |
|---|---|
| Active | The current major version; full support and additive evolution. |
| Deprecated | A newer major version exists; the deprecated version keeps working for ≥12 months. Migration guidance is published in the changelog. |
| Sunset | After the window closes, the version is retired. |
The SDKs track major versions: a breaking API change ships alongside a coordinated new SDK major version, and the changelog records the API-to-SDK version mapping. Client generation is deterministic and pinned, so a given SDK version is reproducible against a given API version.
Deprecated responses carry the Deprecation field defined by RFC 9745. Once
a retirement instant is scheduled they also carry the Sunset field defined by
RFC 8594, plus a Link to the migration guide.
Discover the contract
The OpenAPI document, an interactive reference, and a generated Postman collection are all published from the same route models and versioned with the changelog. Pin your SDK version and watch the changelog for additive updates and deprecation notices.
Rate limits¶
Rate limiting protects tenant isolation and platform stability. Limits are
enforced per (tenant, principal, route-class) using a token bucket: a
sustained rate plus a burst allowance. Bursts within the allowance pass;
sustained traffic above the rate is throttled.
- Tenant isolation. One tenant exhausting its bucket never throttles another.
- Separate buckets for heavy routes. Bulk and export routes carry their own, lower limits, so a large export run does not starve your interactive reads.
- Tenant-specific limits. The plan entitlements on your order form set the
default sustained rate and burst for each route class (
defaultfor interactive calls,bulk, andexport), and Venturi can override them for your tenant without a redeploy. Because the numbers depend on those entitlements and any override, this page publishes no global figures for these route classes; the lookup endpoint below is authoritative for your tenant. - SCIM has its own published default. The
/scim/v2endpoints your identity provider calls sit outside these route classes and carry a published default limit per bearer token; see SCIM. - Fail closed at the edge. A caller with no valid token is rejected with
401before any tenant bucket is evaluated.
Reading your budget¶
Every authenticated response carries the calling credential’s current limit
state in the RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset
headers, whatever scopes the credential holds. To read any credential’s bucket
directly without consuming budget, call GET /api/v1/rate-limit with a
credential that holds the keys:manage scope, which also covers
reading key usage. The query parameters are:
| Parameter | Required | Meaning |
|---|---|---|
principal |
✅ | The credential whose bucket you read: an OAuth client_id or an API key’s ID. |
route_class |
Optional | default (interactive calls), bulk, or export. Defaults to default. |
tenant |
Optional | A cross-check only. The tenant always comes from your credential, and a value that differs from it is rejected 403. |
curl "https://<your-venturi-instance>/api/v1/rate-limit?principal=$VENTURI_CLIENT_ID&route_class=export" \
-H "Authorization: Bearer $VENTURI_TOKEN"
The response is the complete body:
{
"tenant": "tnt_01HF774RGJ2YZB7V8TNEXESE0V",
"principal": "3f6a9c2e-0b1d-4e7a-9c55-2d8b1e4f7a10",
"route_class": "export",
"limit": 40,
"remaining": 37,
"reset_seconds": 6,
"allowed": true
}
allowed is false once the bucket is exhausted, and the response then also
carries Retry-After. The body and the RateLimit-* headers on the same
response come from one snapshot, so they always agree. Your plan entitlements
and remaining quota are also visible under Settings / Admin / Billing in the
admin console.
The 429 contract¶
A throttled request returns 429 Too Many Requests as RFC 9457
application/ with error_code: RATE_LIMITED. It carries the same
three RateLimit-* headers as every other authenticated response, plus
Retry-After:
| Header | Meaning |
|---|---|
Retry-After |
Seconds to wait before retrying. |
RateLimit-Limit |
The bucket’s limit for the window. |
RateLimit-Remaining |
Requests remaining in the current window. |
RateLimit-Reset |
Seconds until the window resets (not an absolute timestamp). |
HTTP/1.1 429 Too Many Requests
Retry-After: 12
RateLimit-Limit: 40
RateLimit-Remaining: 0
RateLimit-Reset: 12
Content-Type: application/problem+json
{
"type": "https://venturi.systems/problems/rate-limited",
"title": "Too Many Requests",
"status": 429,
"detail": "The export route-class rate limit was exceeded.",
"instance": "/v1/exports/focus",
"error_code": "RATE_LIMITED",
"trace_id": "c3a18f5e-7d24-4b9a-a6e1-0f5b82d4c917",
"docs_url": "https://docs.venturi.systems/developers/versioning-and-rate-limits/"
}
The header field names and units are pinned to the IETF HTTPAPI working group
Internet-Draft
draft-ietf-httpapi-ratelimit-headers-06
(December 2022), the last revision that defines the separate RateLimit-Limit,
RateLimit-Remaining, and RateLimit-Reset fields, with RateLimit-Reset in
delta seconds. Later revisions of that draft replace these fields with structured
RateLimit and RateLimit-Policy fields; Venturi keeps the revision 06 names and
units frozen within a major version, so a backoff implementation written today
keeps parsing the same fields for the life of v1.
Handling 429 correctly¶
Honor Retry-After, and add jitter so retrying clients do not synchronize into a
thundering herd:
import random
import time
def call_with_backoff(do_request, max_attempts=5):
for _ in range(max_attempts):
resp = do_request()
if resp.status_code != 429:
return resp
retry_after = int(resp.headers.get("Retry-After", "1"))
time.sleep(retry_after + random.uniform(0, 1)) # jitter
raise RuntimeError("rate limit: retries exhausted")
Let the SDK do it
The TypeScript and Python SDKs honor Retry-After with jittered
backoff automatically and raise a typed RateLimitError only after retries
are exhausted, so you rarely write this loop by hand.
Next steps¶
- REST API: auth, scopes, pagination, errors, idempotency.
- SDKs: clients that handle backoff and versioning for you.
- Webhooks: event-catalog versioning.
Sign in to send feedback. Opens in a new tab.
Copy page details
Docs feedback: Versioning & rate limits
Page: https://docs.venturi.systems/developers/versioning-and-rate-limits/