Public API: Audit & Observability

Every state-changing request authenticated by a service token leaves a trail you can audit, query, and stream. This page covers four signals: the in-app audit log, request-id correlated server logs, the rate-limit headers on every response, and webhooks for state-change subscriptions.

Audit log entries for API actions

Every write that authenticates with a service token produces an audit entry alongside the resource change. The entry records:

  • actorType: API_TOKEN: distinguishes API actions from dashboard (USER) and system actions.
  • source: API: the channel that triggered the action. Other values: DASHBOARD, CLI, SYSTEM.
  • actorId + actorName: the specific token that authenticated the request, and its display name. Both are copied onto the entry the moment it is written, so the name still reads correctly even if you rename or rotate the token later. metadata.tokenId repeats the same token id so you can filter or group on it programmatically.
  • ipAddress + userAgent: the calling client's observable identity.
  • changes: a diff of the resource, one entry per changed field, each holding { old, new } (where applicable).
Info

Service-token audit entries appear identically to dashboard entries in the UI, but with a token-shaped icon and a footer reading via API · {token name}. No fields are redacted. The full diff is visible to anyone with the audit:read scope or org admin access.

Viewing audit entries in-app

The dashboard ships a dedicated audit-log view at /audit-log. Open it from the organization sidebar, or link directly:

Plain text
https://app.avsb.cloud/audit-log
Plain text1 line

Filters available in the UI:

  • Search: free text, matched against the resource name.
  • Action: one action at a time, for example TOGGLED or ARCHIVED.
  • Resource type: Project, Experiment, Flag, Audience, etc.
  • Source: Dashboard, API, CLI, System.
  • Member: narrow to one person's actions. There is no separate picker for a single token; use the API's actorId filter for that (see below).
  • Date range: pick a start and end day on the calendar. There is no hour-level range and no built-in "last N days" preset.

Reading audit entries over the API

GET /api/v1/audit-logs, behind the audit:read scope. Org-level: the organization comes from the token, so there is no id in the path and a token can only read its own organization's trail.

curl "https://app.avsb.cloud/api/v1/audit-logs?limit=20&resourceType=FLAG" \  -H "Authorization: Bearer avsb_svc_..."
Shell2 lines
Response
{  "data": [    {      "id": "<auditEntryId>",      "orgId": "<orgId>",      "actorId": "<tokenId>",      "actorName": "Terraform CI",      "actorEmail": null,      "actorType": "API_TOKEN",      "action": "TOGGLED",      "status": "SUCCESS",      "failureReason": null,      "resourceType": "FLAG",      "resourceId": "<flagId>",      "resourceName": "New checkout",      "projectId": "<projectId>",      "experimentId": null,      "source": "API",      "ipAddress": "203.0.113.42",      "userAgent": "terraform-provider-avsb/0.4.1",      "correlationId": null,      "changes": { "enabled": { "old": true, "new": false } },      "metadata": { "tokenId": "<tokenId>" },      "createdAt": "2026-07-30T12:00:00.000Z"    }  ],  "page": { "nextCursor": null, "hasMore": false }}
JSON28 lines

Filters

ParameterNotes
limit1 to 100, default 20.
cursorThe opaque page.nextCursor from the previous response.
actionOne or more action names, comma-separated (for example TOGGLED,ARCHIVED).
resourceTypeOne or more resource types, comma-separated (for example FLAG,EXPERIMENT).
actorIdA user id or a token id: everything one actor did.
sourceDASHBOARD, API, CLI, or SYSTEM.
statusSUCCESS or FAILED. Failed entries record refused attempts.
projectIdNarrow to one project.
correlationIdGroup the entries written by one operation.
from, toISO-8601 instants. Either or both.

A filter value that is not a real action or resource type returns 400 validation_failed. That is deliberate: silently dropping an unrecognised filter would widen an audit read, which is the worst way for this endpoint to fail.

Info

actorType is USER, API_TOKEN, or SYSTEM, and the API always reports the real one. Telling a token's writes apart from a person's is the reason to read this over the API rather than eyeballing the dashboard.

The audit log is append-only: entries are written as a side effect of the action being audited, so there is no write endpoint. The same entries are browsable in the dashboard's Audit Log page (the sidebar link described above).

Server-side logging and request correlation

Every response from the public API carries an X-Request-Id header: an opaque id unique to that request. When a call fails, especially with a 500, the failure is logged on our side with that same id, the HTTP method, and the path.

Info

Quote the X-Request-Id value when you contact support and we can find the exact log line for your request, without you needing to share the request or response body. Neither is ever logged, and token secrets never are either. If you need the diff of a write, query the audit log above: it is the trail the public API actually hands you, not the server log.

Rate-limit headers as observability signal

Every response carries the standard rate-limit headers: graph them in your observability stack to spot capacity issues before they turn into 429 responses:

  • X-RateLimit-Limit: current quota (requests/minute) for this token: 600 for reads, 120 for writes.
  • X-RateLimit-Remaining: alarm on remaining/limit < 0.1.
  • X-RateLimit-Reset: unix timestamp; use to drive token-bucket back-off.
  • Retry-After: present only on 429 responses; respect it before retrying.
Your own metrics client
// Your own metrics client.declare const metrics: { gauge(name: string, value: number): void }// Forward rate-limit headers into your metrics pipelinefunction recordRateLimit(res: Response): void {  metrics.gauge('avsb_api.rate_limit.remaining', Number(res.headers.get('X-RateLimit-Remaining')))  metrics.gauge('avsb_api.rate_limit.limit', Number(res.headers.get('X-RateLimit-Limit')))}
TypeScript8 lines

Webhooks for state changes

For push-based observability (i.e. you want to be told when a resource changes rather than polling the audit log), subscribe to webhooks. On the public API, webhooks are configured per-project and cover experiment events, flag events, exclusion-group changes, and commerce/background-work events for that project.

There is no dedicated event for token activity or for the audit log itself: subscribe to the experiment and flag events that matter to your integration instead, and keep reading the audit log above for a full, queryable trail of who changed what. The event types most useful for observability:

  • Experiment lifecycle: experiment.launched, .paused, .resumed, .completed, .changes_published.
  • Experiment safety and results: experiment.srm_failed, .guardrail_breached, .guardrail_metric_breached, .code_error_detected, .winner_declared, .results_ready.
  • Flags: flag.published, flag.rule_activated, flag.srm_failed, flag.guardrail_breached, flag.results_ready.
curl https://app.avsb.cloud/api/v1/projects/<projectId>/webhooks \  -X POST \  -H "Authorization: Bearer avsb_svc_..." \  -H "Content-Type: application/json" \  -d '{    "name": "SRM and guardrail alerts",    "url": "https://hooks.your-host/avsb",    "events": ["experiment.srm_failed", "experiment.guardrail_breached"]  }'
Shell9 lines

AvsB generates the signing secret; you never choose one. It rides in the create response's secret field, once. Store it immediately: no read endpoint returns it again, and the only way to get a new one is to rotate, which invalidates the old.

Every webhook delivery is signed with an HMAC-SHA256 over the delivery id, the timestamp, and the body, keyed by that signing secret, and passed as the X-AvsB-Signature header. Reject deliveries whose signature does not verify, and reject deliveries older than five minutes. verifyWebhookSignature in @avsbhq/node does both; see Verifying signatures for the exact scheme.

Info

Webhook delivery is at-least-once with exponential back-off, so always de-duplicate by the delivery id header. See the Webhooks guide for the full signing flow, retry schedule, and replay UI.

Was this helpful?