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, .aab, a zipped macOS .app, or a Windows .msix. 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://www.appverdict.net/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.11.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://www.appverdict.net/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 and 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: |
    curl -sS -X POST https://www.appverdict.net/api/v1/scans \
      -H "Authorization: Bearer ${{ secrets.VERDICT_API_KEY }}" \
      -F "file=@${{ env.IPA_PATH }}" > report.json

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

- name: Fail if this build would be rejected
  run: |
    jq -e '.gate.pass' report.json > /dev/null && exit 0
    jq -r '.gate.failures[] | "::error::" + .' report.json
    exit 1

Commenting on the pull request

A red check tells a reviewer that something is wrong. It does not tell them what. Add one more step and the score and every finding are written into the pull request itself, where the people reviewing the change already are.

This runs entirely inside your repository. It uses the GITHUB_TOKEN GitHub already gives the job, so there is nothing to install and no access granted to us. Give the job pull-requests: write and --edit-last keeps it to a single comment that stays current, rather than one per push.

# permissions: { pull-requests: write } on the job

- name: Write the verdict on this pull request
  env:
    GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  run: |
    {
      jq -r '"### Approval risk \(.score)/100"' report.json
      jq -r '.findings[] | "- `\(.severity)` **\(.ruleId)** \(.title)"' report.json
      jq -r '"\n[Open the full report](https://www.appverdict.net/scan/\(.id))"' report.json
    } > verdict.md

    gh pr comment ${{ github.event.number }} \
      --body-file verdict.md --edit-last --create-if-none

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.