Public API: Projects

A project is the container everything else lives in: experiments, flags, metrics, audiences, orders. Every other project-scoped path needs a {projectId}, so this is the resource an integration starts from.

The org is taken from the token, so the path carries no {orgId}. A token can only see projects in its own organization.

Plain text
https://app.avsb.cloud/api/v1/projects
Plain text1 line

Every endpoint here follows the shared conventions: the { data } envelope, an Idempotency-Key on both writes below, and the standard error shape. Every /api/v1 token is rate-limited the same way everywhere: a scoped token gets 600 reads and 120 writes per minute, and an admin:* token gets 600 requests per minute, reads and writes together. Every endpoint here also needs the Integrations and API access plan feature, or every call fails with 403 feature_disabled. See rate-limit headers and Refusals at 403.

Scopes

A scope is a named permission on your token. It decides exactly what that token is allowed to read or change.

OperationScope
List / get projectsprojects:read
Create / update a projectprojects:write

List projects

GET /api/v1/projects: every project in the organization, newest-created first, cursor-paginated.

curl "https://app.avsb.cloud/api/v1/projects?limit=20" \  -H "Authorization: Bearer avsb_svc_..."
Shell2 lines
JSON
{  "data": [    {      "id": "clxyz456def",      "shortId": 200001,      "orgId": "clorg001",      "name": "Marketing site",      "description": null,      "url": "https://example.com",      "environment": "PRODUCTION",      "type": "WEB_EXPERIMENTATION",      "status": "ACTIVE",      "currency": "USD",      "snippetKey": "snp_abc123",      "snippetDetectedAt": "2026-06-20T09:14:00.000Z",      "attributionWindowDays": 7,      "attributionWindowHours": 0,      "spaHashRouting": false,      "responsiveMode": false,      "antiFlicker": true,      "consentMode": false,      "consentCookieClass": "FUNCTIONAL",      "readGoogleConsent": false,      "autoCollectDataLayer": false,      "isOnlineStore": null,      "profitIncludesShipping": false,      "shopifyCogsSyncEnabled": true,      "defaultStatsEngine": "BAYESIAN",      "defaultVarianceReduction": "AUTO",      "requiresAnalysisPlan": false,      "createdAt": "2026-06-18T00:00:00.000Z",      "updatedAt": "2026-06-19T00:00:00.000Z"    }  ],  "page": { "nextCursor": null, "hasMore": false }}
JSON36 lines

Pass ?limit= (1 to 100, default 20) and the opaque ?cursor= from page.nextCursor to page through results.

Get a project

GET /api/v1/projects/{projectId}: {projectId} accepts either the project id or its numeric shortId, so the number in your dashboard URL works here too. Returns the same shape as List projects above, for one project.

curl https://app.avsb.cloud/api/v1/projects/200001 \  -H "Authorization: Bearer avsb_svc_..."
Shell2 lines

The response carries a weak ETag. Pass it back as X-Avsb-If-Match on a later PATCH to detect a concurrent edit. A projectId that does not exist, or belongs to another organization, answers 404 not_found either way: the API never reveals whether a project exists in someone else's org.

Create a project

POST /api/v1/projects

The smallest request only needs a name and a URL. type defaults to WEB_EXPERIMENTATION, and a URL is required for that type:

curl -X POST https://app.avsb.cloud/api/v1/projects \  -H "Authorization: Bearer avsb_svc_..." \  -H "Content-Type: application/json" \  -H "Idempotency-Key: $(uuidgen)" \  -d '{    "name": "Marketing site",    "url": "https://example.com"  }'
Shell8 lines

A fuller request for a feature-flag project, where the URL is optional and you name the environment explicitly:

cURL, a feature-flag project
curl -X POST https://app.avsb.cloud/api/v1/projects \  -H "Authorization: Bearer avsb_svc_..." \  -H "Content-Type: application/json" \  -H "Idempotency-Key: $(uuidgen)" \  -d '{    "name": "Checkout flags",    "type": "FEATURE_FLAG",    "environment": "STAGING",    "description": "Flags for the new checkout rollout"  }'
Shell10 lines

Returns 201 with the created project, in the same shape as List projects above.

Field notes

  • type is WEB_EXPERIMENTATION (default) or FEATURE_FLAG.
  • url is required for web-experimentation projects and optional for feature-flag projects. It must start with http:// or https://; any other scheme (such as ftp://) is refused with 400, on create and on update.
  • environment is PRODUCTION (default), STAGING, or DEVELOPMENT. It labels the project; it is not the same thing as a feature-flag environment.
  • name can be up to 200 characters, and description and url up to 2000 each. Omit description and it comes back as an empty string, not null.
  • isOnlineStore answers "is this project an online store?". Send true or false to record the answer; leave it out and it stays null, meaning nobody has been asked yet, and the dashboard keeps offering to set up commerce. It is never turned into false on your behalf, because "not a store" and "not asked" are different answers. Accepted on feature-flag projects too: it describes the business, not the project type.
  • A feature-flag project is created with two flag environments, Production and Development, each with its own SDK key. Read them back from the environments API.
  • A web-experimentation project gets its first datafile published immediately, so the snippet has something to fetch the moment you add the tag.
  • Creating a project counts against your plan's project limit. At the limit the call returns 403 with a feature_disabled error.

Error responses

Omit url on a web-experimentation project (the default type) and validation catches it before anything is created:

400, url is required for this project type
{  "error": {    "code": "validation_failed",    "message": "Request body failed validation",    "details": {      "issues": [        { "param": "url", "path": ["url"], "code": "custom", "message": "URL is required for web experimentation projects" }      ]    }  }}
JSON11 lines

At your plan's project limit, the same call instead answers:

403, plan's project limit reached
{  "error": {    "code": "feature_disabled",    "message": "You have reached your plan's limit. Upgrade to add more.",    "details": { "kind": "quota", "resource": "projects", "limit": 3, "current": 3 }  }}
JSON7 lines

Update a project

PATCH /api/v1/projects/{projectId}: send only the fields you want to change.

Shell
curl -X PATCH https://app.avsb.cloud/api/v1/projects/200001 \  -H "Authorization: Bearer avsb_svc_..." \  -H "Content-Type: application/json" \  -H "Idempotency-Key: $(uuidgen)" \  -d '{ "status": "PAUSED", "antiFlicker": false }'
Shell5 lines

Fields you can change:

FieldNotes
name, description, urlProject identity.
environmentPRODUCTION, STAGING, DEVELOPMENT.
statusACTIVE, PAUSED, ARCHIVED. Pausing stops the snippet serving experiments.
currencyThree-letter ISO 4217 code, uppercase (for example USD).
attributionWindowDays, attributionWindowHoursHow long after an exposure a conversion still counts. Days: 0 to 90. Hours: 0 to 23.
spaHashRoutingWhether a change in the URL fragment (#/checkout) counts as a new page view. Single-page-app support itself is always on: the snippet follows history navigation without being told to, and this switch only adds hash handling. In the dashboard this is the Hash-based routing toggle under project settings.
responsiveMode, antiFlickerSnippet behaviour flags.
consentMode, consentCookieClass, readGoogleConsentConsent handling. consentCookieClass is FUNCTIONAL or ANALYTICS.
autoCollectDataLayerWhether the snippet reads your data layer automatically. In the dashboard this is the Read purchases from your data layer toggle under project settings.
isOnlineStoreWhether this project is an online store. true or false only: the API records an answer, it cannot put the question back to unanswered. Same setting as the dashboard's store toggle, and it lands in your audit log the same way.
profitIncludesShippingWhether shipping cost is taken off profit alongside the cost of goods. Applies from the next nightly profit run onward; orders already worked out keep the profit they were given. In the dashboard this is on the Profit settings card under Commerce, Orders & attribution.
shopifyCogsSyncEnabledWhether the nightly Shopify sync keeps your product costs up to date. On by default, and inert on a project with no Shopify connection. Same Profit settings card in the dashboard.

Any change to a snippet-visible setting republishes the project's datafile, so it reaches visitors on their next fetch. Changing currency or autoCollectDataLayer also re-seeds the built-in revenue metrics. The two profit fields are read by the nightly jobs rather than the snippet, so changing them republishes nothing.

Info

Sending a field that is not in the table returns 400 validation_failed rather than being ignored, so a typo is never a silent no-op. The visual editor's project script, shared type definitions, brand palette, internal-traffic rules, and the statistical analysis defaults are dashboard-only and are not accepted here. Allowed origins have their own endpoint.

Error response

Send a stale X-Avsb-If-Match (from an earlier GET) and the write is refused before anything changes:

412, someone else updated the project since you read it
{  "error": {    "code": "precondition_failed",    "message": "The resource changed since you read it. Re-read it and retry.",    "details": { "expected": "W/\"a1b2c3d4e5f60798\"" }  }}
JSON7 lines

Deleting a project

There is no delete endpoint. Deleting a project removes every experiment, flag, metric, and stored result underneath it, which is not an operation to hand to a retrying script on the strength of one token. Set status to ARCHIVED here, and delete from the dashboard if you really mean it.

Next steps

Was this helpful?