Guard API
The agent proposes; COLVO decides, executes exactly once and verifies. Three endpoints cover it: propose, read, decide.
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
| action | parameters |
|---|---|
| refund.create | payment_id, amount_minor (int > 0), currency (3 letters), reason? |
| subscription.cancel | subscription_id, mode: period_end (default) | immediate |
Idempotency
- Send
Idempotency-Keyon every proposal. The same key with the same body returns the original operation (200); the same key with a different body is refused with409 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;approvalcarries 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.