Skip to content

Tokens, webhooks & billing

Organization-scoped administrative endpoints.

Terminal window
export API=https://cavsnode.com/api/v1
export ORG=acme-ai
export H="Authorization: Bearer $CAVS_TOKEN"
export J="Content-Type: application/json"
Endpoint Permission
GET /organizations/{orgID}/tokens tokens.create
POST /organizations/{orgID}/tokens tokens.create
DELETE /organizations/{orgID}/tokens/{tokenID} tokens.revoke
Terminal window
curl -sS "$API/organizations/$ORG/tokens" -H "$H"
curl -sS -X POST "$API/organizations/$ORG/tokens" -H "$H" -H "$J" \
-d '{
"name": "github-actions/vision-build",
"kind": "CI",
"scopes": ["repo:write"],
"expires_in_days": 90
}'
curl -sS -X DELETE "$API/organizations/$ORG/tokens/$TOKEN_ID" -H "$H"
Field Values Notes
name required Say where it lives — that’s what you’ll read when deciding what to revoke
kind PAT (default), REPO, CI Determines the prefix
scopes repo:read, repo:write, repo:admin, org:read, usage:read At least one required
repository_id UUID Required for REPO tokens
expires_in_days int Omit for a token that never expires
201 Created — the secret appears only here
{
"token": { "id": "6f1a…", "name": "github-actions/vision-build", "kind": "CI",
"prefix": "cavs_ci", "scopes": ["repo:write"],
"expires_at": "2026-10-26T12:00:00Z" },
"secret": "cavs_ci_9dK2…"
}

Only the SHA-256 hash is stored. Counts against the plan’s token limit (402 when exceeded). Both actions are audited. Full guidance: API tokens.

Endpoint Permission
GET /organizations/{orgID}/webhooks organization.update
POST /organizations/{orgID}/webhooks organization.update
PATCH /organizations/{orgID}/webhooks/{webhookID} organization.update
DELETE /organizations/{orgID}/webhooks/{webhookID} organization.update
GET /organizations/{orgID}/webhooks/{webhookID}/deliveries organization.update
Terminal window
curl -sS -X POST "$API/organizations/$ORG/webhooks" -H "$H" -H "$J" \
-d '{"url": "https://hooks.example.com/cavs",
"events": ["repository.push", "release.published"]}'
Field Notes
url Must be a valid http/https URL with a host
events At least one. "*" subscribes to everything and collapses the list.

The signing secret is generated by CAVS (whsec_…) and returned once in the create response.

PATCH accepts url, events and active. Setting active: true also clears the circuit breaker. A webhook is auto-disabled after 10 consecutive failures.

Full detail — payloads, signature verification, retries: Webhooks.

Endpoint Permission
GET /organizations/{orgID}/storage/connections storage.read
POST /organizations/{orgID}/storage/connections storage.manage
GET /organizations/{orgID}/storage/connections/{connID} storage.read
PATCH /organizations/{orgID}/storage/connections/{connID} storage.manage
POST /organizations/{orgID}/storage/connections/{connID}/test storage.manage
DELETE /organizations/{orgID}/storage/connections/{connID} storage.manage
Terminal window
curl -sS -X POST "$API/organizations/$ORG/storage/connections" -H "$H" -H "$J" \
-d '{
"name": "acme-prod-eu",
"provider": "aws_s3",
"region": "eu-central-1",
"bucket": "acme-cavs-prod",
"prefix": "cavs/",
"auth_method": "access_keys",
"access_key_id": "AKIA…",
"secret_access_key": "…"
}'
Field Notes
name Display name
provider aws_s3, r2, wasabi, b2, minio, generic
scope Connection scope
endpoint, region, bucket, prefix Target. A prefix is strongly recommended.
path_style, tls_verify Booleans, provider-dependent
kms_key_id, storage_class Optional
auth_method access_keys or assume_role
access_key_id, secret_access_key For access_keys
role_arn, external_id For assume_role

The secret is encrypted with AES-256-GCM before it touches the database and is never returned by any response. Creating or testing runs the seven-check connectivity suite and records the result as the connection’s health.

402 quota_exceeded if your plan doesn’t allow BYOS (or the add-on isn’t active), or at the connection limit.

Full guide: Bring Your Own S3.

Endpoint Permission
GET /organizations/{orgID}/billing billing.read
POST /organizations/{orgID}/billing/checkout billing.update
POST /organizations/{orgID}/billing/portal billing.update
POST /organizations/{orgID}/billing/confirm billing.update
POST /organizations/{orgID}/billing/byos-addon billing.update

billing.read / billing.update belong to OWNER and BILLINGnot ADMIN.

Terminal window
# Current plan, subscription state, usage against limits, invoice preview
curl -sS "$API/organizations/$ORG/billing" -H "$H"
# Start a checkout for a paid plan → { "url": "…" }
curl -sS -X POST "$API/organizations/$ORG/billing/checkout" -H "$H" -H "$J" \
-d '{"plan_id": "team"}'
# Open the provider's billing portal → { "url": "…" }
curl -sS -X POST "$API/organizations/$ORG/billing/portal" -H "$H"
# Confirm a checkout that the webhook hasn't landed yet
curl -sS -X POST "$API/organizations/$ORG/billing/confirm" -H "$H" -H "$J" \
-d '{"plan_id": "team"}'
# Toggle the Developer BYOS add-on
curl -sS -X POST "$API/organizations/$ORG/billing/byos-addon" -H "$H" -H "$J" \
-d '{"enabled": true}'

checkout and portal return a url to redirect the user to. confirm may return 202 Accepted with {"status": "pending_webhook"} when the provider hasn’t confirmed yet — poll GET …/billing.

A plan without a configured provider price fails checkout. free never checks out and business is contact-sales, so neither has one.

See Managing your subscription.

The billing provider’s callback. Public — verified by provider signature rather than a bearer token, and not something you call.

Permission: audit.readADMIN or OWNER. Team and Business plans.

Terminal window
curl -sS "$API/organizations/$ORG/audit" -H "$H"