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, attempts execution through the bound adapter, and appends every step to the per-tenant SHA-256 audit chain.

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

DecisionHTTPToken minted?Meaning
APPROVED200Yes (EdDSA)Policy passed and execution succeeded.
ESCALATED202No (null)A human approval is required; an escalation/approval request was opened.
DENIED403No (null)Fail-closed deny. No token, no execution.

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 and executes, 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, execution result, 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), the execution outcome, 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*Must equal `<github.owner>/<github.repo>` exactly, or validation fails.
proposedActionstring*Human-readable description of the action being requested.
riskContextobject*Risk factors plus the GitHub workflow target. 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.
githubobjectyesWorkflow target: { owner, repo, ref, workflowId?, inputs?, pullRequest? }.

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

targetSystem is cross-validated: it must equal riskContext.github.owner + "/" + riskContext.github.repo. A mismatch returns 400 VALIDATION_ERROR with the offending path. The request body is strict — unknown top-level fields are rejected.

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,
      "github": {
        "owner": "acme",
        "repo": "platform-api",
        "ref": "refs/heads/main",
        "workflowId": "release.yml"
      }
    }
  }'

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.
executionobject{ attempted, status, adapterId?, reason? }. status is executed / skipped / failure.
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...",
  "execution": { "attempted": true, "status": "executed", "adapterId": "github-actions-adapter" },
  "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,
  "execution": { "attempted": false, "status": "skipped", "reason": "AUTHORITY_ESCALATED" },
  "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,
  "execution": { "attempted": false, "status": "skipped", "reason": "AUTHORITY_DENIED" },
  "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 bound adapter consumes it atomically, so a replayed token is rejected. An adapter validates the token before it runs anything (kid pinning, tenant/adapter binding, decision === "APPROVED", expiry, nonce consume). See Decision Token Model.

Failure modes#

StatusCodeCauseNotes
400VALIDATION_ERRORBody failed schema (e.g. targetSystemowner/repo, 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.
403(approved but blocked)Execution blocked despite approvalexecution.status !== "executed" returns 403 with the token still present.
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, executes no adapter, 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