Apps HTTP API
Every Apps endpoint for creating apps, publishing packages, finding apps and managing access, with authentication, retries and errors.
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.jsonlists the keys that sign releases. See signed releases.GET /v1/capabilitiesdescribes 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:
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}.tagsis comma-separated, and an app must have all of them.targetkeeps apps whose current release has a package for it.sortisrelevance(the default),rating,installs,name,updatedornewest.limitis 1 to 100, andnext_offsetis null on the last page. A bad value is a 400 that lists the accepted ones.mineneeds 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 /appsbody{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}/accessbody{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}/publishbody{}→ 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/rotatebody{}→{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}/invitesbody{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}/acceptor/declinebody{}→{status:"accepted"|"declined"}.DELETE /apps/{app_id}/invites/{invite_id}body{}→{status:"cancelled"}. Authors only.POST /apps/{app_id}/authors/leavebody{}→{status:"left"}. The last author can't leave. When the administrator leaves, administration passes to the oldest remaining author.POST /apps/{app_id}/adminbody{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.gzas the body andContent-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 inerror.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}. Theapps.yamlmanifest format is the same one the Rust package crate uses.GET /apps/{app_id}/packages→{items:[Package]}. Authors only.POST /apps/{app_id}/releasesbody{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}/promotebody{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.signatureis our Ed25519 signature over the release manifest, with the signed fields, so check it before you run anything (how).author_signatureis the uploading author's own signature, or null.install_scriptis{path,sha256,size}or null.withdrawnlists the withdrawn releases on the channel. Withoutversion, you get the newest release that isn't withdrawn. An exact version that was withdrawn returns 410release_withdrawnwith the reason and the replacement.POST /apps/{app_id}/releases/{release_id}/withdrawbody{reason}→ Release withwithdrawn:{at,by_uuid,by_id,reason}andreplacement:{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 recordsrelease.withdrawnin 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}/installsbody{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 /keysbody{public_key,name?}→ 201{key}. The key ID isak_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 409author_key_exists.DELETE /keys/{key_id}body{reason?}→{key}withstatus:"revoked". Nothing new can be signed with it.- A package upload signed by its author sends
X-Apps-Author-Key-IdandX-Apps-Author-Signature. If the signature doesn't verify, or the key isn't an active key of yours, you get 422invalid_author_signaturebefore any command runs. Accepted packages carryauthor_signature, and a release whose packages are all signed by their authors hassigned_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}/eventsandGET /eventsreturn the event log in pages:{items:[Event],next_after,has_more,cursor}, withafter,typesandlimit(1 to 500).GET /apps/{app_id}/events/streamandGET /events/streamstream the same events as server-sent events, withLast-Event-IDresume,typesand a heartbeat every 15 seconds.GET|POST /subscriptions,GET|PATCH|DELETE /subscriptions/{id},GET /subscriptions/{id}/deliveries,POST /subscriptions/{id}/secret/rotateandPOST /subscriptions/{id}/pingmanage 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}/reviewbody{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}/reviewbody{}→{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}/webhookbody{url,events:["id_change",...]}→ the Accounts configuration. The secret is shown only when it's generated.POST /apps/{app_id}/webhook/rotatebody{}→{webhook_secret:"whsec_…"}.POST /reportsbody{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 /sessionreturns{authenticated,account:Identity|null}from a secure server-side session.GET /auth/login?return_to=/storeredirects to Silicon Accounts with PKCE and a one-use state bound to the browser.GET /auth/callbackchecks the state, exchanges the code and sets an opaque HttpOnly SameSite=Lax cookie.APPS_ALLOWED_ORIGINScovers both the developer and store origins, and each registered callback must match the Accounts configuration.POST /auth/exchange {slt}andPOST /auth/refresh {refresh_token}return the official AccountsTokenResponse.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
OriginfromAPPS_ALLOWED_ORIGINSwhenever an Apps session cookie is present. CLI requests that carry only a bearer token need no Origin. POST /apps/{app_id}/mediatakes 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_altandbanner_alttake up to 10,000 characters, like each carousel item'salt. POST /platforms {target}records the observed platform of the signed-in account./targets?targets=linux-x86_64,macos-aarch64returnssource:"registered_accounts", apopulationper target,total_population, andtotal_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: offopts 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/rotatecreates or replaces a secret, even before a URL is set.PUT /webhookkeeps an existing secret. It returns{url,secret?}, withsecretonly when there wasn't one before.GET /webhookreturns{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_expiredwithout 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.