API v1.2

Entwickler-API

Eine CI-native REST-API, um Scans auszulösen, Pull Requests bei dataLayer- und Compliance-Regressionen zu blockieren und Ergebnisse direkt in Ihre Pipeline zu holen.

Authentifizierung

Jede Anfrage trägt einen Projekt-API-Schlüssel, erstellt unter Projekteinstellungen › API-Schlüssel. Senden Sie ihn als Bearer-Token oder X-API-Key-Header. Jeder Schlüssel hat Scopes; der vollständige Schlüssel wird nur einmal bei der Erstellung angezeigt.

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

Scopes

Scopes
ScopeBeschreibung
scan:read List targets, journeys, and scan history; poll a scan and evaluate a CI gate.
scan:write Trigger ad-hoc scans, re-run saved tests, and cancel a pending/running scan.
upload:write Upload a CI APK/XAPK artifact for a mobile scan (valid 14 days).
datalayer:write Acknowledge dataLayer diffs from CI: approve/ignore diffs, and set vendor-monitoring states across a target.
admin Manage the project’s own API keys (mint / list / revoke). An `admin` key cannot mint another `admin` key — that is dashboard-only.

Endpunkte

Alle Endpunkte hängen an der API-Basis-URL und erfordern den angegebenen Scope.

Endpunkte
MethodeEndpunktScopeZusammenfassung
GET /api/v1/scans scan:read List scan history
POST /api/v1/scans scan:write Trigger an ad-hoc scan
GET /api/v1/scans/{id} scan:read Poll a scan + evaluate a CI gate
DELETE /api/v1/scans/{id} scan:write Cancel a pending/running scan
POST /api/v1/scans/{id}/explain scan:read Explain a scan in plain language
GET /api/v1/targets scan:read List saved tests
POST /api/v1/targets/{id}/scans scan:write Re-run a saved test
GET /api/v1/journeys scan:read List saved journeys
POST /api/v1/uploads upload:write Upload a CI APK/XAPK
POST /api/v1/journeys/{id}/datalayer/approve datalayer:write Approve a dataLayer diff into the journey baseline
POST /api/v1/journeys/{id}/datalayer/approve-all datalayer:write Approve all pending dataLayer diffs for a scan
POST /api/v1/journeys/{id}/datalayer/ignore datalayer:write Ignore a dataLayer element journey-wide
DELETE /api/v1/journeys/{id}/datalayer/ignores/{decisionId} datalayer:write Remove a standing dataLayer ignore
GET /api/v1/journeys/{id}/datalayer/decisions scan:read List standing dataLayer ignore decisions
PUT /api/v1/targets/{id}/vendors datalayer:write Set vendor-monitoring states across a target's journeys
GET /api/v1/whoami Identify the calling key
GET /api/v1/keys admin List project API keys
POST /api/v1/keys admin Mint a project API key
DELETE /api/v1/keys/{keyId} admin Revoke a project API key

CI-Gate-Optionen

Übergeben Sie beim Abfragen eines Scans ein URL-kodiertes JSON-Gate als ?gate=. Prädikate werden mit ODER kombiniert — das Gate schlägt fehl, sobald ein Prädikat zutrifft. CI sollte mit einem Wert ungleich null enden, wenn gate.passed false ist.

CI-Gate-Optionen
OptionTypBeschreibung
minComplianceScore number (0–100) Fail when the compliance score is below this threshold (score_below_threshold).
failOnNewDataLayerRegressions boolean Fail on new, unacknowledged dataLayer regressions (datalayer_regressions).
failOnSeverity "info" | "warning" | "critical" Fail when any diff meets or exceeds this severity (severity_exceeded).
failOnBrokenStep boolean Fail when the journey replay broke mid-flow (status_broken_step).
failOnStatus "failed" | "cancelled" | "broken_step" | "no_data"[] Fail when the scan ends in any of the listed statuses (status_<status>). Note: status_no_data always fails, whether or not it is listed.
failOnNewVendors boolean Fail when a first-seen vendor appears in the monitored dataLayer (new_vendors).
Warnung
Nicht fehlschlagende Warnungen (z. B. ein dataLayer-Gate für einen Scan ohne Baseline) werden in einem separaten warnings[]-Array zurückgegeben — sie setzen gate.passed niemals auf false.

Diff-Detail (include=diffs)

Fügen Sie ?include=diffs zu einer Scan-Abfrage hinzu, um die vollständige Liste ungelöster Diffs einzubetten. Der Diff ist wertblind: Er meldet Parameternamen und JS-Typen jedes Events, niemals deren Werte.

http
# 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 | consent_unreadable |
# step_not_reached. elementKey is "event:<vendor>::<name>",
# "param:<vendor>::<event>::<param>", "consent:<vendor>::<event>::<country>.<signal>",
# or "step:<index>" for step_not_reached, which belongs to no vendor.

Dry-Run-Vorschau-Gate

Richten Sie einen gespeicherten Test auf eine PR-Vorschau-URL, um das Deployment zu blockieren, ohne die Produktions-Baseline zu beschädigen. Die verknüpfte Journey wird als Dry-Run wiedergegeben: Sie erzeugt Diffs und einen Score für das Gate, schreibt aber nichts in die Journey zurück.

bash
# 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-Paginierung

Listen-Endpunkte verwenden Keyset-Paginierung. Folgen Sie nextCursor, bis es null ist; das ältere Flag truncated bleibt aus Kompatibilitätsgründen erhalten.

bash
# 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"

Ratenlimits

Limits werden pro API-Schlüssel durchgesetzt und in jeder Antwort ausgewiesen.

  • 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.

Idempotenz

Trigger- und Rerun-Endpunkte können mit einem Idempotenzschlüssel sicher wiederholt werden.

  • 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.
bash
# 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"

Fehlercodes

Jede Fehlerantwort hat eine stabile Form: ein error-Objekt mit code, message und optionalem details sowie ein requestId auf oberster Ebene. Gate-Fehlercodes erscheinen in gate.failures[].code, nicht in error.code.

Fehlercodes
CodeBeschreibung
unauthorized No usable credentials were presented.
missing_credentials Neither an Authorization Bearer token nor an X-API-Key header was sent.
invalid_key The API key is not recognised.
key_revoked The API key has been revoked.
key_expired The API key is past its expiry date.
tier_forbidden The project tier does not include programmatic API access (Enterprise only).
insufficient_scope The key lacks the scope this operation requires.
rate_limited Per-key request rate limit exceeded — see the Retry-After header.
too_many_long_polls Too many concurrent long-poll requests for this key (max 5).
invalid_request The request body or query failed validation.
not_found The referenced scan, target, journey, or upload does not exist for this project.
idempotent_in_progress A prior request with the same Idempotency-Key is still in flight.
idempotency_conflict The same Idempotency-Key was reused with a different payload on the same resource (e.g. single-journey vs journeys:"all"). Use a new key when the payload changes.
idempotency_corrupt The stored idempotency record for this key could not be read. Retry with a new Idempotency-Key.
not_cancellable The scan is already terminal and cannot be cancelled.
scan_not_terminal The scan has not reached a terminal status yet, so the requested artifact (SARIF / explain) is not available. Poll GET /scans/{id} first.
journey_mismatch The supplied journeyId does not belong to the target.
journey_required A preview-URL gate needs a journey to replay, but the target has none.
journey_sleeping The journeyId is ranked beyond the plan's per-target journey quota. Reorder the target's journeys or upgrade the plan.
no_url A web/hbbtv scan requires a url.
scan_type_mismatch The scanType disagrees with the target/upload type.
scan_in_progress The target already has an active scan.
invalid_target The target is not valid for this scanType.
dispatch_failed The scan could not be enqueued.
target_has_no_journeys A target-scoped vendor-monitoring write had no journeys to fan out to. Record a journey on the target first.
internal_error Unexpected server-side failure — safe to retry.
file_too_large The uploaded artifact exceeds the size limit.
package_parse_failed The APK/XAPK package name could not be parsed.
upload_failed The artifact could not be stored.
score_below_threshold gate: compliance score fell below minComplianceScore.
datalayer_regressions gate: new, unacknowledged dataLayer regressions were found.
severity_exceeded gate: a diff met or exceeded failOnSeverity.
status_broken_step gate/warning: the journey replay broke mid-flow. It fails only when explicitly configured as a generic API gate predicate; the fixed GitHub policy reports it as a non-blocking warning.
status_failed gate: the scan ended in a failed status.
status_cancelled gate: the scan ended cancelled.
status_no_data gate: the scan produced no trustworthy measurement, so it has no score and cannot be gated on one. Always fails, whether or not it is listed in failOnStatus.
new_vendors gate: a first-seen vendor appeared in the dataLayer.
diffs_not_persisted gate: the scan finished but its findings could not be stored, so the diff list is incomplete. Always fails — an empty diff list here means "not written", not "nothing found". Re-run the scan.

CI-Rezepte

Lösen Sie einen Scan aus, fragen Sie erneut ab, bis er einen Endzustand erreicht, und werten Sie dann das Gate aus. Bei einem Fehlschlag gibt das Rezept die betreffenden Diffs aus und endet mit einem Wert ungleich null.

GitHub Action (mehrere Journeys)

Die Composite Action kapselt die gesamte Schleife aus Auslösen, Abfragen, Gate, SARIF und Kommentar in einem einzigen uses:-Block. Sie führt jede ausführbare, vom Kontingent zugelassene Journey gegen Ihre Vorschau-URL aus (Entwürfe und fehlerhafte Journeys eingeschlossen; pausierte ausgeschlossen), meldet nur direkte dataLayer-Änderungen und blockiert nur kritische Befunde. Warnungen, Hinweise und eine unvollständige Journey-Wiedergabe bleiben nicht blockierende Rückmeldungen; Ausführungs- und Scannerfehler schlagen weiterhin sicher fehl. Sie benötigt das Repository-Secret ANALYTICSPROOF_API_KEY sowie die Berechtigungen contents: read, pull-requests: write, security-events: write.

yaml
# .github/workflows/analyticsproof.yml
name: AnalyticsProof
on: pull_request
concurrency:
  group: analyticsproof-${{ github.ref }}
  cancel-in-progress: true
permissions:
  contents: read           # checkout
  pull-requests: write     # PR comment
  security-events: write   # SARIF upload
jobs:
  gate:
    runs-on: ubuntu-latest
    if: github.event.pull_request.head.repo.full_name == github.repository
    timeout-minutes: 20   # above the Action's default 15-minute poll deadline
    steps:
      - uses: actions/checkout@v4
      - uses: your-org/your-repo/.github/actions/analyticsproof@v1
        with:
          api-key: ${{ secrets.ANALYTICSPROOF_API_KEY }}
          target-id: <TARGET_ID>   # discover via: GET /api/v1/targets
          url: https://preview-${{ github.event.number }}.example.com
          all-journeys: 'true'     # run EVERY runnable, quota-admitted journey
      - if: always() && hashFiles('analyticsproof.sarif') != ''
        uses: github/codeql-action/upload-sarif@v4
        with:
          sarif_file: analyticsproof.sarif
          category: analyticsproof

Test-ID ermitteln

bash
# 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

yaml
# .github/workflows/analyticsproof.yml
- name: AnalyticsProof compliance gate
  env:
    AP_KEY: ${{ secrets.ANALYTICSPROOF_API_KEY }}
    BASE: https://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 '{failOnSeverity:"critical"} | @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&scope=direct-datalayer")
      case "$(echo "$RES" | jq -r .status)" in
        completed|failed|cancelled|broken_step|no_data) 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

yaml
# .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://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://analyticsproof.com/api/v1/scans/$SID?wait=120&gate=$GATE&include=diffs")
        case "$(echo "$RES" | jq -r .status)" in
          completed|failed|cancelled|broken_step|no_data) 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-Spezifikation

Die vollständige API wird durch ein maschinenlesbares OpenAPI-3.0-Dokument beschrieben — zum Lesen ist keine Authentifizierung erforderlich.

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

Die Server-URL ist absolut, Sie können diese URL also direkt in editor.swagger.io oder Redoc (redocly.com/redoc) einfügen und die API von dort aufrufen.