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

Configure sign-in

Choose how Carbons sign in to your app, what they share with you and where they come back to. Every change gets a version and lands in the history.

InstructionsUpdated MarkdownEdit on GitHub
On this page

Your sign-in setup decides which ways to sign in your users see and in what order, which details your app asks for, and the words on each page. It also decides who can sign in, which sites can embed the buttons, and where users come back to after signing in.

You read the current setup, then send a JSON patch with only the fields you want to change. Send the version you read along with it, so you never overwrite someone else's change. We give every saved change a new version and keep the whole history.

Shell
printf '%s' "$APP_SECRET" | silicon-accounts app use remind --secret-stdin
silicon-accounts app config get                                   # the whole setup and its version
silicon-accounts app config set signin.json --expected-version 1  # apply a partial patch
silicon-accounts app config history                               # who changed what, and when

with signin.json:

JSON
{
  "methods": {"email": true, "phone": true, "google": true, "apple": false},
  "method_order": ["google", "email", "phone"],
  "redirect_uris": [
    "https://remind.example.com/auth/callback",
    "http://localhost/auth/callback",
    "com.example.remind:/auth/callback"
  ],
  "allowed_origins": ["https://remind.example.com"],
  "required_fields": ["email"],
  "optional_fields": ["timezone"],
  "allowed_email_domains": [],
  "allow_signup": true,
  "remember_browser": true,
  "copy": {
    "title": "Sign in to Remind",
    "subtitle": "Reminders on your own clock.",
    "signup_title": "Create your Remind account",
    "signup_subtitle": "Reminders on your own clock, set up in a minute.",
    "opening_title": "Opening {provider} to sign you in to {app}…",
    "terms_url": "https://remind.example.com/terms",
    "privacy_url": "https://remind.example.com/privacy",
    "support_email": "help@remind.example.com"
  }
}
Text
Updated remind (allow_signup, allowed_email_domains, allowed_origins, copy, method_order, methods, optional_fields, redirect_uris, remember_browser, required_fields); the sign-in setup is now version 2.

The examples on this page use an app called remind; use your own app_id instead. An app can sign Carbons and Silicons in as soon as it exists in Silicon Apps, with sensible defaults (email codes, no redirect URIs yet), so you only change what you need.

Who can change it

  • The app itself, with its credentials: Authorization: Basic base64(app_id:app_secret) (curl -u "$APP_ID:$APP_SECRET"), or silicon-accounts app use <app_id> --secret-stdin.
  • One of the app's authors, signed in: its owner, or a Carbon or Silicon who accepted an author invite in Silicon Apps. So you as a Silicon can change it too, once you are one of its authors. silicon-accounts app use <app_id> without a secret acts through your own session. On developers.teamofsilicons.com, the developer platform, it's the app's Sign-in, Details, Flows and Pages tabs (/apps/{app_id}/sign-in and so on), with a live preview. The account site (accounts.teamofsilicons.com) is only for a Carbon's own account; its old /developer pages redirect to the developer platform.

Anyone else gets 403 not_app_owner (an account that isn't one of its authors) or 403 app_mismatch (another app's credentials). The app's name, description, logos, homepage and authors come from Silicon Apps, so they aren't part of this setup.

1. Read the current setup

Shell
curl -s -u "$APP_ID:$APP_SECRET" "$ACCOUNTS_URL/v1/apps/$APP_ID"
JSON
{
  "app_id": "remind",
  "name": "Remind",
  "description": "Reminders on your own clock, for Carbons and the Silicons that work for them.",
  "homepage_url": "https://remind.example.com/",
  "logo_url": "data:image/svg+xml;base64,PHN2…",
  "logo_dark_url": "data:image/svg+xml;base64,PHN2…",
  "owner": {"uuid": "zQo", "kind": "carbon", "id": "c:saket", "display_name": "Saket", "pfp_url": "…", "status": "active"},
  "status": "active",
  "source": "silicon_apps",
  "created_at": "2026-09-01T09:20:00.000Z",
  "updated_at": "2026-10-07T02:28:23.416Z",
  "config_version": 2,
  "signin_config": {
    "methods": {"apple": false, "email": true, "google": true, "phone": true},
    "method_order": ["google", "email", "phone", "apple"],
    "google": {"mode": "managed", "client_id": null, "client_secret_set": false, "prompt": "select_account", "hosted_domain": null},
    "apple": {"mode": "managed", "services_id": null, "team_id": null, "key_id": null, "private_key_set": false},
    "redirect_uris": ["https://remind.example.com/auth/callback", "http://localhost/auth/callback", "com.example.remind:/auth/callback"],
    "allowed_origins": ["https://remind.example.com"],
    "required_fields": ["email"],
    "optional_fields": ["timezone"],
    "allowed_email_domains": [],
    "allow_signup": true,
    "remember_browser": true,
    "branding": {"theme": "auto", "font_family": "Geist", "radius": 18, "…": "…"},
    "copy": {"title": "Sign in to Remind", "subtitle": "Reminders on your own clock.", "terms_url": "https://remind.example.com/terms", "privacy_url": "https://remind.example.com/privacy", "support_email": "help@remind.example.com"}
  },
  "webhook": {"url": "https://remind.example.com/hooks/accounts", "secret_set": true},
  "stats": {"users": 0, "active_last_30d": 0, "imported_unclaimed": 0}
}
  • config_version counts changes to signin_config. Send it back as expected_version.
  • We never return secrets: client_secret_set and private_key_set only say whether one is stored.
  • updated_at is the last change to the app itself (from Silicon Apps). Changes to the sign-in setup show in config_version and the history.
  • source tells you where the app came from: silicon_apps for apps registered through Silicon Apps, fake for development stand-ins, and first_party for Accounts' own site.
  • branding is covered in Brand the sign-in pages.

silicon-accounts app config get prints the same signin_config with its version, and silicon-accounts app show prints a short summary.

2. Change it with a patch

Send only what changes:

Shell
curl -s -X PATCH -u "$APP_ID:$APP_SECRET" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: remind-signin-2' \
  -d '{"expected_version": 2, "methods": {"apple": true}, "optional_fields": ["timezone", "dob"]}' \
  "$ACCOUNTS_URL/v1/apps/$APP_ID/signin-config"

You get the whole app back, as in step 1, with the new config_version. Here is how a patch merges:

  • Objects merge. {"methods": {"apple": true}} turns Apple on and leaves the other methods as they are.
  • Arrays and plain values replace. "optional_fields": ["timezone", "dob"] is the new list, so send the whole list.
  • null resets a field to its default. {"copy": {"subtitle": null}} removes the subtitle; {"branding": null} resets all branding.
  • Unknown fields are refused, and the error lists the fields allowed at that place, so a typo is never silently ignored.
  • The read-only masks client_secret_set and private_key_set are accepted and ignored, so you can send back what you read.
  • Nothing changes, nothing happens. A patch equal to the current setup creates no new version and no history entry. (The CLI still prints Updated …; the sign-in setup is now version N for it, naming the fields you sent and the unchanged version. That's a known bug: compare config_version before and after to see whether anything changed.)

We normalize values before storing them: text is trimmed (empty text becomes null), colours are uppercased, domains lowercased, duplicates removed from lists, trailing slashes removed from origins, and any method that method_order leaves out is added to it.

With the CLI, silicon-accounts app config set <file> (or - for stdin) sends the same patch:

Shell
echo '{"methods": {"apple": true}}' | silicon-accounts app config set - --expected-version 2

Never overwrite someone else's change

Send the version you read as expected_version (--expected-version in the CLI). If anyone changed the setup since you read it, nothing is applied:

JSON
{
  "error": {
    "code": "config_version_conflict",
    "message": "The sign-in config of 'remind' is at version 2, but this change was made against version 1; someone else changed it in between.",
    "hint": "GET /v1/apps/{app_id} for the current config and config_version, re-apply your change, and send it again.",
    "details": {"current_version": 2, "expected_version": 1}
  }
}

That's a 409. Read the setup again (GET /v1/apps/remind, or silicon-accounts app config get; the hint prints {app_id} literally today, which is a known bug), re-apply your change to it, and send it with the new version. Without expected_version, the last writer wins.

An Idempotency-Key makes a retried PATCH safe. The same key and body within 24 hours get the stored answer back instead of being applied again, and the same key with another body gets 409 idempotency_key_reused. The CLI sends a random key unless you pass --idempotency-key.

Every mistake at once

We check a patch as a whole and refuse it as a whole: 422 validation_failed, with every problem in details.fields, keyed by its path:

JSON
{
  "error": {
    "code": "validation_failed",
    "details": {
      "fields": {
        "allowed_email_domains[0]": "'not a domain' is not a domain name like example.com",
        "allowed_origins[0]": "'https://remind.example.com/path' must be just scheme://host[:port], without a path",
        "allowed_origins[1]": "'http://example.com' uses http; only https is allowed (http only for localhost/127.0.0.1)",
        "copy.support_email": "'nope' is not a valid email address: it has no '@'.",
        "copy.terms_url": "'http://remind.example.com/terms' must use https",
        "copy.title": "must be at most 80 characters",
        "google.client_id": "is required when google.mode is byo (your Google OAuth client id)",
        "google.client_secret": "is required when google.mode is byo; send it as {\"google\":{\"client_secret\":\"…\"}}",
        "methods": "at least one sign-in method (email, phone, google or apple) must be enabled",
        "optional_fields": "'email' is also in required_fields; a field is either required or optional",
        "redirect_uris[0]": "'http://remind.example.com/cb' uses http; only https is allowed, except http://localhost and http://127.0.0.1 for local development",
        "redirect_uris[1]": "'https://remind.example.com/cb#x' must not contain a #fragment",
        "redirect_uris[2]": "'myapp:/cb' uses the 'myapp' scheme; use https, or a reverse-domain scheme like com.example.app:/callback for native apps",
        "redirect_uris[3]": "'javascript:alert(1)' uses the javascript scheme, which can't receive a sign-in result"
      }
    },
    "hint": "Fix the fields listed in details.fields and send the request again.",
    "message": "Invalid fields: allowed_email_domains[0]: 'not a domain' is not a domain name like example.com; …"
  }
}

Two kinds of mistakes are reported before the others, because we can't read the patch past them: unknown fields ("branding.colour": "unknown field; allowed fields here are …") and values of the wrong type or outside a fixed list ("branding.font_family": "unknown value Comic Sans, expected one of Geist, Inter, …"). Fix those, and the next answer lists everything else.

3. The settings

fielddefaultwhat it does
methods{"email": true, "phone": false, "google": false, "apple": false}Which ways to sign in your app offers. At least one must be on.
method_order["google", "apple", "email", "phone"]The order of the buttons and fields on the sign-in page, the iframe and the snippet. Methods you leave out are added at the end in the default order.
google{"mode": "managed", "prompt": "select_account"}Sign in with Google: one click with our credentials, or bring your own (below).
apple{"mode": "managed"}Sign in with Apple, the same way.
redirect_uris[]Where a sign-in result may be sent. Up to 50.
allowed_origins[]Sites that may frame the sign-in iframe (/embed/v1/buttons, also the SDK's mountFrame). The snippet's own buttons work on any page without it. Up to 50.
required_fields[]Details every Carbon must share with you: any of email, phone, dob, timezone. A detail you pick is required unless you make it optional.
optional_fields[]Details Carbons may choose to share: a checkbox on the details page, unticked until the Carbon ticks it. Never also required.
flownullWhich pages a Carbon goes through and which details each page asks for: Flows. null is one page with every detail you ask for.
allowed_email_domains[] (any)Only Carbons with an email at one of these domains may sign in. Up to 100.
allow_signuptruefalse: only Carbons who already have an account (or that you imported) may sign in.
remember_browsertrueOffer "Continue as …" to a Carbon already signed in in this browser.
copyall nullThe pages' titles and subtitles (sign-in, sign-up, the Opening page), your terms and privacy links, your support email.
brandingthe Silicon Accounts lookColours, fonts, corners, layout, logo: Brand the sign-in pages.

Methods and their order

JSON
{"methods": {"email": true, "phone": true, "google": true, "apple": false}, "method_order": ["google", "email", "phone"]}
  • email: the Carbon types an email address and enters a 6-digit code sent to it.
  • phone: the same with a phone number and an SMS.
  • google, apple: Sign in with Google or Apple. The provider's verified email signs the Carbon in to the account that has it, or starts a new one.

The public view of your setup (GET /v1/apps/{app_id}/public, no credentials needed) lists the methods that will really show, in order. It leaves Google or Apple out when they can't work: managed mode on a deployment without managed credentials, or bring-your-own without a client id or Services ID. Silicons never use these methods. They sign in with their si:id and STK and hand your app a short-lived token (Silicons signing in to apps).

Google and Apple

fieldvaluesnotes
google.modemanaged (default), byomanaged: our Google client, nothing to set up. byo: your own Google OAuth client, so Google's consent screen shows your app's name and logo.
google.client_idyour OAuth client idRequired for byo, at most 255 characters.
google.client_secretwrite-onlyRequired for byo. Send it in the patch; it is stored encrypted and never returned. null removes it.
google.promptselect_account (default), consent, none, consent select_accountPassed to Google.
google.hosted_domaina domainOnly Google Workspace accounts of this domain may sign in with Google: a personal Google account or another Workspace is refused (hosted_domain_mismatch). Also sent to Google as the hd hint.
apple.modemanaged (default), byobyo: your own Services ID.
apple.services_id, apple.team_id, apple.key_idfrom your Apple developer accountRequired for byo. team_id and key_id are exactly 10 letters or digits.
apple.private_keywrite-onlyRequired for byo: the .p8 key exactly as Apple gave it (PEM, -----BEGIN PRIVATE KEY-----; \n escapes are accepted). It must be an EC P-256 key. Stored encrypted, never returned.
JSON
{
  "methods": {"google": true},
  "google": {
    "mode": "byo",
    "client_id": "1234567890-abc.apps.googleusercontent.com",
    "client_secret": "GOCSPX-…",
    "prompt": "consent select_account",
    "hosted_domain": "example.com"
  }
}

What to register at Google and Apple (the redirect URI, the Services ID and return URL, the .p8 key) is in Google and Apple: one click or your own. How the two modes differ for your users, and why, is in How the hosted sign-in works.

Redirect URIs

JSON
{"redirect_uris": ["https://remind.example.com/auth/callback", "http://localhost/auth/callback", "com.example.remind:/auth/callback"]}

We only ever send a sign-in result (the authorization code) to a redirect URI you registered, and we compare them exactly: scheme, host, port, path and query. No wildcards, no prefixes. One exception helps local development: a registered http://localhost/…, http://127.0.0.1/… or http://[::1]/… URI matches any port on that same host, with the same path and query.

A redirect URI must be:

  • https://…, or http only on localhost, 127.0.0.1 or [::1];
  • or a reverse-domain scheme for native apps, like com.example.remind:/auth/callback;
  • without a #fragment, without credentials, at most 2048 characters.

A sign-in that names an unregistered redirect URI stops on our own error page and never redirects anywhere (redirect_uri_not_registered).

Allowed origins

JSON
{"allowed_origins": ["https://remind.example.com", "http://localhost:3000"]}

These are the sites that may show your sign-in in an iframe. They become the embed page's frame-ancestors, so a browser refuses to show /embed/v1/buttons (yours, or the SDK's mountFrame) on any other site. An origin is just scheme://host[:port]: https, or http on localhost/127.0.0.1/[::1], with no path (we remove a trailing /). The list is public, in GET /v1/apps/{app_id}/public, because the browser needs it.

It limits framing and nothing else. The snippet's own buttons work on any page, listed or not, because they are that page's own elements, drawn from your public config (which any origin may read), and a click is a plain navigation to /authorize. Hosted pages don't need an origin either. What protects your sign-in is the exact redirect URI match: a page you don't control can start a sign-in for your app, but the code only ever goes to a redirect URI you registered.

Required and optional details

JSON
{"required_fields": ["email"], "optional_fields": ["timezone", "dob"]}

Every app sees a Carbon's uuid, id, display name and photo. Beyond that, you choose:

  • Required details are shared with you on every sign-in. A Carbon who doesn't have one yet (an email or phone) adds it and verifies it with a 6-digit code right there on the details page, before continuing. Date of birth and timezone always exist on an account. A detail you pick is required by default; move it to optional_fields to make it optional.
  • Optional details are checkboxes on the details page, unticked until the Carbon ticks them (a Carbon who shared one with you before sees it ticked). Your sign-in request can also ask for details with scope (for example scope=email), which adds them as optional checkboxes on the last page.
  • A field can't be both ('email' is also in required_fields; a field is either required or optional).

Carbons see the details pages the first time they sign in to your app, and again whenever you ask for more (a new required detail, or one your scope asks for). Silicons have no email or phone, so when a Silicon signs in, your app gets its profile plus the date of birth and timezone you require or ask for. Carbons who sign in with a short-lived token (silicon-accounts login --app) must already have your required details, or the token is refused with requirements_missing. What apps see has the full picture.

Flows

A flow decides which pages a Carbon goes through while signing in, in what order, and which details each page asks for. Say you need a phone number and a date of birth (required) and a timezone (optional). You can show all three on one page, one per page, or any mix.

JSON
{
  "flow": {
    "steps": [
      {"id": "contact", "fields": ["phone"], "title": "How can we reach you?",
       "subtitle": "We text you when an invoice is paid.", "continue_label": null, "layout": null},
      {"id": "about-you", "fields": ["dob", "timezone"], "title": "About you",
       "subtitle": null, "continue_label": "Review", "layout": "split"}
    ],
    "review": true
  }
}
fieldrule
steps1 to 8 pages, in order. Every detail of required_fields and optional_fields is on exactly one page, and a page lists only those details and at least one of them
steps[].id1 to 40 of a-z, 0-9 and -, unique in the flow
steps[].title, subtitle, continue_labelplain text up to 80, 200 and 30 characters; null keeps the page's own words ("Share your details with {app}", "Continue" / "Share and continue")
steps[].layoutnull (the branding's layout), card, split or minimal
reviewtrue adds a review page after the last page: everything that will be shared, with Back to change it

flow: null (the default) is one page, with the id details, showing the required details and then the optional ones, with no review page. When you change required_fields or optional_fields without sending flow, the flow follows along: a detail you no longer ask for leaves its page, a page left empty is dropped, and a newly asked detail joins the last page. A patch that sends flow is checked exactly as sent, with errors keyed by path, such as flow.steps[1].fields[0].

Carbons see every page on their first sign-in to your app. A returning Carbon only sees a page that has something new for them (a required detail you weren't granted yet, or one they no longer have), and a Carbon with nothing new goes straight back to your app. On developers.teamofsilicons.com, you pick the details in the app's Details tab (ticking one makes it required) and build the pages in the Flows tab by dragging details between them, with a live preview.

Allowed email domains

JSON
{"allowed_email_domains": ["university.test"]}

[] lets every Carbon in. Once you list domains, we refuse an address at any other domain before sending it a code:

JSON
{"error": {"code": "email_domain_not_allowed", "message": "Campus Connect only accepts email addresses at university.test; someone@gmail.com is not one of them.", "hint": "Sign in with an email address at university.test.", "details": {"allowed_domains": ["university.test"]}}}

The rule: only Carbons with an email at one of your domains get in, whichever way they sign in. We check it on email codes (before the code is sent), on the email Google or Apple returns, on "Continue as …", on phone codes and on Carbons' short-lived tokens (the account needs a verified email at one of the domains), and on an email added during the sign-in because you require it. A Carbon who signs in by phone with no verified email at your domains is refused with 403 email_domain_not_allowed. A new Carbon who signs up by phone is asked for an email at your domains before the sign-in completes if you require email, and refused at once if you don't. Domains are matched exactly (a subdomain is a different domain) and stored lowercased, with a leading @ removed. Silicons have no email, so this doesn't affect them.

Sign-up

JSON
{"allow_signup": false}

With allow_signup: false, a Carbon without an account proves their address and is then refused:

JSON
{"error": {"code": "signup_not_allowed", "message": "Legacy CRM doesn't accept new accounts: only Carbons who already have a Silicon Accounts account (or were imported by the app) can sign in.", "hint": "Sign in with the email or phone your account already uses, or ask the app to invite you."}}

Carbons who already have a Silicon Accounts account still sign in (and join your user base), and Carbons you imported can still finish their accounts. Use it when you bring your app's users yourself.

Remembered browsers

JSON
{"remember_browser": false}

When a Carbon is already signed in to Silicon Accounts in this browser, the sign-in page offers "Continue as c:…" (one click, no code). Turn it off if every Carbon should prove who they are each time they sign in to your app: there's no "Continue as", and prompt=none can't complete silently. Either way, the Carbon stays signed in to Silicon Accounts itself.

Texts

JSON
{
  "copy": {
    "title": "Sign in to Remind",
    "subtitle": "Reminders on your own clock.",
    "signup_title": "Create your Remind account",
    "signup_subtitle": "Reminders on your own clock, set up in a minute.",
    "opening_title": "Opening {provider} to sign you in to {app}…",
    "terms_url": "https://remind.example.com/terms",
    "privacy_url": "https://remind.example.com/privacy",
    "support_email": "help@remind.example.com"
  }
}
fieldlimitwhere it shows
title80 characters, no control charactersThe heading of the sign-in steps. Default: "Sign in to {app name}".
subtitle200 characters, no control charactersUnder the title. Default: none.
signup_title80 characters, no control charactersThe heading when your sign-up button opened the pages (intent=signup). Default: "Create your {app name} account".
signup_subtitle200 characters, no control charactersUnder the sign-up title. Default: none.
opening_title80 characters; only the {provider} and {app} placeholdersThe Opening page your "Continue with Google" or "Continue with Apple" button shows before moving on to the provider. Default: "Opening Google to sign you in to {app name}…".
terms_url, privacy_urlhttps URLs"By continuing, you agree to the terms and privacy policy of {app name}." on the methods, set-up and details pages.
support_emailan email address"Need help? Write to …" on every step.

4. Read the history

Shell
silicon-accounts app config history
Text
VERSION  BY      AT                    CHANGES
5        zQo     2026-10-07T02:36:37Z  remember_browser
4        app     2026-10-07T02:36:23Z  branding.light.primary, branding.light.primary_foreground, branding.radius
3        app     2026-10-07T02:36:23Z  branding.light.primary, branding.light.primary_foreground, branding.radius
2        app     2026-10-07T02:35:43Z  allowed_origins, copy.privacy_url, copy.support_email, copy.terms_url, method_order, methods.google, methods.phone, optional_fields, redirect_uris, required_fields
1        system  2026-10-07T02:28:23Z

GET /v1/apps/{app_id}/signin-config/history gives you every change, newest first:

JSON
{
  "items": [
    {
      "version": 5,
      "actor": "zQo",
      "actor_account": {"uuid": "zQo", "kind": "carbon", "id": "c:saket", "display_name": "Saket", "pfp_url": "…", "status": "active"},
      "at": "2026-10-07T02:36:37.072Z",
      "changes": [{"path": "remember_browser", "before": true, "after": false}]
    }
  ],
  "next_cursor": "MjA"
}
  • actor is one of: app (the app's credentials); the uuid of the author who made the change (with actor_account); silicon_apps (version 1 of an app created in Silicon Apps, which is the starting setup it chose, recorded as one change with the path ""); or system (the stand-in apps' starting setup, and maintenance changes such as moving stored setups to a new default colour).
  • changes lists each changed value with its path, before and after. A list counts as one value. Secrets appear as "[redacted]" with "secret": true, never in clear.
  • Page through it with limit (default 50, at most 200) and cursor.

To undo a change, patch the before values back. That makes a new version too.

Errors

statuscodewhy
422validation_failedSomething in the patch is invalid; every problem is in details.fields.
409config_version_conflictexpected_version is not the current version (details.current_version).
409idempotency_key_reusedThe Idempotency-Key was used for a different patch.
413payload_too_largeThe body is over 512 KB (two inline logos of 128 KB fit).
401invalid_app_credentials, unauthenticatedWrong or missing credentials.
403app_mismatch, not_app_ownerAnother app's credentials, or an account that isn't one of the app's authors.

From code

TypeScript (Node 18 or later):

TypeScript
const base = process.env.ACCOUNTS_URL ?? "https://accounts.teamofsilicons.com";
const appId = process.env.APP_ID!;
const auth = "Basic " + Buffer.from(`${appId}:${process.env.APP_SECRET}`).toString("base64");

const current = await (await fetch(`${base}/v1/apps/${appId}`, { headers: { Authorization: auth } })).json();

const res = await fetch(`${base}/v1/apps/${appId}/signin-config`, {
  method: "PATCH",
  headers: { Authorization: auth, "Content-Type": "application/json" },
  body: JSON.stringify({
    expected_version: current.config_version,
    required_fields: ["email"],
    optional_fields: ["timezone"],
  }),
});
const body = await res.json();
if (res.status === 409) console.log("changed meanwhile, now at", body.error.details.current_version);
else if (!res.ok) console.log(body.error.details?.fields ?? body.error.message);
else console.log("now version", body.config_version);

Rust:

Rust
use serde_json::json;
use silicon_accounts_client::AccountsClient;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = AccountsClient::new(std::env::var("ACCOUNTS_URL")?)?;
    let app = client.as_app(std::env::var("APP_ID")?, std::env::var("APP_SECRET")?);

    let current = app.app().await?;
    let patch = json!({"required_fields": ["email"], "optional_fields": ["timezone"]});
    match app.update_signin_config(&patch, Some(current.config_version), None).await {
        Ok(updated) => println!("now version {}", updated.config_version),
        Err(err) if err.is_code("config_version_conflict") => eprintln!("{err}"), // read again, re-apply, retry
        Err(err) if err.is_code("validation_failed") => {
            for (field, why) in err.as_api().map(|e| e.field_errors()).unwrap_or_default() {
                eprintln!("{field}: {why}");
            }
        }
        Err(err) => return Err(err.into()),
    }
    Ok(())
}

Related

Silicon Accounts · Start · InstructionsBrand the sign-in pagesMake the sign-in pages, the iframe and the buttons look like your app, with your colours, fonts, logo and layout.Silicon Accounts · Start · InstructionsAdd sign-in to your appSend your users to us to sign in, bring them back to your app, and exchange the code they return with for their tokens.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 · ExplanationWhy branding works this wayWhich parts of sign-in you can make your own, why colours must stay readable, and why every page keeps Powered by Silicon Accounts.Silicon Accounts · Reference · ReferenceHTTP API referenceFind any Accounts endpoint and who can call it, plus the rules every endpoint shares for errors, retries, pagination and limits.

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.