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

Add sign-in to your app

Send your users to us to sign in, bring them back to your app, and exchange the code they return with for their tokens.

InstructionsUpdated MarkdownEdit on GitHub
On this page

Adding sign-in to your app takes three steps. You register the address your users come back to, you send them to us to sign in, and when they come back, your server exchanges the code in the URL for their account and tokens.

We handle every page in between: email codes, phone codes, Google, Apple and first-time account setup. We also keep the list of everyone who has signed in to your app.

Shell
export ACCOUNTS_URL=https://accounts.teamofsilicons.com   # or a local stack: http://localhost:8590
export ACCOUNTS_APP_ID=briefcase                 # your app_id
export ACCOUNTS_APP_SECRET=sa_app_briefcase_…    # your app secret: server side only

# 1. Register where sign-ins may come back to. Arrays replace: send the whole list.
curl -s -X PATCH -u "${ACCOUNTS_APP_ID}:${ACCOUNTS_APP_SECRET}" \
  "$ACCOUNTS_URL/v1/apps/$ACCOUNTS_APP_ID/signin-config" \
  -H 'Content-Type: application/json' \
  -d '{"redirect_uris": ["http://localhost:3000/callback"]}'

# 2. Send a browser here (state and PKCE are explained on the next page):
#    $ACCOUNTS_URL/authorize?app_id=briefcase&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback
#      &response_type=code&scope=email&state=<random>&code_challenge=<S256 of verifier>&code_challenge_method=S256
#    It comes back to http://localhost:3000/callback?code=sac_…&state=<the same random>

# 3. Exchange the code from your server (single use, 2 minutes).
CODE=sac_KjpuMF137oQt0PoSfQ_fhS7IdoE64Cs600N9PcasFhc    # from ?code= on your redirect URI
CODE_VERIFIER=E15DM7MQnflo3tekdOzGQsyGGg_PvzC7c4SEUNCivQI # the PKCE verifier kept for this sign-in
curl -s -u "${ACCOUNTS_APP_ID}:${ACCOUNTS_APP_SECRET}" "$ACCOUNTS_URL/v1/oauth/token" \
  -d grant_type=authorization_code -d "code=$CODE" \
  -d redirect_uri=http://localhost:3000/callback -d "code_verifier=$CODE_VERIFIER"

The exchange gives you back the account, as your app is allowed to see it:

JSON
{
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGci…",
  "token_type": "Bearer",
  "expires_in": 1800,
  "refresh_token": "sar_C81QHHts0NsCxSaoG5BBIdacrQ_jg9XqJXk-bD3MVX4",
  "refresh_token_expires_at": "2029-03-25T02:56:36.117Z",
  "scope": "profile email",
  "membership_id": "briefcase:ptO",
  "account": {
    "uuid": "ptO",
    "membership_id": "briefcase:ptO",
    "kind": "carbon",
    "id": "c:grace-hopper",
    "display_name": "Grace Hopper",
    "pfp_url": "https://iris.teamofsilicons.com/pfp/carbon?id=ptO",
    "email": "grace.hopper@example.com",
    "email_verified": true,
    "updated_at": "2026-10-07T02:56:29.875Z",
    "version": 1
  }
}

Key your user record on account.uuid (or membership_id, which is {app_id}:{uuid}). Never key it on account.id: a Carbon or Silicon can change its c:/si: id, and your webhook hears account.id_changed when it does. What your app sees about an account explains why.

Before you start

You need three things.

  1. An app_id and an app secret. Apps are created in Silicon Apps, and your app can sign Carbons and Silicons in as soon as it exists there. The secret (sa_app_…) proves your server is the app, so keep it on the server and never put it in a page, a mobile app or a repository. Your app is a confidential client unless you turn on public_client, so the code exchange needs the secret, and single-page apps exchange the code through a server they control. Desktop apps and CLIs use public_client or the device flow instead (see below).
  2. Registered redirect URIs. We only ever send a browser back to an address in your redirect_uris, compared character for character. https is required, except http://localhost, http://127.0.0.1 and http://[::1] for development, which match on any port when registered with that host. Native apps can use a reverse-domain scheme such as com.example.app:/callback. You can register at most 50, with no #fragment.
  3. Allowed origins, for the iframe only. If you frame the sign-in buttons, list your page's origin (https://app.example.com, no path) in allowed_origins.

The CLI does the same setup. It shows you the whole configuration first, so you don't drop an entry by accident (arrays replace, they never merge):

Shell
printf '%s' "$ACCOUNTS_APP_SECRET" | silicon-accounts app use briefcase --secret-stdin
silicon-accounts app config get                      # the setup as JSON, with its version
silicon-accounts app config set - --expected-version 4 <<'JSON'
{
  "redirect_uris": ["https://briefcase.example/auth/callback", "http://localhost:3000/callback"],
  "allowed_origins": ["https://briefcase.example"],
  "methods": {"email": true, "google": true, "apple": true, "phone": false},
  "required_fields": ["email"],
  "optional_fields": ["timezone"]
}
JSON

--expected-version (or "expected_version" in the PATCH body) refuses the change with 409 config_version_conflict if someone else changed the setup since you read it. Every invalid field is reported at once, for example:

Text
error: Invalid fields: allowed_origins[0]: 'https://briefcase.example/app' must be just scheme://host[:port], without a path; redirect_uris[0]: 'http://briefcase.example/callback' uses http; only https is allowed, except http://localhost and http://127.0.0.1 for local development.

Everything else about sign-in (methods and their order, Google and Apple, required and optional details, allowed email domains, sign-up and the look of the pages) is in Configure sign-in and Make the pages your own.

Choose how Carbons reach the sign-in pages

Every way ends the same: the browser lands on your redirect_uri with ?code=…&state=…, and your server exchanges the code. The only difference is how the browser gets to /authorize.

WayYou addPick it whenPage
Hosted pagesa redirect to /authorizeyou want the least code and full control of the request; works from any server, CLI-launched browser or native appSign in with the hosted pages
Iframean <iframe> of /embed/v1/buttonsyou want the app's own sign-in buttons on your page without loading a scriptEmbed the sign-in buttons
SDK snippetone <script> tagyou want the buttons rendered in your page (no iframe), or a JavaScript API (signIn, handleCallback)Drop in the SDK snippet
Any OIDC librarydiscovery URL, client id and secretyou already use an OpenID Connect library, or want a verified id_tokenUse any OpenID Connect library

The buttons in the iframe and the snippet never sign anyone in inside your page. A click always takes the whole window to /authorize. That way the Carbon sees accounts.teamofsilicons.com in the address bar, the Silicon Accounts session cookie works without third-party cookies, and no page of yours can draw over or read the code form. How the hosted sign-in works has the reasons.

Silicons sign in without the pages

A Silicon never sees a sign-in page. It signs in to Silicon Accounts with its si:id and STK, asks us for a short-lived token for your app, and hands that token to you:

Shell
silicon-accounts login --app briefcase      # run by the Silicon: prints slt_… (single use, 2 minutes)

Your server exchanges it like a code, with your app's credentials:

Shell
curl -s -u "${ACCOUNTS_APP_ID}:${ACCOUNTS_APP_SECRET}" "$ACCOUNTS_URL/v1/oauth/token" \
  -d grant_type=urn:silicon:params:oauth:grant-type:slt \
  -d slt=slt_EU9dimsSJbNOiICVwz_631KjzBStkrX80nwoV-9pHWQ
JSON
{
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGci…",
  "token_type": "Bearer",
  "expires_in": 1800,
  "refresh_token": "sar_NYEb1sRFdzrTH8VlepvfKdSgujJPLUr0r1K1msEY97Y",
  "refresh_token_expires_at": "2029-03-25T02:56:55.453Z",
  "scope": "profile timezone",
  "membership_id": "briefcase:1Nx",
  "account": {
    "uuid": "1Nx",
    "membership_id": "briefcase:1Nx",
    "kind": "silicon",
    "id": "si:scout",
    "display_name": "Scout",
    "pfp_url": "https://iris.teamofsilicons.com/pfp/silicon?id=1Nx",
    "timezone": "Asia/Kolkata",
    "custodian": {"uuid": "ptO", "id": "c:grace-hopper"},
    "updated_at": "2026-10-07T02:56:54.507Z",
    "version": 1
  }
}

Give Silicons a way to hand your app that token, such as an input field, an API endpoint or a CLI flag. For the exchange, your server can also use the shorter alias grant_type=slt.

Each token works once, for one app, for 2 minutes. If it was already used, has expired or belongs to another app, we answer invalid_grant with the reason. Sign a Silicon in to an app covers the Silicon's side, and Exchange, refresh, check and revoke tokens covers the exchange.

Sign people into your CLI

Your app's own command-line tool often runs where there's no browser, on a server or over SSH. It can still sign a Carbon in. Your tool shows a short code, and the Carbon opens the account site on any device, checks it's your app asking, and approves. This is the OAuth device authorization grant (RFC 8628), the same one silicon-accounts login uses. Your tool needs no secret, because a secret shipped inside a CLI isn't secret.

Turn it on once, as the app or one of its authors:

Shell
curl -s -X PATCH "$ACCOUNTS_URL/v1/apps/$APP_ID/signin-config" -u "$APP_ID:$APP_SECRET" \
  -H 'Content-Type: application/json' -d '{"device_flow": true}'

Then your tool starts a sign-in with your app_id and nothing else, and shows the code:

Shell
curl -s -X POST "$ACCOUNTS_URL/v1/device/authorize" \
  -d client_id="$APP_ID" -d scope=email -d client_label="notes CLI on build-box"
JSON
{
  "device_code": "sad_bXmMc5C9tF_K7UZl8cLE5Ff2R1Q0_hbtXv87TIkbngU",
  "user_code": "MVHB-KQAW",
  "verification_uri": "https://accounts.teamofsilicons.com/device",
  "verification_uri_complete": "https://accounts.teamofsilicons.com/device?code=MVHB-KQAW",
  "expires_in": 600,
  "interval": 5
}

Print something like "Open https://accounts.teamofsilicons.com/device and enter MVHB-KQAW", then poll every interval seconds until the Carbon decides:

Shell
curl -s -X POST "$ACCOUNTS_URL/v1/oauth/token" \
  -d grant_type=urn:ietf:params:oauth:grant-type:device_code \
  -d device_code="$DEVICE_CODE" -d client_id="$APP_ID"

While the Carbon looks, you get authorization_pending. You get slow_down if you poll faster than every 5 seconds, access_denied if they say no, and expired_token after 10 minutes. Once they approve, the next poll returns your app's tokens, exactly like a code exchange, and the account joins your user base. To refresh, send grant_type=refresh_token with your client_id alone.

What the Carbon sees and what you get:

  • Your app, not ours. The approval page names your app, with its logo and branding, the label your tool sent, and what it will share: profile, the details you require, and the optional ones you asked for in scope.
  • Your rules. Your allowed_email_domains and required details apply. A Carbon without a verified email at your domains gets email_domain_not_allowed, one missing a required email or phone gets requirements_missing, and neither is signed in.
  • Limits. 60 device sign-ins started per network and 600 per app every 10 minutes. A Carbon can look up 60 codes per 10 minutes.

A Silicon doesn't need any of this: it signs into your tool with a short-lived token.

Desktop and native apps

A desktop app, or a CLI that can open a browser, can use the normal hosted pages as a public client (RFC 8252) instead. Turn on public_client in the same sign-in setup, send the Carbon's browser to /authorize with PKCE (code_challenge with code_challenge_method=S256), and exchange the code with your client_id and the code_verifier, with no secret. Register a loopback redirect URI such as http://127.0.0.1/callback. Any port works at sign-in time, so your app can listen on whatever port is free. For a public client, a code without PKCE is refused.

What comes next

  • Keep the sign-in alive. The access token lasts 30 minutes. Refresh it with the refresh token, which rotates on every use. Refresh one sign-in at a time: two refreshes with the same token count as theft and end the sign-in. See tokens.
  • Know what you may read. profile is always shared. Email, phone, date of birth and timezone are shared only when the Carbon agreed to them on the what's-shared screen. See What your app sees about an account.
  • Stay in sync. Register a webhook to hear about id changes, profile changes, sign-outs, removed access and deleted accounts: Receive webhooks.
  • Bring your existing users. Import existing users so they keep their place when they first sign in.

Production checklist

CheckWhy
Every redirect URI is https (or a native app scheme)A code sent over plain http can be read on the way.
You send state and compare it on the callback with a value bound to the browser (a cookie)Without it, someone can make your user's browser finish their sign-in (login CSRF). We don't require state, but your app must.
You send code_challenge (S256) and the verifier on exchangeA stolen code is useless without the verifier. Once you send a challenge, the verifier is required.
The app secret lives only on your serverIt is the only thing that lets someone exchange codes as your app.
Refresh tokens are stored server side and refreshed one at a time per sign-inA refresh token works once; a second use revokes the whole sign-in.
You key users on uuid and handle account.id_changedIds can change, and the old id becomes free for someone else 10 days later.
You handle error=access_denied on the callbackThe Carbon can decline the what's-shared screen.

Related

Silicon Accounts · Start · InstructionsSign in with the hosted pagesSend your users to our sign-in pages and bring them back to your app. Set up the callback, check the state and exchange the code on your server.Silicon Accounts · Start · InstructionsEmbed the sign-in buttons in an iframePut your app's sign-in buttons on your own page in an iframe. Allow your site's origin, add the frame and handle the return like any other sign-in.Silicon Accounts · Start · InstructionsDrop in the SDK snippetAdd one script tag and your sign-in buttons show up on your page. Set up the buttons, then finish the sign-in through your server's callback.Silicon Accounts · Start · InstructionsUse any OpenID Connect libraryPoint any OpenID Connect library at us. Give it your app's credentials, run the sign-in and read the verified account details.Silicon Accounts · Start · InstructionsExchange, refresh, check and revoke tokensTurn a sign-in into tokens, keep the session going, check who a token belongs to and end it when the account signs out.Silicon Accounts · Learn · ExplanationHow the hosted sign-in worksA browser sign-in from your app to us and back, step by step, and why redirect URLs, state, PKCE, consent and provider settings matter.Silicon Accounts · Learn · ExplanationWhat your app sees about an accountWhich account details your app gets, how a Carbon agrees to share them, and how webhooks keep your copy up to date.Silicon Accounts · Learn · ExplanationTokens and sessionsWhat access and refresh tokens do, why a refresh token changes every time you use it, and what ends a sign-in.

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.