Connect

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.

POST/enforce

Request

FieldTypeWhat it does
tool*stringWhat the agent wants to do, such as issue_refund. Up to 200 characters; longer is rejected, not truncated.
systemstringWhich system it acts on — stripe, salesforce. Lets one policy cover a tool name that exists in several places.
classificationstringOne of read, write, destructive, financial, unknown.
argumentsobjectWhat it would be called with. Policies can read these; secrets are redacted before anything is stored.
transactionobject{ workflowKey, externalRef } — the business job this belongs to.
workflowobject{ key, runId } — tags the call for the workflow view. Visibility only; nothing is gated on it.
waitbooleanDefault true. Set false to get an immediate pending instead of blocking on an approval.
bash
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.

json
{ "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.

python
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 is not deny
A malformed request and an unreachable Navige both come back as 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

GET/enforce/status/:approvalId

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.

bash
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.

Something here wrong or missing? Tell us — a person reads it.