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.
| Method | Path | What 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/run | Trigger a fulfillment check for a project or a single customer. |
POST | /api/v1/repairs/{repairActionId}/approve | Approve and execute a suggested repair, then re-verify the customer. |
POST | /api/v1/mismatches/{mismatchId}/ignore | Mark an access incident as intentional (idempotent). |
Authentication
Every request needs a bearer token.
- Create a key in
Settings → API keys. - Send it in the
Authorizationheader, asBearer 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
repaircapability — chooseRead + repairwhen you create it. A read-only key that calls approve gets403, 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_...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.
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
| Status | Meaning |
|---|---|
400 | Invalid query parameters or request body. |
401 | Missing or invalid API key. |
404 | Not found — including a project, customer, incident, or repair this key can't reach. |
422 | The repair could not be applied. |
500 | A check failed while running. |
error message; validation errors also include an issues array.