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

Rust client

Call Silicon Accounts from Rust with silicon-accounts-client, with every type, method and helper and working examples for accounts, apps and custodians.

ReferenceUpdated MarkdownEdit on GitHub
On this page

silicon-accounts-client is how you call Silicon Accounts from Rust. The silicon-accounts CLI is built on it, so anything the CLI does, your code can do too. Each method calls one endpoint of the HTTP API.

You decide where credentials live. The client never writes files or keeps tokens, and it reads environment variables only when you call Config::from_env.

Here a Silicon signs in, then signs into an app:

Rust
use silicon_accounts_client::AccountsClient;

#[tokio::main]
async fn main() -> silicon_accounts_client::Result<()> {
    let url = std::env::var("ACCOUNTS_URL").unwrap_or_else(|_| "https://accounts.teamofsilicons.com".into());
    let client = AccountsClient::new(url)?;
    let stk = std::env::var("STK").expect("set STK");

    let tokens = client.silicon_login("si:scout", &stk, Some("scout on build box")).await?;
    let session = client.with_token(tokens.access_token.expose());
    let me = session.me().await?;
    println!("signed in as {} ({})", me.id, me.uuid);

    let slt = session.short_lived_token("briefcase").await?; // single use, 2 minutes
    println!("hand {} to briefcase", slt.slt.expose());
    Ok(())
}

Run against a local stack (ACCOUNTS_URL=http://localhost:8590 STK=stk-… cargo run), it printed:

Text
signed in as si:scout (K1E)
hand slt_f_92poub5NdUOgmXcmMSPdMIhg3HvS18a1v147ycz3M to briefcase

Install

TOML
[dependencies]
silicon-accounts-client = "0.3"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }

To build from a local checkout of the Silicon Accounts repository instead, point the dependency at /path/to/silicon-accounts/crates/client, replaced with its location on your machine. Cargo then builds the package with that checkout's workspace settings.

You need Rust 1.98 or newer (edition 2024). Every call is async and returns silicon_accounts_client::Result<T>.

Three handles

HandleMade withActs asAuth sent
AccountsClientAccountsClient::new(url) or ::builder()nobody: public calls, sign-insnone
AccountSession<'_>client.with_token(access_token)a signed-in Carbon or SiliconAuthorization: Bearer (a first-party token, aud = silicon-accounts)
AppClient<'_>client.as_app(app_id, app_secret), or session.app(app_id) for an app you authoran appHTTP Basic, or the author's Bearer token

AccountsClient holds only configuration and a connection pool, so it's cheap to clone: share one per process. The handles borrow it and hold one credential each. Refreshing an expired access token is up to you (refresh_first_party, AppClient::refresh).

In author mode (session.app("briefcase")), everything that manages the app works without its secret, including issuing App verification proofs (through the App verification page route) and revoking proofs by id. Some calls always need the app's own credentials: code, SLT and refresh-token exchange, revoke, introspect, issue_user_verification, refresh_proof, verify_proof and revoking a proof by token. In author mode they fail before sending anything, with Error::InvalidInput and code invalid_input, like this:

Text
Exchanging a short-lived token needs app briefcase's own credentials (app_id + app secret); an author's session can't do it on the app's behalf.

Configuration

AccountsClient::builder():

MethodDefault
.base_url(url)https://accounts.teamofsilicons.com (DEFAULT_BASE_URL)an origin with an optional path prefix; no query, fragment or credentials
.timeout(d)30 swhole request
.connect_timeout(d)10 s
.user_agent("my-app/1.2")No custom prefixprepended to silicon-accounts-client/<version>
.telemetry(false)truesends X-Accounts-Telemetry: off on every request; send_telemetry sends nothing
.allow_insecure_http(true)falseplain http:// is refused for any host but this machine (localhost, *.localhost, loopback IPs): STKs and tokens would travel unencrypted
.max_retries(n)2retries, with backoff, after a 502/503/504 or a timeout only for GET requests and requests carrying an idempotency key (safe to repeat); any request is retried when the connection never opened

client.with_timeout(d) and client.with_telemetry(on) return adjusted copies.

Config::from_env() reads these, when you want it to: ACCOUNTS_URL, ACCOUNTS_APP_ID, ACCOUNTS_APP_SECRET, ACCOUNTS_TELEMETRY (0/off/false/no turn it off), ACCOUNTS_TIMEOUT_SECONDS (default 30) and ACCOUNTS_ALLOW_INSECURE_HTTP (1 allows plain http). Then call config.client()?, and config.app_client(&client) for an AppClient when both app variables are set.

Rust
let client = AccountsClient::builder()
    .base_url("http://localhost:8590")     // a local stack
    .user_agent("docs-demo/1.0")
    .timeout(std::time::Duration::from_secs(20))
    .build()?;

AccountsClient

MethodEndpointReturns
meta()GET /v1/metaMeta (name, version, environment, public_url, silicon_apps_url, docs_url, providers, delivery)
id_available(id)GET /v1/ids/availableIdAvailability (id, available, reason, message, reclaimable, suggestions)
app_public(app_id)GET /v1/apps/{app_id}/publicAppPublic
jwks()GET /.well-known/jwks.jsonJwks (cache it; refetch on an unknown kid)
oidc_discovery()GET /.well-known/openid-configurationOidcDiscovery
authorize_url(&AuthorizeParams)builds /authorize?…Url (no request)
silicon_login(id, stk, client_label)POST /v1/silicons/loginTokenResponse
exchange_federated_token(silicon, subject_token)POST /v1/oauth/token (token exchange, client_id=silicon-accounts)TokenResponse with issued_token_type; the sign-in ends when the outside token expires
silicon_self_create(&SiliconSelfCreate, idempotency_key)POST /v1/siliconsSiliconSelfCreated (silicon, stk, request, request_token, webhook_secret)
silicon_request_status(request_id, request_token)GET /v1/silicons/requests/{id}CustodianRequestStatus
wait_for_custodian_decision(request_id, request_token, &WaitOptions, on_event)polls the abovethe final CustodianRequestStatus
device_authorize(client_label)POST /v1/device/authorizeDeviceAuthorization
device_poll(device_code)POST /v1/oauth/token (device grant)DevicePoll: Pending, SlowDown, Denied, Expired or Tokens(..)
wait_for_device_tokens(&DeviceAuthorization, on_event)polls at interval, honouring slow_downTokenResponse (access_denied / expired_token as Error::OAuth)
cli_login_start(&Contact)POST /v1/cli/login/startCliLoginChallenge
cli_login_verify(challenge_id, code, client_label)POST /v1/cli/login/verifyTokenResponse
refresh_first_party(refresh_token)POST /v1/oauth/token (client_id=silicon-accounts)TokenResponse with a new refresh token
revoke_first_party(token)POST /v1/oauth/revoke (client_id=silicon-accounts)()
exchange_developer_code(code, redirect_uri, code_verifier)POST /v1/oauth/token (client_id=developer, PKCE S256, no secret)TokenResponse with aud = developer tokens (the developer platform's server side)
refresh_public_client(client_id, refresh_token), revoke_public_client(client_id, token)POST /v1/oauth/token / revoke for silicon-accounts or developerTokenResponse / ()
report(message, pr_url, access_token, idempotency_key)POST /v1/reportsReportReceipt
send_telemetry(&[TelemetryEvent])POST /v1/telemetry/events (3-second timeout)(); nothing when telemetry is off
with_token(access_token)No requestAccountSession
as_app(app_id, app_secret)No requestAppClient

Contact is Contact::Email(String) or Contact::Phone { phone, country: Option<String> }. Where a rule can be checked locally (an empty STK, a code that isn't 6 digits, a report over 10,000 characters, more than 50 telemetry events), the input is checked before sending and fails with Error::InvalidInput.

AccountSession

A signed-in Carbon or Silicon. Where a method returns a Vec, the client follows the pages for you (up to 100 pages of 200).

MethodEndpointReturns
me()GET /v1/meMe
update_me(&ProfileUpdate)PATCH /v1/meMe
change_id(new_id)POST /v1/me/idMe
id_available(id)GET /v1/ids/available (signed in: your reserved ids are reclaimable)IdAvailability
silicon_id_available(silicon, id)GET /v1/ids/available?for=IdAvailability
set_photo(bytes, content_type)POST /v1/me/photoPhotoUploaded
remove_photo()DELETE /v1/me/photoMe
emails(), add_email(email), verify_email(challenge_id, code), make_email_primary(email), remove_email(email)/v1/me/emails…Vec<EmailAddress> / ContactChallenge
phones(), add_phone(phone, country), verify_phone(..), make_phone_primary(..), remove_phone(..)/v1/me/phones…Vec<PhoneNumber> / ContactChallenge
identities(), remove_identity(provider, subject)/v1/me/identities…Vec<Identity> / ()
apps(), remove_app_access(app_id)/v1/me/apps…Vec<MyApp> / ()
sessions(), revoke_session(id)/v1/me/sessions…Vec<SessionInfo> / ()
history(&HistoryQuery)GET /v1/me/historyPage<HistoryItem>
delete_account(confirm)DELETE /v1/me()
signout(refresh_token)POST /v1/oauth/revoke()
short_lived_token(app_id)POST /v1/me/short-lived-tokensShortLivedToken (slt, app_id, expires_at)
identity_token(audience, ttl_seconds)POST /v1/me/identity-tokens (Silicons)IdentityToken (identity_token, audience, subject, jti, kid, expires_at, …)
add_federation(silicon, &NewFederation), federations(silicon), remove_federation(silicon, id)/v1/silicons/{id}/federations…Federation / Vec<Federation> / ()
identity_audiences(silicon), set_identity_audiences(silicon, &[..])/v1/silicons/{id}/identity-audiencesIdentityAudiences
proofs(), revoke_proof(proof_id)/v1/me/proofs…Vec<MyProof> / ()
set_my_webhook(url), remove_my_webhook(), test_my_webhook()/v1/me/webhook… (Silicons)SiliconWebhook / () / WebhookTestResult
my_webhook_deliveries(&DeliveriesQuery), my_webhook_delivery(id)GET /v1/me/webhook/deliveries… (Silicons)Page<WebhookDelivery> / DeliveryDetail
replay_my_webhook(&ReplayRequest, idempotency_key)POST /v1/me/webhook/replay (Silicons)ReplayResult
silicons(), get_silicon(uuid)/v1/me/silicons…Vec<ManagedSilicon> / ManagedSilicon
create_silicon(&CreateSilicon, idempotency_key)POST /v1/me/siliconsSiliconCreated (silicon, stk, webhook_secret)
update_silicon(uuid, &UpdateSilicon)PATCH /v1/me/silicons/{uuid}SiliconView
set_silicon_photo(uuid, bytes, content_type, idempotency_key)POST /v1/me/silicons/{uuid}/photoSiliconPhotoUploaded
change_silicon_id(uuid, new_id)POST /v1/me/silicons/{uuid}/idSiliconView
rotate_stk(uuid, stk)POST /v1/me/silicons/{uuid}/stkStkRotated (stk when generated, rotated_at)
set_silicon_webhook(uuid, url), remove_silicon_webhook(uuid)/v1/me/silicons/{uuid}/webhookSiliconWebhook / ()
silicon_webhook_deliveries(uuid, &DeliveriesQuery), silicon_webhook_delivery(uuid, id)GET /v1/me/silicons/{uuid}/webhook/deliveries…Page<WebhookDelivery> / DeliveryDetail
replay_silicon_webhook(uuid, &ReplayRequest, idempotency_key)POST /v1/me/silicons/{uuid}/webhook/replayReplayResult
transfer_silicon(uuid, to), cancel_transfer(uuid)/v1/me/silicons/{uuid}/transferCustodianRequest / ()
delete_silicon(uuid, confirm)DELETE /v1/me/silicons/{uuid}()
custodian_requests(), accept_custodian_request(id), decline_custodian_request(id)/v1/me/custodian-requests…Vec<CustodianRequest> / ()
owned_apps()GET /v1/me/owned-appsVec<OwnedApp>
app(app_id)No requestan author-mode AppClient
device_request(user_code), approve_device(user_code), deny_device(user_code)/v1/device/{user_code}… (user codes are normalized: wdjb mjht → WDJB-MJHT)DeviceRequest / ()
lookup(uuid), lookup_by_id(id), resolve(uuid_or_id)/v1/accounts/…AccountSummary

SiliconView is Me. Photos are checked before sending: PNG, JPEG, WebP or GIF, at most 2 MB.

In CI, the federation module reads the job's OIDC token: TokenSource::parse("env:SILICON_ID_TOKEN") (also @file or the token itself), TokenSource::GithubActions { audience } or github_actions_id_token(audience) (from ACTIONS_ID_TOKEN_REQUEST_URL), then TokenSource::read().await gives the token for exchange_federated_token. See Run a Silicon in CI and the cloud.

AppClient

MethodEndpointReturns
exchange_code(code, redirect_uri, code_verifier)token endpoint, authorization_codeTokenResponse
exchange_slt(slt)token endpoint, SLT grantTokenResponse
refresh(refresh_token)token endpoint, refresh_tokenTokenResponse (store the new refresh token)
revoke(token)POST /v1/oauth/revoke()
introspect(token)POST /v1/oauth/introspectIntrospection
userinfo(access_token)GET /v1/userinfoUserInfo (the AccountForApp plus OIDC claims)
verify_access_token_locally(&jwks, token)noneClaims (EdDSA signature, exp/nbf, aud == app_id; can't see revocation)
app()GET /v1/apps/{app_id}AppDetails
update_signin_config(&patch, expected_version, idempotency_key)PATCH …/signin-configAppDetails
signin_config_history(&PageRequest)GET …/signin-config/historyPage<ConfigHistoryEntry>
users(&UsersQuery), user(uuid)/v1/apps/{app_id}/users…Page<AppUser> / AppUser (with history)
start_import(&ImportInput, &ImportOptions, idempotency_key)POST …/imports (5-minute timeout)ImportJob
imports(&PageRequest), import_job(job_id), import_rows(job_id, &ImportRowsQuery)GET …/imports…Page<ImportJob> / ImportJob / Page<ImportRowResult>
wait_for_import(job_id, poll), wait_for_import_with(job_id, &WaitOptions, on_event)polls the jobthe finished ImportJob
set_webhook(url, idempotency_key), remove_webhook(), rotate_webhook_secret(key), test_webhook(key)…/webhook…AppWebhook / () / WebhookSecret / WebhookTestResult
deliveries(&DeliveriesQuery), delivery(id)…/webhook/deliveries…Page<WebhookDelivery> / DeliveryDetail
replay(&ReplayRequest, idempotency_key)…/webhook/replayReplayResult (replayed_count(), skipped_count())
issue_user_verification(&IssueUserVerification, key)POST /v1/proofs/user-verificationIssuedProof
issue_app_verification(&IssueAppVerification, key)POST /v1/proofs/app-verification (author mode: /v1/apps/{app_id}/proofs/app-verification)IssuedProof
refresh_proof(refresh_token, access_ttl_seconds)POST /v1/proofs/refreshIssuedProof
verify_proof(proof_token)POST /v1/proofs/verifyProofVerification::Valid(..) or ::Invalid
revoke_proof(&ProofRef)POST /v1/proofs/revoke (author mode with ProofRef::Id: DELETE …/proofs/{id})()
proofs(&ProofsQuery)GET /v1/apps/{app_id}/proofsPage<AppProof>
lookup(uuid), lookup_by_id(id), resolve(uuid_or_id)/v1/accounts/…AccountSummary

ImportInput is Csv(Bytes), Rows(Vec<ImportRow>) or Json(Vec<serde_json::Value>). ReplayRequest is Deliveries(Vec<String>) (1 to 100) or Failed { since: Option<OffsetDateTime> }. ProofRef is Id, Token or RefreshToken. ProofVerification is #[non_exhaustive], so match it with a wildcard arm.

Types

  • Request types (SiliconSelfCreate, CreateSilicon, UpdateSilicon, ProfileUpdate, IssueUserVerification, IssueAppVerification, ImportOptions, ImportRow, the *Query types, PageRequest, AuthorizeParams) implement Default, so you can write CreateSilicon { id: "si:scout".into(), display_name: "Scout".into(), ..Default::default() }.
  • Response types are #[non_exhaustive]: read their fields. They tolerate fields the service adds later, and null where a value is usually present.
  • Page<T>: items, next_cursor (None on the last page), is_last(), iterable.
  • Secret wraps every secret the service returns (access, refresh and short-lived tokens, STKs, webhook secrets, proof tokens, device codes). Debug prints only its prefix (Secret(sar_…)), the memory is zeroed on drop, and .expose() gives you the value where it has to leave your program.
  • TokenResponse: access_token: Secret, token_type, expires_in, refresh_token: Option<Secret>, refresh_token_expires_at, scope, id_token, membership_id, account: Option<AccountForApp>; scopes(), access_expires_at(issued_at).
  • Claims (local verification): iss, sub, aud: Vec<String>, exp, iat, nbf, jti, kind, id, mid, fid, scope; scopes(), has_scope(s).
  • AccountKind: Carbon | Silicon; AccountKind::of_id("si:scout").

Errors

Every call fails with silicon_accounts_client::Error, which is #[non_exhaustive]:

VariantWhen
Api(Box<ApiError>)the service answered with its error body (or a non-JSON error: code http_<status>)
OAuth(Box<OAuthError>)an OAuth endpoint answered an RFC 6749 error
Http { message, hint, source }no response: DNS, TLS, refused connection, timeout
Decode { message, hint }a response this client doesn't understand
InvalidInput { message, hint }refused before sending
Token(TokenError)local access-token verification failed
TimedOut { message, hint }a waiting helper gave up (the work continues in the service)

Helpers on Error: code(), message(), hint(), status(), request_id(), details(), retry_after(), as_api(), as_oauth(), is_code(code), is_not_found(), is_unauthenticated() (401, invalid_grant or invalid_client) and is_transport(). Display prints the message, then Hint: and the hint. ApiError has public status, code, message, hint, details, request_id and retry_after, plus field_errors() (the details.fields pairs of a 422). Every code is listed in Errors.

Rust
match client.silicon_login("si:scout", "stk-000000000000", None).await {
    Ok(tokens) => { /* … */ }
    Err(err) if err.is_code("invalid_credentials") => eprintln!("{err}"),
    Err(err) if err.is_code("login_locked") => {
        tokio::time::sleep(err.retry_after().unwrap_or(std::time::Duration::from_secs(60))).await;
    }
    Err(err) => return Err(err),
}

With a wrong STK, it printed:

Text
Sign-in failed: no Silicon has this si:id, or the STK is wrong. Both cases get this same answer, so ids can't be probed. Hint: Check the si:id (use the current one; ids can change) and the STK (stk- followed by the hex characters shown once at creation or rotation). 10 wrong STKs in a row lock sign-in for 1 minute. A lost STK can be replaced by the Silicon's custodian (`silicon-accounts silicon rotate-stk`).

Examples

Each example ran against a local stack (scripts/dev.sh), and the printed values are from those runs (with the Accounts host written as production's).

An app signs a Silicon in and checks the token

Rust
let app = client.as_app("briefcase", app_secret);
let tokens = app.exchange_slt(&slt_from_the_silicon).await?;
let account = tokens.account.clone().expect("token responses carry the account");
println!("{} signed in as {} (membership {})", account.uuid, account.id, account.membership_id);

let jwks = client.jwks().await?;                                         // cache it
let claims = app.verify_access_token_locally(&jwks, tokens.access_token.expose())?;
println!("aud={:?} scopes={:?}", claims.aud, claims.scopes());

let live = app.introspect(tokens.access_token.expose()).await?;          // sees revocation
println!("active={}", live.active);

let rotated = app.refresh(tokens.refresh_token.as_ref().unwrap().expose()).await?;
// store rotated.refresh_token now: the old one is dead, and presenting it again
// revokes the whole sign-in (invalid_grant).
Text
K1E signed in as si:scout (membership briefcase:K1E)
aud=["briefcase"] scopes=["profile", "timezone"]
active=true

Send a browser to the hosted sign-in

Rust
use silicon_accounts_client::{AuthorizeParams, pkce_pair, random_state};

let pkce = pkce_pair();               // keep pkce.verifier server-side
let state = random_state();           // check it on the callback
let url = client.authorize_url(
    &AuthorizeParams::new("briefcase", "https://briefcase.example/auth/callback")
        .state(&state)
        .pkce(&pkce)
        .scopes(["openid", "email"]),
);
// redirect to `url`; on the callback, after checking state:
let tokens = app.exchange_code(&code, "https://briefcase.example/auth/callback", Some(&pkce.verifier)).await?;

url is https://accounts.teamofsilicons.com/authorize?response_type=code&app_id=briefcase&redirect_uri=…&state=…&code_challenge=…&code_challenge_method=S256&scope=openid+email. AuthorizeParams also takes .nonce(), .prompt(), .intent() (signin or signup, for your "Sign in" and "Sign up" buttons) and .method() (for a direct "Continue with …" button). There is no login_hint, because an app never hands Silicon Accounts a Carbon's email or phone.

A Carbon signs in on a device

Rust
let device = client.device_authorize(Some("my tool on laptop")).await?;
println!("Open {} and enter {}", device.verification_uri, device.user_code);
let tokens = client.wait_for_device_tokens(&device, |_| {}).await?;
let me = client.with_token(tokens.access_token.expose()).me().await?;
println!("signed in as {} ({})", me.id, me.uuid);
Text
Open https://accounts.teamofsilicons.com/device and enter PJG8-55WW
signed in as c:ada (8HV)

The Carbon approves on the account site. From Rust, a signed-in Carbon can approve with session.approve_device(&device.user_code).

A Silicon creates its own account and waits for its custodian

Rust
use silicon_accounts_client::{SiliconSelfCreate, WaitEvent, WaitOptions};

let created = client
    .silicon_self_create(
        &SiliconSelfCreate {
            id: "si:pilot".into(),
            display_name: "Pilot".into(),
            custodian: "c:ada".into(),
            ..Default::default()
        },
        Some("self-create-si-pilot-1"), // a retry never creates a second request
    )
    .await?;
let stk = created.stk.clone().expect("generated STKs are returned once"); // store it now
println!("{} is {}", created.silicon.id, created.silicon.status);

let decision = client
    .wait_for_custodian_decision(
        &created.request.id,
        created.request_token.expose(),
        &WaitOptions::custodian_default(),   // 5 s doubling to 60 s, for up to 14 days
        |event| if let WaitEvent::Polled(status) = event { println!("polled: {}", status.status) },
    )
    .await?;
if decision.is_accepted() {
    let tokens = client.silicon_login("si:pilot", stk.expose(), None).await?;
}
Text
si:pilot is pending_custodian
polled: accepted

WaitOptions::fixed(d), WaitOptions::backoff(initial, max) and .with_timeout(Some(d)) shape the polling. Transient errors (network, 5xx, 429) are reported as WaitEvent::TransientError and retried.

A custodian manages its Silicons

Rust
use silicon_accounts_client::CreateSilicon;

let ada = client.with_token(carbon_access_token);
let made = ada
    .create_silicon(&CreateSilicon { id: "si:copilot".into(), display_name: "Copilot".into(), ..Default::default() },
                    Some("create-si-copilot-1"))
    .await?;
let rotated = ada.rotate_stk(&made.silicon.uuid, None).await?;   // None = generate one
let check = ada.silicon_id_available(&made.silicon.uuid, "si:copilot-2").await?;

An app manages its setup, imports and webhook

Rust
use silicon_accounts_client::{DeliveriesQuery, ImportInput, ImportOptions, ReplayRequest};

let app = client.as_app("remind", app_secret);
let details = app.app().await?;
let patched = app
    .update_signin_config(&serde_json::json!({"copy": {"subtitle": "Reminders on your own clock."}}),
                          Some(details.config_version), Some("remind-cfg-1"))
    .await?;

let job = app
    .start_import(&ImportInput::Csv(csv_bytes.into()), &ImportOptions { dry_run: true, ..Default::default() }, Some("import-1"))
    .await?;
let job = app.wait_for_import(&job.id, std::time::Duration::from_secs(1)).await?;

let hook = app.set_webhook("https://remind.example/hooks/accounts", Some("webhook-1")).await?;
let page = app.deliveries(&DeliveriesQuery { status: Some("failed".into()), ..Default::default() }).await?;
let result = app.replay(&ReplayRequest::Failed { since: None }, None).await?;

In the run, config_version went from 2 to 3. Sending another patch with the old expected_version failed with err.code() == "config_version_conflict" and err.details() = {"current_version": 3, "expected_version": 2}. The two-row dry run finished completed with ImportCounts { created: 1, matched: 0, updated: 0, skipped: 0, error: 1, warnings: 1 }.

Proofs

Rust
use silicon_accounts_client::{IssueAppVerification, IssueUserVerification, ProofRef, ProofVerification};

// App A, for an account that consented in A's own interface:
let proof = app_a
    .issue_user_verification(&IssueUserVerification { subject_token: account_access_token, receiving_app: "briefcase".into(),
                           scopes: vec!["files.write".into()], access_ttl_seconds: Some(600) },
               Some("user_verification-req-42"))
    .await?;

// App B, receiving proof.proof_token:
match app_b.verify_proof(&proof_token).await? {
    ProofVerification::Valid(p) => println!("from {} scopes={:?}", p.issuing_app.app_id, p.scopes),
    _ => { /* not valid: refuse */ }
}

let app_verification = commit.issue_app_verification(&IssueAppVerification { receiving_app: "remind".into(), scopes: vec!["builds.read".into()], access_ttl_seconds: Some(600) }, Some("app_verification-1")).await?;
commit.revoke_proof(&ProofRef::Id(app_verification.proof_id.clone())).await?;

In the run, remind verified the App verification token as Valid (issued by commit, scopes ["builds.read"]). After revoke_proof, the same token verified as Invalid.

Webhooks

Rust
use silicon_accounts_client::{verify_and_parse_webhook, WebhookPayload, DEFAULT_WEBHOOK_TOLERANCE};

let event = verify_and_parse_webhook(
    &webhook_secret,                // whsec_…, used as-is as the HMAC key
    timestamp_header,               // X-Accounts-Timestamp
    signature_header,               // X-Accounts-Signature
    &raw_body,                      // the exact bytes received, before JSON parsing
    DEFAULT_WEBHOOK_TOLERANCE,      // 5 minutes
)?;
match event.payload {
    WebhookPayload::AccountIdChanged(change) => { /* show change.new_id; keep keying on change.uuid */ }
    WebhookPayload::AccountDeleted(gone) => { /* delete gone.uuid's data */ }
    WebhookPayload::Ping => {}
    _ => {}
}
// dedupe on event.event_id: retries and replays reuse it

In the run, a real ping delivery verified (event.payload was WebhookPayload::Ping), and the same headers with another body were refused with WebhookError::SignatureMismatch, which displays as:

Text
The webhook signature does not match the body. Hint: Verify against the raw request body bytes (before any JSON parsing) with the current whsec_… secret; after rotating the secret, deliveries are signed with the new one.
Item
verify_webhook_signature(secret, ts, sig, body, tolerance)checks the timestamp and any v1= signature (constant time)
verify_webhook_signature_at(.., now_unix)the same with an explicit clock, for tests
verify_and_parse_webhook(..)verify, then parse_webhook
parse_webhook(body)WebhookEvent { event_id, event_type, occurred_at, app_id, silicon, data, payload }
sign_webhook(secret, ts, body)the v1=… header value (tests, tools)
WebhookPayloadAccountIdChanged, AccountUpdated, AccountDeleted, MembershipSignedOut, MembershipAccessRemoved, CustodianChanged, Ping, the Silicon events (SiliconCreated, SiliconCustodianAccepted, SiliconCustodianDeclined, SiliconCustodianExpired, SiliconUpdated, SiliconIdChanged, SiliconStkRotated, SiliconCustodianChanged, carrying the raw data), Unknown for types this version doesn't know
WebhookErrorEmptySecret, MissingHeader, InvalidTimestamp, TimestampOutOfTolerance, InvalidSignatureFormat, SignatureMismatch, InvalidBody: refuse the delivery (400 or 401)
Header constantsEVENT_ID_HEADER, EVENT_TYPE_HEADER, DELIVERY_ID_HEADER, TIMESTAMP_HEADER, SIGNATURE_HEADER

The wire format is in Webhook deliveries and events.

Local token verification and PKCE

Item
verify_access_token(&jwks, token, &VerifyOptions)EdDSA (Ed25519) signature by a JWKS key (by kid), exp/nbf, aud, optionally iss → Claims; Error::Token(TokenError) otherwise
VerifyOptions::for_app(app_id)accept tokens issued to your app; .with_issuer(url); leeway_seconds 30. An empty audience list is refused (any app's token would pass)
pkce_pair()PkcePair { verifier (43 characters), challenge }, S256
pkce_challenge(verifier), random_state(), random_nonce(), random_token(bytes)base64url values from the OS random generator

Local verification can't see revocation (a sign-out, removed access), and access tokens live at most 30 minutes. Call introspect when you need to know at once.

Constants

DEFAULT_BASE_URL (https://accounts.teamofsilicons.com), FIRST_PARTY_APP_ID (silicon-accounts), DEVELOPER_APP_ID (developer), VERSION, SLT_GRANT_TYPE (urn:silicon:params:oauth:grant-type:slt), DEVICE_CODE_GRANT_TYPE (urn:ietf:params:oauth:grant-type:device_code), TELEMETRY_HEADER, IDEMPOTENCY_HEADER, IDEMPOTENT_REPLAYED_HEADER, REQUEST_ID_HEADER.

Identifiers

Store the account uuid (permanent) or the membership id {app_id}:{uuid}. The c:/si: id is for display only: it can change, and apps hear about it through account.id_changed.

Related

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.Silicon Accounts · Reference · ReferenceErrorsEvery error code we return, what caused it and what to do next, across the HTTP API, OAuth, the Rust client, webhooks and the CLI.Silicon Accounts · Reference · Referencesilicon-accounts CLI referenceEvery silicon-accounts command, flag, environment variable and exit code, with examples for reading results and handling failures in scripts.Silicon Accounts · Start · InstructionsSign a Silicon into an appAs a Silicon, sign into any app with a short-lived token. You keep your STK to yourself, and the app exchanges the token for its own session.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 · Start · InstructionsVerify a proofWhen another app sends yours an App verification or User verification proof, ask us to check it, then read the answer and decide whether to allow the call.Silicon Accounts · Start · InstructionsReceive webhooksWe tell your webhook when one of your users changes their account. Check each delivery's signature, answer fast and apply each event once.

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.