Aller au contenu principal

HTTP routes reference

A precise reference for the AUSUS HTTP surface. Use this page when building a non-React client (mobile, Vue, server-to-server, CLI). The narrative version lives at Backend · HTTP API; this page is the lookup table.

The Router is a single PSR-15 RequestHandlerInterface shipped in ausus/api-http. It serves three resource routes under a configurable prefix (default /api) plus CORS preflight.

Route table

MethodPathPurposeHeaders required
GET/_healthLiveness probe; returns the compiled-graph hash.
GET/projections/{fqn}Render a projection to a ViewSchema.X-Tenant-ID
POST/actions/{fqn}Invoke an action through the full runtime chain.X-Tenant-ID (+ X-Actor-Roles for any action with requireRole)
OPTIONS*CORS preflight; always 204 No Content with Access-Control-Allow-*.

{fqn} is URL-decoded; pass the dotted FQN directly (billing.invoice.summary, not URL-encoded).

The prefix is set by the Router constructor's last argument (default '/api') or via ApplicationConfig::apiPrefix('/v2').

Request headers

HeaderRequiredFormatBehaviour
X-Tenant-IDyes on /projections/* and /actions/*non-empty stringThe active tenant for the request. Missing → 400 BadRequest.
X-Actor-IdnostringThe acting actor id. Default 'anon' when absent.
X-Actor-Rolesnocomma-separated role names, e.g. invoice.creator,invoice.viewerWhitespace around each role is trimmed; empty entries are dropped. Missing or empty → roleless actor → every action with requireRole(...) returns 403 PolicyDenied. No fallback role set is substituted.
Content-Typeyes on POST /actions/*application/jsonOther values are parsed best-effort; a body that does not JSON-decode to an object raises 400 BadRequest.

GET /_health

curl -s http://localhost:8080/api/_health

Always returns 200 OK with:

{ "ok": true, "service": "ausus/api-http", "graphHash": "7c1e9b3a…" }

The graphHash is the SHA-256 of the canonical MetadataGraph form; two processes serving identical plugin sets return the same hash. Useful as a rolling-deploy sanity check.

GET /projections/{fqn}

Query parameters

NameRequiredEffect
subjectnoWhen present, the projection renders in detail form for the entity whose identity is the value. When absent, list form.
localenoReserved. v0.1.x emits locale: 'en-US' regardless.
renderernoReserved. v0.1.x targets react.web.v1.
acceptSchemaVersionsnoReserved. v0.1.x emits schemaVersion: '1.0.0'.

Examples

List view:

curl -s -H 'X-Tenant-ID: acme' \
'http://localhost:8080/api/projections/billing.invoice.summary'

Detail view:

curl -s -H 'X-Tenant-ID: acme' \
'http://localhost:8080/api/projections/billing.invoice.detail?subject=01J7HG3WC0D3K…'

Response

200 OK with the full ViewSchema JSON document:

{
"schemaVersion": "1.0.0",
"targetProfile": "react.web.v1",
"metadata": { "projection": "billing.invoice.summary", "entity": "billing.invoice",
"tenant": "acme", "locale": "en-US", "generatedAt": "2026-05-26T20:14:00Z" },
"fields": [ /* FieldDescriptor[] */ ],
"actions": [ /* ActionDescriptor[] — inputs[] always emitted, initialValues on update + detail */ ],
"filters": [],
"data": { "items": [ /* rows */ ], "pagination": { "nextCursor": null, "pageSize": 3 } }
}

Or { "item": { … } | null } in the data slot for detail.

Projection routes do not invoke a policy in v0.1.x — they return the full row set for the tenant. Plan authorisation outside the Router or narrow projections per-tenant.

POST /actions/{fqn}

Request body

{
"subject": { "tenantId": "acme", "entityFqn": "billing.invoice",
"identityHandle": "01J…" } | null,
"inputs": { /* per-action key/value map */ }
}
  • subject is null for create actions; an object for transition and update actions. The tenantId MUST match X-Tenant-ID — mismatch raises 403 TenantBoundaryViolation.
  • inputs carries the action's payload. Shape rules:
    • string / enum → string;
    • integer → number;
    • datetime → ISO-8601 string;
    • money{ "amount": "12.34", "currency": "USD" };
    • nullable fields → null is accepted and stored as SQL NULL. See ViewSchema wire reference for the per-input metadata that drives this.

Examples

Create:

curl -s -X POST http://localhost:8080/api/actions/billing.invoice.create \
-H 'X-Tenant-ID: acme' \
-H 'X-Actor-Roles: invoice.creator' \
-H 'Content-Type: application/json' \
-d '{
"subject": null,
"inputs": {
"number": "INV-2026-001",
"customer_name": "ACME",
"amount": { "amount": "1500.00", "currency": "USD" }
}
}'

Transition:

curl -s -X POST http://localhost:8080/api/actions/billing.invoice.issue \
-H 'X-Tenant-ID: acme' \
-H 'X-Actor-Roles: invoice.issuer' \
-H 'Content-Type: application/json' \
-d '{"subject":{"tenantId":"acme","entityFqn":"billing.invoice","identityHandle":"01J…"},"inputs":{}}'

Update (partial PATCH):

curl -s -X POST http://localhost:8080/api/actions/tracker.issue.rename \
-H 'X-Tenant-ID: acme' \
-H 'X-Actor-Roles: tracker.member' \
-H 'Content-Type: application/json' \
-d '{"subject":{"tenantId":"acme","entityFqn":"tracker.issue","identityHandle":"01J…"},
"inputs":{"title":"new title"}}'

Success response

{ "ok": true, "outputs": { /* effect outputs */ } }

outputs content depends on the action's effect:

Effectoutputs keys
createnew entity id + every field the create payload included (or that the workflow seeded).
transitionthe state field's new value, every stamped field, _version.
updateevery patched field + _version (PATCH semantics — untouched fields are not echoed).

Error response

{ "ok": false, "error": { "kind": "<KindName>", "message": "<human-readable>" } }

Status code table

ErrorMapper classifies every thrown exception by its short PHP class name. The mapping is the same on every route.

Statuserror.kindCause
200 OKSuccessful response.
204 No ContentOPTIONS * preflight only.
400 BadRequestBadRequestMissing required header, malformed JSON body, missing subject on a transition/update action.
400PolicySubjectRequiredAction declared with subjectRequired: true invoked with subject: null.
400ActorRequired / TenantContextRequiredDeclared but never thrown by v0.1.x runtime — reserved.
403 ForbiddenPolicyDeniedThe actor does not hold a role the action's policy requires.
403TenantBoundaryViolationThe subject.tenantId differs from the active tenant.
403WorkflowGuardDeniedDeclared, reserved; not thrown by v0.1.x.
404 Not FoundUnknownActionThe action FQN does not exist in the graph.
404NotFoundAn update or transition referenced a subject that does not exist.
404WorkflowSubjectNotFoundWorkflow guard could not find the subject mid-evaluation.
404ProjectionNotFoundThe projection FQN does not exist in the graph.
409 ConflictWorkflowStateMismatchTransition source does not match the entity's current state.
409ConcurrencyConflictOptimistic lock failed — the row was modified since the read.
500 Internal Server ErrorEffectFailedA custom effect threw an exception not in the closed taxonomy. The wrapped cause is logged but not exposed.
500AuditEmissionFailedAudit-sink write failed inside the transaction.
500InternalErrorAny other unrecognised throwable.

CORS

Every response — success or error — carries:

Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, X-Tenant-ID, X-Actor-Id, X-Actor-Roles
Access-Control-Max-Age: 600

OPTIONS * returns 204 No Content with the same headers. Narrow the origin (and verify request signatures) in front of the Router — see Operations · Authenticated gateway.

What the Router does not do

  • No authentication. No JWT verification, no session lookup. Treat X-Tenant-ID / X-Actor-* as untrusted unless an authenticated gateway sets them.
  • No rate limiting.
  • No CSRF protection (POSTs are JSON and require Content-Type: application/json — adequate against form-style CSRF but not a general-purpose mitigation).
  • No request validation beyond the kernel taxonomy. Custom validation belongs in a custom Policy or Effect.
  • No projection-level authorisation enforcement (v0.1.x limitation).