Mandates & connections
A mandate is authority, declared once by your trusted backend: which customer, which actions, which limits, until when. Guard evaluates every proposal against it. A connection is the provider account the actions run on.
Register a mandate
POST /v1/mandates — backend key or session (editor+). One mandate per ticket or conversation is the usual grain.
{
"project_id": "…",
"connection_id": "…", // optional: defaults to the project's Guard connection
"source_request_id": "ticket_4711", // your id; ties every operation back to the request
"subject": { "customer_id": "cus_01", "payment_ids": ["pi_01"], "subscription_ids": ["sub_01"] },
// Shopify: "order_ids": ["#1042"] — the orders order.cancel may touch
"actions": ["refund.create", "subscription.cancel"],
"limits": {
"currency": "eur",
"max_amount_minor": 5000, // total refundable under this mandate; above → DENY
"auto_allow_up_to_minor": 2000, // per operation; above (but within max) → REVIEW
"max_operations": 2,
"cancel_modes_allowed": ["period_end"],
"cancel_modes_review": ["immediate"], // allowed only after a human approves
"max_pause_days": 30, // subscription.pause: longest pause; longer → DENY
"pause_open_ended": "review", // a pause with no end date: never | review | allow
"allowed_price_ids": ["price_basic"], // subscription.change_plan: plans it may move to
"change_plan_at": ["now", "period_end"],
"auto_allow_proration_up_to_minor": 1500, // a switch that charges more today → REVIEW
"allowed_coupons": ["WINBACK20"] // coupon.apply
},
"expires_at": "2026-10-01T12:00:00Z",
"approver": "[email protected]" // optional label shown to reviewers
}Response 201: the mandate with its mandate_id and version. Mandates are immutable; register a new one to change authority. An expired or exhausted mandate makes every further proposal DENY with reason mandate_inactive.
How limits map to decisions
| Condition | Decision |
|---|---|
action not in actions, wrong subject, wrong currency, over max_amount_minor, over max_operations, a pause longer than max_pause_days (or with no end date when pause_open_ended is never), a plan not in allowed_price_ids, a coupon not in allowed_coupons, a switch charging more than max_proration_minor | DENY |
amount above auto_allow_up_to_minor but within the max; cancel mode in cancel_modes_review; a pause with no end date when pause_open_ended is review; a plan switch charging more than auto_allow_proration_up_to_minor today | REVIEW (human approval) |
provider state unreadable, per-end-user cap exceeded with HOLD | HOLD (re-proposable later) |
| everything within limits, guardrails clear | ALLOW → executed once → verified |
Connections
A connection is a provider account attached to a project. Two kinds exist today:
- Sandbox (simulated Stripe) — POST
/v1/projects/{id}/connections/sandboxcreates an isolated world from a template fixture, optionally with faults (refund.create/subscription.cancel/readfailures, delays). Test runs create their own worlds per attempt; this endpoint is for trying Guard without a Stripe account. - Stripe — connected from the project page (or
POST /v1/projects/{id}/connections/stripewithsecret_key+webhook_secret) with a restricted key; live keys must be restricted (rk_live_), and the permissions are probed read-only before saving. Rotate with…/connections/{connection_id}/rotate; remove withDELETE(refused while active mandates use it) (refunds and subscriptions write, read for verification) and a webhook signing secret. The key is encrypted per organisation and only the executor uses it. - Paddle Billing — connected from the project page (Add a connection → Paddle Billing) or
POST /v1/projects/{id}/connections/paddlewithapi_key(pdl_sdbx_apikey_…orpdl_live_apikey_…) +webhook_secret(the notification destination’spdl_ntfset_…secret key). The key needs Transactions read, Subscriptions read and write, Adjustments read and write, Customers read; a key that can also write products is refused. On Paddle COLVO can refund (a refund adjustment, which waits for Paddle’s review before it counts as verified), cancel, pause, resume and undo a scheduled cancel — not change plans or apply discounts yet. Paddle has no idempotency keys: COLVO tags each refund’s reason with[colvo:<operation id>]and looks for it before any retry. - Chargebee — connected from the project page (Add a connection → Chargebee) or
POST /v1/projects/{id}/connections/chargebeewithsite(e.g.acme-test) +api_key. Chargebee keys can’t be limited to some actions: use a full-access key of the Write key type made only for COLVO (a read-only key is refused; a key that can delete gets a warning). The answer carries the webhook URL, username and password (shown once). On Chargebee a refund names an invoice (invoice_id, orpayment_id= the invoice id) and becomes a refundable credit note; cancel, pause, resume, undo cancel, plan changes (item prices) and coupons work as on Stripe. Writes carrychargebee-idempotency-key= the operation id; Chargebee keeps keys for 30 minutes, so COLVO never re-sends a write after that — it opens an incident for a person to check.
Each project has one default Guard connection; a mandate may name another.
Per-end-user caps
GET / PUT /v1/projects/{id}/end-user-caps — up to six caps per project, keyed by the mandate subject’s customer_id:
{ "caps": [
{ "window": "day", "max_actions": 3, "action_on_exceed": "HOLD" },
{ "window": "month", "max_amount_minor": 20000, "action_on_exceed": "DENY" }
] }