Guard API

The agent proposes; COLVO decides, executes exactly once and verifies. Three endpoints cover it: propose, read, decide.

Written from the code · updated 26 Sep 2026 · Something wrong or missing? Tell us

Propose an operation

POST /v1/operations — agent or backend key, or session (editor+). Returns 202 immediately with the decision; execution and verification continue asynchronously.

POST /v1/operations
Authorization: Bearer cak_…
Idempotency-Key: ticket_4711:refund
Content-Type: application/json

{ "mandate_id": "…", "source_request_id": "ticket_4711",
  "action": "refund.create",
  "parameters": { "payment_id": "pi_01", "amount_minor": 5000, "currency": "eur", "reason": "duplicate charge" },
  "context": { "user_message": "…", "agent_reply": "…", "retrieved": ["…"] } }

Actions and parameters

actionparameters
refund.createpayment_id, amount_minor (int > 0), currency (3 letters), reason?
subscription.cancelsubscription_id, mode: period_end (default) | immediate

Idempotency

  • Send Idempotency-Key on every proposal. The same key with the same body returns the original operation (200); the same key with a different body is refused with 409 idempotency_key_reused.
  • Independently of the header, COLVO derives a business key from mandate + action + target; a second proposal of the same effect is refused with 409 business_key_conflict. A retry never doubles a refund.

Response

{
  "operation_id": "…", "project_id": "…", "mandate_id": "…",
  "action": "refund.create", "parameters": { … }, "source_request_id": "ticket_4711", "business_key": "…",
  "decision": "REVIEW",
  "decision_reasons": ["amount 5000 above auto-allow 2000 (within max 5000)"],
  "guardrail_hits": [{ "rail": "pii_input", "stage": "input", "action": "REDACT", "advisory": false, "reason": "email redacted" }],
  "exec_state": "NOT_SENT", "verify_state": "NOT_DUE",
  "provider_id": null,
  "approval": { "approval_id": "…", "digest": "sha256:…", "expires_at": "…", "decision": "PENDING" },
  "policy_version": "policy-v1", "mandate_version": 1, "evidence_count": 3, "last_error": null,
  "created_at": "…", "updated_at": "…"
}

Decisions

  • ALLOW — queued for execution now.
  • REVIEW — waits for a human; approval carries the digest they must sign.
  • HOLD — cannot be done safely now (state unreadable, cap exceeded, project paused). Re-propose later.
  • DENY — policy violation; nothing will happen. Reasons are explicit.

Lifecycle

exec_state: NOT_SENT → QUEUED → SENDING → ACKNOWLEDGED | PROVIDER_REVIEW | FAILED | UNKNOWN | CANCELED_BEFORE_SEND. verify_state: NOT_DUE → PENDING → VERIFIED | MISMATCH | UNVERIFIABLE. MISMATCH and UNVERIFIABLE open an incident and fire alerts.

Read an operation

GET /v1/operations/{id} — any key or session. Same shape as above, refreshed. Poll it after a REVIEW to learn the outcome, or subscribe to alerts.

Approve or reject

POST /v1/operations/{id}/decision — session only (editor+). The reviewer signs the exact digest; a stale digest is refused (409 digest_mismatch), an expired approval too (409 approval_expired).

{ "decision": "approve", "approval_digest": "sha256:…", "rationale": "customer confirmed duplicate charge" }

Approval notifications

A REVIEW notifies the people who can decide: an email to every owner and editor, and an alert to each project alert channel subscribed to approval.requested / approval.reminder / approval.expired (Slack, email, signed webhook). The approval window is the shorter of 24 hours and the mandate’s expiry. A reminder goes out at 75 % of the window; at the end an undecided approval becomes EXPIRED, the operation CANCELED_BEFORE_SEND, and the approvers are told. Emails link to the approval — the decision itself always happens signed in, against the exact digest.

Kill-switch

POST /v1/projects/{id}/pause with { "reason": "…" } stops new writes: new proposals get HOLD, queued executions wait. POST /v1/projects/{id}/unpause re-queues held operations. Both are session-only (editor+) and logged.

Entitlement

Proposing outside a test attempt requires the Guard capability. Without it the API answers 403 entitlement_required with an upgrade hint; the console shows “Unlock Guard”. See pricing.

Guard API · Docs · COLVO