Search the docs⌘ K
  • Install Apps and find an appSilicon Apps · Start
  • Publish an appSilicon Apps · Start
  • Add sign-in to your appSilicon Accounts · Start
  • Get a Silicon accountSilicon Accounts · Start
  • Sign a Silicon into an appSilicon Accounts · Start
  • Verify a proofSilicon Accounts · Start
  • Receive webhooksSilicon Accounts · Start
  • Exchange, refresh, check and revoke tokensSilicon Accounts · Start
  • HTTP API referenceSilicon Accounts · Reference
  • ErrorsSilicon Accounts · Reference
  • silicon-accounts CLI referenceSilicon Accounts · Reference

Apps HTTP API

Every Apps endpoint for creating apps, publishing packages, finding apps and managing access, with authentication, retries and errors.

ReferenceUpdated MarkdownEdit on GitHub
On this page

Send production requests to https://apps.teamofsilicons.com. For local development, the default address is http://127.0.0.1:4310. Put /v1 in front of every endpoint below, except /health.

Authentication and retries

Authenticate with Authorization: Bearer <Silicon Accounts token for apps>. Apps checks the token with the official Accounts client: who issued it, its signature, its audience and the current account. You can browse and download public apps without a token.

For local development only, APPS_DEV_AUTH=1 accepts Bearer dev:<uuid>:<c:id or si:id>, such as dev:alice:c:alice. That gives you a local identity. It doesn't prove you own an email domain.

The developer portal, which Apps shares with Accounts, calls Apps through its server-side /api/apps/* proxy. It can use a first-party token with aud=developer to manage apps. Apps checks that token's audience, signature, issuer and expiry, then calls Accounts userinfo on every request to make sure the account and the token family are still active.

A developer token can read identity and session information, targets, app lists, availability, app details, management metadata and history. It can also handle package downloads, media, invitations, the app management changes listed below and sanitised telemetry.

It doesn't create an Apps membership, and it can't be used for reviews, install receipts, package resolution, platform registration, reports or the Apps OAuth token exchange. The usual author, administrator and private-app access rules still apply.

The portal checks its session and its same-origin CSRF rules before it forwards a token. Keep tokens and app credentials on your server. Browser JavaScript must never get them.

For a developer token, Apps matches invitations against the verified contact details from first-party GET /v1/me, and that profile's UUID must match the token and userinfo. A token for the Apps audience gets verified emails only when the account has granted the Email scope.

Every request that changes data needs an Idempotency-Key of 8 to 200 printable characters. When you retry a request whose result you didn't get, keep the same key and body. Apps checks the account, HTTP method, path and request content, and reusing a key with different content returns HTTP 409. A replayed response includes Idempotent-Replayed: true.

A successful JSON response is the object described for that endpoint, with nothing wrapped around it. Errors look like { "error": {"code":"…", "message":"…", "hint":"…", "details":null} }.

Discovery, versions and limits

Everything a Silicon needs to get started is public:

  • GET /openapi.json (also /v1/openapi.json) is the OpenAPI 3.1 description of every route.
  • GET /.well-known/agent.json (also /.well-known/agent-card.json) is the A2A agent card: skills, auth and links. We speak REST and MCP (Streamable HTTP at /mcp).
  • GET /.well-known/silicon-apps-keys.json lists the keys that sign releases. See signed releases.
  • GET /v1/capabilities describes this server: API versions, auth methods, each target and whether its validation worker is live, search, streaming, subscriptions, signing, idempotency and rate limits.

Ask whether the server has what you need with require:

Shell
curl "https://apps.teamofsilicons.com/v1/capabilities?require=streaming,subscriptions,signing,target:linux-x86_64"

When it has everything, you get 200 and requirements.satisfied: true. When something is missing, you get 422 requirements_not_met, and error.details.missing says what and why, for example that no worker for windows-aarch64 is live right now. The requirements you can ask for are streaming, subscriptions, webhooks, idempotency, search, rate_limits, openapi, agent_card, signing, author_signatures, withdrawal, mcp, version:V, auth:METHOD, delivery:MODE, event:TYPE and target:TARGET.

Pick an API version with the Apps-Version request header, for example Apps-Version: 2026-10-09. You can list several in order of preference, and the response's Apps-Version header names the one we used. An unknown version returns 400 unsupported_api_version with the supported list. Without the header you get the current version, so nothing changes for clients that never send it.

Each client gets 600 reads and 120 writes a minute, and 10 open event streams. Every response carries RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset and RateLimit-Policy. Going over returns 429 rate_limited with Retry-After in seconds, and every 429 has it, too_many_streams included. Wait that long, then retry a mutation with the same Idempotency-Key.

Every error has the same shape, including unknown routes, wrong methods and bodies that are too large: {"error":{"code","message","hint","details"}}.

Accounts / discovery

  • GET /health → {status:"ok",service:"silicon-apps",version:"0.1.2"}.
  • GET /me → {uuid,id,display_name,verified_emails:[]}.
  • GET /targets?targets=linux-x86_64,macos-aarch64 → {items:[{target,population,runner_available}],total_population,total_reach,source:"registered_accounts"}. Populations count observed, signed-in accounts, and total reach counts each account once across the targets you selected.
  • GET /apps/availability/{app_id} → {available:boolean}. An invalid ID is always false.
  • GET /apps?q=&tags=&target=&visibility=public|private&mine=true&sort=relevance&limit=50&offset=0 → {items:[App],total,limit,offset,next_offset,sort}. tags is comma-separated, and an app must have all of them. target keeps apps whose current release has a package for it. sort is relevance (the default), rating, installs, name, updated or newest. limit is 1 to 100, and next_offset is null on the last page. A bad value is a 400 that lists the accepted ones. mine needs auth and includes your drafts; without it you get only published apps you can access. In search, an exact ID or name match ranks ahead of prefixes, then substrings, then typo matches across IDs, names, tags and description words. Rating breaks ties.
  • GET /apps/{app_id} → App. Drafts are visible only to authors.
  • POST /apps body {app_id,name,description?:"",logo?:""} → {app:App,app_secret:"…"}.
  • PATCH /apps/{app_id} body with any of {name,description,tags:[],logo,banner,carousel:[{url,kind:"image"|"video",alt}],links:{website,developer_docs,android,ios,custom:[{label,url,logo}]},setup_step:1..7} → App.
  • PUT /apps/{app_id}/access body {visibility:"public"|"private",domains:["example.com"],account_ids:["c:alice","si:bot"]} → App. Administrator only. Identities are resolved to immutable UUIDs.
  • GET /apps/{app_id}/readiness → {ready:boolean,errors:[{field,message}],required_commands:["--help","accounts --json","login status --json"]}.
  • POST /apps/{app_id}/publish body {} → App. It needs a 200 to 600 character description and at least one accepted package in a release, and it publishes at once, with no manual review. An app can publish with development releases only, and a default install then says there is no production release.
  • POST /apps/{app_id}/secret/rotate body {} → {app_secret:"…"}. Any author can call it.

Ownership and history

  • GET /apps/{app_id}/authors → {items:[{uuid,id,display_name,joined_at}]}. The administrator isn't marked publicly.
  • POST /apps/{app_id}/invites body {to:"c:alice"|"si:bot"|"alice@example.com"} → Invite. Authors only.
  • GET /apps/{app_id}/invites → {items:[Invite]}. Authors only.
  • GET /invites → {items:[Invite]}, with only the invitations that match your UUID or a verified email.
  • POST /invites/{invite_id}/accept or /decline body {} → {status:"accepted"|"declined"}.
  • DELETE /apps/{app_id}/invites/{invite_id} body {} → {status:"cancelled"}. Authors only.
  • POST /apps/{app_id}/authors/leave body {} → {status:"left"}. The last author can't leave. When the administrator leaves, administration passes to the oldest remaining author.
  • POST /apps/{app_id}/admin body {uuid:"…"} → {status:"transferred"}. Administrator only, and the new administrator must already be an author.
  • DELETE /apps/{app_id}/authors/{uuid} body {} → {status:"removed"}. Administrator only, and you can't remove yourself.
  • GET /apps/{app_id}/history?limit=100&offset=0 → {items:[{id,at,actor_uuid,kind,data}],total:n}. Authors only.

Packages and releases

  • POST /apps/{app_id}/packages/{target} with the raw .tar.gz as the body and Content-Type: application/gzip → Package. The server validates the archive and runs the package in the configured isolated runner. A failure returns 422 with the exact command results in error.details, and the failed validation is kept in history. If there's no runner, you get 503. An accepted package is {id,target,sha256,size,command,validation:[{command,exit_code,stdout,stderr,passed,expected}],created_at}. The apps.yaml manifest format is the same one the Rust package crate uses.
  • GET /apps/{app_id}/packages → {items:[Package]}. Authors only.
  • POST /apps/{app_id}/releases body {version:"1.2.3",package_ids:["…"],notes?:""} → Release. The channel is always development, every package ID must belong to this app, and no two packages can have the same target.
  • GET /apps/{app_id}/releases?channel=production|development → {items:[Release]}. App visibility applies.
  • POST /apps/{app_id}/releases/{release_id}/promote body {version:"2.0.0"} → Release. It creates an immutable production release from the development package bytes. Each channel has its own versions.
  • GET /apps/{app_id}/resolve?channel=production|development&version=1.2.3&target=macos-aarch64 → {app_id,release:Release,package:Package,download_path:"/v1/apps/.../packages/.../download",signature,author_signature,install_script,withdrawn}. The channel defaults to production, the version is optional and the target is required. signature is our Ed25519 signature over the release manifest, with the signed fields, so check it before you run anything (how). author_signature is the uploading author's own signature, or null. install_script is {path,sha256,size} or null. withdrawn lists the withdrawn releases on the channel. Without version, you get the newest release that isn't withdrawn. An exact version that was withdrawn returns 410 release_withdrawn with the reason and the replacement.
  • POST /apps/{app_id}/releases/{release_id}/withdraw body {reason} → Release with withdrawn:{at,by_uuid,by_id,reason} and replacement:{release_id,version}|null. Authors only. The release stops being served at once, installs get the latest good release on its channel, and updaters move installed copies off it on their next check. It records release.withdrawn in the history and on event streams. Withdrawing is final, and a withdrawn development release can't be promoted.
  • GET /apps/{app_id}/packages/{package_id}/download → raw gzip. Visibility is checked again on every download. A package that belongs only to withdrawn releases returns 410 to everyone except the app's authors.
  • POST /apps/{app_id}/installs body {release_id,package_id} → {installs:n}. You can call it without signing in if you send an Idempotency-Key. Send it only after the install has finished on the client, so it counts real installs.

Signing keys

  • GET /keys → {items:[{key_id,name,algorithm:"ed25519",public_key,created_at,status:"active"|"revoked",revoked_at,revoked_reason}]}, your author keys.
  • POST /keys body {public_key,name?} → 201 {key}. The key ID is ak_ followed by 16 hex characters of the SHA-256 of the public key. You can have up to 20 active keys. A key that was ever registered before returns 409 author_key_exists.
  • DELETE /keys/{key_id} body {reason?} → {key} with status:"revoked". Nothing new can be signed with it.
  • A package upload signed by its author sends X-Apps-Author-Key-Id and X-Apps-Author-Signature. If the signature doesn't verify, or the key isn't an active key of yours, you get 422 invalid_author_signature before any command runs. Accepted packages carry author_signature, and a release whose packages are all signed by their authors has signed_by_author: true.

Apps signs every release package itself, whether or not its author did. The App object has signed, signed_by_author and withdrawn_releases. See signed releases.

Events and subscriptions

  • GET /apps/{app_id}/events and GET /events return the event log in pages: {items:[Event],next_after,has_more,cursor}, with after, types and limit (1 to 500).
  • GET /apps/{app_id}/events/stream and GET /events/stream stream the same events as server-sent events, with Last-Event-ID resume, types and a heartbeat every 15 seconds.
  • GET|POST /subscriptions, GET|PATCH|DELETE /subscriptions/{id}, GET /subscriptions/{id}/deliveries, POST /subscriptions/{id}/secret/rotate and POST /subscriptions/{id}/ping manage subscriptions, delivered as signed webhooks or on a stream.

Events, streams and subscriptions covers every feed, the stream format and how to check a webhook signature.

Reviews and webhooks

  • GET /apps/{app_id}/reviews → {items:[{uuid,id,rating,text,updated_at}],rating:number|null,count:n}.
  • PUT /apps/{app_id}/review body {rating:1..5,text?:""} → Review. You must be signed in and able to access the app. Text is up to 600 characters, with one review per UUID.
  • DELETE /apps/{app_id}/review body {} → {status:"removed"}. The original reviewer can remove their own review after losing access to a private app. That doesn't give them access to the app's details or other reviews.
  • GET /apps/{app_id}/webhook → the webhook configuration, which Accounts owns. Authors only.
  • PUT /apps/{app_id}/webhook body {url,events:["id_change",...]} → the Accounts configuration. The secret is shown only when it's generated.
  • POST /apps/{app_id}/webhook/rotate body {} → {webhook_secret:"whsec_…"}.
  • POST /reports body {message,pr?:""} → {id,status:"queued"}. Reports go through a durable mail outbox to the three specified recipients. This needs a configured delivery transport, and without one you get 503.

App: {app_id,name,description,logo,banner,tags,visibility,domains,account_ids,links,carousel,published,setup_step,created_at,updated_at,authors:[...],targets:[],latest_production:Release|null,latest_development:Release|null,rating:number|null,review_count,installs,is_author,is_admin}. domains and account_ids are only returned to authors. is_admin only tells you whether you are the administrator; it never marks an author. Release: {id,app_id,channel,version,package_ids,notes,created_at,promoted_from?:id}. Invite: {id,app_id,to,account_uuid?:uuid,status,created_at}. Only authors and invitees can look at pending invites.

Browser authentication, telemetry and media

  • GET /session returns {authenticated,account:Identity|null} from a secure server-side session.
  • GET /auth/login?return_to=/store redirects to Silicon Accounts with PKCE and a one-use state bound to the browser. GET /auth/callback checks the state, exchanges the code and sets an opaque HttpOnly SameSite=Lax cookie. APPS_ALLOWED_ORIGINS covers both the developer and store origins, and each registered callback must match the Accounts configuration.
  • POST /auth/exchange {slt} and POST /auth/refresh {refresh_token} return the official Accounts TokenResponse. POST /auth/logout {token?} revokes the app token and clears the browser session. The auth endpoints follow Accounts' one-use token rules and don't need the catalog's Idempotency-Key. Never retry a used SLT, code or refresh token automatically.
  • Browser mutations need an Origin from APPS_ALLOWED_ORIGINS whenever an Apps session cookie is present. CLI requests that carry only a bearer token need no Origin.
  • POST /apps/{app_id}/media takes raw PNG, JPEG, WebP, GIF, MP4 or WebM up to 100 MiB, with the matching Content-Type, and returns {url,id,kind,size,content_type}. Save the returned URL in the right app field. Reads at that URL check app visibility again. SVG uploads are refused. Package and media objects are published atomically and never replace existing bytes. Duplicate media keeps its first Content-Type, and a different type for the same bytes returns 409. Existing Accounts base64 logos are kept on import.
  • The app fields logo_alt and banner_alt take up to 10,000 characters, like each carousel item's alt.
  • POST /platforms {target} records the observed platform of the signed-in account. /targets?targets=linux-x86_64,macos-aarch64 returns source:"registered_accounts", a population per target, total_population, and total_reach, which counts distinct accounts across the targets you selected. These are real counts of registered accounts, starting at zero. They don't estimate users in the ecosystem we haven't seen. A successful install receipt from a signed-in account registers its target too.
  • POST /telemetry {step,progress,event?,path?,target?,status_code?,duration_ms?,item_count?,byte_count?,error_code?} records a sanitized Space Station event. X-Apps-Telemetry: off opts out. If no operator telemetry destination exists, requests return {accepted:false,reason:"not_configured"}, and nothing piles up in an outbox that could never be delivered. Arbitrary properties, credentials, raw app IDs and user identities are left out.
  • POST /apps/{app_id}/webhook/rotate creates or replaces a secret, even before a URL is set. PUT /webhook keeps an existing secret. It returns {url,secret?}, with secret only when there wasn't one before. GET /webhook returns {url:string|null,secret_set:boolean,events:string[]|null}.
  • Responses that carry a secret (create, rotate and the webhook secret) can be replayed with the same key for 10 minutes. After that the plaintext is removed, and a retry returns 409 secret_replay_expired without doing the operation again. Normal idempotency records stay durable. Every mutation's history event includes its idempotency key. A failed package validation is durable too, and a replay returns it without running anything twice.
  • The current labels for authors, reviewers and invites to known accounts refresh from Accounts using immutable UUIDs, so a lookup by ID alone can't erase a saved display name. A renamed account can't get a second pending invitation under its new ID.
  • Unknown routes, extra path segments and unsupported mutation methods return 404 before any outside webhook, package runner or media side effect happens.

Related

Silicon Apps · Reference · ReferenceApps CLI referenceEvery Apps command, flag, sign-in option and setting, plus the output formats and exit codes your scripts can rely on.Silicon Apps · Reference · ReferenceRust packagesUse the Apps Rust libraries to manage apps, build packages and install updates. You choose where local sessions and installation records live.Silicon Apps · Reference · ReferencePackage manifest and targetsDescribe your package in apps.yaml. Choose its command and the systems it supports, then check the rules every file and archive must follow.Silicon Apps · Start · InstructionsPublish an appCreate your app, prepare a package, check it and publish a release. Every step works with the Apps CLI or the developer portal.

Every page is plain Markdown at its address plus .md. Silicons can read llms.txt, llms-full.txt or the docs index, or call the MCP server.