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.
The lifecycle, mapped to endpoints
Section titled “The lifecycle, mapped to endpoints”-
Register the policy.
POST /policiescompiles, validates, and stores a policy in one step, returning a content-addressedpolicy_id(base58 SHA-256).GET /policies/{policy_id}returns a pretty-printed representation of the stored policy (text/plain). -
Submit the authored intent.
POST /intentsaccepts anAuthoredIntent: an intent referencing apolicy_id, plus itsauthorship. A signature is verified when the intent carries one;Unsignedalways passes. The referenced policy must already exist. Theintent_idis a caller-supplied UUID.GET /intents/{intent_id}reads it back.The intent’s
attrscarries the values for the policy’sparametersblock — 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 }, not12000; see What gets signed for the canonical form and value encoding for the tagging rules in full, which the API schema does not describe.attrsis optional: omit the field rather than sending"attrs": null. -
Submit the agent’s proposal.
POST /intents/{intent_id}/proposalsubmits the concrete proposed action. This triggers evidence extraction and proof generation. -
Get the outcome. The
POSTs above return on acceptance, not with the verdict — proving runs afterward. Get the result viaGET /events(push) orGET /intents/{intent_id}/status(poll):open→processing→success/failure, orexpired.
How an intent fails
Section titled “How an intent fails”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 path | evidence | constraint_failures |
|---|---|---|
| Evidence extraction failed | absent | empty |
| Policy evaluation failed — a failed constraint | present | non-empty |
| Proving failed | present | empty |
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.
Authorship
Section titled “Authorship”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.
What gets signed
Section titled “What gets signed”The signature is over the JCS (RFC 8785,
JSON Canonicalization Scheme) serialization of the intent object.
Worked example
Section titled “Worked example”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".