Skip to content

Backend API reference

Control plane for the Katena Portal Store: suite catalog, per-org entitlements, and entitlement-gated signed bundle downloads. Trusts tokens Better Auth issues (validated by userinfo introspection).

Axum service in api/, default port 7002. This page is generated from the live OpenAPI 3.1 document; the handlers’ #[utoipa::path] annotations are the source of truth.

  • docs/openapi.json — checked-in OpenAPI 3.1 document (import into Postman / Insomnia / openapi-generator, or diff it in review).
  • Live, when the server is running: GET /openapi.json (same document) and GET /docs (API reference UI).

Three independent mechanisms — a route uses one or none, never two:

Mechanism Used by How
Bearer token (Authorization: Bearer <token>) Every route marked auth Opaque OAuth access token from Better Auth (the server/ provider), validated by userinfo introspection; the caller’s org is derived from it.
HMAC signature Routes marked HMAC Signed over the exact raw body; no bearer. /webhooks/payments uses X-Katena-Signature, /webhooks/stripe uses Stripe-Signature.
Robot signature Routes marked robot (/robot-agent/*) ed25519 over METHOD\nPATH\nunix_ts\nnonce\nsha256_hex(body) with the robot’s enrolled key, in x-katena-robot, x-katena-ts (±60 s), x-katena-nonce (16 to 64 of [A-Za-z0-9_-], accepted once per key) and x-katena-sig. Enroll is signed with the key it registers.

Auth’d routes are org-scoped: you only see your own org’s data. Missing/invalid token → 401; valid token without an active org → 403.

Each auth’d route also needs a permission from the caller’s org role (owner, admin, technician, event_operator, viewer, legacy member), carried as org_role in userinfo; a role without it gets 403 forbidden: <resource>:<action>. /admin/* needs the katena_admin platform role (or an org in the legacy ADMIN_ORG_IDS), else 403 not_admin. The matrix lives in server/access.js, mirrored by api/src/authz.rs.

The ARC member and Open Call routes (/me/*, /open-call/*) are per person (the token’s sub), not per org, and need no active org. /admin/open-call/* and /admin/points need the open_call:review platform permission (open_call_staff or katena_admin). Their errors read <code>: <sentence>; match the code.

auth = bearer required · HMAC = signature-authenticated webhook · robot = signed by an enrolled robot · signed = short-lived signed URL (exp + sig), no bearer · — = public.

store — Suite catalog + the caller org’s library

Section titled “store — Suite catalog + the caller org’s library”
Method Path Auth Purpose
GET /catalog — Public catalog — browsable without login (Steam store front page).
POST /entitlements/claim-free auth Claim a free suite for the caller’s org.
POST /entitlements/request auth Request access to a restricted suite.
GET /library auth The caller org’s owned suites (Steam library).

bundles — Entitlement-gated, signed bundle downloads (robot deploy path)

Section titled “bundles — Entitlement-gated, signed bundle downloads (robot deploy path)”
Method Path Auth Purpose
GET /bundles/{suite}/download signed Validate the signed URL, then stream the file.
POST /bundles/{suite}/url auth Entitlement gate → signed URL + manifest.

commerce — Checkout, orders, and the payment webhook (P4)

Section titled “commerce — Checkout, orders, and the payment webhook (P4)”
Method Path Auth Purpose
POST /checkout auth Start a purchase: validate, then create (or idempotently reuse) the pending order.
GET /orders/{id} auth Poll one of your org’s orders (the desktop’s “waiting for payment…” target).
POST /webhooks/payments HMAC The payment provider’s webhook.

admin — Curated publishing: upload → publish → yank (katena_admin platform role, or legacy ADMIN_ORG_IDS; P5)

Section titled “admin — Curated publishing: upload → publish → yank (katena_admin platform role, or legacy ADMIN_ORG_IDS; P5)”
Method Path Auth Purpose
GET /admin/access-requests auth List access requests by status (default pending) so an admin can find the id to approve.
POST /admin/access-requests/{id}/approve auth Approve a pending access request: grant the entitlement (source access_grant) and mark the row approved, in one transaction.
POST /admin/orders/{id}/refund auth Admin-initiated Stripe refund.
POST /admin/products auth Authoritative product upsert.
GET /admin/products/{id}/releases auth Every release of a product, drafts and yanked included — the admin console view.
POST /admin/products/{id}/releases auth Create a draft release.
PUT /admin/products/{id}/releases/{version}/artifact auth Upload an artifact’s bytes for a release.
POST /admin/products/{id}/releases/{version}/publish auth Publish a draft release (requires ≥1 uploaded artifact).
POST /admin/products/{id}/releases/{version}/yank auth Yank a published release — it disappears from public releases and resolve immediately.

downloads — Entitlement-gated resolve for local installs (releases, artifacts, signed manifests)

Section titled “downloads — Entitlement-gated resolve for local installs (releases, artifacts, signed manifests)”
Method Path Auth Purpose
POST /downloads/resolve auth Resolve an entitlement-gated download for local install: release + artifact + signed manifest + short-lived signed URL.
GET /products/{id}/releases — A product’s published releases, newest first (public — release notes are browsable pre-login).

sync — Per-org sync of desktop state (profiles, recent combos)

Section titled “sync — Per-org sync of desktop state (profiles, recent combos)”
Method Path Auth Purpose
GET /sync/{kind} auth The caller org’s last-synced document of this kind.
PUT /sync/{kind} auth Replace the caller org’s document of this kind (last-write-wins).

market — Motion Market: Spark-priced motion purchases, likes, and wallet top-ups

Section titled “market — Motion Market: Spark-priced motion purchases, likes, and wallet top-ups”
Method Path Auth Purpose
POST /market/buy auth Spend Spark to own a motion.
GET /market/library auth The caller org’s owned + liked motions and Spark balance.
POST /market/like auth Toggle a like on a motion.
POST /market/topup auth Add Spark to the caller org’s wallet (bounded).

fleet — Per-org live station roster, deployments, and install status

Section titled “fleet — Per-org live station roster, deployments, and install status”
Method Path Auth Purpose
GET /fleet auth The caller org’s live station roster.
POST /fleet/deploy auth Deploy (or re-target) a product on a station.
POST /fleet/deploy/{station}/{product}/status auth A station reports install progress.
GET /fleet/deployments auth The caller org’s deployment roster (every station × product).
POST /fleet/heartbeat auth Upsert the caller station’s current status for its org.

devices — Robot device-identity registry (ed25519 public keys)

Section titled “devices — Robot device-identity registry (ed25519 public keys)”
Method Path Auth Purpose
GET /devices auth The caller org’s device-credential roster.
POST /devices/register auth Register (idempotently) a station’s device public key.
Section titled “robots — Claimed robots and the install jobs that target them (V2 robot link); robots from ARC on their way (DM-11)”
Method Path Auth Purpose
POST /robot-jobs/{id}/cancel auth Cancel a job at a safe point (pending, preflight, downloading).
GET /robots auth The caller org’s robots with connectivity and last heartbeat.
POST /robots/claim-codes auth Mint a one-time code to claim a robot (10 min).
GET /robots/incoming auth Robots ARC has shipped to the caller’s organization that are not enrolled yet: on their way, or delivered and ready to enroll with a claim code.
GET /robots/{id} auth One robot in the caller’s org.
GET /robots/{id}/jobs auth The robot’s job history, newest first.
POST /robots/{id}/jobs auth Install, roll back or uninstall a product on the robot.
POST /robots/{id}/revoke auth Stop trusting the robot’s key and cancel its live jobs.

robot-agent — Outbound Robot Agent: enroll, heartbeat, jobs (signed by the robot’s key)

Section titled “robot-agent — Outbound Robot Agent: enroll, heartbeat, jobs (signed by the robot’s key)”
Method Path Auth Purpose
POST /robot-agent/enroll robot Claim a robot with a one-time code.
POST /robot-agent/heartbeat robot Inventory, health and installed packages (signed).
GET /robot-agent/jobs robot The robot’s live jobs, oldest first (signed).
POST /robot-agent/jobs/{id}/status robot The agent moves its job one step (signed).

partners — Partner orgs that sell to their own customers: join codes, activations (each grants the entitlement and records a sale for ARC billing)

Section titled “partners — Partner orgs that sell to their own customers: join codes, activations (each grants the entitlement and records a sale for ARC billing)”
Method Path Auth Purpose
GET /admin/partners auth Every partner org.
PUT /admin/partners/{org_id} auth Make an org a partner, or pause / resume one.
GET /partner auth The caller’s own partner record (to show the join code). 404 if the org is not a partner.
POST /partner/activations auth Activate a product for one of the partner’s customers: grants the entitlement and records the sale (and its event for ARC) in one transaction. 409 if the customer already owns it, so a double click never bills twice.
GET /partner/customers auth The partner’s customers.
POST /partner/join auth A customer org joins a partner with the partner’s join code.
GET /partner/sales auth The partner’s sales, newest first (latest 500).

members — ARC member profile, member number (Founding 500) and points ledger; per person, no org needed

Section titled “members — ARC member profile, member number (Founding 500) and points ledger; per person, no org needed”
Method Path Auth Purpose
GET /me/points auth The caller’s points: the total and every entry, newest first.
GET /me/profile auth The caller’s member profile.
PUT /me/profile auth Replace the caller’s fan profile (handle, city, bio, socials).

open-call — ARC Open Call: the applicant’s own application and CV uploads (signed upload URLs); per person, no org needed

Section titled “open-call — ARC Open Call: the applicant’s own application and CV uploads (signed upload URLs); per person, no org needed”
Method Path Auth Purpose
GET /open-call/application auth The caller’s own application with its status and uploads. 404 no_application if none.
PUT /open-call/application auth Create the caller’s application, or edit it while it is still applied.
POST /open-call/application/withdraw auth Withdraw the caller’s application (from any status).
GET /open-call/booking auth The upcoming times with places taken, and the caller’s own booking.
POST /open-call/booking auth Take one place.
DELETE /open-call/booking auth Give the caller’s place back, any time before the session starts.
POST /open-call/uploads auth Start an upload: checks the type, size and count limits and returns a signed URL to PUT the file to.
PUT /open-call/uploads/{id} signed The file body for an upload, sent to the signed put_url (no bearer token). Content-Type must be the declared one and the body exactly the declared size.
DELETE /open-call/uploads/{id} auth Delete one of the caller’s uploads (and its file) while the application is applied.
Section titled “open-call-staff — ARC Open Call staff (open_call:review: open_call_staff or katena_admin): review, status moves with email, hand point awards, signed file links”
Method Path Auth Purpose
GET /admin/open-call/applications auth Applications, newest first (latest 500). status filters by one status; q searches name, email, phone and city.
GET /admin/open-call/applications/{id} auth One application with signed download links for its files and the status history.
POST /admin/open-call/applications/{id}/status auth Move an application to a new status (plan section 3; others answer 409).
GET /admin/open-call/booking auth Every slot from one day ago onward, with who booked it.
DELETE /admin/open-call/booking/slots/{id} auth Remove a slot nobody holds.
POST /admin/open-call/booking/windows auth Open a window of times.
POST /admin/open-call/booking/{id}/cancel auth Cancel someone’s booking.
POST /admin/points auth Give (or, negative, take back) points by hand, with a reason.
GET /open-call/uploads/{id}/file signed Download an uploaded file through a signed link from the staff detail (no bearer token).

leads — Forms from the public ARC site (newsletter, pre-registration, partners, sales): no sign-in, bot checks, rate limited, only from CORS_ORIGINS

Section titled “leads — Forms from the public ARC site (newsletter, pre-registration, partners, sales): no sign-in, bot checks, rate limited, only from CORS_ORIGINS”
Method Path Auth Purpose
POST /public/leads — Send a form from the ARC site.

leads-staff — Read the ARC site’s form submissions (lead:view: katena_admin, open_call_staff, arc_* roles), as JSON or CSV

Section titled “leads-staff — Read the ARC site’s form submissions (lead:view: katena_admin, open_call_staff, arc_* roles), as JSON or CSV”
Method Path Auth Purpose
GET /admin/leads auth Leads, newest first.
GET /admin/leads.csv auth Every lead as a CSV file (one column per form field), newest first.
Method Path Auth Purpose
GET /health —
GET /health/ready —

Some wired routes are excluded on purpose — they carry no Katena Portal schema:

  • GET /checkout/done — Stripe post-payment browser landing (HTML, informational; the entitlement arrives via the webhook, not this redirect).
  • POST /webhooks/stripe — Stripe’s own webhook. The body is Stripe’s raw event JSON, not a Katena Portal type; HMAC-verified via Stripe-Signature like /webhooks/payments.
  • GET /dev/checkout/{order_id} + POST /dev/checkout/{order_id}/act — the fake payment page, mounted whenever ENV is not production.

The API crate’s unit tests (cargo test -p katenaos-api) cover the security-critical pure logic: HMAC + Stripe signature verify, the commerce & admin state-transition tables, and the fail-closed config guards. There are no HTTP/DB integration tests exercising the wired handlers end-to-end yet — that surface is verified manually against a live Postgres.

Terminal window
cd api
cargo run --quiet openapi > ../docs/openapi.json # machine-readable spec
cargo run --quiet openapi-md > ../docs/API.md # this file