Reference
API reference
Every endpoint with its request, response, parameters, errors, examples and SDK equivalent, organized by the resource it acts on.
Organised by the resource each endpoint acts on. This is the curated reference rather than a generated schema dump — the endpoints that matter, the errors that actually happen, and the behaviours per resource that a schema cannot tell you.
For the resource model and the conventions you inherit — idempotency, pagination, versioning, how an error is shaped — the API overview covers those once, and this page is the part you come back to.
Base URL and auth
https://api.opsai.dev/v1
Authorization: Bearer $OPSAI_API_KEY
Content-Type: application/jsonKeys are scoped. sk_read_ reads, sk_evaluate_ evaluates, sk_admin_ publishes policy versions and manages connections. A key of the wrong scope gets 403 scope_insufficient rather than a partial result.
Actions
The resource everything else exists to support. An action is a proposed change to a system of record, and evaluating one is the core call.
- POST /actions/evaluate — decide whether an action is allowed.
- GET /actions/:id — the action with its decision and authority chain.
- GET /actions/:id/trace — the six stages, whatever the outcome.
- GET /actions — list and filter, including by decision.
curl -X POST https://api.opsai.dev/v1/actions/evaluate \
-H "Authorization: Bearer $OPSAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"agent": "refund-resolver",
"action": "issue.refund",
"subject": "ORD-40122",
"target": "Razorpay",
"amount": { "currency": "INR", "value": 18400 },
"idempotency_key": "ACT-7512"
}'{
"id": "ACT-7512",
"decision": "authorized",
"bound": "refund.ceiling",
"bound_version": "v7",
"risk": "high",
"evidence": "EV-7512",
"latency_ms": 11,
"grant": {
"token": "grt_...",
"scope": ["issue.refund"],
"subject": "ORD-40122",
"expires_in": 120
}
}A refusal returns 422 with the failing check named. Filtering by decision is how you find them:
curl "https://api.opsai.dev/v1/actions?decision=denied&since=2026-09-01" \
-H "Authorization: Bearer $OPSAI_API_KEY"Approvals
A held action is waiting on a person. The key behaviour is that a hold expires — there is no endpoint to extend one, because a hold that can be extended indefinitely is a queue, and a queue’s default outcome is eventually yes.
- GET /approvals — outstanding holds with their expiry.
- POST /approvals/:id/decide — approve or refuse, attributed to a named human.
{
"outcome": "approved",
// Required. An approval nobody can be identified with is not an approval.
"actor": "rahul@acme.internal",
"note": "Verified against the supplier statement."
}Bounds
A policy compiles to a bound. Versions are published, never edited — there is noPATCH here, and that absence is the feature.
- GET /bounds — the register with owners and versions in force.
- POST /bounds/:id/versions — publish a new version.
- POST /bounds/:id/replay — re-evaluate past actions against any version.
curl -X POST https://api.opsai.dev/v1/bounds/refund.ceiling/replay \
-H "Authorization: Bearer $OPSAI_API_KEY" \
-d '{ "version": "v7", "from": "2026-06-01", "to": "2026-09-01" }'Agents
The inventory of AI systems. owner is required on creation and cannot be set to a team — one accountable human, because “the platform team” does not answer “who approved this”.
- POST /agents — register an AI system that already exists.
- GET /agents/:id — including autonomy rung and delegation depth.
- POST /agents/:id/autonomy — move a rung. Moving up requires an actor and a reason.
Connections
- POST /connections — create, supplying the credential once.
- GET /connections/:id — never returns the credential value.
- POST /connections/:id/rotate — begin a staged rotation.
credential.value is absent from every response at every scope. Not redacted — there is no code path that reads a stored credential back out.
Evidence
Read-only, by construction. There is no endpoint that writes or amends a record, which is what makes “sealed” a property rather than a promise.
- GET /evidence/:id — one record with its digest chain links.
- GET /evidence/verify — walk and verify a range of the chain.
- GET /evidence/export — a range as a signed archive.
{
"id": "EV-7512",
"action": "ACT-7512",
"bound_version": "v7",
"previous_digest": "d5bdfde76116b2a5cf853ad280bb989659848df5e0ab78af500ef78f968fb61d",
"digest": "d7ffd0b3bab589c9f5e902bddacf4f875e6f108129d6fb13e0e5eea8b1f6b278",
"sealed": true
}Verification is yours to run rather than something OpsAI asserts. If a record were removed, the chain fails to verify at that point and names it.
Errors
Every error carries a code as well as a status, because the status alone is rarely enough to know what to do.
| Status | Code | Cause |
|---|---|---|
| 400 | invalid_request | A field is missing or malformed. The response names the field path. |
| 401 | unauthenticated | No key, or a key that has been rotated out. |
| 403 | scope_insufficient | A read key attempting an evaluation, or an evaluate key publishing a policy. |
| 404 | agent_not_registered | The AI system proposing the action is not in the inventory. Register it first. |
| 409 | action_already_decided | A retry of an evaluation that already produced a decision. The original decision is returned, not a new one. |
| 422 | bound_violation | A check failed. The response names which bound, which check and the value that failed it. |
| 422 | action_not_permitted | The action is not in the connection’s `allows` list, so no policy could authorize it. |
| 429 | rate_limited | Retry after the interval in the `Retry-After` header. Evaluation limits are per tenant, not per key. |
{
"error": {
"code": "bound_violation",
"bound": "refund.ceiling",
"bound_version": "v7",
"check": "amount <= ceiling",
"observed": { "amount": 41200, "ceiling": 25000 },
"evidence": "EV-7601"
}
}A refusal names the check and the values, and it produces an evidence record just as an authorization does. That symmetry is deliberate: a control that only records its successes cannot answer the question an incident review asks.
Conventions
- Idempotency. Every mutating endpoint accepts idempotency_key. A repeat returns the original decision rather than making a second one — which is what stops a retried evaluation becoming a duplicate payment.
- Pagination. Cursor-based. next_cursor is present until it is null; do not construct cursors.
- Timestamps. RFC 3339, always UTC.
- Amounts. Always an object with a currency and an integer minor unit. Never a bare float.
- Versioning. The version is in the path. Additive changes ship within a version; anything that could break a caller gets a new one.
SDKs wrap all of this with typed responses, and webhooks cover the events emitted for holds, expiries and escalations.