Inspector Issues
Read Inspector issues and observed event shapes over HTTP
Two GET endpoints expose Inspector data over HTTP: a single issue, and the observed event shapes (“variations”) behind an issue. Both are addressed by an issueId, which you’ll find 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 path | Returns | Reach for it when |
|---|---|---|
GET /workspaces/:workspaceId/inspector/issues/v3/:issueId | A single issue | You need per-app-version counts for one issue, or a window other than 24 hours. |
GET /workspaces/:workspaceId/inspector/issues/:issueId/variations | The event shapes behind an issue, as JSON or CSV | You 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.
| Code | Body | Condition |
|---|---|---|
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, unlike the error key the endpoints themselves use for 400, 404 and 500.
Every lookup is scoped to your workspace, so an issueId that does not belong to it returns 404 — the same response as an id that does not exist at all.
Rate limits
No rate limit is currently enforced on these endpoints, but Avo may introduce one. Build retries with backoff into your integration and handle 429.
Your first call
Once you have a credential and an issueId, the event shapes behind that issue are a single request:
$ curl -H "authorization: Basic <Base64 encoded token>" \
-X GET "https://api.avo.app/workspaces/:workspaceId/inspector/issues/:issueId/variations"That returns {"variations": [...], "variationsTruncated": false} — every shape that event was seen in over the last 24 hours, each flagged with whether it is one of the shapes 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 behaviors are not visible anywhere in the response body, and each one produces a plausible-looking but wrong integration when it is assumed away. A fifth applies to the variations endpoint only: property names are the names the SDK sent.
Data freshness and time windows
The variations endpoint always looks back a fixed 24 hours — there is no parameter to widen or shift it. The single-issue endpoint is the one that takes a window, via its time parameter.
Do not use these endpoints to verify a deploy you just shipped.
Expect roughly an hour of lag on the counts, and on the variations endpoint the most recent hour is not visible at all — a deploy 20 minutes old shows nothing there. If you are validating an implementation as you ship it, use the Inspector Debugger instead.
eventCount is not “events affected by this issue”
eventCount is the total volume of that event on that source in the window — every shape, healthy ones included. issueCount is the violating subset.
The number worth reporting is the ratio between them. issueCount: 1428 against eventCount: 96204 is a 1.5% violation rate on a high-volume event; reading eventCount as “affected events” overstates the blast radius by two orders of magnitude.
issueId is a handle, sharedIssueId is the identity
issueId identifies one issue on one source, and it is not stable. It changes when the event or property behind the issue is edited in the tracking plan, and when a newly observed runtime type is added to an InconsistentType issue. When it changes, the old issue stops being updated with its original firstSeen, and a new issue starts with no history.
Do not persist issueId as a long-lived key. Treat it as a handle valid within one response or one session — safe to pass straight to /variations, not safe to store in your own database as durable identity.
sharedIssueId is the stable identity. It leaves out the source, which is what groups one logical problem across several sources, and for InconsistentType it is unaffected by newly observed types. For the other issue types it still changes when the event or property behind the issue is edited in the tracking plan.
Variant attribution is not available
Nothing in either response — JSON or CSV — tells you which event variant Inspector matched against, and it cannot be recovered from the id. If your tracking plan leans on variants, expect to reconcile variant identity yourself.
Retrieving a single issue
GET https://api.avo.app/workspaces/:workspaceId/inspector/issues/v3/:issueIdReturns one issue with its counts broken down per app version, over a window you choose. Reach for it when you need to know which release a problem is concentrated in, or when 24 hours is the wrong window.
Query parameters
| Parameter | Type | Required | Default when omitted | Accepted values | On invalid input |
|---|---|---|---|---|---|
time | string | Optional | 24h | Matches ^(\d+)([hd])$, case-insensitive — for example 12h, 7d, 30D | Silently coerced to 24 hours. No 400. |
There is no format, no filtering and no pagination on this endpoint.
Response
A single object, not wrapped in an envelope.
| Field | Type | Notes |
|---|---|---|
issueId | string, never null | See issueId is a handle above. |
sharedIssueId | string, never null | Stable identity across sources. |
schemaId | string | Your workspace ID. |
sourceId | string | A single source — an issue row is per-source. |
eventName | string | The event name as observed. |
propertyName | string | null | null for event-level issue types. |
issueType | object | Tagged union, see below. |
oldestAppVersion | string | |
newestAppVersion | string | |
firstSeen | string (ISO 8601) | Earliest first-seen for this issue. |
lastSeen | string (ISO 8601) | Max last-seen across versions. |
issueCount | number | Occurrences that violated, summed across versions. |
eventCount | number | Total occurrences of that event on that source, all shapes including healthy ones, summed across versions. |
appVersions | object | A dictionary keyed by version string, not an array. Each value is {"appVersion": string, "issueCount": number, "eventCount": number, "lastSeen": string | null}. |
issueStatus | object | {status, updatedAt: string | null, updatedBy: string | null} |
regression | boolean | Always present. true when this issue had been marked Resolved and was then observed again — see below. |
branchIds | string[] | Always present; [] when the issue is not linked to any branch. |
issueType
A tagged union: type plus a payload key. The concepts behind each type are documented in 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"] } }Every payload key is camelCase except PropertyTypeInconsistentWithTrackingPlan, whose payload key repeats the PascalCase type name. eventId inside that payload is nullable; the other payloads’ ids are not.
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"}.
Note the naming shift between the label you set in the Avo web app and the value you read back:
| Avo web app label | issueStatus.status.type |
|---|---|
| Unresolved | Unresolved |
| Ignore | Ignored |
| Resolved | Resolved |
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 had marked Resolved is observed again past the point at which it was supposed to be fixed. Inspector then moves the issue back to Unresolved and flags it. “Past the point it was supposed to be fixed” is exactly the validateIn recorded on the resolution:
validateIn | Regresses 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 |
Never | never — the issue is not reopened and never flagged |
Two things to code around:
Ignoreddoes not produce a regression. An ignored issue that resurfaces is also moved back toUnresolved, butregressionstaysfalse. OnlyResolvedsets it.- The flag is cleared the moment anyone sets the status manually again, to any value. A newly created issue is never a regression.
Read regression together with issueStatus.status: the Avo web app only surfaces it while the status is Unresolved, which is the only state it is meaningful in.
Status codes
| Code | Body | Condition |
|---|---|---|
200 | The issue object | At least one row matched. |
401 | See authentication | Missing 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 exact casing. |
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/variationsA variation is one observed shape of an event: a distinct combination of property names and property types, per app version, per source. The single-issue endpoint tells you that an event is wrong and how often; this endpoint tells you how it is wrong, by returning every shape that event was seen in alongside a causingIssue flag and an occurrence count.
Put the causing shape next to the healthy one and the diff — a property missing here, a type differing there, and the volume split between them — is usually the whole story. Available as JSON or, with ?format=csv, as a two-section CSV built for exactly that diff.
Query parameters
| Parameter | Type | Required | Default when omitted | Accepted values | On invalid input |
|---|---|---|---|---|---|
format | string | Optional | json | csv, case-insensitive | Anything else — including "" and xml — returns JSON. Never errors. |
sourceId | string | Optional | No source filter | One exact source ID | Blank or whitespace means no filter. |
appVersion | string | Optional | No version filter | One exact app version | Blank or whitespace means no filter. |
sourceId and appVersion take single values only. The filters are exact matches, so ?sourceId=a,b matches the literal string "a,b" and returns nothing, and repeating a parameter — ?sourceId=a&sourceId=b — is silently ignored with no error.
The issue’s own source is not applied as a filter. Without ?sourceId=, you get variations of that event name across every source in the workspace, not just the source the issue was reported on. If you want the issue’s own source, pass its sourceId explicitly.
Staying under the 400-row cap
The response is capped at 400 rows, and one logical event shape yields one row per app version per source. An event with modest shape diversity across several versions and sources reaches the cap on cardinality alone.
variationsTruncated: true means you reached that cap and the response is a partial view of the shapes for this event. The cap is applied before you can filter client-side, so the shape you care about may already have been cut. Narrow the query itself instead: pass ?sourceId= — taking the sourceId from the issue — and ?appVersion= to scope the response to the source and release you care about, then read the flag again to confirm you are now under the cap.
Response
The envelope is {"variations": [...], "variationsTruncated": bool}, where variationsTruncated is true if the response hit the 400-row cap. Each row has exactly these 13 fields:
| Field | Type | Notes |
|---|---|---|
eventVariationKey | string | Identifies this shape on this source and app version. |
causingIssue | boolean | Whether this shape is one of the shapes causing the issue you asked about. |
count | number | Occurrences of this shape in the window. Sampling-adjusted, not a raw tally — on a sampled source this is an extrapolated estimate, so treat it as one when comparing against counts from your own systems. |
eventName | string | The event name as observed. |
sourceId | string | The Avo Source ID. |
schemaId | string | Your workspace ID. |
appVersion | string | null | Always populated on this endpoint. |
minCreatedAt | string | null | ISO 8601. null when the timestamp is unavailable. |
maxCreatedAt | string | null | ISO 8601, same fallback. |
eventKey | string | null | Internal grouping value with no integration use. |
sourceKey | string | null | Internal 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. |
propertyNameSignature | string[] | Observed property names, sorted by name. |
propertyTypeSignature | string[] | Observed property types. Strictly 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. If you diff these against tracking-plan property names, reconcile naming conventions first or you will report false discrepancies.
Names that look like data are redacted for privacy and surface as the literals <Object redacted by Avo>, <ID string redacted by Avo> and <URL redacted by Avo>. Redaction can map two distinct names onto the same placeholder, so propertyNameSignature is not guaranteed to be free of duplicates. In the CSV those duplicates collapse into a single column and the last type wins.
The window here is a fixed 24 hours, and the most recent hour is not visible — see data freshness and time windows above.
CSV output
?format=csv returns the same rows shaped for diffing: causing shapes in one section, healthy shapes in another, with one column per property name so the two halves line up column for column. Reach for it when you want to eyeball a shape difference or hand the result to 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:
- Line 0 is always the truncation marker, emitted for both verdicts:
# variationsTruncated: trueor# variationsTruncated: false. # Variations causing the issue, then a header line, then the causing rows.# Variations not causing the issue, then the same header line again, then the remaining rows.
Both section headers are emitted even when a section is empty. Causing rows come first.
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: the union of propertyNameSignature across all rows, deduped in first-appearance order, with the causing rows scanned first. causing_issue is an explicit column rendered true / false.
Each property cell holds the type of that property in that row, and an empty cell when the row does not carry the property. A cell can also hold the literal unknown, meaning the row supplied the name but no type at that position — worth handling if you parse strictly. Date cells fall back to an empty cell rather than failing on an invalid timestamp.
Quoting: every cell — including the header line — is wrapped in double quotes, except an empty string, which stays bare. Internal " is doubled. A cell starting with =, +, -, @, tab, CR or LF is prefixed with ' as a CSV injection guard.
# 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 that example the second causing row has no payment_method property, so its cell is bare.
Status codes
| Code | Body | Condition |
|---|---|---|
200 | JSON or CSV | Success. |
400 | {"error": "Invalid request"} | A malformed route parameter — for example a duplicated :issueId. Checked before authentication. |
401 | See authentication | Missing 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
}The two shapes carry the same property names and differ only in the type of revenue — string on the shape causing the issue, float on the healthy one. That diff, plus the count ratio, is what these endpoints are for.
What’s next?
Now that you can read issues over HTTP, the conceptual docs explain what you are looking at and what to do about it:
- Issue types in Inspector — what each
issueTypedetects, in the same language the Avo web app uses. - Inspector issues view — the same issues in the Avo web app, including issue statuses and regressions.
- Fixing issues found in Inspector — turning a variation diff into a tracking plan or implementation change.
- Authentication — creating a service account and building the
Authorizationheader.