Public API: Metric bindings

A metric binding is a saved, reusable definition of what you want to measure. It applies a measure, such as "unique conversions per visitor" or a profit-style "total value per visitor", over one or more tracked metrics. Experiments attach bindings as their primary, secondary, or guardrail metrics (metrics you are not trying to improve but do not want to make worse, like page load time). A binding is the unit you create once and reuse across many tests.

Metric bindings are organization-scoped on the public API. Your token already names its organization, so the path carries no {orgId}, and bindings are listed across the whole org. Every endpoint here authenticates with a Bearer token: a service token or a personal access token. Like metrics, a binding has an optional home project: projectId is a string (the home project) or null (an org-wide binding usable in every project). You choose org-wide or project-level on create, and change it later with PATCH.

Plain text
https://app.avsb.cloud/api/v1/metric-bindings
Plain text1 line

Every endpoint here follows the shared conventions: the { data } envelope, an Idempotency-Key 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. Every endpoint here also needs a plan that includes Integrations and API access; without it, every call answers 403 feature_disabled. See rate-limit headers and Refusals at 403.

Info

This API serves the modern binding shape: a measure over tracked metrics, with joined metric details so you can render the binding sentence without a follow-up call. It does not return the older legacy metric-type reshaping.

Org-wide bindings have no extra admin gate here

The AvsB dashboard requires an organization admin to create, edit, or delete an org-wide binding. This API does not add that check: any token carrying metrics:write can create, promote, demote, or delete an org-wide binding. Grant that permission only to tokens you trust with it.

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 bindingsmetrics:read
Create / update / delete a bindingmetrics:write

The binding shape

Every endpoint that returns a single binding wraps it in { "data": <binding> }. The list endpoint returns the same full shape for each item, under { "data": [<binding>], "page": { ... } }.

JSON
{  "data": {    "id": "<metricBindingId>",    "shortId": 7,    "orgId": "<orgId>",    "projectId": "<projectId>",    "name": "Add to cart rate",    "description": null,    "measure": "UNIQUE_CONVERSIONS_PER_VISITOR",    "direction": "INCREASE",    "isLibrary": true,    "metricId": "<metricId>",    "numeratorMetricId": null,    "denominatorMetricId": null,    "percentile": null,    "valueSource": null,    "winsorization": null,    "composition": null,    "archivedAt": null,    "createdAt": "2026-06-18T00:00:00.000Z",    "updatedAt": "2026-06-18T00:00:00.000Z",    "createdByUserId": "<userId>",    "metric": {      "id": "<metricId>",      "shortId": 3,      "projectId": "<projectId>",      "name": "Add to cart click",      "description": null,      "type": "CLICK",      "cssSelector": ".add-to-cart",      "urlMatch": null,      "urlMatchMode": null,      "pageUrl": null,      "eventKey": null,      "hasValueField": false,      "archivedAt": null    },    "numeratorMetric": null,    "denominatorMetric": null  }}
JSON41 lines

Field notes

  • measure is one of UNIQUE_CONVERSIONS_PER_VISITOR, TOTAL_EVENTS, UNIQUE_VISITORS_WHO_FIRED, TOTAL_VALUE_PER_VISITOR, TOTAL_VALUE, PERCENTILE, RATE, or COMPOSITE. The measure decides which metric fields are required (see below).
  • metric / numeratorMetric / denominatorMetric are the joined tracked-metric rows. Single-metric measures populate metric; a RATE populates the numerator/denominator pair; a COMPOSITE references its components through composition instead, and leaves all three joined fields null.
  • percentile applies to the PERCENTILE measure (a number greater than 0 and less than 100, or null).
  • winsorization, when set, is { "enabled": boolean, "upperPercentile": number, "lowerPercentile": number, "scope": "POOLED" | "PER_ARM" }. Defaults are 99, 0, and "POOLED"; lowerPercentile must be strictly less than upperPercentile.
  • composition, for a COMPOSITE binding, is a list of { "metricId": string, "weight": number } entries: 2 to 10 of them, weights summing to a positive number, no repeated metricId.
  • archivedAt reflects whether the binding is archived elsewhere in AvsB. This API has no endpoint that sets it: PATCH does not accept an archivedAt field, and DELETE removes the row outright rather than archiving it.

List metric bindings

GET /api/v1/metric-bindings: every binding in the organization, newest first. Cursor-paginated.

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

Pass ?limit= (1 to 100, default 20) and the opaque ?cursor= from the previous page's page.nextCursor to walk through results. Each item in data is the full binding shown under The binding shape above, joined metric and all, not a trimmed summary.

Response
{  "data": [    {      "id": "<metricBindingId>",      "shortId": 7,      "orgId": "<orgId>",      "projectId": "<projectId>",      "name": "Add to cart rate",      "description": null,      "measure": "UNIQUE_CONVERSIONS_PER_VISITOR",      "direction": "INCREASE",      "isLibrary": true,      "metricId": "<metricId>",      "numeratorMetricId": null,      "denominatorMetricId": null,      "percentile": null,      "valueSource": null,      "winsorization": null,      "composition": null,      "archivedAt": null,      "createdAt": "2026-06-18T00:00:00.000Z",      "updatedAt": "2026-06-18T00:00:00.000Z",      "createdByUserId": "<userId>",      "metric": {        "id": "<metricId>",        "shortId": 3,        "projectId": "<projectId>",        "name": "Add to cart click",        "description": null,        "type": "CLICK",        "cssSelector": ".add-to-cart",        "urlMatch": null,        "urlMatchMode": null,        "pageUrl": null,        "eventKey": null,        "hasValueField": false,        "archivedAt": null      },      "numeratorMetric": null,      "denominatorMetric": null    }  ],  "page": { "nextCursor": null, "hasMore": false }}
JSON44 lines

Create a metric binding

POST /api/v1/metric-bindings: create a binding. The projectId key is required, but its value may be null. Pass a project id to home the binding there, or null for an org-wide binding usable across every project. Omitting the key entirely is a validation error: the scope choice is always explicit.

curl -X POST https://app.avsb.cloud/api/v1/metric-bindings \  -H "Authorization: Bearer avsb_svc_..." \  -H "Content-Type: application/json" \  -H "Idempotency-Key: $(uuidgen)" \  -d '{    "measure": "UNIQUE_CONVERSIONS_PER_VISITOR",    "name": "Add to cart rate",    "projectId": "<projectId>",    "metricId": "<metricId>",    "direction": "INCREASE"  }'
Shell11 lines

The required fields depend on measure:

  • Single-metric (UNIQUE_CONVERSIONS_PER_VISITOR, TOTAL_EVENTS, UNIQUE_VISITORS_WHO_FIRED, TOTAL_VALUE_PER_VISITOR, TOTAL_VALUE, PERCENTILE): require metricId. Value measures also accept valueSource (defaults to "value"); PERCENTILE also requires percentile.
  • RATE: requires numeratorMetricId and denominatorMetricId.
  • COMPOSITE: requires composition, a list of 2 to 10 { "metricId": ..., "weight": ... } entries whose weights sum to a positive number.

direction defaults to "INCREASE" and isLibrary defaults to true. Returns 201 with { "data": <binding> }. This endpoint is idempotent: replay the same request with the same Idempotency-Key and it changes nothing the second time.

422: composite needs at least 2 components
{  "error": {    "code": "validation_failed",    "message": "composite must have at least 2 components",    "docUrl": "https://docs.avsb.cloud/docs/developer-reference/public-api/conventions#validation-errors",    "requestId": "req_9f2c41ab7e0b4d1e8c35a6f0d2b91e77"  }}
JSON8 lines

Get a metric binding

GET /api/v1/metric-bindings/{metricBindingId}

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

Returns the same shape as The binding shape above.

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

A binding in another org resolves the same way: 404, so a cross-org id never reveals whether it exists.

Update a metric binding

PATCH /api/v1/metric-bindings/{metricBindingId}: change any field. Send only the fields you want to change.

curl -X PATCH https://app.avsb.cloud/api/v1/metric-bindings/<metricBindingId> \  -H "Authorization: Bearer avsb_svc_..." \  -H "Content-Type: application/json" \  -H "Idempotency-Key: $(uuidgen)" \  -d '{ "name": "Add to cart rate (mobile)" }'
Shell5 lines

Returns { "data": <binding> }. Changing a measure or its metric references re-runs the same validation as create, for example the RATE and COMPOSITE checks above. This endpoint is idempotent.

Promote and demote (changing scope)

projectId is an ordinary PATCH field, so scope changes reuse this endpoint: there are no separate promote/demote routes. Send "projectId": null to promote the binding to org-wide, or a different project id to demote or move it.

A demote (or move) is rejected with 409 when the binding is still referenced from other projects: experiments that attach it, or composite bindings that name it as a component, outside the target project. The response lists every blocker:

409: still referenced elsewhere
{  "error": {    "code": "validation_failed",    "message": "Cannot move this metric binding to a project while it is referenced by other projects",    "details": {      "blockers": [        { "kind": "experiment", "id": "<experimentId>", "name": "Homepage hero", "projectId": "<otherProjectId>" }      ]    },    "docUrl": "https://docs.avsb.cloud/docs/developer-reference/public-api/conventions#conflict-errors",    "requestId": "req_9f2c41ab7e0b4d1e8c35a6f0d2b91e77"  }}
JSON13 lines

Delete a metric binding

DELETE /api/v1/metric-bindings/{metricBindingId}

curl -X DELETE https://app.avsb.cloud/api/v1/metric-bindings/<metricBindingId> \  -H "Authorization: Bearer avsb_svc_..." \  -H "Idempotency-Key: $(uuidgen)"
Shell3 lines
Response
{ "data": { "id": "<metricBindingId>", "deleted": true } }
JSON1 line

A binding that is still attached to an experiment cannot be deleted:

400: still attached to an experiment
{  "error": {    "code": "validation_failed",    "message": "Cannot delete: this metric is still attached to experiments",    "details": { "experimentsReferencing": [{ "experimentId": "<experimentId>", "experimentName": "Homepage hero" }] },    "docUrl": "https://docs.avsb.cloud/docs/developer-reference/public-api/conventions#validation-errors",    "requestId": "req_9f2c41ab7e0b4d1e8c35a6f0d2b91e77"  }}
JSON9 lines
Delete does not check composite bindings

Moving or demoting a binding blocks the request when another composite binding still names it as a component (the 409 above). Deleting does not run that same check: only experiment attachments block a delete. Deleting a binding that another composite binding still uses as a component leaves that composite with a dangling reference. Check for composite users yourself before deleting a binding you expect others to reuse.

Next steps

Was this helpful?