Skip to content

Orchestrator API

For the backend that calls delta. Assumes nothing; the lifecycle below is the whole model.

The Orchestrator API is the HTTP / OpenAPI surface your backend integrates against. It drives the whole lifecycle — registering policies, accepting authored intents, accepting agent proposals, and reporting verified outcomes.

This page is the narrative. Every endpoint and schema is generated from the committed OpenAPI spec, and the reference carries its own navigation and version picker:

The same surface as a machine-readable document, for client generation and for agents: openapi.json.

  1. Register the policy. POST /policies compiles, validates, and stores a policy in one step, returning a content-addressed policy_id (base58 SHA-256). GET /policies/{policy_id} returns a pretty-printed representation of the stored policy (text/plain).

  2. Submit the authored intent. POST /intents accepts an AuthoredIntent: an intent referencing a policy_id, plus its authorship. A signature is verified when the intent carries one; Unsigned always passes. The referenced policy must already exist. The intent_id is a caller-supplied UUID. GET /intents/{intent_id} reads it back.

    The intent’s attrs carries the values for the policy’s parameters block — the limits the user decides when defining the intent. The orchestrator passes them to the evaluator alongside the extracted evidence, so one policy serves many intents at different limits. Each value is type-tagged — { "Int": 12000 }, not 12000; see What gets signed for the canonical form and value encoding for the tagging rules in full, which the API schema does not describe. attrs is optional: omit the field rather than sending "attrs": null.

  3. Submit the agent’s proposal. POST /intents/{intent_id}/proposal submits the concrete proposed action. This triggers evidence extraction and proof generation.

  4. Get the outcome. The POSTs above return on acceptance, not with the verdict — proving runs afterward. Get the result via GET /events (push) or GET /intents/{intent_id}/status (poll): openprocessingsuccess / failure, or expired.

A failure always carries a reason. Whether it also carries evidence or constraint_failures depends on how far the pipeline got — there are three ways to fail, and only the middle one is about the policy:

Failure pathevidenceconstraint_failures
Evidence extraction failedabsentempty
Policy evaluation failed — a failed constraintpresentnon-empty
Proving failedpresentempty

Two traps follow. An empty constraint_failures does not mean extraction failed — a prover error looks the same and still carries evidence. And evidence is absent only in the first row, so code that reads it unconditionally breaks exactly when extraction is what went wrong.

Each entry in constraint_failures is a ConstraintFailure, carrying the constraint’s index, its expression re-printed for display, and its own reason — see the reference for the shape.

Every intent carries an authorship saying how it is authorized — either Unsigned, which asserts no cryptographic authorship and always verifies, or Signed with one of these schemes:

  • Ed25519
  • Passkey (WebAuthn / ECDSA)
  • Multisig (weighted signers + threshold)

Unsigned changes only who is provably the author. Evidence extraction, policy evaluation, and the proof over the evaluation are identical either way.

The orchestrator does not require a signature: an Unsigned intent always passes this check. Where you send unsigned intents, authorship rests on your own authentication of the user.

The signature is over the JCS (RFC 8785, JSON Canonicalization Scheme) serialization of the intent object.

Start from the intent:

{
"id": "3f7c1b9e-1f2a-4c6d-9b8e-2a5d7c0f4e11",
"policy_id": "9xQeWLpsPZCTPnCLqYd7YRQ8mgUYcLNPTZBcgLPnQxYs",
"attrs": { "fields": { "max_total_usd_cents": { "Int": 12000 } } }
}

Canonicalize and sign those bytes:

import canonicalize from "canonicalize";
const jcs = canonicalize(json_intent)
const bytes = new TextEncoder().encode(jcs)
const signed = signer.signBytes(bytes)

jcs is exactly this — keys sorted at every level, no whitespace (161 bytes):

{"attrs":{"fields":{"max_total_usd_cents":{"Int":12000}}},"id":"3f7c1b9e-1f2a-4c6d-9b8e-2a5d7c0f4e11","policy_id":"9xQeWLpsPZCTPnCLqYd7YRQ8mgUYcLNPTZBcgLPnQxYs"}

Note attrs sorts first even though it is written last above — that reordering is the whole point of canonicalizing.

Then POST /intents the envelope. Ed25519 carries the base58 public key alongside the base58 signature, so the orchestrator can verify without a key lookup:

{
"intent": {
"id": "3f7c1b9e-1f2a-4c6d-9b8e-2a5d7c0f4e11",
"policy_id": "9xQeWLpsPZCTPnCLqYd7YRQ8mgUYcLNPTZBcgLPnQxYs",
"attrs": { "fields": { "max_total_usd_cents": { "Int": 12000 } } }
},
"authorship": {
"Signed": {
"Ed25519": {
"pub_key": "<base58 32-byte public key>",
"signature": "<base58 64-byte signature>"
}
}
}
}

The unsigned form of the same submission replaces the authorship value with the bare string "Unsigned".