ReferenceAvo Public APIInspector Issues

Inspector Issues

Read Inspector issues and observed event shapes over HTTP

Two GET endpoints let you read Inspector data over HTTP: one returns a single issue, the other returns the event shapes (“variations”) behind it. Both take an issueId. You’ll find one in the Avo web app URL when you open an issue: https://www.avo.app/schemas/{workspaceId}/inspector/issues/ii/{issueId}.

The base URL for the Avo public API is https://api.avo.app.

Endpoints

Method and pathReturnsUse it when
GET /workspaces/:workspaceId/inspector/issues/v3/:issueIdA single issueYou need per-app-version counts for one issue, or a window other than 24 hours.
GET /workspaces/:workspaceId/inspector/issues/:issueId/variationsThe event shapes behind an issue, as JSON or CSVYou want to see exactly what the event looked like when it triggered the issue.

:workspaceId is the ID of your workspace. You’ll find it in the URL of your Avo tab after /schemas/. It is also returned as schemaId on every response object.

Authentication

Both endpoints require an authorization header containing a Base64 encoded service account name and secret.

CodeBodyCondition
401{"message": "Authorization header missing"}No Authorization header at all.
401{"message": "Invalid authorization"}A bad secret, an unknown service account, or a service account that is not registered in this workspace.

Authentication errors use a message key. The endpoints themselves use error for 400, 404 and 500.

Lookups are scoped to your workspace. An issueId from another workspace returns 404, the same as an id that doesn’t exist.

Rate limits

Treat these endpoints as rate limited. We don’t throttle them today, but the rest of the Avo public API soft-enforces 1 request per second per service account. Design for that, and your integration won’t break if we start enforcing a limit here too.

Retry on 429 Too Many Requests instead of failing. Back off exponentially, and respect Retry-After if it’s there. Poll on a schedule, not in a tight loop.

If you need a higher sustained request rate, reach out.

Your first call

With a credential and an issueId, one request gets you the event shapes behind that issue:

$ curl -H "authorization: Basic <Base64 encoded token>" \
       -X GET "https://api.avo.app/workspaces/:workspaceId/inspector/issues/:issueId/variations"

You get back {"variations": [...], "variationsTruncated": false}: every shape that event was seen in over the last 24 hours, each flagged with whether it’s causing the issue. From there you can narrow the response to one source and app version, or switch it to CSV, or read the issue’s own counts broken down per app version.

Before you integrate

Four things about this data aren’t visible in the response body. Get any of them wrong and your integration will look correct while reporting the wrong numbers. A fifth applies only to the variations endpoint: property names are the names the SDK sent.

Data freshness and time windows

The variations endpoint always looks back 24 hours. There’s no parameter to widen or shift that window. Only the single-issue endpoint lets you pick one, with time.

⚠️

Don’t use these endpoints to check a deploy you just shipped.
Counts lag by about an hour, and the variations endpoint can’t see the most recent hour at all, so a deploy that went out 20 minutes ago shows nothing. To validate an implementation as you ship it, use the Inspector Debugger.

eventCount is not “events affected by this issue”

eventCount is the total volume of that event on that source in the window, counting every shape including the healthy ones. issueCount is just the part that violated.

What you usually want is the ratio. issueCount: 1428 out of eventCount: 96204 is a 1.5% violation rate. Read eventCount as “events affected” and you’ll report a problem about 70 times bigger than it is.

Which id to store

issueId identifies one issue on one source, but it isn’t stable. Editing the event or property behind the issue in your tracking plan changes it, and so does a new runtime type showing up on an InconsistentType issue. When it changes you get a new issue with no history, and the old one stops updating.

⚠️

Don’t store issueId as a long-lived key. Holding it for a response or a session is fine, and passing it straight to /variations is fine. Keying your own database on it isn’t.

sharedIssueId is the stable one. It ignores the source, so the same problem on three sources shares a single sharedIssueId, and new runtime types don’t change it on InconsistentType issues. It does still change on other issue types if you edit the event or property in your tracking plan.

Variant attribution is not available

Neither response tells you which event variant Inspector matched against, in JSON or in CSV, and you can’t work it out from the id. If you use variants, you’ll have to match them up yourself.

Retrieving a single issue

GET https://api.avo.app/workspaces/:workspaceId/inspector/issues/v3/:issueId

Returns one issue with its counts broken down per app version, over a window you choose. Use it when you need to know which release a problem is concentrated in, or when 24 hours is the wrong window.

Query parameters

ParameterTypeRequiredDefault when omittedAccepted valuesOn invalid input
timestringOptional24hMatches ^(\d+)([hd])$, case-insensitive — for example 12h, 7d, 30DSilently coerced to 24 hours. No 400.

This endpoint has no format, filtering or pagination.

Response

A single object, with no envelope around it.

FieldTypeNotes
issueIdstring, never nullSee which id to store above.
sharedIssueIdstring, never nullStable identity across sources.
schemaIdstringYour workspace ID.
sourceIdstringA single source. Each issue row covers one source.
eventNamestringThe event name as observed.
propertyNamestring | nullnull for event-level issue types.
issueTypeobjectTagged union, see below.
oldestAppVersionstring
newestAppVersionstring
firstSeenstring (ISO 8601)Earliest first-seen for this issue.
lastSeenstring (ISO 8601)Max last-seen across versions.
issueCountnumberOccurrences that violated, summed across versions.
eventCountnumberTotal occurrences of that event on that source, all shapes including healthy ones, summed across versions.
appVersionsobjectA dictionary keyed by version string, not an array. Each value is {"appVersion": string, "issueCount": number, "eventCount": number, "lastSeen": string | null}.
issueStatusobject{status, updatedAt: string | null, updatedBy: string | null}
regressionbooleanAlways present. true when this issue had been marked Resolved and was then observed again — see below.
branchIdsstring[]Always present; [] when the issue is not linked to any branch.

issueType

A tagged union: a type plus a payload key. For what each type means, see issue types in Inspector.

{ "type": "EventNotInTrackingPlan" }
{ "type": "UnexpectedEvent" }
{ "type": "MissingExpectedProperty", "missingExpectedProperty": { "eventId": "...", "propertyId": "...", "propertyName": "..." } }
{ "type": "PropertyTypeInconsistentWithTrackingPlan", "PropertyTypeInconsistentWithTrackingPlan": { "eventId": "..." , "propertyId": "...", "propertyName": "...", "expectedPropertyType": "...", "actualPropertyType": "..." } }
{ "type": "UnexpectedProperty", "unexpectedProperty": { "eventId": "...", "propertyName": "...", "propertyType": "..." } }
{ "type": "InconsistentType", "inconsistentType": { "propertyName": "...", "propertyTypes": ["string", "int"] } }

Payload keys are camelCase, except on PropertyTypeInconsistentWithTrackingPlan, where the key repeats the PascalCase type name. Its eventId can be null; the ids in the other payloads can’t.

issueStatus.status

{ "type": "Unresolved" }
{ "type": "Ignored",  "validateIn": { "type": "NextAppVersion", "appVersion": "8.15.0" } }
{ "type": "Resolved", "validateIn": { "type": "Never" } }

validateIn is one of {"type":"CurrentAppVersion","appVersion":string}, {"type":"NextAppVersion","appVersion":string}, {"type":"CustomAppVersion","appVersion":string}, {"type":"Date","date":ISO 8601} or {"type":"Never"}.

The label you set in the Avo web app doesn’t always match the value you read back:

Avo web app labelissueStatus.status.type
UnresolvedUnresolved
IgnoreIgnored
ResolvedResolved

An issue that has never had a status set reads as Unresolved. See issue status for what each one means.

regression

regression is true when an issue someone marked Resolved shows up again after the point it was meant to be fixed. Inspector moves it back to Unresolved and sets the flag. What counts as “after the point it was meant to be fixed” comes from the validateIn recorded when it was resolved:

validateInRegresses when the newly observed variation is
CurrentAppVersion(v)on app version ≥ v
CustomAppVersion(v)on app version ≥ v
NextAppVersion(v)on app version strictly > v
Date(t)seen after t
Nevernever — the issue is not reopened and never flagged

Two things to watch for:

  • Ignored never produces a regression. An ignored issue that comes back also moves to Unresolved, but regression stays false. Only Resolved sets it.
  • Setting the status manually clears the flag, whatever you set it to. A new issue is never a regression.

Read regression alongside issueStatus.status. The Avo web app only shows it while the status is Unresolved, which is the only state where it means anything.

Status codes

CodeBodyCondition
200The issue objectAt least one row matched.
401See authenticationMissing or invalid credential.
404{"error": "Issue Not found"}No issue with this id in your workspace. Covers an unknown id, a malformed id, and an id belonging to a different workspace. Note the capital N in Not.
500{"error": "Internal Server Error"}Server error.

There is no 400 on this endpoint.

Example

Request

$ curl -H "authorization: Basic <Base64 encoded token>" \
       -X GET "https://api.avo.app/workspaces/hAtPI0dEsq/inspector/issues/v3/2f1c9b8e4d7a05c3e6b1a94f8d2c70b5e93a17d4c8f0b62a5d1e7c3948fb0a26?time=7d"

Response

{
  "issueId": "2f1c9b8e4d7a05c3e6b1a94f8d2c70b5e93a17d4c8f0b62a5d1e7c3948fb0a26",
  "sharedIssueId": "8b4d0f6a1c93e57204ab8d1f6e3c9057b24da8f1093c6e5b7d20a41fc8e93b56",
  "schemaId": "hAtPI0dEsq",
  "sourceId": "9Zq7YAo0R",
  "eventName": "Checkout Completed",
  "propertyName": "revenue",
  "issueType": {
    "type": "PropertyTypeInconsistentWithTrackingPlan",
    "PropertyTypeInconsistentWithTrackingPlan": {
      "eventId": "yT2rKpQ4Xa",
      "propertyId": "Bv8nLm1Zq0",
      "propertyName": "revenue",
      "expectedPropertyType": "float",
      "actualPropertyType": "string"
    }
  },
  "oldestAppVersion": "8.13.1",
  "newestAppVersion": "8.14.2",
  "firstSeen": "2026-08-11T09:42:18.000Z",
  "lastSeen": "2026-08-24T06:00:00.000Z",
  "issueCount": 9871,
  "eventCount": 644390,
  "appVersions": {
    "8.13.1": {
      "appVersion": "8.13.1",
      "issueCount": 7204,
      "eventCount": 402118,
      "lastSeen": "2026-08-23T21:00:00.000Z"
    },
    "8.14.2": {
      "appVersion": "8.14.2",
      "issueCount": 2667,
      "eventCount": 242272,
      "lastSeen": "2026-08-24T06:00:00.000Z"
    }
  },
  "issueStatus": {
    "status": { "type": "Unresolved" },
    "updatedAt": null,
    "updatedBy": null
  },
  "regression": false,
  "branchIds": []
}

Listing event variations

GET https://api.avo.app/workspaces/:workspaceId/inspector/issues/:issueId/variations

A variation is one observed shape of an event: a particular combination of property names and types, per app version, per source. The single-issue endpoint tells you an event is wrong and how often. This one shows you what was actually sent, returning every shape the event was seen in with a causingIssue flag and a count.

Comparing a causing shape against a healthy one usually shows you the problem: a missing property, a type that changed, and how the volume splits between them. Use ?format=csv for a two-section CSV laid out for that comparison.

Query parameters

ParameterTypeRequiredDefault when omittedAccepted valuesOn invalid input
formatstringOptionaljsoncsv, case-insensitiveAnything else — including "" and xml — returns JSON. Never errors.
sourceIdstringOptionalNo source filterOne exact source IDBlank or whitespace means no filter.
appVersionstringOptionalNo version filterOne exact app versionBlank or whitespace means no filter.

sourceId and appVersion take one value each and match exactly. ?sourceId=a,b looks for a source literally named a,b and finds nothing. Repeating a parameter, as in ?sourceId=a&sourceId=b, is ignored without an error.

⚠️

The issue’s own source isn’t applied as a filter. Without ?sourceId=, you get that event name across every source in your workspace, not just the source the issue was reported on. Pass the issue’s sourceId if that’s what you want.

Staying under the 400-row cap

The response is capped at 400 rows, and each event shape produces one row per app version per source. An event with only a handful of shapes can still hit the cap if it’s live across a lot of versions and sources.

variationsTruncated: true means you hit that cap and you’re only seeing part of the picture. The cap applies before anything reaches you, so filtering the response afterwards won’t bring back a shape that was cut. Narrow the query instead: pass ?sourceId= (use the issue’s own source) and ?appVersion=, then check the flag again.

Response

The envelope is {"variations": [...], "variationsTruncated": bool}. variationsTruncated is true if the response hit the 400-row cap. Each row has these 13 fields:

FieldTypeNotes
eventVariationKeystringIdentifies this shape on this source and app version.
causingIssuebooleanWhether this shape is one of the shapes causing the issue you asked about.
countnumberOccurrences of this shape in the window. Sampling-adjusted, not a raw tally. On a sampled source it’s an estimate, so expect it to differ from counts in your own systems.
eventNamestringThe event name as observed.
sourceIdstringThe Avo Source ID.
schemaIdstringYour workspace ID.
appVersionstring | nullAlways populated on this endpoint.
minCreatedAtstring | nullISO 8601. null when the timestamp is unavailable.
maxCreatedAtstring | nullISO 8601, same fallback.
eventKeystring | nullInternal grouping value with no integration use.
sourceKeystring | nullInternal grouping value with no integration use — it is not the same value as sourceId. Use sourceId for anything that has to match an Avo Source.
propertyNameSignaturestring[]Observed property names, sorted by name.
propertyTypeSignaturestring[]Observed property types. Parallel to propertyNameSignature: same length, same order, so propertyTypeSignature[i] is the type of propertyNameSignature[i].

Property names are the names the SDK sent

⚠️

propertyNameSignature holds the names the SDK actually sent, not tracking-plan names. Line these up against your tracking-plan property names before comparing them, or you’ll report differences that aren’t real.

Names that look like data are redacted for privacy and come back as <Object redacted by Avo>, <ID string redacted by Avo> or <URL redacted by Avo>. Two different names can redact to the same placeholder, so propertyNameSignature can contain duplicates. In the CSV they collapse into one column and the last type wins.

The window here is a fixed 24 hours, and the most recent hour isn’t visible. See data freshness and time windows above.

CSV output

?format=csv returns the same rows arranged for comparison: causing shapes in one section, healthy shapes in another, with one column per property name so the two halves line up. Use it when you want to eyeball a difference or open the result in a spreadsheet rather than parse it.

The response is text/csv; charset=utf-8, lines joined with \n and no trailing newline. The structure is fixed:

  1. Line 0 is always the truncation marker, present either way: # variationsTruncated: true or # variationsTruncated: false.
  2. # Variations causing the issue, then a header line, then the causing rows.
  3. # Variations not causing the issue, then the same header line again, then the remaining rows.

Causing rows come first, and both section headers appear even when a section is empty.

Columns, in order:

event_variation_key, causing_issue, count, event_name, source_id, app_version,
min_created_at, max_created_at

…followed by one column per property name: every name in propertyNameSignature across all rows, in the order they first appear, with the causing rows scanned first. causing_issue is its own column, rendered true / false.

Each property cell holds the type of that property in that row, or is empty if the row doesn’t carry the property. A cell can also read unknown, meaning the row gave a name but no type at that position, so handle it if you parse strictly. Date cells come back empty rather than failing on an invalid timestamp.

Every cell is wrapped in double quotes, including the header line. Empty cells stay bare, and an internal " is doubled. A cell starting with =, +, -, @, tab, CR or LF gets a ' in front of it to guard against CSV injection.

# variationsTruncated: false
# Variations causing the issue
"event_variation_key","causing_issue","count","event_name","source_id","app_version","min_created_at","max_created_at","currency","payment_method","revenue"
"5d2b81f0a37c94e618df05b2c7a3e9410fb86d24c503a1e79b0d4f6238ca7e15","true","1428","Checkout Completed","9Zq7YAo0R","8.14.2","2026-08-23T07:00:00.000Z","2026-08-24T06:00:00.000Z","string","string","string"
"b0f47ac125d3e896402fc7b13a5d90e648127cf3ab05d9e7261340bfc85a92d6","true","96","Checkout Completed","9Zq7YAo0R","8.13.1","2026-08-23T07:00:00.000Z","2026-08-24T05:00:00.000Z","string",,"string"
# Variations not causing the issue
"event_variation_key","causing_issue","count","event_name","source_id","app_version","min_created_at","max_created_at","currency","payment_method","revenue"
"e93c4a70b1d582f6047ae3c9128d5b0f76a2e841c30f9b57d6812ac4053e7fb9","false","94776","Checkout Completed","9Zq7YAo0R","8.14.2","2026-08-23T07:00:00.000Z","2026-08-24T06:00:00.000Z","string","string","float"

In the example above, the second causing row has no payment_method property, so that cell is empty.

Status codes

CodeBodyCondition
200JSON or CSVSuccess.
400{"error": "Invalid request"}A malformed route parameter — for example a duplicated :issueId. Checked before authentication.
401See authenticationMissing or invalid credential.
404{"error": "Issue not found"}No issue with this id in your workspace. Note the lowercase not found, unlike the single-issue endpoint’s Issue Not found.
500{"error": "Internal Server Error"}Server error.

Example

Request

$ curl -H "authorization: Basic <Base64 encoded token>" \
       -X GET "https://api.avo.app/workspaces/hAtPI0dEsq/inspector/issues/2f1c9b8e4d7a05c3e6b1a94f8d2c70b5e93a17d4c8f0b62a5d1e7c3948fb0a26/variations?sourceId=9Zq7YAo0R&appVersion=8.14.2"

Response

{
  "variations": [
    {
      "eventVariationKey": "5d2b81f0a37c94e618df05b2c7a3e9410fb86d24c503a1e79b0d4f6238ca7e15",
      "causingIssue": true,
      "count": 1428,
      "eventName": "Checkout Completed",
      "sourceId": "9Zq7YAo0R",
      "schemaId": "hAtPI0dEsq",
      "appVersion": "8.14.2",
      "minCreatedAt": "2026-08-23T07:00:00.000Z",
      "maxCreatedAt": "2026-08-24T06:00:00.000Z",
      "eventKey": "a4e1c07b93d5f28601ab7c4e9d0f3b2586c1a97e4f0b3d8c25e6a1470bf9d3c8",
      "sourceKey": "hAtPI0dEsq-9Zq7YAo0R",
      "propertyNameSignature": ["currency", "payment_method", "revenue"],
      "propertyTypeSignature": ["string", "string", "string"]
    },
    {
      "eventVariationKey": "e93c4a70b1d582f6047ae3c9128d5b0f76a2e841c30f9b57d6812ac4053e7fb9",
      "causingIssue": false,
      "count": 94776,
      "eventName": "Checkout Completed",
      "sourceId": "9Zq7YAo0R",
      "schemaId": "hAtPI0dEsq",
      "appVersion": "8.14.2",
      "minCreatedAt": "2026-08-23T07:00:00.000Z",
      "maxCreatedAt": "2026-08-24T06:00:00.000Z",
      "eventKey": "a4e1c07b93d5f28601ab7c4e9d0f3b2586c1a97e4f0b3d8c25e6a1470bf9d3c8",
      "sourceKey": "hAtPI0dEsq-9Zq7YAo0R",
      "propertyNameSignature": ["currency", "payment_method", "revenue"],
      "propertyTypeSignature": ["string", "string", "float"]
    }
  ],
  "variationsTruncated": false
}

Both shapes carry the same property names. The only difference is the type of revenue: string on the shape causing the issue, float on the healthy one. That, together with the count on each row, is usually enough to find the bug.

What’s next?

For what the data means and what to do about it: