Public API: Conventions
Every endpoint in the AvsB public API obeys the same wire-format contract: envelope shape, HTTP status codes, retry-safe writes via Idempotency-Key, optimistic concurrency via If-Match / ETag, rate-limit headers, opaque cursor pagination, and date-based version pinning. This page is the terse reference — keep it open while integrating.
Response envelope
Successful responses use a data wrapper:
1{ "data": { "id": "tok_abc", "name": "Terraform CI" } }List endpoints add cursor pagination fields:
1{2 "data": [ ... ],3 "next_cursor": "eyJpZCI6ImFiYyIsInNvcnRWYWx1ZSI6IjIwMjYtMDUtMTciLCJzY2hlbWFWZXJzaW9uIjoxfQ",4 "has_more": true5}Errors use a structured error wrapper:
1{2 "error": {3 "code": "scope_missing",4 "message": "Token is missing required scope: experiments:write",5 "details": { "missingScope": "experiments:write" }6 }7}HTTP status codes
The API uses a fixed, predictable set of status codes:
200 OK— successful read, update, or idempotent replay.201 Created— successful resource creation.204 No Content— successful delete or no-body response.400 Bad Request— request body failed Zod validation.401 Unauthorized— missing, invalid, expired, or revoked token.403 Forbidden— token authenticated but missing the required scope.404 Not Found— resource does not exist or is outside the org.409 Conflict— idempotency-key collision on a different body.412 Precondition Failed—If-MatchETag mismatch.429 Too Many Requests— rate limit exhausted; checkRetry-After.500 Internal Server Error— unexpected server fault; safe to retry.
Idempotency-Key
Every state-changing request (POST / PUT / PATCH / DELETE) accepts an optional Idempotency-Key header. Supply a unique value (UUID, ULID, or any string up to 255 chars) per logical request.
Semantics within the 24-hour window:
- First call: processed normally, response cached with a fingerprint of the request body.
- Replay (same key, same body): returns the cached response with status
200and anIdempotency-Replayed: trueheader — no side-effect runs twice. - Conflict (same key, different body): returns
409with error codeidempotency_conflict. Pick a new key.
1# Initial create2curl https://app.avsb.cloud/api/orgs/<orgId>/projects \3 -X POST \4 -H "Authorization: Bearer avsb_svc_..." \5 -H "Content-Type: application/json" \6 -H "Idempotency-Key: $(uuidgen)" \7 -d '{"name":"Checkout"}'8
9# Retrying with the same key returns the same response, no duplicate created10# Response header: Idempotency-Replayed: true1const key = crypto.randomUUID()2await fetch('/api/orgs/<orgId>/projects', {3 method: 'POST',4 headers: {5 'Authorization': 'Bearer avsb_svc_...',6 'Content-Type': 'application/json',7 'Idempotency-Key': key,8 },9 body: JSON.stringify({ name: 'Checkout' }),10})ETag / If-Match
Every individual-resource GET response carries a weak ETag header derived from the resource's updatedAt timestamp + identifier. Pass it back as If-Match on subsequent writes to detect concurrent edits.
If-Match succeed unconditionally — opt-in to optimistic concurrency only where you need it.1# 1. Read2curl -i https://app.avsb.cloud/api/orgs/<orgId>/projects/proj_1233# < ETag: W/"a1b2c3d4e5f60798"4
5# 2. Update — succeeds only if no one else has written since the read6curl -X PATCH https://app.avsb.cloud/api/orgs/<orgId>/projects/proj_123 \7 -H "Authorization: Bearer avsb_svc_..." \8 -H "Content-Type: application/json" \9 -H "If-Match: W/\"a1b2c3d4e5f60798\"" \10 -d '{"name":"Checkout v2"}'11# 412 Precondition Failed if the resource has changed since the read1const read = await fetch(url, { headers: { Authorization: token } })2const etag = read.headers.get('ETag')3const project = (await read.json()).data4
5const write = await fetch(url, {6 method: 'PATCH',7 headers: {8 Authorization: token,9 'Content-Type': 'application/json',10 'If-Match': etag ?? '',11 },12 body: JSON.stringify({ name: project.name + ' v2' }),13})14if (write.status === 412) {15 // someone else updated; re-read and decide16}Rate-limit headers
Every response — success or error — carries the rate-limit headers for the token + scope family that authenticated the request:
X-RateLimit-Limit— requests permitted per minute.X-RateLimit-Remaining— requests left in the current window.X-RateLimit-Reset— unix timestamp (seconds) when the window resets.Retry-After— present only on429responses; seconds to wait.
1# Inspect rate-limit state from any response2curl -i https://app.avsb.cloud/api/orgs/<orgId>/projects \3 -H "Authorization: Bearer avsb_svc_..."4# < X-RateLimit-Limit: 6005# < X-RateLimit-Remaining: 5976# < X-RateLimit-Reset: 17475264001const res = await fetch(url, { headers: { Authorization: token } })2const remaining = Number(res.headers.get('X-RateLimit-Remaining'))3if (remaining < 10) {4 await sleep(Number(res.headers.get('X-RateLimit-Reset')) * 1000 - Date.now())5}Cursor pagination
List endpoints page via an opaque cursor — a base64url-encoded JSON blob with a versioned schema. Treat it as an opaque string; the schema is internal and may evolve under schemaVersion.
1# Decoded cursor (illustrative — never parse this yourself)2{3 "id": "exp_abc",4 "sortValue": "2026-05-17T13:24:00Z",5 "schemaVersion": 16}1# Page 12curl https://app.avsb.cloud/api/orgs/<orgId>/experiments?limit=50 \3 -H "Authorization: Bearer avsb_svc_..."4# → { "data": [ ... ], "next_cursor": "eyJpZ...", "has_more": true }5
6# Page 2 — pass the previous next_cursor verbatim7curl 'https://app.avsb.cloud/api/orgs/<orgId>/experiments?limit=50&cursor=eyJpZ...' \8 -H "Authorization: Bearer avsb_svc_..."schemaVersion changes, in-flight cursors are rejected with a structured cursor_invalid error.Path parameters: short or canonical IDs
Every *Id path parameter on the API accepts either the canonical cuid (e.g. cmnoa4mfm000209l4dzq85lqf) or the short numeric ID shown in the dashboard URL (e.g. 200008). Both forms address the same resource, so you can copy whichever string is in front of you.
1# These two requests target the same project and return the same data:2curl https://app.avsb.cloud/api/orgs/<orgId>/projects/cmnoa4mfm000209l4dzq85lqf \3 -H "Authorization: Bearer avsb_svc_..."4
5curl https://app.avsb.cloud/api/orgs/<orgId>/projects/200008 \6 -H "Authorization: Bearer avsb_svc_..."Audit-log entries, ETag values, Location headers on POST, and webhook payloads always reference the canonical cuid form. The short ID is a path-parameter convenience only — request bodies must use the canonical cuid where they reference foreign resources.
Parameters that accept either form: projectId, experimentId, variationId, flagId, audienceId, segmentId, metricId, bindingId, exclusionGroupId. orgId accepts only the canonical cuid.
API versioning
The API uses date-based version pinning via the AvsB-API-Version request header. Omit the header to track the latest non-breaking version. Pin to a date to lock to a known-good contract:
1curl https://app.avsb.cloud/api/orgs/<orgId>/projects \2 -H "Authorization: Bearer avsb_svc_..." \3 -H "AvsB-API-Version: 2026-05-17"Breaking changes land behind a new date. Every response echoes the version it served as AvsB-API-Version in the response headers, so you can detect drift if you forgot to pin.