Skip to content

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: coper stays 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 (default for interactive calls, bulk, and export), 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/v2 endpoints 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 401 before 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.
Bash
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:

JSON
{
  "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/problem+json 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
HTTP/1.1 429 Too Many Requests
Retry-After: 12
RateLimit-Limit: 40
RateLimit-Remaining: 0
RateLimit-Reset: 12
Content-Type: application/problem+json
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:

Python
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.
Give feedback on this page

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/