Guard API
L’agente propone; COLVO decide, esegue una sola volta e verifica. Bastano tre endpoint: proporre, leggere, decidere.
Proporre un’operazione
POST /v1/operations — chiave agente o backend, o sessione (editor+). Risponde subito con 202 e la decisione; esecuzione e verifica proseguono in modo asincrono.
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": ["…"] } }Azioni e parametri
| action | parametri |
|---|---|
| refund.create | uno tra payment_id | invoice_id | charge_id (pagamenti più vecchi, precedenti ai PaymentIntents), amount_minor (intero > 0), currency (3 lettere), reason? |
| subscription.cancel | subscription_id, mode: period_end (predefinito) | immediate |
| subscription.pause | subscription_id, resumes_at? (data o data e ora; se assente = nessuna data di fine), behavior: void (predefinito) | keep_as_draft. La fatturazione si ferma, l’accesso resta. |
| subscription.resume | subscription_id — toglie la pausa; non viene inviato nulla se l’abbonamento non è in pausa. |
| subscription.change_plan | subscription_id, price_id, at: now (predefinito, con proration) | period_end (programmato; il piano attuale resta fino alla data di fine). |
| coupon.apply | subscription_id, coupon_id. Mai sopra uno sconto già presente: in quel caso l’operazione fallisce prima di inviare qualcosa. |
| order.cancel | Solo Shopify. order_id (#1042, id numerico o gid), refund (predefinito false: se true, quanto resta sull’ordine viene rimborsato nello stesso passaggio), restock (predefinito true), reason: customer (predefinito) | fraud | inventory | declined | other. COLVO legge prima l’ordine: un ordine già annullato → DENY, un ordine che non riesce a leggere → HOLD; con un rimborso, i limiti si applicano all’importo rimborsabile. |
| subscription.cancel_undo | subscription_id — rimuove una disdetta programmata. Un abbonamento già terminato non si può ripristinare: l’operazione fallisce prima di inviare qualcosa. |
Limiti del mandato per le pause: max_pause_days (predefinito 30; oltre → DENY) e pause_open_ended: never | review (predefinito) | allow per una pausa senza data di fine. La verifica rilegge l’abbonamento: pause_collection impostato (o rimosso), cancel_at_period_end false dopo un ripristino, e l’abbonamento ancora attivo con accesso.
Cambi di piano e coupon: allowed_price_ids, change_plan_at (now / period_end), auto_allow_proration_up_to_minor (un cambio che oggi addebita di più → REVIEW; i crediti non richiedono mai revisione), max_proration_minor (oltre → DENY) e allowed_coupons. Per un cambio “now” COLVO chiede al provider un’anteprima dell’addebito prima di decidere; se non riesce a leggerla, la decisione è HOLD. La verifica controlla il nuovo prezzo (o il cambio programmato) e il coupon sull’abbonamento.
Su Shopify l’ordine fa la parte del pagamento: refund.create riceve l’ordine in payment_id e rimborsa denaro su di esso (nulla torna a magazzino, l’ordine resta aperto). Le azioni sugli abbonamenti vengono rifiutate su una connessione Shopify.
Idempotenza
- Invia
Idempotency-Keycon ogni proposta. La stessa chiave con lo stesso body restituisce l’operazione originale (200); la stessa chiave con un body diverso viene rifiutata con409 idempotency_key_reused. - Indipendentemente dall’header, COLVO ricava una chiave di business da mandato + azione + destinatario; una seconda proposta con lo stesso effetto viene rifiutata con
409 business_key_conflict. Un nuovo tentativo non raddoppia mai un rimborso.
Risposta
{
"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": "…"
}Decisioni
ALLOW— messa subito in coda per l’esecuzione.REVIEW— attende una persona;approvalcontiene il digest che deve firmare.HOLD— ora non si può fare in sicurezza (stato non leggibile, limite superato, progetto in pausa). Riproponila più tardi.DENY— violazione della policy; non succederà nulla. I motivi sono espliciti.
Ciclo di vita
exec_state: NOT_SENT → QUEUED → SENDING → ACKNOWLEDGED | PROVIDER_REVIEW | FAILED | UNKNOWN | CANCELED_BEFORE_SEND. verify_state: NOT_DUE → PENDING → VERIFIED | MISMATCH | UNVERIFIABLE. MISMATCH e UNVERIFIABLE aprono un incidente e inviano gli alert.
Leggere un’operazione
GET /v1/operations/{id} — qualsiasi chiave o sessione. Stessa forma di sopra, aggiornata. Interrogala dopo un REVIEW per conoscere l’esito, oppure iscriviti agli alert.
Approvare o rifiutare
POST /v1/operations/{id}/decision — solo sessione (editor+). Chi rivede firma il digest esatto; un digest non aggiornato viene rifiutato (409 digest_mismatch), così come un’approvazione scaduta (409 approval_expired).
{ "decision": "approve", "approval_digest": "sha256:…", "rationale": "customer confirmed duplicate charge" }Notifiche di approvazione
Un REVIEW avvisa chi può decidere: un’email a ogni owner ed editor, e un alert a ogni canale di alert del progetto iscritto a approval.requested / approval.reminder / approval.expired (Slack, email, webhook firmato). La finestra di approvazione è la più breve tra 24 ore e la scadenza del mandato. Un promemoria parte al 75 % della finestra; alla fine un’approvazione non decisa diventa EXPIRED, l’operazione CANCELED_BEFORE_SEND, e chi approva viene avvisato. Le email puntano all’approvazione — la decisione avviene sempre da utente autenticato, sul digest esatto.
Kill-switch
POST /v1/projects/{id}/pause con { "reason": "…" } ferma le nuove scritture: le nuove proposte ricevono HOLD, le esecuzioni in coda attendono. POST /v1/projects/{id}/unpause rimette in coda le operazioni trattenute. Entrambi sono solo sessione (editor+) e registrati.
Abilitazione
Proporre fuori da un tentativo di test richiede la funzionalità Guard. Senza di essa l’API risponde 403 entitlement_required con un suggerimento di upgrade; la console mostra “Sblocca Guard”. Vedi i prezzi.