Decision API
Ask before you act: POST /enforce, and how to handle each answer.
The proxy covers model calls. This covers everything else your agent does — issuing a refund, deleting a record, sending a message to a customer. You describe the action, Navige decides, and your code acts on the answer.
Request
| Field | Type | What it does |
|---|---|---|
| tool* | string | What the agent wants to do, such as issue_refund. Up to 200 characters; longer is rejected, not truncated. |
| system | string | Which system it acts on — stripe, salesforce. Lets one policy cover a tool name that exists in several places. |
| classification | string | One of read, write, destructive, financial, unknown. |
| arguments | object | What it would be called with. Policies can read these; secrets are redacted before anything is stored. |
| transaction | object | { workflowKey, externalRef } — the business job this belongs to. |
| workflow | object | { key, runId } — tags the call for the workflow view. Visibility only; nothing is gated on it. |
| wait | boolean | Default true. Set false to get an immediate pending instead of blocking on an approval. |
curl -X POST https://api.navige.ai/enforce \
-H "x-navige-key: $NAVIGE_KEY" \
-H "Content-Type: application/json" \
-d '{
"tool": "issue_refund",
"system": "stripe",
"classification": "financial",
"arguments": { "amount_usd": 1240, "customer": "cus_PZ8" },
"transaction": { "workflowKey": "billing.refund", "externalRef": "ticket-88412" }
}'Answers
Every response carries a decision, and most carry a transactionId so you can report the outcome later.
{ "decision": "allow", "transactionId": "tx_01J…" }
{ "decision": "deny", "reason": "Refunds over $500 require a human", "transactionId": "tx_01J…" }
{ "decision": "pending", "approvalId": "apr_01J…", "expiresAt": "2026-10-14T09:12:00Z" }
{ "decision": "approved" }
{ "decision": "rejected", "reason": "Not this customer" }
{ "decision": "expired", "reason": "No answer within 5 minutes" }
{ "decision": "error", "reason": "navige unavailable", "unavailable": true }Handling them
Three of these mean go, three mean stop, and one means Navige could not say. Treating them as a boolean is the mistake worth avoiding.
r = enforce(tool="issue_refund", ...)
if r["decision"] in ("allow", "approved"):
do_the_thing()
elif r["decision"] in ("deny", "rejected", "expired"):
stop(r.get("reason"))
elif r["decision"] == "pending":
# Only when you sent wait=false.
park_and_poll(r["approvalId"])
elif r["decision"] == "error":
# unavailable=True is Navige being down, not your request being wrong.
# Your own fail-open or fail-closed choice decides this branch.
handle_unavailable(r)error, told apart by unavailable. Mapping either one to “denied” means an outage silently stops your agents; mapping both to “allowed” means a typo in your payload silently ungoverns them.Long holds
With wait left at its default, the call blocks up to five minutes waiting for a human. If your caller cannot hold a request open that long — Salesforce Apex caps around 120 seconds — send "wait": false and poll this instead. It returns the same decision shapes.
Reporting the outcome
Navige knows whether the call was allowed. It does not know whether the work succeeded, and will not infer it from an HTTP 200.
curl -X POST https://api.navige.ai/v1/transactions/$TX_ID/outcome \
-H "x-navige-key: $NAVIGE_KEY" \
-H "Content-Type: application/json" \
-d '{"succeeded": true}'Until you do, the outcome is unjudged — which is not the same as failed, and is counted as neither.