/Docs

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:

json
1{ "data": { "id": "tok_abc", "name": "Terraform CI" } }

List endpoints add cursor pagination fields:

json
1{
2 "data": [ ... ],
3 "next_cursor": "eyJpZCI6ImFiYyIsInNvcnRWYWx1ZSI6IjIwMjYtMDUtMTciLCJzY2hlbWFWZXJzaW9uIjoxfQ",
4 "has_more": true
5}

Errors use a structured error wrapper:

json
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 FailedIf-Match ETag mismatch.
  • 429 Too Many Requests — rate limit exhausted; check Retry-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 200 and an Idempotency-Replayed: true header — no side-effect runs twice.
  • Conflict (same key, different body): returns 409 with error code idempotency_conflict. Pick a new key.
bash
1# Initial create
2curl 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 created
10# Response header: Idempotency-Replayed: true
javascript
1const 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.

Info
Writes without If-Match succeed unconditionally — opt-in to optimistic concurrency only where you need it.
bash
1# 1. Read
2curl -i https://app.avsb.cloud/api/orgs/<orgId>/projects/proj_123
3# < ETag: W/"a1b2c3d4e5f60798"
4
5# 2. Update — succeeds only if no one else has written since the read
6curl -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 read
javascript
1const read = await fetch(url, { headers: { Authorization: token } })
2const etag = read.headers.get('ETag')
3const project = (await read.json()).data
4
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 decide
16}

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 on 429 responses; seconds to wait.
bash
1# Inspect rate-limit state from any response
2curl -i https://app.avsb.cloud/api/orgs/<orgId>/projects \
3 -H "Authorization: Bearer avsb_svc_..."
4# < X-RateLimit-Limit: 600
5# < X-RateLimit-Remaining: 597
6# < X-RateLimit-Reset: 1747526400
javascript
1const 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.

text
1# Decoded cursor (illustrative — never parse this yourself)
2{
3 "id": "exp_abc",
4 "sortValue": "2026-05-17T13:24:00Z",
5 "schemaVersion": 1
6}
bash
1# Page 1
2curl 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 verbatim
7curl 'https://app.avsb.cloud/api/orgs/<orgId>/experiments?limit=50&cursor=eyJpZ...' \
8 -H "Authorization: Bearer avsb_svc_..."
Warning
Cursors are opaque. Don't parse, mutate, or persist them across major API versions — when 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.

bash
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:

bash
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.