Skip to content

guides

Intended Documentation

Physical-AI Quickstart

From sign-up to your first verified Authority Token for a physical action — register an actor, classify a structured goal, snapshot state, and mint a short-lived RS256 token.

Physical-AI Quickstart#

No robot required. You will register an actor on your fleet roster, classify a structured agent goal, mint a short-lived Authority Token bound to that action, and inspect the wire format your edge / firmware team integrates against.

What you will have at the end#

  1. An Intended tenant and API key (the same key as the digital flow).
  2. A registered actor — token issuance refuses any actorIdentity not on the roster.
  3. Your first Open Intent (OI) classification on a structured goal.
  4. Your first physical Authority Token — an RS256 JWT whose lifetime is the action's deadlineMs.

Prerequisites#

curl, or Python 3.10+, or ROS2 Humble+. A robot is not required — the example uses a JSON snapshot of work-cell state, so the whole loop runs on a laptop.

bash
export INTENDED_API_KEY=mrt_live_xxxxxxxxxxxxxxxxxxxx   # mrt_live_… or mrt_test_…
export INTENDED_API_URL=https://api.intended.so
export INTENDED_TENANT_ID=tenant_acme_prod
export INTENDED_ACTOR_IDENTITY=cobot-east-3

ACTOR_IDENTITY should ultimately be the IEEE 802.1AR DevID of your robot. For the demo, any stable string works — but it must be registered before it can be authorized.

1 — Get an API key#

Sign up at console.intended.so/register. If you already have a tenant from the digital flow, the same mrt_live_… / mrt_test_… key works here. Every /v1/physical/* call carries Authorization: Bearer $INTENDED_API_KEY and x-tenant-id: $INTENDED_TENANT_ID.

2 — Register the actor#

Token issuance for an unregistered actor returns 403 actor_not_registered. Register the robot once on your tenant's fleet roster:

bash
curl -X POST "$INTENDED_API_URL/v1/physical/actors" \
  -H "Authorization: Bearer $INTENDED_API_KEY" \
  -H "x-tenant-id: $INTENDED_TENANT_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "actorIdentity": "cobot-east-3",
    "actorKind": "cobot",
    "displayName": "Cell East — UR10e"
  }'

actorKind is one of robot, cobot, drone, agv, amr, rov, vehicle, other. The server defaults actorIdentityKind to ieee-802-1ar-devid.

3 — Classify a structured goal#

POST /v1/physical/classify maps a non-text goal to an OI category. It does not mint a token — use it to inspect the classification before requesting authority.

bash
curl -X POST "$INTENDED_API_URL/v1/physical/classify" \
  -H "Authorization: Bearer $INTENDED_API_KEY" \
  -H "x-tenant-id: $INTENDED_TENANT_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "goal": {
      "schema": "ros2:action:moveit_msgs/Pick",
      "verb": "pick",
      "object": "workpiece-A4-7",
      "parameters": { "frame_id": "cell_origin" },
      "actor": { "kind": "cobot", "identifier": "cobot-east-3" }
    }
  }'

If failClosed is true, no rule matched the goal with enough confidence. Treat that as a denial and transition to your declared safe-default — do not request a token.

4 — Mint an Authority Token#

POST /v1/physical/authority-tokens runs the full loop: classify (if you do not supply oiCode), evaluate policy over physicalState, and — on ALLOW — mint the token. Send an Idempotency-Key so a retry never double-issues.

bash
curl -X POST "$INTENDED_API_URL/v1/physical/authority-tokens" \
  -H "Authorization: Bearer $INTENDED_API_KEY" \
  -H "x-tenant-id: $INTENDED_TENANT_ID" \
  -H "Idempotency-Key: pick-step-1-0001" \
  -H "Content-Type: application/json" \
  -d '{
    "intent": {
      "structuredGoal": {
        "schema": "ros2:action:moveit_msgs/Pick",
        "verb": "pick",
        "object": "workpiece-A4-7",
        "parameters": { "frame_id": "cell_origin" },
        "actor": { "kind": "cobot", "identifier": "cobot-east-3" }
      }
    },
    "dagNode": {
      "nodeId": "pick-step-1",
      "oiCode": "OI-1502",
      "deadlineMs": 200,
      "safeDefault": "hold-position",
      "realTimeTier": "rt-soft"
    },
    "claims": {
      "actorIdentity": "cobot-east-3",
      "deadlineMs": 200,
      "safeDefault": "hold-position",
      "safetyBit": true,
      "safetyCitations": ["OI-2903", "OI-2904", "OI-2906"]
    },
    "physicalState": {
      "safety/emergency_stop": {
        "kind": "boolean",
        "value": false,
        "asOfTimestampMs": 1735689600000,
        "attestation": {
          "channel": "cell-safety-plc/estop-chain",
          "safetyRated": true,
          "protocol": "profisafe"
        }
      }
    }
  }'

A successful ALLOW returns:

json
{
  "decision": "ALLOW",
  "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6Imt…",
  "oiCode": "OI-1502",
  "expiresAtMs": 1735689600200,
  "safetyBit": true,
  "safetyCitations": ["OI-2903", "OI-2904", "OI-2906"],
  "physicalStateRef": "state_…",
  "tokenJti": "01HQ…"
}

The token's lifetime is your deadline

The physical token's exp / expiresAtMs is set to issuance time + dagNode.deadlineMs — typically sub-second. This is deliberately tighter than the platform's digital Authority Token (a flat 300s cap): a physical token is meant to authorize this one action within its real-time budget, not a session. Request a fresh token per DAG node.

5 — Try the deny path#

Set safety/emergency_stop to true and re-issue. The default policy denies with 422 policy_denied:

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": ["OI-2901", "OI-2902"]
    }
  }
}

The default policy denies three conditions out of the box: emergency-stop (OI-2901/OI-2902), light-curtain breach (OI-2902), and human-in-cell (OI-2902/OI-2904). Upload your own policy with POST /v1/physical/policy to extend it.

6 — Inspect what happened#

The audit entry for any issued token is retrievable by its jti:

bash
curl -H "Authorization: Bearer $INTENDED_API_KEY" \
  -H "x-tenant-id: $INTENDED_TENANT_ID" \
  "$INTENDED_API_URL/v1/physical/audit/<tokenJti>"

The token itself is an RS256 JWT (aud: "intended-edge-verifier"). Its body carries the physical claims under an intended object:

json
{
  "iss": "https://api.intended.so",
  "sub": "cobot-east-3",
  "aud": "intended-edge-verifier",
  "jti": "01HQ…",
  "intended": {
    "version": 2,
    "oiCode": "OI-1502",
    "tenantId": "tenant_acme_prod",
    "actorIdentity": "cobot-east-3",
    "actorIdentityKind": "ieee-802-1ar-devid",
    "deadlineMs": 200,
    "expiresAtMs": 1735689600200,
    "safeDefault": "hold-position",
    "safetyBit": true,
    "safetyCitations": ["OI-2903", "OI-2904", "OI-2906"],
    "dagNodeId": "pick-step-1",
    "realTimeTier": "rt-soft"
  }
}

Verify against the right key — and the right audience

The token signer's public key is served at /.well-known/jwks.json. In dev/sandbox the signer is an ephemeral per-process keypair (it changes on restart), so cache JWKS short and refresh on a kid miss. Also note: tokens mint aud: "intended-edge-verifier", while the shipped intended-ros2 verifier samples default expected_audience="intended-edge". Set your verifier's expected audience to intended-edge-verifier to match what the issuer actually signs.

7 — Map it to your stack#

Your componentWhat you do
ROS2 agentDrop in the intended-ros2 package — see ROS2 integration.
Industrial PLC (TwinCAT / Studio 5000 / TIA Portal)Run a non-RT mediator against the HTTP API — see C++ industrial controls.
Real-time motion controller (Rust)The native Rust verifier is roadmap; verify against cloud-cached JWKS today — see Rust safety-critical firmware.
ML perception / planning (Python)Python ML pipelines — same SDK as above.
Existing safety PLC + busStays where it is. You implement a PhysicalStateProvider and submit the snapshot; Intended never reads your tags directly.

Next#

Physical-AI Quickstart | Intended