Public API: Metrics

A metric is a tracking definition: it says what behaviour to capture. There are three kinds. A Click watches a CSS selector. A Pageview watches for a matching URL. A Custom metric watches for an event key you send yourself, optionally carrying a number.

Experiments and feature-flag rules reference metrics to measure their impact.

Metrics belong to your whole organization, not to one project. So this API has no {orgId} or {projectId} in its path: your service token already names the org. Each metric can still have one home project, or none. Send a projectId string to give it a home project, or null to make it an org-wide metric usable in every project. You set this when you create the metric, and can move it later with PATCH.

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

Every endpoint here follows the shared conventions: the { data } envelope, an Idempotency-Key you can send on every write 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. See rate-limit headers for the response headers that show your remaining budget.

Scopes

Each operation below needs your service token to carry a specific scope (a named permission on the token that controls exactly what it may read or change).

OperationScope
List metrics, get one metricmetrics:read
Create, update, delete a metricmetrics:write
Archive or unarchive a metricmetrics:write

List metrics

GET /api/v1/metrics: every metric in the org, newest first. Cursor-paginated: pass ?limit= (1 to 100, default 20) and ?cursor=. Requires metrics:read.

curl "https://app.avsb.cloud/api/v1/metrics?limit=20" \  -H "Authorization: Bearer avsb_svc_..."
Shell2 lines
Response
{  "data": [    {      "id": "<metricId>",      "shortId": 7,      "projectId": "<projectId>",      "sourceProjectId": null,      "name": "Add to cart",      "description": null,      "type": "CLICK",      "cssSelector": ".add-to-cart",      "urlMatch": null,      "urlMatchMode": null,      "pageUrl": null,      "eventKey": null,      "hasValueField": false,      "defaultDirection": "INCREASE",      "archivedAt": null,      "createdAt": "2026-06-16T00:00:00.000Z",      "updatedAt": "2026-06-16T00:00:00.000Z",      "createdByUserId": "<userId>"    }  ],  "page": { "nextCursor": null, "hasMore": false }}
JSON25 lines

A limit outside 1 to 100 is refused with 400 pagination_invalid rather than being silently capped: see Conventions for that error shape.

type is one of CLICK, PAGEVIEW, CUSTOM or PURCHASE. PURCHASE marks the built-in revenue metrics AvsB adds to a project for you; you cannot create one through this API, and it updates like a CUSTOM metric.

Create a metric

POST /api/v1/metrics: create a metric. Requires metrics:write. The body is discriminated by type: CLICK, PAGEVIEW, or CUSTOM.

The projectId key is required, though its value may be null. Send a project id to give the metric a home there, or null to create an org-wide metric. Leaving the key out entirely fails validation on purpose: it forces you to choose a home rather than silently defaulting to one.

curl -X POST https://app.avsb.cloud/api/v1/metrics \  -H "Authorization: Bearer avsb_svc_..." \  -H "Content-Type: application/json" \  -d '{    "type": "CLICK",    "name": "Add to cart",    "projectId": "<projectId>",    "cssSelector": ".add-to-cart"  }'
Shell9 lines

Returns 201 with the created metric, in the same shape shown under List metrics above.

Org-wide metrics need only the metrics:write scope

Creating, updating, archiving, or deleting an org-wide metric (projectId: null) needs nothing beyond the metrics:write scope every write on this page already needs. The dashboard's browser session has an extra organization-admin check for org-wide metrics. This public API is reached only with a service token, though, and that extra check is skipped for every token call.

Body shapes by type

  • CLICK: name, projectId, cssSelector. Optional pageUrl scopes the click to matching pages. urlMatchMode is required whenever pageUrl is set (one of the six modes below) and says how pageUrl is compared; a scoped create that leaves it out is refused. Metrics stored before this rule keep matching by substring until they are saved again with a mode.
  • PAGEVIEW: name, projectId, urlMatch, and a required urlMatchMode: one of SIMPLE, EXACT, PATH, SUBSTRING, PATTERN, REGEX.
  • CUSTOM: name, projectId, eventKey (letters, digits, _, -, : only), and a required hasValueField saying whether the event carries a number.

Every type also accepts an optional defaultDirection, either "INCREASE" (the default) or "DECREASE", for which way counts as a win. Use "DECREASE" for a metric that is better when it goes down, like bounce rate, load time, or refunds. Each metric binding created for an experiment starts from this default and can override it per experiment. Leave the field out and you get "INCREASE", so existing integrations keep working unchanged.

defaultDirection also matters when you read results back. A result's goalAlignedLiftPct is worked out using this direction, so a positive number always means the metric got better. See Results.

Source project (optional)

Every metric also accepts sourceProjectId, separate from the home projectId. It names the project whose event stream supplies this metric's data. Use it only for the rare case where that project differs from the one the metric lives in or is used in. Leave it out, or send null, and the metric's data comes from whichever project is running the experiment: what almost every metric wants. It appears in every response too, right after projectId.

Info

feature-flag projects (projects built for on/off settings in your code, rather than web experiments) accept Custom metrics only. Sending a Click or Pageview body for a feature-flag project is refused.

409: a metric with this name already exists
{  "error": {    "code": "conflict",    "message": "A metric with this name already exists in this organization",    "docUrl": "https://docs.avsb.cloud/docs/developer-reference/public-api/conventions#conflict-errors",    "details": {      "field": "name",      "conflict": {        "id": "<metricId>",        "name": "Add to cart",        "projectId": "<projectId>",        "projectShortId": 12,        "projectName": "Marketing site"      }    },    "requestId": "req_9f2c41ab7e0b4d1e8c35a6f0d2b91e77"  }}
JSON18 lines

The same shape fires when a CUSTOM metric's eventKey collides, worded for the key instead of the name and with "field": "eventKey". conflict names the existing metric so you can deep-link to it, or edit it instead of retrying the create. A rename through PATCH that lands on a taken name answers the same way.

Get a metric

GET /api/v1/metrics/{metricId}: one metric by id, either its cuid or its short numeric id. A metric outside your org answers 404, the same as one that does not exist at all. Requires metrics:read.

curl https://app.avsb.cloud/api/v1/metrics/<metricId> \  -H "Authorization: Bearer avsb_svc_..."
Shell2 lines

Returns 200 with the metric, in the same shape shown under List metrics above.

404: metric not found
{  "error": {    "code": "not_found",    "message": "Metric not found",    "docUrl": "https://docs.avsb.cloud/docs/developer-reference/public-api/conventions#not-found-errors",    "requestId": "req_9f2c41ab7e0b4d1e8c35a6f0d2b91e77"  }}
JSON8 lines

Update a metric

PATCH /api/v1/metrics/{metricId}: partial update. Requires metrics:write. A metric's type never changes after create, so only the fields that type accepts are allowed (for a Click metric, that is name, description, cssSelector, pageUrl, or urlMatchMode). A field another type uses is refused with 400. The OpenAPI document and the MCP update_metric tool describe the body as one shape per type: Click, Pageview, and Custom (which Purchase metrics share).

URL matching travels as a pair, so a page filter always says how it is compared:

  • On a Click metric, a PATCH carrying a non-null pageUrl must carry urlMatchMode in the same request.
  • urlMatchMode: null is accepted only alongside pageUrl: null, because clearing the mode on its own would leave a still-scoped metric matching by substring. Sending pageUrl: null by itself is fine: the mode stays parked on the metric and comes back if you add a filter again.
  • On a Pageview metric, a PATCH carrying urlMatch must carry urlMatchMode in the same request. Sending urlMatchMode on its own is fine.
curl -X PATCH https://app.avsb.cloud/api/v1/metrics/<metricId> \  -H "Authorization: Bearer avsb_svc_..." \  -H "Content-Type: application/json" \  -d '{ "name": "Add to cart (header)" }'
Shell4 lines
Response (truncated)
{  "data": {    "id": "<metricId>",    "name": "Add to cart (header)",    "type": "CLICK",    "updatedAt": "2026-06-17T09:00:00.000Z",    "...": "the rest of the fields shown under List metrics, unchanged"  }}
JSON9 lines

Promote and demote (changing scope)

projectId is a normal PATCH field, so a scope change goes through this same endpoint. There is no separate promote or demote route.

  • Leave projectId out: nothing about scope changes.
  • Send "projectId": null: promote the metric to org-wide.
  • Send "projectId": "<otherProjectId>": demote or move the metric to that project.
cURL: promote to org-wide
curl -X PATCH https://app.avsb.cloud/api/v1/metrics/<metricId> \  -H "Authorization: Bearer avsb_svc_..." \  -H "Content-Type: application/json" \  -d '{ "projectId": null }'
Shell4 lines

A demote or move is refused with 409 in one case: the metric is still used by a binding or a flag rule that lives outside the target project. The response lists every blocker so you can detach them first:

409: still referenced from other projects
{  "error": {    "code": "validation_failed",    "message": "Cannot move this metric to a project while it is referenced by other projects",    "docUrl": "https://docs.avsb.cloud/docs/developer-reference/public-api/conventions#validation-errors",    "details": {      "blockers": [        { "kind": "binding", "id": "<metricBindingId>", "name": "Add to cart rate", "projectId": "<otherProjectId>" },        { "kind": "flag-rule", "id": "<flagRuleMetricId>", "name": "Checkout flag", "projectId": "<otherProjectId>" }      ]    },    "requestId": "req_9f2c41ab7e0b4d1e8c35a6f0d2b91e77"  }}
JSON14 lines

Every blocker is a binding (which may itself be attached to one or more experiments) or a flag-rule. Promoting to org-wide, or editing a metric that is already org-wide, needs nothing beyond metrics:write: see the callout under Create a metric.

Delete a metric

DELETE /api/v1/metrics/{metricId}: permanently deletes the metric. In the same step it detaches the metric from every experiment and feature-flag rule that used it, then republishes the affected project's datafile (the small JSON file the snippet downloads, listing every live experiment, flag, and audience for that project) so the snippet stops looking for the deleted metric. Requires metrics:write.

curl -X DELETE https://app.avsb.cloud/api/v1/metrics/<metricId> \  -H "Authorization: Bearer avsb_svc_..."
Shell2 lines
Response
{  "data": { "id": "<metricId>", "deleted": true }}
JSON3 lines
Warning

Deleting a metric removes it from any running experiment or flag rule using it, immediately. The dashboard shows a usage-aware confirmation before deleting. Over the API there is no such guard, so call GET first if you want to know what a delete would affect.

A missing or cross-org metric answers the same 404 not_found shape shown under Get a metric above. Deleting an org-wide metric follows the same rule as creating one: see the callout under Create a metric.

Archive a metric

POST /api/v1/metrics/{metricId}/archive: the reversible alternative to deleting. Requires metrics:write. The metric stops firing, and is detached from every experiment and flag rule using it, the same way a delete detaches it. The row survives, though, so you can bring it back.

curl -X POST https://app.avsb.cloud/api/v1/metrics/<metricId>/archive \  -H "Authorization: Bearer avsb_svc_..."
Shell2 lines

Returns 200 with the metric, now carrying an archivedAt timestamp, in the same shape shown under List metrics above. Safe to retry: archiving an already-archived metric answers 200, not an error, and archivedAt keeps the time it was first archived.

Response (truncated)
{  "data": {    "id": "<metricId>",    "archivedAt": "2026-06-18T10:00:00.000Z",    "...": "the rest of the fields shown under List metrics, unchanged"  }}
JSON7 lines

Archiving an org-wide metric stops it firing in every project in the organization. That still needs nothing beyond metrics:write: see the callout under Create a metric. A missing or cross-org metric answers the same 404 shape shown under Get a metric.

Unarchive a metric

POST /api/v1/metrics/{metricId}/unarchive: clears archivedAt so the metric fires again. Requires metrics:write.

curl -X POST https://app.avsb.cloud/api/v1/metrics/<metricId>/unarchive \  -H "Authorization: Bearer avsb_svc_..."
Shell2 lines

Returns 200 with the metric, archivedAt now null, in the same shape shown under List metrics above.

Info

Unarchiving does not re-attach the metric to the experiments and flag rules the archive detached it from. Restoring a metric must never silently change what a running test measures, so re-attach deliberately with the metric-bindings API.

Next steps

Was this helpful?