Verdict

API

Scan every build, not just the one you remembered.

The most expensive rejections come from builds nobody thought to check. Put Verdict in the pipeline that already produces your artifact and the check stops being something you have to remember.

Authentication

Create a key on your account page. It is shown once, so store it in your CI secret manager immediately. Keys are stored as a SHA-256 hash, which means we cannot show it to you again and cannot leak it back to you if we are breached.

Send it as a bearer token on every request. API access is part of Pro, and requests are limited to 60 scans per hour per account.

Authorization: Bearer vd_live_a1b2c3...

POST /api/v1/scans

Upload a build and get the complete report back in the same response. Accepts .ipa, .apk, and .aab. Multipart is simplest for typical CI artifacts; for very large builds, upload to storage first and pass a blobUrl in a JSON body instead.

curl -X POST https://verdict-umber.vercel.app/api/v1/scans \
  -H "Authorization: Bearer $VERDICT_API_KEY" \
  -F "file=@build/Trailhead.ipa"

A successful response, abridged:

{
  "id": "9f3c1a7e-...",
  "store": "ios",
  "score": 62,
  "counts": { "blocker": 1, "warning": 2, "advisory": 3 },
  "app": { "name": "Trailhead", "bundleId": "com.example.trailhead", "version": "2.1.0" },
  "findings": [
    {
      "ruleId": "VD-PRIV-01",
      "severity": "blocker",
      "guideline": "Guideline 5.1.1 - Legal - Privacy - Data Collection and Storage",
      "title": "Camera API is used but NSCameraUsageDescription is missing",
      "evidence": "The app binary references AVCaptureDevice, which requires a purpose string...",
      "fix": "Add NSCameraUsageDescription to Info.plist with one sentence that..."
    }
  ],
  "engineVersion": "1.7.0"
}

GET /api/v1/scans/:id

Fetch a stored report later, for example to attach it to a release ticket. Only the key owner can read it.

curl https://verdict-umber.vercel.app/api/v1/scans/$SCAN_ID \
  -H "Authorization: Bearer $VERDICT_API_KEY"

Failing the build

Every scan response carries a gate object: gate.pass and, when it fails, gate.failureswith one sentence per broken rule. The verdict is computed server side from your team's release policy — set on the Team page, applied to every member's key — or from the default when there is none: any blocker fails the gate.

- name: Verdict pre submission scan
  run: |
    REPORT=$(curl -sS -X POST https://verdict-umber.vercel.app/api/v1/scans \
      -H "Authorization: Bearer ${{ secrets.VERDICT_API_KEY }}" \
      -F "file=@${{ env.IPA_PATH }}")

    echo "$REPORT" | jq -r '"Score: \(.score)/100"'
    echo "$REPORT" | jq -r '.findings[] | "[\(.severity)] \(.ruleId) \(.title)"'

    if [ "$(echo "$REPORT" | jq -r '.gate.pass')" != "true" ]; then
      echo "$REPORT" | jq -r '.gate.failures[] | "::error::" + .'
      exit 1
    fi

Errors

401 missing or invalid key. 402 the account is not on Pro. 422 the file could not be analyzed, with a human readable message and a code such as encrypted or not-ipa. 429 rate limit reached; the x-ratelimit-remaining header is on every successful response.