# Assure API: first run on one page

This page takes you to a first verdict from the HTTP API with the `postgresql-observed-baseline` profile, which needs no declaration. The full reference is QUICKSTART-API.md.

Base URL: `https://api.symbolia.ai`

The API runs the checker on Symbolia's server. You send collected files. You get back a verdict.

## 1. Get a key

Symbolia issues keys by hand. Ask Symbolia for a key, and you receive it once over a private channel. Symbolia keeps only a hash of it. If you lose it, ask for a new one; the old one is revoked.

Keep the key in an environment variable. The examples below read it from `$ASSURE_API_KEY`. Send it on every request except `/healthz`:

```
Authorization: Bearer $ASSURE_API_KEY
```

A key expires 30 days after it is issued. A key past its expiry is refused `key_expired`: ask Symbolia for a new key.

From a GitHub Actions workflow you need no stored key. QUICKSTART-API.md, "1. Get a key", describes the sign-in with the workflow's GitHub identity.

## 2. Collect

The API never connects to your database. You collect the files first, on a machine that can reach the server.

The collector is `action/collector/collect_pg.py` in the public `Symbolia-Assurance/assure-action` repository. Fetch it from the `beta` branch:

```sh
git clone https://github.com/Symbolia-Assurance/assure-action assure-action
git -C assure-action checkout beta
git -C assure-action rev-parse HEAD
```

The last command prints the full 40-character SHA of the commit you fetched. Record it with your results; to collect at the same commit again, check out that SHA in place of `beta`.

Create the collection role as in QUICKSTART-ACTION.md, "2. Create a collection role". Set the standard libpq variables (`PGHOST`, `PGPORT`, `PGDATABASE`, `PGUSER`, `PGPASSWORD`, `PGSSLMODE`), then run, from `assure-action/action/collector/`:

```sh
python3.14 -B collect_pg.py --out out \
  --role assure_collector \
  --privileges pg_read_all_settings,pg_read_all_stats \
  --data-dir /var/lib/postgresql/18/main
```

`--data-dir` must be a directory this machine can read. If the server keeps its configuration files outside the data directory (the Debian layout uses `/etc/postgresql/<major>/<cluster>`), add `--extra-root <that directory>`, once per directory. Exit 0 or 1 means the output is ready to send (1: with gaps the sidecar names).

## 3. Input files

Send only the files the profile reads. `GET /v1/profiles` lists, for each profile, the required names, the accepted names and patterns, and each one's size limit. `postgresql-observed-baseline` requires `raw/catalog_snapshot.json`, and its check must also observe the server's major version, so send `raw/COLLECTION-SIDECAR.json` from the same collection. Without it the check is refused `profile_refused` with `detail.kind` `major_not_observed`, and that refusal is charged.

Gather the collector's output into one `raw/` folder:

```sh
mkdir -p bundle/raw
cp -R out/raw/. bundle/raw/
cp out/COLLECTION-SIDECAR.json out/TIMING.json bundle/raw/
```

Send the collector's own output from one run, never a sidecar kept from an earlier run. Do not send `REDACTION-MANIFEST.json`; it is not an accepted name.

## 4. Submit a check

`POST /v1/checks` takes a JSON body of two keys, `profile` and `files`, where each file is base64. This builds the body from `bundle/` with standard tools:

```sh
PROFILE=postgresql-observed-baseline
cd bundle
{
  printf '{"profile":"%s","files":{' "$PROFILE"
  sep=''
  find raw -type f | LC_ALL=C sort | while IFS= read -r name; do
    printf '%s"%s":"%s"' "$sep" "$name" "$(base64 < "$name" | tr -d '\n')"
    sep=','
  done
  printf '}}'
} > ../request.json
cd ..
```

Send `request.json`:

```sh
curl -sS https://api.symbolia.ai/v1/checks \
  -H "Authorization: Bearer $ASSURE_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(openssl rand -hex 16)" \
  --data-binary @request.json \
  -o response.json -w '%{http_code}\n'
```

The server needs `Content-Length`; `--data-binary @file` sets it. Chunked bodies are refused. Make one new random `Idempotency-Key` for each check, and send the same value again only when you resend that same request, for example after a lost connection.

## 5. Read the answer

- **200**: the check finished. The body is the verdict, schema `assure.serve.verdict/v1`. `policy.exit` is the exit code a CI job should use, and `policy.reason` says why. `nothing_disproven` is exit 0 and `colour` green: nothing is disproven within the bounds the first line states. `disproven` is exit 1, red. `could_not_look` is exit 3, yellow: the check was stopped or no machine was read. The first line names what was not read; VERDICTS.md, "9. The policy exit", gives it word for word.
- **202**: the check is still running 20 seconds after the API received the request (its upload and admission count in those 20 seconds; when they take longer than 19.5 seconds, the check is given half a second after its admission before the 202). The body is `{"check_id": "…", "status": "running"}`. Poll for it every few seconds:

```sh
curl -sS https://api.symbolia.ai/v1/checks/<check_id> \
  -H "Authorization: Bearer $ASSURE_API_KEY"
```

A running check answers 202 again. A finished check answers 200 with the verdict.

- **Any other code**: a typed outcome (QUICKSTART-API.md, "9. Typed outcomes"). The body is schema `assure.serve.failure/v1` with `outcome`, `reason` and `action`.

The verdict stays readable at `GET /v1/checks/<check_id>` for 30 days. Only your account can read it.

## What is sent

Your files are written to a scratch folder for one check, and the folder is deleted when the check ends. On the Premium tier a sealed copy is kept for the formal shadow (see DATA.md). The verdict is kept 30 days. Details are in DATA.md. What a verdict claims is in SCOPE.md.
