SafeCommit
AAPI reference

Four calls, one guarantee

Quote a change to learn what it costs and how risky it is. Execute to authorize payment and run it. Poll for status. Roll back on demand. Every response is JSON; every failure carries a stable error.code.

  • POST/api/quotePrice a change, get its risk
  • POST/api/executeAuthorize payment and run it
  • GET/api/runs/:idPoll status and artifacts
  • POST/api/runs/:id/rollbackUndo on demand
  • GET/api/runsList the account's runs
  • GET/api/metaEnums and deployment info
01

Authentication

Agents authenticate with a token created on the tokens page. Send it as a bearer token. Each token carries its own spending policy: a per-run ceiling, a daily ceiling, a list of approved repositories, and whether production is permitted at all. Those limits are checked when you request a quote and again when you execute.

header
Authorization: Bearer sc_live_8f21_a1b2c3d4e5f6…
02

POST /api/quote

Describe the change. You get a price, a risk score, and a run_id held in quoted state. Nothing is applied and nothing is charged. Policy violations surface here rather than at execute time, so an agent learns it is out of bounds before doing any work.

request
POST /api/quote
Authorization: Bearer sc_live_8f21_…
Content-Type: application/json

{
  "kind": "supabase_migration",
  "repository": "northwind/checkout-api",
  "target": "production",
  "summary": "Retire the legacy shipping address constraint",
  "payload": "BEGIN;\n\nALTER TABLE public.orders\n  DROP CONSTRAINT orders_shipping_address_text_check;\n\nCOMMIT;",
  "files_changed": 4
}
response
201 Created

{
  "run_id": "run_9c2f5e10",
  "status": "quoted",
  "risk": {
    "level": "high",
    "score": 64,
    "factors": [
      { "code": "target_env",         "label": "Targets production",           "weight": 30, "detail": "…" },
      { "code": "sql_drop_constraint","label": "Drops a constraint",           "weight": 14, "detail": "…" },
      { "code": "no_down_migration",  "label": "No down migration supplied",   "weight": 12, "detail": "…" },
      { "code": "kind_migration",     "label": "Schema migration",             "weight":  8, "detail": "…" }
    ]
  },
  "quote": {
    "total_cents": 850,
    "lines": [
      { "code": "protection_base", "label": "Protected run",                    "amount_cents": 200 },
      { "code": "checkpoint",      "label": "Database checkpoint",              "amount_cents": 180 },
      { "code": "monitoring",      "label": "Post-apply monitoring (production)","amount_cents": 150 },
      { "code": "risk_surcharge",  "label": "Risk surcharge (high, 64/100)",    "amount_cents": 320 }
    ]
  },
  "expires_at": "2026-07-27T14:22:11.000Z"
}

Risk scoring is a rule table, not a model. Each factor carries a fixed weight and an explanation, the weights sum to a 0–100 score, and the same input always produces the same result. Price is a pure function of that score, so an agent can predict its own spend.

03

POST /api/execute

Authorize payment from the credit balance and run the protected pipeline: checkpoint, gates, apply, monitor. The call returns once the run has reached a terminal state: committed, rolled_back, or failed. Send max_price_cents to refuse execution if the quote no longer matches what you agreed to.

request
POST /api/execute
Authorization: Bearer sc_live_8f21_…
Content-Type: application/json

{
  "run_id": "run_9c2f5e10",
  "max_price_cents": 1000
}
response
200 OK

{
  "run_id": "run_9c2f5e10",
  "status": "rolled_back",
  "outcome": { "committed": false, "rolled_back": true, "failed": false },
  "charged_cents": 850,
  "refunded_cents": 0,
  "credit_balance_cents": 5570,
  "checkpoint": {
    "id": "ckpt_k3f9a21b7c",
    "kind": "postgres_snapshot",
    "restore_command": "npx supabase db restore --project-ref … --snapshot ckpt_k3f9a21b7c",
    "retained_until": "2026-08-03T09:14:02.000Z"
  },
  "rollback": {
    "trigger": "monitor_breach",
    "reason": "p95 query latency reached 940ms against a 120ms threshold (baseline 31ms).",
    "restored_checkpoint_id": "ckpt_k3f9a21b7c",
    "duration_ms": 31420,
    "evidence": [ "…" ]
  }
}
committed

Gates green, monitors inside baseline. Charged in full.

rolled_back

Applied, a monitor breached, checkpoint restored. Charged in full; the rollback is the product working.

failed

A gate blocked it pre-apply. Nothing shipped. Charged in full: catching it is the outcome you were paying for.

04

GET /api/runs/:id

Full status with every artifact: test gates and their console output, the checkpoint, monitor readings against baseline, rollback evidence, and the timeline. Readable by any caller holding the run id, so a polling loop stays a single unauthenticated GET.

response
GET /api/runs/run_9c2f5e10

200 OK
{
  "run_id":  "run_9c2f5e10",
  "status":  "rolled_back",
  "terminal": true,
  "risk":     { "level": "high", "score": 64 },
  "tests":    [ { "name": "shadow-apply", "status": "passed", "duration_ms": 8412, "output": [ "…" ] } ],
  "monitors": [ { "metric": "pg_p95_ms", "baseline": "31ms", "observed": "940ms", "status": "breached" } ],
  "rollback": { "trigger": "monitor_breach", "…": "…" },
  "timeline": [ { "at": "…", "phase": "checkpoint", "detail": "…", "level": "success" } ]
}
05

POST /api/runs/:id/rollback

Automatic rollback already covers monitor breaches. This is the escape hatch for everything the monitors cannot see: a product decision, a customer report, a downstream system that only noticed an hour later. Restores the same checkpoint and records the same evidence trail. Only valid on a run in committed state; reason is required.

request
POST /api/runs/run_4b81c7a2/rollback
Authorization: Bearer sc_live_8f21_…
Content-Type: application/json

{
  "reason": "Support reports checkout failing for EU customers since this shipped."
}
06

A complete agent loop

Everything an agent needs, end to end: price it, decide, authorize, and read back what happened.

agent.ts
import { setTimeout as sleep } from 'node:timers/promises'

const API = 'https://safecommit.dev/api'
const headers = {
  Authorization: `Bearer ${process.env.SAFECOMMIT_TOKEN}`,
  'Content-Type': 'application/json',
}

export async function protectedChange(change) {
  // 1. Price it. Nothing is applied and nothing is charged.
  const quote = await fetch(`${API}/quote`, {
    method: 'POST', headers, body: JSON.stringify(change),
  }).then((r) => r.json())

  if (quote.error) throw new Error(quote.error.message)

  // 2. Decide. The risk assessment is a rule table, not a model;
  //    the same input always produces the same score.
  if (quote.risk.level === 'critical') {
    return { skipped: true, reason: 'critical risk', risk: quote.risk }
  }

  // 3. Authorize. Returns once the run has reached a terminal state.
  const run = await fetch(`${API}/execute`, {
    method: 'POST',
    headers,
    body: JSON.stringify({
      run_id: quote.run_id,
      max_price_cents: quote.quote.total_cents,
    }),
  }).then((r) => r.json())

  if (run.status === 'rolled_back') {
    // The change went out and came back. Read why, then try something else.
    return { reverted: true, reason: run.rollback.reason }
  }

  if (run.status === 'failed') {
    // A gate caught it before apply. The logs say which one.
    const detail = await fetch(`${API}/runs/${run.run_id}`).then((r) => r.json())
    return { blocked: true, tests: detail.tests.filter((t) => t.status === 'failed') }
  }

  return { committed: true, checkpoint: run.checkpoint.id }
}
07

Vocabulary

status
quotedauthorizedrunningcommittedrolled_backfailed
risk.level
lowelevatedhighcritical
kind
vercel_deploymentsupabase_migration
target
productionstagingpreview

Machine-readable at GET /api/meta.

08

Errors

Every failure returns { "error": { "code", "message" } }. Validation failures add error.fields keyed by field name.

401unauthorizedNo bearer token and no session cookie.
403repository_not_allowedThe token is not approved for that repository.
403production_not_allowedThe token may not target production.
403per_run_limit_exceededThe quote is above the token's per-run ceiling.
403daily_limit_exceededThe token has spent its daily allowance.
402insufficient_creditThe account balance will not cover the run.
404run_not_foundNo run exists with that id.
409invalid_transitionThe run is not in a state that allows this call.
409price_above_limitThe quote exceeds the `max_price_cents` you sent.
422validation_failedA field is missing or malformed; see `error.fields`.
09

What this build actually does

This is an MVP. No production infrastructure is modified. Vercel deployments and Supabase checkpoints are simulated: the test logs, checkpoint ids, monitor readings and rollback evidence are generated by a deterministic simulator (src/lib/simulate.ts) as a pure function of the run, so the same run always renders identical artifacts.

Everything around it is real and unchanged by that substitution: the risk rules, the pricing, the spending policy, the credit ledger, the state machine, and the API surface. Swapping the simulator for adapters that call the Vercel and Supabase Management APIs leaves every consumer working against the same RunArtifacts shape.

persistence: supabase · billing: placeholder · execution: simulated