Skip to content

guides

Intended Documentation

Physical Authority API Reference

Every endpoint, every claim, every error code for /v1/physical/* — ground truth for direct HTTP integration.

Authority API Reference (DOC-P7)#

Audience: anyone integrating directly against the cloud HTTP API (typically because there's no SDK for your language yet).

Versioning: all endpoints under /v1/physical/ are stable. Breaking changes ship as /v2/physical/; we keep /v1/ running for ≥12 months after /v2/ GA.

Base URL#

https://api.intended.so

Sandbox (issuance returns clearly tagged sandbox tokens, rejected by production verifiers):

https://api.sandbox.intended.so

Authentication#

All endpoints require an Authorization: Bearer <api_key> header. API keys are tenant-scoped and are issued from the Intended console.

Authorization: Bearer mrt_abc…
Content-Type:  application/json

API keys are NOT credentials for token-bound robots. The actorIdentity on the request body is what binds an issued token to a specific robot. TOK-P5 (IEEE 802.1AR DevID binding) hardens this further once shipped.

Common error envelope#

All non-2xx responses use this shape:

json
{
  "error": {
    "code": "validation_error",
    "message": "human-readable explanation",
    "details": { ... },         // optional, error-specific
    "requestId": "req_…"        // include this when filing support tickets
  }
}

Error codes:

HTTPcodemeaning
400validation_errorrequest body failed schema validation; details.issues lists offending fields
401unauthenticatedmissing or invalid API key
403forbiddenAPI key lacks permission for this tenant or resource
404not_foundresource (token, snapshot, ticket) does not exist
409conflictduplicate requestId with different body (idempotency violation)
422policy_deniedrequest was syntactically valid but the policy denies; details.safeDefault carries the fallback action
423audit_gap_exceedededge audit chain has been disconnected longer than time_bounded_audit_gap_policy.max_gap_ms
429rate_limitedper-tenant or per-actor rate limit exceeded; Retry-After header set
500internal_errorserver-side bug; safe to retry idempotent requests
503unavailableclassifier or signer is temporarily unhealthy; safe to retry

Endpoints#

POST /v1/physical/classify

Classifies a structured goal into an OIL v2 category. Does NOT issue a token — use /authority-tokens for that. Useful when your policy evaluation pipeline wants to inspect the classification before deciding whether to request a token.

Request:

json
{
  "goal": {
    "schema": "ros2:action:moveit_msgs/Pick",
    "verb": "pick-workpiece",
    "object": "workpiece-A4-7",
    "parameters": { "frame_id": "cell_origin", "...": "..." },
    "actor": { "kind": "cobot", "identifier": "cobot-east-3" },
    "sourceTimestampMs": 1735689600000
  }
}

Response (200):

json
{
  "oilCode": "OIL-1501",
  "confidence": 0.94,
  "safetyBit": true,
  "safetyCitations": ["OIL-2902"],
  "alternatives": [
    { "oilCode": "OIL-1502", "confidence": 0.04 }
  ],
  "failClosed": false,
  "explanation": "verb 'pick-workpiece' + ROS2 MoveIt Pick schema → manipulation/grasp"
}

failClosed: true indicates the classifier could not produce a confident result. Callers MUST treat this as a denial and transition to their declared safe-default.

POST /v1/physical/authority-tokens

Issues a short-lived Authority Token for a physical action.

Request:

json
{
  "intent": {
    "structuredGoal": { ...same shape as /classify... },
    "oilCode": "OIL-1501"          // optional pre-classification; server may override
  },
  "dagNode": {
    "nodeId": "pick-step-1",
    "oilCode": "OIL-1501",
    "deadlineMs": 200,
    "safeDefault": "hold-position",
    "realTimeTier": "rt-soft",
    "streaming": false
  },
  "claims": {
    "actorIdentity": "cobot-east-3",
    "deadlineMs": 200,
    "safeDefault": "hold-position",
    "safetyBit": true,
    "safetyCitations": ["OIL-2902"]
  },
  "physicalState": {
    "safety/emergency_stop": {
      "kind": "boolean",
      "value": false,
      "asOfTimestampMs": 1735689600000,
      "attestation": {
        "channel": "cell-safety-plc/estop-chain",
        "safetyRated": true,
        "protocol": "profisafe"
      }
    }
  },
  "operatorTicketId": null         // populated when a prior ESCALATE was approved
}

Response (200, ALLOW):

json
{
  "decision": "ALLOW",
  "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6Imt…",
  "oilCode": "OIL-1501",
  "expiresAtMs": 1735689600200,
  "safetyBit": true,
  "safetyCitations": ["OIL-2902"],
  "physicalStateRef": "state_…",   // reference into the audit chain
  "tokenJti": "01HQ…"
}

Response (422, DENY):

json
{
  "error": {
    "code": "policy_denied",
    "message": "deny-on-emergency-stop: safety/emergency_stop == true",
    "details": {
      "decision": "DENY",
      "clauseId": "deny-on-emergency-stop",
      "safeDefault": "stop",
      "safetyBit": true,
      "safetyCitations": ["OIL-2902"],
      "evaluatedPredicates": [
        { "predicate": "safety/emergency_stop", "value": true, "satisfied": true }
      ]
    }
  }
}

Response (200, ESCALATE):

json
{
  "decision": "ESCALATE",
  "escalationTicketId": "esc_…",
  "approvalUrl": "https://app.intended.so/approvals/esc_…",
  "safeDefault": "request-operator",
  "expiresAtMs": 1735689660000      // ticket TTL, not token TTL
}

The caller polls /approvals/{ticketId} until approval, then re-calls /authority-tokens with operatorTicketId set to the approved ticket.

GET /v1/physical/approvals/{ticketId}

Polls the status of an escalated approval ticket.

Response (200):

json
{
  "ticketId": "esc_…",
  "status": "pending",            // "pending" | "approved" | "rejected" | "expired"
  "approvedBy": null,
  "approvedAtMs": null,
  "rejectionReason": null,
  "expiresAtMs": 1735689660000
}

When status == "approved", the caller may re-issue against /authority-tokens with operatorTicketId.

POST /v1/physical/snapshots

Records a PhysicalStateProvider snapshot in the audit chain without issuing a token. Useful for forensic checkpoints or for pre-staging state ahead of a sequence of related issuances.

Request:

json
{
  "physicalState": {
    "safety/emergency_stop": { "kind": "boolean", "value": false, "...": "..." }
  }
}

Response (200):

json
{
  "physicalStateRef": "state_…",
  "asOfTimestampMs": 1735689600000
}

GET /v1/physical/audit/{tokenJti}

Returns the audit-chain entry for a previously-issued token. Used by forensic replay tooling and by the SDK's debug helpers.

Response (200):

json
{
  "tokenJti": "01HQ…",
  "issuedAtMs": 1735689600000,
  "expiresAtMs": 1735689600200,
  "actorIdentity": "cobot-east-3",
  "oilCode": "OIL-1501",
  "decision": "ALLOW",
  "structuredGoal": { ... },
  "dagNode": { ... },
  "physicalStateRef": "state_…",
  "physicalState": { ... },
  "safetyBit": true,
  "safetyCitations": ["OIL-2902"],
  "verifications": [
    { "atMs": 1735689600050, "edgeNodeId": "verifier-1", "result": "ok" }
  ],
  "chainHashPrev": "…",
  "chainHashSelf": "…"
}

verifications[] is appended to as edge verifiers report their checks (when TOK-P1 ships).

GET /.well-known/jwks.json

Public JWKS for the cloud token signer. Verifiers cache this. Rotation is signaled by adding new kid values; old kids remain valid until their last token expires (~1 hour after rotation).

Idempotency#

POST /v1/physical/authority-tokens accepts an Idempotency-Key header. If the same key is submitted with the same body within 24 hours, the original token is returned. With a different body, the endpoint returns 409 conflict.

Recommended use: set the key to the DAG node ID + monotonic counter so retries on transient errors don't double-issue.

Rate limits#

Default per-tenant limits:

  • /classify: 100 req/sec sustained, 500 burst
  • /authority-tokens: 50 req/sec sustained, 200 burst
  • Per actorIdentity: 10 req/sec sustained, 30 burst

Higher limits available on enterprise plans. Limits are enforced per-region; the global limit is the sum.

Wire format details#

PhysicalStateValue discriminated union#

ts
type PhysicalStateValue =
  | { kind: "boolean";     value: boolean;     asOfTimestampMs: number; attestation: Attestation }
  | { kind: "number";      value: number;      unit?: string; asOfTimestampMs: number; attestation: Attestation }
  | { kind: "string";      value: string;      asOfTimestampMs: number; attestation: Attestation }
  | { kind: "unavailable"; reason: string;     asOfTimestampMs: number };

type Attestation = {
  channel:      string;     // free-form bus / topic identifier
  safetyRated:  boolean;
  protocol:     "fsoe" | "profisafe" | "pilz-pss" | "opc-ua-safety"
              | "ros2-quality-of-service" | "trusted-bus" | "untrusted";
};

The unavailable variant has no attestation; it represents a predicate the customer's provider could not resolve in time.

safeDefault enum#

"stop" | "hold-position" | "request-operator" |
"transition-safe-state" | "abort-mission" | "ignore"

ignore exists for non-safety-critical actions (e.g. a logging-only agent) and SHOULD NOT appear in any policy clause that touches OIL-2900 (Embodied AI Safety).

realTimeTier enum#

"best-effort" | "rt-soft" | "rt-hard"

rt-hard is reserved for nodes whose deadline miss is a safety event. Use it sparingly; the policy evaluator may apply stricter latency budgets.

See also#

Physical Authority API Reference | Intended