Skip to content

api reference

Intended Documentation

API Reference: Intents

Submit an IntentRequest to the authority runtime and get an APPROVED, ESCALATED, or DENIED decision with a single-use Ed25519 token. Full request/response contract, status codes, and error modes.

Intents API#

POST /intent is the core of the runtime. You submit an IntentRequest — who wants to do what, to which system, with what risk context — and the route runs the full authority loop in one round trip: it evaluates policy, computes a risk score, reaches a decision, mints a single-use Ed25519 token only if the action is approved, and appends every step to the per-tenant SHA-256 audit chain. Intended decides and signs; it never runs the action. Your agent acts after an APPROVED decision, carrying the token, and the service it calls verifies the token first.

The decision is one of three legacy-enum outcomes, each with its own HTTP status:

DecisionHTTPToken minted?Meaning
APPROVED200Yes (EdDSA)Policy passed. Your agent may act, carrying the token.
ESCALATED202No (null)A human approval is required; an escalation/approval request was opened.
DENIED403No (null)Fail-closed deny. No token; the action must not run.

Warning

These are the only values /intent returns. The modern POST /authority/evaluate engine uses a different vocabulary (ALLOW / DENY / REQUIRE_APPROVAL / ESCALATE) — see the Policies API. Do not mix them in one client.

The authority loop
POST /intent runs compile, risk scoring, policy evaluation, and a most-severe-wins decision; APPROVED mints a single-use Ed25519 token, ESCALATED opens an approval, DENIED stops — all branches append to the audit chain.

A single POST /intent request runs the entire loop and returns the decision, token, and audit head together.

Submit an Intent#

POST/intentRequires auth

Runs the authority loop for one intent and returns the decision, the token (on approval), and the audit head.

tenantIdstring*Must match the tenant your credential resolves to, or 403 TENANT_MISMATCH.
actorobject*{ id: string, type: 'user' | 'service' }. The identity requesting the action.
targetSystemstring*The system the action targets, for example `acme/platform-api` or `stripe/subscriptions`.
proposedActionstring*Human-readable description of the action being requested.
riskContextobject*Risk factors and optional flat context. See the riskContext table.
intentTypestringOptional canonical id <domain>.<resource>.<verb> (lowercase, digits, underscores).

riskContext fields#

FieldTypeRequiredNotes
baseRiskScoreinteger 0–100yesStarting risk before modifiers.
policyCompliantbooleanyesCaller's own compliance assertion.
requiresPrivilegedAccessbooleanno (default false)Adds +20 to risk when true.
touchesProductionbooleanno (default false)Adds +20 to risk when true.
containsSensitiveDatabooleanno (default false)Adds +15 to risk when true.
justificationstringnoFree-text rationale, carried into the record.
contextobjectnoFlat string key/value context recorded with the decision (up to 25 keys).

The final risk score is baseRiskScore + (privileged?20) + (production?20) + (sensitive?15), capped at 100. The modifier weights (20/20/15) are tenant-overridable defaults.

Info

The request body is strict — unknown fields are rejected with 400 VALIDATION_ERROR and the offending path. Put the action's details (ids, counts, tool-call arguments) in the optional top-level arguments object.

Request#

bash
curl -X POST https://api.intended.so/intent \
  -H "Authorization: Bearer $INTENDED_API_KEY" \
  -H "x-tenant-id: tenant_acme_prod" \
  -H "Content-Type: application/json" \
  -d '{
    "tenantId": "tenant_acme_prod",
    "actor": { "id": "svc-ci-bot", "type": "service" },
    "targetSystem": "acme/platform-api",
    "proposedAction": "dispatch release workflow",
    "riskContext": {
      "baseRiskScore": 42,
      "policyCompliant": true,
      "requiresPrivilegedAccess": false,
      "touchesProduction": false,
      "containsSensitiveData": false
    }
  }'

Response fields#

FieldTypeNotes
intentIdstring (uuid)The persisted intent's id.
correlationIdstring (uuid)Ties together every audit entry for this request.
authorityDecisionobject{ decision, riskScore, policyCompliant, rationale[], gateTrace[] }.
authorityDecisionTokenstring | nullEd25519 JWT on APPROVED; null on ESCALATED / DENIED.
auditobject{ entriesWritten, latestHash } — count of entries appended and the new chain head.
escalationIdstringEscalate only. The opened escalation record id.
approvalRequestIdstring | nullEscalate only. The approval request id, or null.
explanation / summaryobject / stringHuman-readable rendering of the decision rationale.

The nested authorityDecision.gateTrace[] is an ordered list of { gate, outcome: "PASSED"|"FAILED"|"SKIPPED", reason } entries — the recorded path the loop took to its decision.

Response — Approved (200)#

json
{
  "intentId": "8aa3f5f6-b1a9-4c5b-a29f-b489f7d0be58",
  "correlationId": "a9d903e0-8b45-4a2f-9d30-6f9ce7d04d78",
  "authorityDecision": {
    "decision": "APPROVED",
    "riskScore": 42,
    "policyCompliant": true,
    "rationale": ["matched allow rule"],
    "gateTrace": [{ "gate": "authority_rules", "outcome": "PASSED", "reason": "matched allow rule" }]
  },
  "authorityDecisionToken": "eyJhbGciOiJFZERTQSIsImtpZCI6InRlbmFudC0xNzA...",
  "audit": { "entriesWritten": 6, "latestHash": "<entryHash>" }
}

Response — Escalated (202)#

json
{
  "intentId": "8aa3f5f6-b1a9-4c5b-a29f-b489f7d0be58",
  "correlationId": "a9d903e0-8b45-4a2f-9d30-6f9ce7d04d78",
  "escalationId": "esc_123",
  "approvalRequestId": "apr_123",
  "authorityDecision": {
    "decision": "ESCALATED",
    "riskScore": 67,
    "policyCompliant": true,
    "rationale": ["production change requires approval"],
    "gateTrace": [{ "gate": "approval", "outcome": "FAILED", "reason": "approval required" }]
  },
  "authorityDecisionToken": null,
  "audit": { "entriesWritten": 4, "latestHash": "<entryHash>" }
}

Response — Denied (403)#

json
{
  "intentId": "8aa3f5f6-b1a9-4c5b-a29f-b489f7d0be58",
  "correlationId": "a9d903e0-8b45-4a2f-9d30-6f9ce7d04d78",
  "authorityDecision": {
    "decision": "DENIED",
    "riskScore": 91,
    "policyCompliant": false,
    "rationale": ["risk score exceeds deny threshold"],
    "gateTrace": [{ "gate": "authority_rules", "outcome": "FAILED", "reason": "deny rule matched" }]
  },
  "authorityDecisionToken": null,
  "audit": { "entriesWritten": 3, "latestHash": "<entryHash>" }
}

About the token#

When the decision is APPROVED, authorityDecisionToken is an Ed25519 JWT signed with a per-tenant key (header carries alg: "EdDSA", kid). Its TTL defaults to and is capped at 300 seconds, and its nonce is single-use. The service your agent calls validates the token before it acts (kid pinning, tenant and target binding, decision === "APPROVED", expiry) and consumes the nonce atomically, so a replayed token is rejected. See Verify Tokens & Decisions and Decision Token Model.

Failure modes#

StatusCodeCauseNotes
400VALIDATION_ERRORBody failed schema (e.g. missing field, unknown field)issues[] carries the Zod paths.
401UNAUTHORIZED / INVALID_PORTAL_SESSIONMissing/invalid credentialSee Authentication.
403TENANT_MISMATCHBody tenantId ≠ credential tenantCross-tenant boundary.
403(denied decision)DENIED outcomeThis is a decision, not an auth error — body carries the full authorityDecision.
429RATE_LIMITEDPer-IP rate limit exceededBack off and retry.
500AUTHORITY_LOOP_FAILEDAny uncaught failure in the loopFail-closed — treat as a deny.

List Intents#

GET/intentsRequires auth

Returns persisted intent records for the tenant, newest first, with offset pagination.

tenantIdstring*Tenant identifier (query).
limitnumber1–100 (default 25).
offsetnumberPagination offset (default 0).
decisionstringFilter: APPROVED, ESCALATED, or DENIED.
bash
curl "https://api.intended.so/intents?tenantId=tenant_acme_prod&decision=ESCALATED&limit=20" \
  -H "Authorization: Bearer $INTENDED_API_KEY" \
  -H "x-tenant-id: tenant_acme_prod"

Response: { tenantId, total, limit, offset, intents: [...] }.

Get Intent Detail#

GET/intents/:idRequires auth

Retrieve a single intent record by id. Returns 404 INTENT_NOT_FOUND if no record exists for the tenant.

idstring*Intent id (path).
tenantIdstring*Tenant identifier (query).
bash
curl "https://api.intended.so/intents/8aa3f5f6-b1a9-4c5b-a29f-b489f7d0be58?tenantId=tenant_acme_prod" \
  -H "Authorization: Bearer $INTENDED_API_KEY" \
  -H "x-tenant-id: tenant_acme_prod"

Simulate Without Persistence#

POST/intent/simulateRequires auth

Evaluates an intent with the same authority logic but mints no token and writes no records. Use it to preview a decision in CI or a policy console.

The response is explicit about being a dry run:

json
{
  "simulation": true,
  "intentId": "8aa3f5f6-b1a9-4c5b-a29f-b489f7d0be58",
  "authorityDecision": { "decision": "APPROVED", "riskScore": 42, "rationale": ["..."] },
  "note": "This is a simulation. No records were persisted, no tokens were minted, and no execution occurred."
}

Next steps#

API Reference: Intents | Intended