{"openapi":"3.1.0","info":{"title":"Katena Portal API","description":"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).","license":{"name":""},"version":"0.1.0"},"paths":{"/admin/access-requests":{"get":{"tags":["admin"],"summary":"List access requests by status (default `pending`) so an admin can find the id to approve.\nThe approval path must be operable via tooling before any UI exists.","operationId":"list_access_requests","parameters":[{"name":"status","in":"query","description":"pending (default) | approved | denied","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Matching access requests, oldest first","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AdminAccessRequest"}}}}},"401":{"description":"Missing or invalid bearer token"},"403":{"description":"Caller's org is not a platform admin"}},"security":[{"bearerAuth":[]}]}},"/admin/access-requests/{id}/approve":{"post":{"tags":["admin"],"summary":"Approve a pending access request: grant the entitlement (source `access_grant`) and mark the\nrow approved, in one transaction. Idempotent — re-approving an already-approved request just\nre-ensures the entitlement exists.","operationId":"approve_access_request","parameters":[{"name":"id","in":"path","description":"Access-request id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The approved request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminAccessRequest"}}}},"401":{"description":"Missing or invalid bearer token"},"403":{"description":"Caller's org is not a platform admin"},"404":{"description":"No such request"}},"security":[{"bearerAuth":[]}]}},"/admin/orders/{id}/refund":{"post":{"tags":["admin"],"summary":"Admin-initiated Stripe refund. Starts the refund with Stripe; does NOT revoke the entitlement.\nOwnership is removed only when Stripe's signed `charge.refunded` webhook arrives — the same\nsingle-door boundary that grants it. Requires a paid order that was paid via Stripe (a\n`provider_ref`/payment_intent on file). Dev-provider orders refund through the dev checkout\npage, not here.","operationId":"refund_order","parameters":[{"name":"id","in":"path","description":"Order id (uuid)","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Refund started with Stripe; entitlement revoked later via webhook"},"400":{"description":"Order was not paid via Stripe (no payment_intent on file)"},"401":{"description":"Missing or invalid bearer token"},"403":{"description":"Caller's org is not a platform admin"},"404":{"description":"Unknown order"},"409":{"description":"Order is not in a refundable (paid) state"},"500":{"description":"Stripe refused the refund or was unreachable"}},"security":[{"bearerAuth":[]}]}},"/admin/products":{"post":{"tags":["admin"],"summary":"Authoritative product upsert. Validation mirrors the DB CHECKs so admins get a clean 400,\nnot a constraint-violation 500.","operationId":"upsert_product","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProductUpsertRequest"}}},"required":true},"responses":{"200":{"description":"Product created/updated"},"400":{"description":"Invalid id / license / price combination"},"401":{"description":"Missing or invalid bearer token"},"403":{"description":"Caller's org is not a platform admin"}},"security":[{"bearerAuth":[]}]}},"/admin/products/{id}/releases":{"get":{"tags":["admin"],"summary":"Every release of a product, drafts and yanked included — the admin console view.","operationId":"list_releases_admin","parameters":[{"name":"id","in":"path","description":"Product id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"All releases, any status","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Release"}}}}},"401":{"description":"Missing or invalid bearer token"},"403":{"description":"Caller's org is not a platform admin"},"404":{"description":"Unknown product"}},"security":[{"bearerAuth":[]}]},"post":{"tags":["admin"],"summary":"Create a draft release. Drafts are invisible to every public surface until published.","operationId":"create_draft_release","parameters":[{"name":"id","in":"path","description":"Product id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DraftReleaseRequest"}}},"required":true},"responses":{"200":{"description":"Draft release created"},"400":{"description":"Bad version (not 3-part semver) or channel"},"401":{"description":"Missing or invalid bearer token"},"403":{"description":"Caller's org is not a platform admin"},"404":{"description":"Unknown product"},"409":{"description":"This version already exists"}},"security":[{"bearerAuth":[]}]}},"/admin/products/{id}/releases/{version}/artifact":{"put":{"tags":["admin"],"summary":"Upload an artifact's bytes for a release. The server owns the file name\n(`<product>-<version>-<os>-<arch>.tar.gz` under BUNDLES_DIR) and computes size + sha256 —\na publisher can never place a file, only content. Re-upload replaces (draft iteration).","operationId":"upload_artifact","parameters":[{"name":"id","in":"path","description":"Product id","required":true,"schema":{"type":"string"}},{"name":"version","in":"path","description":"Release version","required":true,"schema":{"type":"string"}},{"name":"os","in":"query","description":"Target OS (default \"any\")","required":false,"schema":{"type":"string"}},{"name":"arch","in":"query","description":"Target arch (default \"any\")","required":false,"schema":{"type":"string"}}],"requestBody":{"content":{"application/gzip":{"schema":{"type":"array","items":{"type":"integer","format":"int32","minimum":0}}}},"required":true},"responses":{"200":{"description":"Artifact stored; returns file_name/size/sha256"},"400":{"description":"Empty body or bad os/arch"},"401":{"description":"Missing or invalid bearer token"},"403":{"description":"Caller's org is not a platform admin"},"404":{"description":"Unknown product/version"}},"security":[{"bearerAuth":[]}]}},"/admin/products/{id}/releases/{version}/publish":{"post":{"tags":["admin"],"summary":"Publish a draft release (requires ≥1 uploaded artifact). Published releases become visible\nto `GET /products/:id/releases` and resolvable/downloadable.","operationId":"publish_release","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"version","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Release published"},"400":{"description":"Release has no artifact"},"401":{"description":"Missing or invalid bearer token"},"403":{"description":"Caller's org is not a platform admin"},"404":{"description":"Unknown product/version"},"409":{"description":"Invalid lifecycle transition"}},"security":[{"bearerAuth":[]}]}},"/admin/products/{id}/releases/{version}/yank":{"post":{"tags":["admin"],"summary":"Yank a published release — it disappears from public releases and resolve immediately.\nTerminal: cut a new version to re-ship, never resurrect a yanked one.","operationId":"yank_release","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"version","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Release yanked"},"401":{"description":"Missing or invalid bearer token"},"403":{"description":"Caller's org is not a platform admin"},"404":{"description":"Unknown product/version"},"409":{"description":"Invalid lifecycle transition"}},"security":[{"bearerAuth":[]}]}},"/bundles/{suite}/download":{"get":{"tags":["bundles"],"summary":"GET /bundles/:suite/download?exp=&sig= — validate the signed URL, then stream the file.\nGated by the unguessable signature + expiry, not the user token (presigned-URL model).","operationId":"download","parameters":[{"name":"suite","in":"path","description":"Suite id","required":true,"schema":{"type":"string"}},{"name":"exp","in":"query","description":"Expiry (unix seconds) from the signed URL","required":true,"schema":{"type":"integer","format":"int64","minimum":0}},{"name":"sig","in":"query","description":"HMAC signature from the signed URL","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The bundle bytes","content":{"application/gzip":{}}},"403":{"description":"Bad signature"},"410":{"description":"Signed URL expired"}}}},"/bundles/{suite}/url":{"post":{"tags":["bundles"],"summary":"POST /bundles/:suite/url — entitlement gate → signed URL + manifest.","operationId":"sign_url","parameters":[{"name":"suite","in":"path","description":"Suite id, e.g. `pnlink-upper`","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Signed, entitlement-gated download manifest","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BundleManifest"}}}},"401":{"description":"Missing or invalid bearer token"},"403":{"description":"Caller's org is not entitled to this suite"},"404":{"description":"Unknown suite"}},"security":[{"bearerAuth":[]}]}},"/catalog":{"get":{"tags":["store"],"summary":"Public catalog — browsable without login (Steam store front page). A pure read; `?q=` runs\nPostgres FTS, no `q` returns everything.","operationId":"get_catalog","parameters":[{"name":"q","in":"query","description":"Full-text search over title/description/mocap/retargeting","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"The public suite catalog (filtered when q is given)","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SuiteEntry"}}}}}}}},"/checkout":{"post":{"tags":["commerce"],"summary":"Start a purchase: validate, then create (or idempotently reuse) the pending order.\nNever grants anything — ownership only ever arrives via the payment webhook.","operationId":"checkout","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckoutRequest"}}},"required":true},"responses":{"200":{"description":"Pending order (fresh or reused) + hosted checkout URL","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckoutResponse"}}}},"401":{"description":"Missing or invalid bearer token"},"403":{"description":"Product is not purchasable (license is not `paid`), or no active org"},"404":{"description":"Unknown suite"},"409":{"description":"The org already owns this suite"}},"security":[{"bearerAuth":[]}]}},"/devices":{"get":{"tags":["devices"],"summary":"GET /devices — the caller org's device-credential roster.","operationId":"list","responses":{"200":{"description":"Device credentials","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/DeviceCredentialInfo"}}}}}},"security":[{"bearerAuth":[]}]}},"/devices/register":{"post":{"tags":["devices"],"summary":"POST /devices/register — register (idempotently) a station's device public key. Re-registering\nthe same key un-revokes it. Only the key's sha256 hash is ever returned.","operationId":"register","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceRegistration"}}},"required":true},"responses":{"200":{"description":"Credential registered","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceCredentialInfo"}}}},"404":{"description":"Unknown station"}},"security":[{"bearerAuth":[]}]}},"/downloads/resolve":{"post":{"tags":["downloads"],"summary":"Resolve an entitlement-gated download for local install: release + artifact + signed manifest\n+ short-lived signed URL.","operationId":"resolve","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResolveRequest"}}},"required":true},"responses":{"200":{"description":"Everything the desktop needs to install locally","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResolvedDownload"}}}},"400":{"description":"Unknown channel (must be stable | beta | nightly)"},"401":{"description":"Missing or invalid bearer token"},"403":{"description":"Caller's org is not entitled to this suite, or no active org"},"404":{"description":"Unknown suite, or no published artifact for this platform"}},"security":[{"bearerAuth":[]}]}},"/entitlements/claim-free":{"post":{"tags":["store"],"summary":"Claim a free suite for the caller's org. Idempotent: repeat claims return the existing\nentitlement with `already_owned = true`. Restricted/paid products are never claimable here.","operationId":"claim_free","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClaimFreeRequest"}}},"required":true},"responses":{"200":{"description":"The org owns the suite (newly granted or already owned)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClaimFreeResponse"}}}},"401":{"description":"Missing or invalid bearer token"},"403":{"description":"Product is not claimable (license is not `free`), or no active org"},"404":{"description":"Unknown suite"}},"security":[{"bearerAuth":[]}]}},"/entitlements/request":{"post":{"tags":["store"],"summary":"Request access to a `restricted` suite. In dev/test the request is auto-approved and the\nentitlement granted in this one call; in staging/prod it stays pending for an admin. Idempotent\nfor an org that already owns the suite. Free suites use claim-free, paid suites use checkout.","operationId":"request_access","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccessRequestBody"}}},"required":true},"responses":{"200":{"description":"Approved+granted (dev), already owned, or pending (prod)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccessRequestResponse"}}}},"401":{"description":"Missing or invalid bearer token"},"403":{"description":"Product is not restricted (license is not `restricted`), or no active org"},"404":{"description":"Unknown suite"}},"security":[{"bearerAuth":[]}]}},"/fleet":{"get":{"tags":["fleet"],"summary":"GET /fleet — the caller org's live station roster.","operationId":"list_fleet","responses":{"200":{"description":"The org's fleet roster","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/FleetStation"}}}}},"401":{"description":"Missing or invalid bearer token"},"403":{"description":"Token has no active organization"}},"security":[{"bearerAuth":[]}]}},"/fleet/deploy":{"post":{"tags":["fleet"],"summary":"POST /fleet/deploy — deploy (or re-target) a product on a station. This is the ONE gate where\ncommerce, catalog, and fleet meet: the org must own the product, the station must belong to the\norg, and the target release must be published. On success a `pending` deployment is (re)created.","operationId":"deploy","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeploymentRequest"}}},"required":true},"responses":{"200":{"description":"Deployment requested","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeploymentInfo"}}}},"403":{"description":"Org not entitled to the product / no active org"},"404":{"description":"Unknown station or no such published release"}},"security":[{"bearerAuth":[]}]}},"/fleet/deploy/{station}/{product}/status":{"post":{"tags":["fleet"],"summary":"POST /fleet/deploy/:station/:product/status — a station reports install progress. Bounded to the\nreportable statuses; pending is server-only. Goes through `verify_station_reporter` (see seam).","operationId":"install_status","parameters":[{"name":"station","in":"path","description":"Station id","required":true,"schema":{"type":"string"}},{"name":"product","in":"path","description":"Product id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InstallStatusUpdate"}}},"required":true},"responses":{"200":{"description":"Status recorded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeploymentInfo"}}}},"400":{"description":"Non-reportable status"},"404":{"description":"Unknown station or no such deployment"}},"security":[{"bearerAuth":[]}]}},"/fleet/deployments":{"get":{"tags":["fleet"],"summary":"GET /fleet/deployments — the caller org's deployment roster (every station × product).","operationId":"list_deployments","responses":{"200":{"description":"Deployment roster","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/DeploymentInfo"}}}}}},"security":[{"bearerAuth":[]}]}},"/fleet/heartbeat":{"post":{"tags":["fleet"],"summary":"POST /fleet/heartbeat — upsert the caller station's current status for its org.","operationId":"heartbeat","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StationHeartbeat"}}},"required":true},"responses":{"200":{"description":"Heartbeat stored"},"401":{"description":"Missing or invalid bearer token"},"403":{"description":"Token has no active organization"}},"security":[{"bearerAuth":[]}]}},"/health":{"get":{"tags":["meta"],"operationId":"health","responses":{"200":{"description":"Service is live"}}}},"/health/ready":{"get":{"tags":["meta"],"operationId":"health_ready","responses":{"200":{"description":"Process live and database reachable"},"503":{"description":"Database unreachable"}}}},"/library":{"get":{"tags":["store"],"summary":"The caller org's owned suites (Steam library). Requires a valid token. A pure read —\nownership is only ever created by an explicit claim (or, later, a purchase webhook /\nadmin grant), never as a side effect of a GET.","operationId":"get_library","responses":{"200":{"description":"Suites the caller's org is entitled to","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SuiteEntry"}}}}},"401":{"description":"Missing or invalid bearer token"},"403":{"description":"Token has no active organization"}},"security":[{"bearerAuth":[]}]}},"/market/buy":{"post":{"tags":["market"],"summary":"Spend Spark to own a motion. Idempotent — already-owned re-buys are not charged.","operationId":"buy","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuyRequest"}}},"required":true},"responses":{"200":{"description":"Purchased (or already owned) — the org's full market state","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketState"}}}},"401":{"description":"Missing or invalid bearer token"},"402":{"description":"Wallet can't cover the price (`insufficient_spark`)"},"403":{"description":"Token has no active organization"},"404":{"description":"Unknown or non-motion product"}},"security":[{"bearerAuth":[]}]}},"/market/library":{"get":{"tags":["market"],"summary":"The caller org's owned + liked motions and Spark balance.","operationId":"get_market","responses":{"200":{"description":"Owned + liked motion ids and the org's Spark balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketState"}}}},"401":{"description":"Missing or invalid bearer token"},"403":{"description":"Token has no active organization"}},"security":[{"bearerAuth":[]}]}},"/market/like":{"post":{"tags":["market"],"summary":"Toggle a like on a motion.","operationId":"like","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LikeRequest"}}},"required":true},"responses":{"200":{"description":"The org's full market state after the toggle","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketState"}}}},"401":{"description":"Missing or invalid bearer token"},"403":{"description":"Token has no active organization"}},"security":[{"bearerAuth":[]}]}},"/market/topup":{"post":{"tags":["market"],"summary":"Add Spark to the caller org's wallet (bounded).","operationId":"topup","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TopupRequest"}}},"required":true},"responses":{"200":{"description":"The org's full market state after the top-up","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketState"}}}},"401":{"description":"Missing or invalid bearer token"},"403":{"description":"Token has no active organization"}},"security":[{"bearerAuth":[]}]}},"/orders/{id}":{"get":{"tags":["commerce"],"summary":"Poll one of your org's orders (the desktop's \"waiting for payment…\" target).","operationId":"get_order","parameters":[{"name":"id","in":"path","description":"Order id (uuid)","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The order","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderInfo"}}}},"401":{"description":"Missing or invalid bearer token"},"404":{"description":"Unknown order — including other orgs' orders"}},"security":[{"bearerAuth":[]}]}},"/products/{id}/releases":{"get":{"tags":["downloads"],"summary":"A product's published releases, newest first (public — release notes are browsable pre-login).","operationId":"get_releases","parameters":[{"name":"id","in":"path","description":"Product id, e.g. `pnlink-upper`","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Published releases, newest first","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Release"}}}}},"404":{"description":"Unknown product"}}}},"/sync/{kind}":{"get":{"tags":["sync"],"summary":"GET /sync/:kind — the caller org's last-synced document of this kind.","operationId":"get_sync","parameters":[{"name":"kind","in":"path","description":"Document kind: `profiles` or `recent`","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The synced document (payload null if never written)"},"400":{"description":"Unknown kind"},"401":{"description":"Missing or invalid bearer token"},"403":{"description":"Token has no active organization"}},"security":[{"bearerAuth":[]}]},"put":{"tags":["sync"],"summary":"PUT /sync/:kind — replace the caller org's document of this kind (last-write-wins).","operationId":"put_sync","parameters":[{"name":"kind","in":"path","description":"Document kind: `profiles` or `recent`","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"The JSON document to store","content":{"application/json":{"schema":{}}},"required":true},"responses":{"200":{"description":"Stored; returns the new updated_at"},"400":{"description":"Unknown kind"},"401":{"description":"Missing or invalid bearer token"},"403":{"description":"Token has no active organization"}},"security":[{"bearerAuth":[]}]}},"/webhooks/payments":{"post":{"tags":["commerce"],"summary":"The payment provider's webhook. Authenticated by HMAC signature, not a Bearer.","operationId":"payments_webhook","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookBody"}}},"required":true},"responses":{"200":{"description":"Event processed; `applied:false` = benign replay / out-of-order"},"400":{"description":"Valid signature but unparseable body"},"403":{"description":"Missing/malformed/stale/mismatching signature"},"404":{"description":"Unknown order id"}}}}},"components":{"schemas":{"AccessRequestBody":{"type":"object","required":["product_id"],"properties":{"product_id":{"type":"string","description":"The restricted product to request, e.g. `pnlink-full`."}}},"AccessRequestResponse":{"type":"object","description":"Response of `POST /entitlements/request` (restricted suites). Every field is always present.\nIn dev the request is auto-approved and the entitlement granted in one call\n(`status=\"approved\"`, `granted=true`); in production it stays `status=\"pending\"` until an\nadmin approves. A repeat request the org already owns is idempotent\n(`status=\"approved\"`, `granted=false`, `already_owned=true`).","required":["product_id","status","granted","already_owned"],"properties":{"already_owned":{"type":"boolean"},"granted":{"type":"boolean","description":"true only when THIS call created the entitlement."},"product_id":{"type":"string"},"status":{"type":"string","description":"\"approved\" | \"pending\"."}}},"AdminAccessRequest":{"type":"object","description":"One access request, as an admin sees it (`GET /admin/access-requests`).","required":["id","org_id","product_id","requested_by","status","requested_at"],"properties":{"decided_at":{"type":["string","null"]},"decided_by":{"type":["string","null"]},"id":{"type":"string"},"org_id":{"type":"string"},"product_id":{"type":"string"},"requested_at":{"type":"string"},"requested_by":{"type":"string"},"status":{"type":"string"}}},"Artifact":{"type":"object","description":"One downloadable file of a release for a platform. `os`/`arch` may be \"any\" —\nsuite bundles (retargeting profiles, robot agent packages) are platform-agnostic.","required":["product_id","version","os","arch","file_name","size_bytes","sha256"],"properties":{"arch":{"type":"string","description":"\"x64\" | \"arm64\" | \"any\"."},"file_name":{"type":"string"},"os":{"type":"string","description":"\"macos\" | \"windows\" | \"linux\" | \"any\"."},"product_id":{"type":"string"},"sha256":{"type":"string","description":"SHA-256 of the artifact bytes (hex) — the integrity gate."},"signature":{"type":["string","null"],"description":"ed25519 signature over the artifact bytes (hex) — the authenticity gate."},"size_bytes":{"type":"integer","format":"int64","minimum":0},"version":{"type":"string"}}},"BundleManifest":{"type":"object","description":"A signed, entitlement-gated bundle download. `url` is short-lived; the desktop verifies\n`sha256` before pushing to a robot. `cached` is set when the desktop served this from its\noffline cache (the URL may be expired) rather than a live API call.","required":["url","sha256","expires_at"],"properties":{"cached":{"type":"boolean"},"expires_at":{"type":"integer","format":"int64","description":"Unix seconds at which `url` stops working.","minimum":0},"key_id":{"type":["string","null"],"description":"Names the signing key that produced `signature` (e.g. \"katena-prod-2026-07\") so a\nfuture key rotation can identify which trust anchor an artifact needs. Optional:\nabsent on dev/legacy manifests; presence changes nothing about verification today."},"sha256":{"type":"string"},"signature":{"type":["string","null"],"description":"ed25519 signature over the bundle bytes (hex), proving authenticity. Verified against the\npinned public key before deploy. Optional: legacy/unsigned bundles still pass the sha256\ngate (verify-if-present), but the gated download path always sets it."},"url":{"type":"string"}}},"BuyRequest":{"type":"object","required":["product_id"],"properties":{"product_id":{"type":"string"}}},"CheckoutRequest":{"type":"object","required":["product_id"],"properties":{"product_id":{"type":"string","description":"The paid product to buy, e.g. `motion-lab-pro`."}}},"CheckoutResponse":{"type":"object","description":"Response of `POST /checkout` — a pending (or idempotently reused) order + where to pay.\n`checkout_url` is opaque to the desktop: open it in the system browser, never parse it.","required":["order_id","status","checkout_url","amount_cents","currency"],"properties":{"amount_cents":{"type":"integer","format":"int32","minimum":0},"checkout_url":{"type":"string"},"currency":{"type":"string"},"order_id":{"type":"string"},"status":{"type":"string","description":"\"pending\" | \"paid\" | \"refunded\" | \"failed\""}}},"ClaimFreeRequest":{"type":"object","required":["product_id"],"properties":{"product_id":{"type":"string","description":"The product to claim, e.g. `pnlink-upper`."}}},"ClaimFreeResponse":{"type":"object","description":"Response of `POST /entitlements/claim-free`. Idempotent: a repeat claim returns the\nexisting entitlement with `already_owned = true`.","required":["entitlement"],"properties":{"already_owned":{"type":"boolean"},"entitlement":{"$ref":"#/components/schemas/EntitlementGrant"}}},"DeploymentInfo":{"type":"object","description":"One deployment: what a station should run and what it actually reports running.\n`installed_version` present with `status = \"pending\"` is the \"update pending\" state.","required":["station_id","product_id","target_version","status","requested_by","requested_at","updated_at"],"properties":{"error":{"type":["string","null"]},"installed_at":{"type":["string","null"]},"installed_version":{"type":["string","null"]},"product_id":{"type":"string"},"requested_at":{"type":"string","description":"Server timestamps as text (RFC3339-ish); the desktop treats them as opaque labels."},"requested_by":{"type":"string","description":"The katena_auth user id that requested the deploy."},"station_id":{"type":"string"},"status":{"type":"string","description":"\"pending\" | \"downloading\" | \"installed\" | \"failed\"."},"target_version":{"type":"string"},"updated_at":{"type":"string"}}},"DeploymentRequest":{"type":"object","description":"Request body of `POST /fleet/deploy` — deploy (or re-target) `product_id` on `station_id` to\n`target_version`. The server enforces the invariant: the org is entitled to the product, the\nstation belongs to the org, and the target release is published.","required":["station_id","product_id","target_version"],"properties":{"product_id":{"type":"string"},"station_id":{"type":"string"},"target_version":{"type":"string"}}},"DeviceCredentialInfo":{"type":"object","description":"One registered device credential (the public key itself is never returned — only its hash).","required":["id","station_id","public_key_hash","created_at"],"properties":{"created_at":{"type":"string"},"id":{"type":"string"},"public_key_hash":{"type":"string"},"revoked_at":{"type":["string","null"]},"station_id":{"type":"string"}}},"DeviceRegistration":{"type":"object","description":"Body of `POST /devices/register` — a station registering its ed25519 device public key (hex).","required":["station_id","public_key"],"properties":{"public_key":{"type":"string"},"station_id":{"type":"string"}}},"DraftReleaseRequest":{"type":"object","required":["version"],"properties":{"changelog":{"type":"string"},"channel":{"type":"string","description":"\"stable\" | \"beta\" | \"nightly\"."},"min_desktop_version":{"type":"string"},"version":{"type":"string","description":"3-part semver, e.g. \"1.2.0\"."}}},"EntitlementGrant":{"type":"object","description":"An ownership record, as returned by the claim endpoint.","required":["product_id","status","source"],"properties":{"product_id":{"type":"string"},"source":{"type":"string","description":"How ownership was created: \"free_claim\" | \"purchase\" | \"access_grant\" (restricted\nsuites, approved automatically in dev or by an admin)."},"status":{"type":"string","description":"\"active\" (expiry/revocation are later stages)."}}},"FleetStation":{"type":"object","description":"One station in the org's live fleet roster: its last heartbeat plus server-computed liveness.","required":["station_id","station_name","session_state","app_version","last_seen","online"],"properties":{"app_version":{"type":"string"},"bvh_fps":{"type":["number","null"],"format":"float"},"last_fault":{"type":["string","null"]},"last_seen":{"type":"integer","format":"int64","description":"Unix seconds of the last heartbeat (server clock). Crosses to TS as `bigint`."},"online":{"type":"boolean","description":"True if the last heartbeat arrived within the 30s online window."},"robot_name":{"type":["string","null"]},"session_state":{"type":"string"},"station_id":{"type":"string"},"station_name":{"type":"string"}}},"InstallManifest":{"type":"object","description":"The signed install manifest delivered with every resolved download: what this artifact is,\nwhether it can run here, what it exposes, and what it's allowed to touch. The desktop\nvalidates + verifies the signature **before** any bytes land on disk.","required":["schema_version","product_id","version","kind","entrypoints","requires","permissions"],"properties":{"entrypoints":{"type":"array","items":{"$ref":"#/components/schemas/ManifestEntrypoint"}},"kind":{"type":"string","description":"\"suite-bundle\" today; \"robot-driver\" | \"app\" | … later."},"permissions":{"type":"array","items":{"type":"string"},"description":"Declared capability strings, e.g. \"ssh.robot\", \"udp.listen\", \"network.local\"."},"product_id":{"type":"string"},"requires":{"$ref":"#/components/schemas/ManifestRequires"},"schema_version":{"type":"integer","format":"int32","minimum":0},"version":{"type":"string"}}},"InstallStatusUpdate":{"type":"object","description":"Body of `POST /fleet/deploy/:station/:product/status` — a station reporting install progress.","required":["status"],"properties":{"error":{"type":["string","null"]},"installed_version":{"type":["string","null"]},"status":{"type":"string","description":"\"downloading\" | \"installed\" | \"failed\" (pending is server-set at deploy time)."}}},"LikeRequest":{"type":"object","required":["product_id","liked"],"properties":{"liked":{"type":"boolean"},"product_id":{"type":"string"}}},"ManifestEntrypoint":{"type":"object","description":"A launchable command inside an installed product. `command` is a path **relative to the\nversioned install dir** — never remote-supplied shell. The desktop resolves it inside the\nverified install folder only (see `installer::resolve_entrypoint`).","required":["id","label","command","args"],"properties":{"args":{"type":"array","items":{"type":"string"}},"command":{"type":"string"},"id":{"type":"string"},"label":{"type":"string"}}},"ManifestRequires":{"type":"object","description":"What an artifact needs from the machine/rig. \"any\" entries are wildcards.","required":["desktop_min_version","os","arch","robots"],"properties":{"arch":{"type":"array","items":{"type":"string"}},"desktop_min_version":{"type":"string"},"os":{"type":"array","items":{"type":"string"}},"robots":{"type":"array","items":{"type":"string"},"description":"Robot models this product targets, e.g. [\"unitree-g1\"]. Empty = not robot-specific."}}},"MarketState":{"type":"object","description":"The caller org's market library: owned + liked motion ids and current Spark balance.","required":["owned","liked","spark"],"properties":{"liked":{"type":"array","items":{"type":"string"}},"owned":{"type":"array","items":{"type":"string"}},"spark":{"type":"integer","format":"int64"}}},"OrderInfo":{"type":"object","description":"One order, as seen by its own org (`GET /orders/:id`). The provider's own transaction\nreference is deliberately never exposed to the desktop.","required":["order_id","product_id","status","amount_cents","currency"],"properties":{"amount_cents":{"type":"integer","format":"int32","minimum":0},"currency":{"type":"string"},"order_id":{"type":"string"},"product_id":{"type":"string"},"status":{"type":"string"}}},"PaymentEvent":{"type":"string","enum":["completed","refunded","failed"]},"ProductUpsertRequest":{"type":"object","required":["id","title","mocap","retargeting","description","license"],"properties":{"currency":{"type":["string","null"],"description":"3-letter ISO-4217, required when license = \"paid\"."},"description":{"type":"string"},"id":{"type":"string","description":"Product id/slug, `[A-Za-z0-9._-]+`."},"license":{"type":"string","description":"\"free\" | \"restricted\" | \"paid\" | \"subscription\" | \"enterprise\"."},"mocap":{"type":"string"},"price_cents":{"type":["integer","null"],"format":"int32","description":"Required (with `currency`) when license = \"paid\"; must be > 0.","minimum":0},"ready":{"type":"boolean"},"retargeting":{"type":"string"},"title":{"type":"string"}}},"Release":{"type":"object","description":"A published version of a product. Today every product has the degenerate single release\nseeded from its bundle file; channels/updates arrive in P3.","required":["product_id","version","channel","changelog","min_desktop_version","status"],"properties":{"changelog":{"type":"string"},"channel":{"type":"string","description":"\"stable\" | \"beta\" | \"nightly\"."},"min_desktop_version":{"type":"string","description":"Minimum Katena Portal Desktop version this release supports."},"product_id":{"type":"string"},"status":{"type":"string","description":"\"draft\" | \"published\" | \"yanked\"."},"version":{"type":"string","description":"Semver, e.g. \"1.0.0\"."}}},"ResolveRequest":{"type":"object","required":["product_id","os","arch"],"properties":{"arch":{"type":"string","description":"Caller architecture: \"x64\" | \"arm64\"."},"channel":{"type":["string","null"],"description":"Update-channel preference: \"stable\" (default) | \"beta\" | \"nightly\". A preference includes\nsafer channels (beta ⇒ stable + beta) and the newest included version wins."},"os":{"type":"string","description":"Caller platform: \"macos\" | \"windows\" | \"linux\"."},"product_id":{"type":"string","description":"The product to install, e.g. `pnlink-upper`."},"version":{"type":["string","null"],"description":"Pin an exact version (the fleet-deploy path — install precisely `target_version`). When set,\nchannel/newest logic is ignored; only a published release of this version resolves."}}},"ResolvedDownload":{"type":"object","description":"Response of `POST /downloads/resolve`: everything the desktop needs to install locally —\nthe release + artifact metadata, the signed manifest, and a short-lived signed URL.","required":["release","artifact","manifest","manifest_signature","url","expires_at"],"properties":{"artifact":{"$ref":"#/components/schemas/Artifact"},"expires_at":{"type":"integer","format":"int64","description":"Unix seconds at which `url` stops working. Crosses to TS as `bigint`.","minimum":0},"manifest":{"$ref":"#/components/schemas/InstallManifest"},"manifest_signature":{"type":"string","description":"ed25519 over the canonical manifest JSON (hex). Verified against the pinned key."},"release":{"$ref":"#/components/schemas/Release"},"url":{"type":"string"}}},"StationHeartbeat":{"type":"object","description":"A heartbeat a desktop station posts (~10s) describing its current rig status. The server\nstamps the receive time; the client never sends timestamps. Org comes from the token.","required":["station_id","station_name","session_state","app_version"],"properties":{"app_version":{"type":"string","description":"Desktop app version (CARGO_PKG_VERSION)."},"bvh_fps":{"type":["number","null"],"format":"float","description":"Live BVH framerate, if a stream is being observed."},"last_fault":{"type":["string","null"],"description":"The current launch blocker / fault label, if any."},"robot_name":{"type":["string","null"],"description":"The robot this station is currently driving, if any."},"session_state":{"type":"string","description":"Coarse `SessionState` label, e.g. \"MotionFollowActive\" | \"Fault\" | \"Idle\"."},"station_id":{"type":"string","description":"Stable per-install id (uuid), generated once and persisted by the desktop."},"station_name":{"type":"string","description":"Operator-facing station name (hostname by default, editable)."}}},"SuiteEntry":{"type":"object","description":"A store suite — a catalog entry or library item. The shared *data*; UI-only metadata\n(icons) lives in the frontend and is paired back by `id`.","required":["id","title","mocap","retargeting","description","ready"],"properties":{"currency":{"type":["string","null"],"description":"ISO-4217 code, e.g. \"USD\". Present only when license = \"paid\"."},"description":{"type":"string"},"id":{"type":"string"},"license":{"type":["string","null"],"description":"Product license: \"free\" | \"restricted\" | \"paid\" | \"subscription\" | \"enterprise\".\nFree products are claimable via `POST /entitlements/claim-free`; restricted ones render\nas Locked / Request access. Optional for wire-compat with pre-P1 caches."},"mocap":{"type":"string"},"owned":{"type":["boolean","null"],"description":"null on the public catalog; true in an org's library."},"price_cents":{"type":["integer","null"],"format":"int32","description":"Price in the smallest currency unit (cents). Present only when license = \"paid\".","minimum":0},"ready":{"type":"boolean"},"retargeting":{"type":"string"},"title":{"type":"string"}}},"TopupRequest":{"type":"object","required":["amount"],"properties":{"amount":{"type":"integer","format":"int64","description":"Spark to add; clamped to 0…100000 so a demo can't mint absurd balances."}}},"WebhookBody":{"type":"object","required":["order_id","event"],"properties":{"event":{"$ref":"#/components/schemas/PaymentEvent"},"order_id":{"type":"string"}}}},"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"The opaque OAuth access token from Better Auth."}}},"tags":[{"name":"store","description":"Suite catalog + the caller org's library"},{"name":"bundles","description":"Entitlement-gated, signed bundle downloads (robot deploy path)"},{"name":"commerce","description":"Checkout, orders, and the payment webhook (P4)"},{"name":"admin","description":"Curated publishing: upload → publish → yank (allowlisted admin orgs, P5)"},{"name":"downloads","description":"Entitlement-gated resolve for local installs (releases, artifacts, signed manifests)"},{"name":"sync","description":"Per-org sync of desktop state (profiles, recent combos)"},{"name":"market","description":"Motion Market: Spark-priced motion purchases, likes, and wallet top-ups"},{"name":"fleet","description":"Per-org live station roster, deployments, and install status"},{"name":"devices","description":"Robot device-identity registry (ed25519 public keys)"},{"name":"meta","description":"Liveness"}]}