API v1.1

Developer API

A CI-native REST API to trigger scans, gate pull requests on dataLayer and compliance regressions, and pull results straight into your pipeline.

View OpenAPI 3.0 spec Enterprise tier

Authentication

Every request carries a project API key, minted in Project Settings → API Keys. Send it as a Bearer token or an X-API-Key header. Each key is scoped; the full key is shown once on creation.

# Preferred
Authorization: Bearer ap_live_xxxxxxxxxxxxxxxx
# Alternative
X-API-Key: ap_live_xxxxxxxxxxxxxxxx

Scopes

ScopeDescription
scan:readList targets, journeys, and scan history; poll a scan and evaluate a CI gate.
scan:writeTrigger ad-hoc scans, re-run saved tests, and cancel a pending/running scan.
upload:writeUpload a CI APK/XAPK artifact for a mobile scan (valid 14 days).

Endpoints

All endpoints are rooted at the API base URL and require the listed scope.

MethodEndpointScopeSummary
GET/api/v1/scansscan:readList scan history
POST/api/v1/scansscan:writeTrigger an ad-hoc scan
GET/api/v1/scans/{id}scan:readPoll a scan + evaluate a CI gate
DELETE/api/v1/scans/{id}scan:writeCancel a pending/running scan
GET/api/v1/targetsscan:readList saved tests
POST/api/v1/targets/{id}/scansscan:writeRe-run a saved test
GET/api/v1/journeysscan:readList saved journeys
POST/api/v1/uploadsupload:writeUpload a CI APK/XAPK

CI gate options

Pass a URL-encoded JSON gate as ?gate= when polling a scan. Predicates combine with OR — the gate fails if any predicate matches. CI should exit non-zero when gate.passed is false.

OptionTypeDescription
minComplianceScorenumber (0–100)Fail when the compliance score is below this threshold (score_below_threshold).
failOnNewDataLayerRegressionsbooleanFail on new, unacknowledged dataLayer regressions (datalayer_regressions).
failOnSeverity"info" | "warning" | "critical"Fail when any diff meets or exceeds this severity (severity_exceeded).
failOnBrokenStepbooleanFail when the journey replay broke mid-flow (status_broken_step).
failOnStatus"failed" | "cancelled" | "broken_step"[]Fail when the scan ends in any of the listed statuses (status_<status>).
failOnNewVendorsbooleanFail when a first-seen vendor appears in the monitored dataLayer (new_vendors).
Non-failing warnings (for example a dataLayer gate requested against a scan with no baseline) are returned in a separate warnings[] array — they never flip gate.passed to false.

Diff detail (include=diffs)

Add ?include=diffs to a scan poll to embed the full unresolved-diff list. The diff is value-blind: it reports each event's parameter names and JS types, never their values.

# Ask for the full unresolved-diff list on a terminal scan:
GET /api/v1/scans/{id}?include=diffs

# Response adds a diffs[] array (value-blind: name + JS-type only):
{
  "status": "completed",
  "gate": { "passed": false, "failures": [ { "code": "new_vendors", "detail": "1 new-vendor diff(s)" } ] },
  "warnings": [],
  "diffs": [
    {
      "stepIndex": 2,
      "elementKey": "event:tiktok::purchase",
      "vendor": "tiktok",
      "diffType": "extra_event",
      "severity": "warning",
      "description": "Event present in the scan but not in the expected schema"
    }
  ]
}
# diffType is one of: missing_event | extra_event | event_drifted | missing_param |
# extra_param | param_type_changed | consent_value_changed. elementKey is
# "event:<vendor>::<name>", "param:<vendor>::<event>::<param>", or
# "consent:<vendor>::<event>::<signal>".

Dry-run preview gating

Point a saved test at a per-PR preview URL to gate the deploy without corrupting the production baseline. The linked journey is replayed as a dry-run: it produces diffs and a score for the gate but writes nothing back to the journey.

# Gate a per-PR preview deploy WITHOUT mutating the saved journey baseline.
# The target's linked journey is replayed against $PREVIEW_URL as a dry-run:
# it still produces diffs + score for the gate, but writes nothing back to the
# journey's expected schema, vendor watch-list, or regressions.
SID=$(curl -fsS -X POST \
  -H "Authorization: Bearer $AP_KEY" \
  -H 'content-type: application/json' \
  -d "{\"url\":\"$PREVIEW_URL\"}" \
  "$BASE/api/v1/targets/$TARGET_ID/scans" | jq -r .scanId)
# 400 journey_required is returned when the target has no journey to replay.

Cursor pagination

List endpoints use keyset pagination. Follow nextCursor until it is null; the legacy truncated flag is retained for back-compatibility.

# Keyset pagination (ordered createdAt DESC, id DESC):
curl -fsS -H "Authorization: Bearer $AP_KEY" \
  "$BASE/api/v1/scans?limit=20" | jq '{next: .nextCursor, count: (.scans | length)}'

# Pass the previous response's nextCursor to fetch the next page;
# nextCursor is null on the final page (truncated is kept for back-compat):
curl -fsS -H "Authorization: Bearer $AP_KEY" \
  "$BASE/api/v1/scans?limit=20&cursor=$NEXT_CURSOR"

Rate limits

Limits are enforced per API key and surfaced on every response.

  • 60 requests / minute per key. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (seconds until the window resets).
  • A 429 response adds Retry-After (seconds).
  • 5 concurrent long-polls per key. Exceeding the cap returns 429 too_many_long_polls with Retry-After: 5.

Idempotency

Trigger and re-run endpoints are safe to retry with an idempotency key.

  • Send Idempotency-Key (1–200 chars) as a header, or an idempotencyKey body field — the header wins.
  • An in-flight duplicate is held for 5 minutes; a completed key replays for 24 hours. A replay returns 200 with idempotentReplay: true.
  • A key identifies one logical operation, scoped per project across both trigger endpoints — reusing the same key for a different call replays the original scan's id rather than starting a new scan, so use a fresh key (e.g. the commit SHA) per distinct scan.
# Make retries safe: repeat the same Idempotency-Key header (or "idempotencyKey"
# body field) and a replay returns HTTP 200 with idempotentReplay=true and the
# original scanId instead of starting a second scan.
curl -fsS -X POST \
  -H "Authorization: Bearer $AP_KEY" \
  -H "Idempotency-Key: $GITHUB_SHA" \
  -H 'content-type: application/json' -d '{}' \
  "$BASE/api/v1/targets/$TARGET_ID/scans"

Error codes

Every error response has a stable shape: an error object carrying code, message, and optional details, plus a top-level requestId. Gate-failure codes appear in gate.failures[].code, not in error.code.

CodeDescription
unauthorizedNo usable credentials were presented.
missing_credentialsNeither an Authorization Bearer token nor an X-API-Key header was sent.
invalid_keyThe API key is not recognised.
key_revokedThe API key has been revoked.
key_expiredThe API key is past its expiry date.
tier_forbiddenThe project tier does not include programmatic API access (Enterprise only).
insufficient_scopeThe key lacks the scope this operation requires.
rate_limitedPer-key request rate limit exceeded — see the Retry-After header.
too_many_long_pollsToo many concurrent long-poll requests for this key (max 5).
invalid_requestThe request body or query failed validation.
not_foundThe referenced scan, target, journey, or upload does not exist for this project.
idempotent_in_progressA prior request with the same Idempotency-Key is still in flight.
not_cancellableThe scan is already terminal and cannot be cancelled.
journey_mismatchThe supplied journeyId does not belong to the target.
journey_requiredA preview-URL gate needs a journey to replay, but the target has none.
no_urlA web/hbbtv scan requires a url.
scan_type_mismatchThe scanType disagrees with the target/upload type.
scan_in_progressThe target already has an active scan.
invalid_targetThe target is not valid for this scanType.
dispatch_failedThe scan could not be enqueued.
internal_errorUnexpected server-side failure — safe to retry.
file_too_largeThe uploaded artifact exceeds the size limit.
package_parse_failedThe APK/XAPK package name could not be parsed.
upload_failedThe artifact could not be stored.
score_below_thresholdgate: compliance score fell below minComplianceScore.
datalayer_regressionsgate: new, unacknowledged dataLayer regressions were found.
severity_exceededgate: a diff met or exceeded failOnSeverity.
status_broken_stepgate: the journey replay broke mid-flow.
status_failedgate: the scan ended in a failed status.
status_cancelledgate: the scan ended cancelled.
new_vendorsgate: a first-seen vendor appeared in the dataLayer.

CI recipes

Trigger a scan, re-poll until it reaches a terminal status, then evaluate the gate. On failure the recipe prints the offending diffs and exits non-zero.

Discover the target ID

# Find the TARGET_ID of a saved test (project-scoped):
curl -fsS -H "Authorization: Bearer $AP_KEY" \
  "$BASE/api/v1/targets" | jq -r '.targets[] | "\(.id)\t\(.name)"'

GitHub Actions

# .github/workflows/analyticsproof.yml
- name: AnalyticsProof compliance gate
  env:
    AP_KEY: ${{ secrets.ANALYTICSPROOF_API_KEY }}
    BASE: https://app.analyticsproof.com
    TARGET_ID: <TARGET_ID>   # discover via: GET $BASE/api/v1/targets
  run: |
    set -euo pipefail
    # Trigger a re-scan of the saved target (idempotent per commit SHA)
    SID=$(curl -fsS -X POST \
      -H "Authorization: Bearer $AP_KEY" \
      -H "Idempotency-Key: $GITHUB_SHA" \
      -H 'content-type: application/json' -d '{}' \
      "$BASE/api/v1/targets/$TARGET_ID/scans" | jq -r .scanId)

    GATE=$(jq -rn '{minComplianceScore:90,failOnNewDataLayerRegressions:true,failOnBrokenStep:true} | @uri')

    # Re-poll until the scan reaches a terminal status.
    # ?wait long-polls up to 120s per attempt (server cap 300s).
    RES=''
    for _ in $(seq 1 20); do
      RES=$(curl -fsS -H "Authorization: Bearer $AP_KEY" \
        "$BASE/api/v1/scans/$SID?wait=120&gate=$GATE&include=diffs")
      case "$(echo "$RES" | jq -r .status)" in
        completed|failed|cancelled|broken_step) break ;;
      esac
    done

    echo "$RES" | jq '{status, score, gate, warnings}'
    if [ "$(echo "$RES" | jq -r '.gate.passed')" != "true" ]; then
      echo "::error::AnalyticsProof gate failed"
      echo "$RES" | jq -r '.diffs[]? | "  [\(.severity)] step \(.stepIndex) \(.vendor // "-"): \(.description)"'
      exit 1
    fi

GitLab CI

# .gitlab-ci.yml
analyticsproof:
  stage: verify
  image: alpine:latest
  before_script:
    - apk add --no-cache curl jq
  script:
    - |
      set -euo pipefail
      SID=$(curl -fsS -X POST \
        -H "Authorization: Bearer $ANALYTICSPROOF_API_KEY" \
        -H "Idempotency-Key: $CI_COMMIT_SHA" \
        -H 'content-type: application/json' -d '{}' \
        "https://app.analyticsproof.com/api/v1/targets/$TARGET_ID/scans" | jq -r .scanId)

      GATE=$(jq -rn '{minComplianceScore:90,failOnNewDataLayerRegressions:true,failOnBrokenStep:true} | @uri')

      RES=''
      for _ in $(seq 1 20); do
        RES=$(curl -fsS -H "Authorization: Bearer $ANALYTICSPROOF_API_KEY" \
          "https://app.analyticsproof.com/api/v1/scans/$SID?wait=120&gate=$GATE&include=diffs")
        case "$(echo "$RES" | jq -r .status)" in
          completed|failed|cancelled|broken_step) break ;;
        esac
      done

      echo "$RES" | jq '{status, score, gate, warnings}'
      [ "$(echo "$RES" | jq -r '.gate.passed')" = "true" ] || {
        echo "Compliance gate failed"
        echo "$RES" | jq -r '.diffs[]?'
        exit 1
      }

OpenAPI spec

The full API is described by a machine-readable OpenAPI 3.0 document — no authentication required to read it.

https://analyticsproof.com/api/v1/openapi.json

The server URL is absolute, so you can paste this URL straight into editor.swagger.io or Redoc (redocly.com/redoc) and call the API from there.