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 | one of payment_id | invoice_id | charge_id (older payments made before PaymentIntents), amount_minor (int > 0), currency (3 letters), reason? |
| subscription.cancel | subscription_id, mode: period_end (default) | immediate |
| subscription.pause | subscription_id, resumes_at? (date or date-time; absent = no end date), behavior: void (default) | keep_as_draft. Billing stops, access stays. |
| subscription.resume | subscription_id — clears the pause; nothing is sent if the subscription is not paused. |
| subscription.change_plan | subscription_id, price_id, at: now (default, prorated) | period_end (scheduled; the current plan stays until the end date). |
| coupon.apply | subscription_id, coupon_id. Never on top of an existing discount: that operation fails before anything is sent. |
| order.cancel | Shopify 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_undo | subscription_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-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.