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.createone of payment_id | invoice_id | charge_id (older payments made before PaymentIntents), amount_minor (int > 0), currency (3 letters), reason?
subscription.cancelsubscription_id, mode: period_end (default) | immediate
subscription.pausesubscription_id, resumes_at? (date or date-time; absent = no end date), behavior: void (default) | keep_as_draft. Billing stops, access stays.
subscription.resumesubscription_id — clears the pause; nothing is sent if the subscription is not paused.
subscription.change_plansubscription_id, price_id, at: now (default, prorated) | period_end (scheduled; the current plan stays until the end date).
coupon.applysubscription_id, coupon_id. Never on top of an existing discount: that operation fails before anything is sent.
order.cancelShopify only. order_id (#1042, numeric id or gid), refund (default false: when true, what is left on the order is refunded in the same step), restock (default true), reason: customer (default) | fraud | inventory | declined | other. COLVO reads the order first: an order already cancelled → DENY, an order it cannot read → HOLD, and with a refund the limits apply to the refundable amount.
subscription.cancel_undosubscription_id — removes a scheduled cancellation. An ended subscription cannot be un-cancelled: the operation fails before anything is sent.

Mandate limits for pauses: max_pause_days (default 30; longer → DENY) and pause_open_ended: never | review (default) | allow for a pause with no end date. Verification reads the subscription back: pause_collection set (or cleared), cancel_at_period_end false after an undo, and the subscription still active with access.

Plan changes and coupons: allowed_price_ids, change_plan_at (now / period_end), auto_allow_proration_up_to_minor (a switch that charges more today → REVIEW; credits never need review), max_proration_minor (more → DENY) and allowed_coupons. For a change “now” COLVO previews the charge with the provider before deciding; if it cannot be read the decision is HOLD. Verification checks the new price (or the scheduled change) and the coupon on the subscription.

On Shopify the order plays the part of the payment: refund.create takes the order in payment_id and refunds money on it (nothing restocked, the order stays open). Subscription actions are refused on a Shopify connection.

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