API reference

The v1 REST API — bearer auth, the four endpoints (state, run, approve, ignore), and error codes.

The v1 REST API lets you check a customer, run a scan, approve a repair, and ignore an incident — straight from your own code or scripts. Reach for it when you need to do something MCP can’t: MCP only reads and triggers checks, so approving a repair or ignoring an incident has to go through here (or the dashboard).

What you can do

Four endpoints cover everything below.

MethodPathWhat it does
GET/api/v1/customers/{customerId}/state?project_id={uuid}Billing promise vs live app state, plus the customer's open access incidents.
POST/api/v1/reconciliation/runTrigger a fulfillment check for a project or a single customer.
POST/api/v1/repairs/{repairActionId}/approveApprove and execute a suggested repair, then re-verify the customer.
POST/api/v1/mismatches/{mismatchId}/ignoreMark an access incident as intentional (idempotent).

Authentication

Every request needs a bearer token.

  • Create a key in Settings → API keys.
  • Send it in the Authorization header, as Bearer sk_prail_....
  • Keys start with sk_prail_ and are shown once, at creation — store the value somewhere safe.
  • A key is scoped to your organization, or narrowed to a single project.
  • Read-only by default. A new key can read state, run scans, and ignore incidents. To approve or execute a repair, the key needs the repair capability — choose Read + repair when you create it. A read-only key that calls approve gets 403, so a leaked key can never write to your database.
  • Keys are stored only as hashes, never in plain text, and you can revoke one at any time.
Authorization: Bearer sk_prail_...
A missing or invalid key gets 401. A read-only key that tries to approve a repair gets 403. A key that can’t reach the requested project gets 404— the same response you’d get for something that genuinely doesn’t exist, so a key can never be used to probe what projects are out there.

Get customer state

GET /api/v1/customers/{customerId}/state?project_id={uuid}

Returns the latest billing promise, live app state, and open access incidents for a Stripe customer, from ParityRail’s most recent snapshot. Run a check first if the customer has never been scanned.

Start with mismatchesin the response — it’s the list of this customer’s open access incidents, the part most callers actually want.

Request

curl "https://<host>/api/v1/customers/cus_123/state?project_id=<uuid>" \
  -H "Authorization: Bearer sk_prail_..."

Response

{
  "customer_id": "cus_123",
  "subject_type": "user",
  "subject_key": "user@example.com",
  "billing_state": {
    "customer_id": "cus_123",
    "subscription_status": "active",
    "plan": "pro",
    "price_id": "price_123",
    "quantity": 1,
    "cancel_at_period_end": false,
    "current_period_end": "2026-08-01T00:00:00.000Z",
    "trial_end": null,
    "mrr_cents": 4900,
    "entitlements": []
  },
  "app_state": {
    "email": "user@example.com",
    "stripe_customer_id": "cus_123",
    "plan": "free",
    "is_premium": false,
    "trial_ends_at": null,
    "active_member_count": null,
    "entitlements": []
  },
  "mismatches": [
    {
      "id": "9f2c...",
      "type": "paid_but_no_access",
      "severity": "critical",
      "status": "open",
      "first_detected_at": "2026-07-01T09:12:00.000Z",
      "last_detected_at": "2026-07-02T08:00:00.000Z"
    }
  ],
  "captured_at": "2026-07-02T08:00:00.000Z"
}

Run a fulfillment check

POST /api/v1/reconciliation/run

Runs a check and returns the run id and stats. Use scope: "all_customers" for a full scan, or scope: "customer" with a customer_id for one customer.

A full scan runs synchronously and can take a while on a large workspace — set a generous timeout on the caller side.

Request

curl -X POST "https://<host>/api/v1/reconciliation/run" \
  -H "Authorization: Bearer sk_prail_..." \
  -H "Content-Type: application/json" \
  -d '{ "project_id": "<uuid>", "scope": "customer", "customer_id": "cus_123" }'

Response

{
  "run_id": "5b1a...",
  "stats": {
    "customersScanned": 1,
    "subjectsScanned": 1,
    "mismatchesFound": 1,
    "mismatchesNew": 0,
    "mismatchesResolved": 0,
    "bySeverity": { "critical": 1 },
    "byType": { "paid_but_no_access": 1 },
    "revenueAtRiskCents": 4900,
    "durationMs": 128
  }
}

Approve a repair

POST /api/v1/repairs/{repairActionId}/approve

Approves and executes a pending repair, then re-checks the customer. The response tells you whether the follow-up check confirmed the access promise is fulfilled.

If the repair can’t be applied — say the incident already closed, or the live values drifted since it was suggested — you get 422 with an error.

Request

curl -X POST "https://<host>/api/v1/repairs/<repairActionId>/approve" \
  -H "Authorization: Bearer sk_prail_..."

Response

{ "ok": true, "verified": true, "detail": "Access promise fulfilled." }

Ignore an incident

POST /api/v1/mismatches/{mismatchId}/ignore

Marks an access incident as intentional. It’s idempotent — safe to call twice, since calling it again on an already-ignored incident just succeeds and does nothing.

Request

curl -X POST "https://<host>/api/v1/mismatches/<mismatchId>/ignore" \
  -H "Authorization: Bearer sk_prail_..."

Response

{ "ok": true, "status": "ignored" }

Error codes

StatusMeaning
400Invalid query parameters or request body.
401Missing or invalid API key.
404Not found — including a project, customer, incident, or repair this key can't reach.
422The repair could not be applied.
500A check failed while running.
Errors return a JSON body with an error message; validation errors also include an issues array.