Silicon IAMDocs

silicon-iam-client

Batch login authenticates once: select organizations per app, and receive up to 100 independent SLTs. See the batch login guide for browser, API, Rust and CLI examples.

A Rust client for the Silicon IAM API: identity, organization governance, application login, and delegated access.

The public integration surfaces have typed methods here, using the contract's own shapes. The wire types are generated from docs/openapi.yaml, and changes to that contract produce a reviewable source diff. Platform administration, provider callbacks, and browser navigations remain outside this crate.

[dependencies]
silicon-iam-client = "1.7.0"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }

The client speaks HTTP API major v1 and requires Rust 1.98 or newer. The crate SemVer and HTTP API major are separate: upgrading the crate within the 1.x line does not select a different wire major. Client::new and ClientBuilder::build perform no network handshake; call client.system().negotiate().await? during startup when you want an upfront compatibility check. It validates the service identity, ordered version catalog, highest shared selection, selected-version header/body agreement, and the required Vary header. A 406 becomes Error::ApiVersionUnsupported; an inconsistent success becomes Error::Decode. Every request advertises v1.

This guide describes the current official API contract. Use the matching client, CLI, and backend release together. The complete hosted manual is at docs.iam.teamofsilicons.com/client.

Applications declare IAM and external endpoint permissions separately from webhook event subscriptions. Direct IAM login reviews those permissions before organization selection. The SDK exposes scope review discussions, application bundles, cross-organization OBO, and application-initiated test environments with transitive dependency provisioning.

The service URL must use HTTPS, except for literal localhost, 127.0.0.1, or ::1 during local development. The builder rejects missing hosts, embedded credentials, port zero, queries, and fragments. Requests do not follow redirects, so IAM authority stays on the configured endpoint, and every response body is capped at 4 MiB. An oversized response becomes Error::ResponseTooLarge.

Runtime state stays with your application

Client does not store sessions, cache API responses, or refresh credentials behind your back. An expired token produces an error; deciding what to do about it stays with you, because only your program knows where its credentials live.

If you want the stateful version — a token store, automatic refresh, a configured default service — that is the silicon-iam-cli crate, which is built on nothing but this one.

Automatic crate updates

Automatic dependency maintenance is on by default. After an IAM request finishes and its response has been decoded, the client compares its compiled version with the newest stable silicon-iam-client release on crates.io if no attempt is recorded or the last attempt was at least one hour ago. There is no idle timer or daemon: another completed request triggers the next due check. When it finds a newer release and can locate a Cargo.toml from the process working directory, it runs the equivalent of:

cargo update --manifest-path /path/to/Cargo.toml \
  -p silicon-iam-client --precise <latest-version>

That advances the consuming project's Cargo.lock. Rust cannot replace a library already compiled into a running process, so the current process finishes on its existing version and the next Cargo build uses the update. Registry or Cargo failures are best-effort and never fail the IAM request; inspect client.update_status() after a request to see whether the client was current, updated, skipped, or could not update at its latest check.

Clones share an in-memory last-attempt time and only one check can run at a time; concurrent requests do not wait for another request's check. The request that performs due maintenance waits for it before returning, but maintenance never changes its IAM result. Failed or cancelled attempts retain the hourly throttle. Cancellation releases the single-flight slot; a Cargo operation already started keeps that slot until it finishes on a blocking worker. Separately built clients have independent schedules, and restarting the process resets this in-memory schedule. No maintenance runs while the client is idle, and compiled code changes only after rebuilding.

Disable every check and Cargo invocation in code:

let client = Client::builder("https://backend.iam.teamofsilicons.com")?
    .auto_update(false)
    .build()?;

Or opt out for a deployed process without recompiling it:

SILICON_IAM_CLIENT_AUTO_UPDATE=false ./your-application

If the application starts outside its project directory, point the updater at the correct manifest with .update_manifest("/path/to/Cargo.toml"). If there is no manifest, the client reports UpdateStatus::NoCargoProject and leaves the installed application alone.

Your first Application login

use silicon_iam_client::{Client, Credential, Mutation};

#[tokio::main]
async fn main() -> silicon_iam_client::Result<()> {
    let app_id = "acme>checkout";
    let application = Client::new("https://backend.iam.teamofsilicons.com")?
        .with_credential(Credential::application(app_id, "ask_your_application_secret"));

    // Receive this only from the IAM-hosted login redirect or token screen.
    let slt = "slt_from_iam";
    let tokens = application
        .oauth()
        .login(app_id, slt, &Mutation::new())
        .await?;
    println!("Application session expires in {} seconds", tokens.expires_in);
    Ok(())
}

An Application never starts or verifies an OTP challenge. IAM performs that identity ceremony on its own hosted login surface; the Rust client accepts only the resulting single-use SLT for a new Application login.

What the API groups look like

Every group hangs off the client and borrows it, so obtaining one is free:

Group Covers
client.system() Version negotiation, liveness, readiness
client.signup() Creating a Carbon
client.auth() IAM refresh/logout, step-up, Silicon authentication, and SLT minting; direct Carbon login is CLI-feature-only
client.carbons() The signed-in Carbon; sessions; Carbon lookup
client.organizations() Organization tenancy and ownership
client.members() Members and the directory view of them
client.invitations() Inviting Carbons, and joining
client.tags() Organization tags
client.trust() Advisory trust: default, rules, evaluation
client.governance() Approvals, direct role and tag changes, history
client.silicons() Silicons, credentials, webhooks
client.applications() Applications, secrets, webhooks
client.oauth() Short-lived-token exchange, introspection, revocation
client.obo() Catalog-bound signing and delegated access between applications
client.sso() An organization's SSO configuration
client.environments() Testing environments

Platform administration, the inbound provider webhooks and the browser login screen are deliberately absent: they belong to the operator, to the provider, and to the browser.

Idempotency is explicit

Every mutating route requires an idempotency key, so mutations take a Mutation that carries one. The service binds the key to the caller, the route and the exact body, then replays the original response for a repeat of the same request — which only helps if a retry presents the same key:

# use silicon_iam_client::{Client, Mutation, models};
# async fn run(client: &Client, input: &models::OrganizationCreate) -> silicon_iam_client::Result<()> {
let creating = Mutation::new();

let organization = match client.organizations().create(input, &creating).await {
    Ok(organization) => organization,
    // The same `creating` replays the first outcome instead of creating a
    // second organization.
    Err(error) if error.is_retryable() => {
        client.organizations().create(input, &creating).await?
    }
    Err(error) => return Err(error),
};
# let _ = organization;
# Ok(())
# }

Routes that change an existing resource take its version as an ordinary argument, so optimistic concurrency cannot be forgotten. Routes that change authority or reveal a credential also need a step-up assertion, attached with Mutation::step_up.

JSON Merge Patch deliberately distinguishes three states. In generated patch models, a field typed Option<Option<T>> uses None to omit the field and leave it unchanged, Some(None) to send JSON null and clear it, and Some(Some(value)) to replace it. Do not collapse the outer and inner options.

Signing users in to an application

A login produces one short-lived token, and that token is the only thing your application ever receives — never a password, never a verification code.

Send someone to <auth_base_url>/login?app_id=…&redirect_uri=…; they come back to your callback with ?slt=…; you trade it for a session:

# use silicon_iam_client::{Client, Credential, Mutation};
# async fn run(base: &str, app_id: &str, app_secret: &str, slt: String)
#     -> silicon_iam_client::Result<()> {
let application = Client::new(base)?
    .with_credential(Credential::application(app_id, app_secret));

let tokens = application
    .oauth()
    .login(app_id, &slt, &Mutation::new())
    .await?;
# let _ = tokens;
# Ok(())
# }

The token lives two minutes and is good for one exchange. OAuth::login has no OTP or refresh-token input: the SLT is the only credential that can begin an Application session. Renew an existing session separately with OAuth::refresh(app_id, refresh_token, mutation).

For a direct IAM Carbon/Silicon session, call auth().login_organizations(app_id) and display its scopes before selecting organizations. After the user approves, call auth().short_lived_token_for_organizations(app_id, &selected_org_ids, choices.scope_version, &approved_scopes, &Mutation::new()). Supply the exact reviewed flat scope strings, including obo:{app_id}:{endpoint_id} external permissions. The selected organization set is additive on the same parent login. For a fully specified request, short_lived_token(&ShortLivedTokenRequest, mutation) also supports a callback. An application change invalidates stale consent; fetch a new view and obtain consent again. Applications never call these methods with their own credentials; initiate IAM browser login and receive an SLT instead.

See organization consent for scope consent and CLI details.

For OBO, hash the exact downstream bytes with api::obo::body_sha256, build one OboExchangeRequest, and pass that request, the discovered audience catalog, and one Mutation to obo().exchange_signed(...). The client selects the registered path, uses the same Application secret for Basic authentication and HMAC, and signs a fresh timestamp. An immediate uncertain retry reuses the request and Mutation but gets a new timestamp/signature; restoring an old timestamp can fall outside the 60-second signature window.

Refresh, introspection, revocation, and logout

Refresh one Application session with the same Application Basic client. Keep one refresh in flight per family and persist the Mutation key before sending:

# use silicon_iam_client::{Client, Mutation};
# async fn refresh(application: &Client, app_id: &str, old: &str) -> silicon_iam_client::Result<()> {
let refreshing = Mutation::new();
let key_to_persist = refreshing.key().as_str().to_owned();
let replacement = application.oauth().refresh(app_id, old, &refreshing).await?;
// Atomically replace `old` with `replacement`; retain `key_to_persist`
// until the operation is committed.
# let _ = (replacement, key_to_persist);
# Ok(())
# }

Reuse of a consumed refresh token under a new key revokes that Application refresh family and its related access authority. It does not revoke the parent IAM session, other devices, or unrelated Applications. Recover an uncertain request with Mutation::with_key(IdempotencyKey::parse(saved_key)?) and the exact same input. The client does not expose the Idempotency-Replayed response header.

Tokens are opaque. Ask for their current state and optional exact organization context with oauth().introspect(&TokenIntrospectionRequest, Some("acme")). A well-formed organization mismatch, unknown token, expiry, or revocation returns active: false; malformed organization context is an API error. Revoke with oauth().revoke(&OAuthRevocationRequest, &Mutation): an access token revokes only itself, while a refresh token revokes its family. Unknown tokens deliberately succeed.

Application-triggered global logout is a different operation. Build a bearer client from the Carbon Application access token and call auth().logout(&LogoutRequest { mode: None }, &Mutation). If the token's client and audience are that Application, IAM revokes the parent IAM session and all authority bound to it. This form cannot request account-wide all_sessions.

Applications, discovery, and secret rotation

Application creation takes a local handle and an owning organization. IAM returns the canonical public identifier {org_id}>{handle}; use that canonical value for every later login, credential, path, discovery, and OBO call. The required base_url is the pathless application-backend origin without a trailing slash, such as https://billing.example. It is not a login redirect and IAM does not call it automatically.

# use silicon_iam_client::{Client, Mutation, models};
# async fn create(client: &Client) -> silicon_iam_client::Result<()> {
let created = client.applications().create(
    &models::ApplicationCreate {
        app_id: "billing".to_owned(),
        org_id: "acme".to_owned(),
        app_name: Some("Billing".to_owned()),
        app_logo: None,
        webhook_url: "https://billing.example/hooks/iam".to_owned(),
        webhook_secret: "replace-with-at-least-32-random-characters".to_owned(),
        base_url: "https://billing.example".to_owned(),
        obo_endpoints: None,
    },
    &Mutation::new(),
).await?;
// Persist created.app_secret; IAM generated no webhook secret.
# Ok(())
# }

An Application authenticating with Credential::application can discover any verified Application's base URL, even across organizations:

# use silicon_iam_client::{Client, Credential};
# async fn discover(base: &str, caller_secret: &str) -> silicon_iam_client::Result<()> {
let caller = Client::new(base)?.with_credential(Credential::application(
    "acme>checkout",
    caller_secret,
));
let billing = caller
    .applications()
    .discover_base_url("other>billing")
    .await?;
println!("{}", billing.base_url);
# Ok(())
# }

Client and webhook signing credentials rotate independently. Both operations take the current Application version, an idempotency key, and a verified-channel step-up assertion. The Application supplies its own webhook successor during explicit webhook rotation:

# use silicon_iam_client::{Client, Mutation, models};
# async fn rotate(client: &Client, step_up: &str) -> silicon_iam_client::Result<()> {
let app = client.applications().get("acme>checkout").await?;
let mutation = Mutation::new().step_up(step_up);
let rotated = client
    .applications()
    .rotate_webhook_secret(
        &app.app_id,
        app.version,
        &models::ApplicationWebhookSecretRotate {
            webhook_secret: "replace-with-32-or-more-random-characters".to_owned(),
        },
        &mutation,
    )
    .await?;
assert_eq!(rotated.webhook_secret_version, app.webhook.secret_version + 1);
# Ok(())
# }

The webhook rotation assertion uses action application.webhook_secret.rotate; client-secret rotation uses application.client_secret.rotate. A webhook URL change is a separate operation and reuses a test-owned or production signing secret unless its request explicitly supplies a new one. A testing URL replacement installs the supplied secret or generates a fresh test-only secret and returns it as webhook_signing_secret.

Approving a pending webhook

applications().approve_webhook(app_id, version, &mutation) activates a verified Application's pending first or replacement endpoint. The authenticated Carbon must currently be the owning organization's owner or admin, or an IAM platform administrator with applications.review. Creating the Application confers no separate authority. This operation changes neither Application status nor scopes, and cannot bypass platform review of an Application whose status is still under_review.

Read applications().webhook(app_id) to obtain application_id, the internal UUID available to both organization managers and platform webhook reviewers. Obtain verified-channel step-up for models::StepUpAction::ApplicationWebhookApprove with that UUID as the resource. Use the assertion and current aggregate version:

# use silicon_iam_client::{Client, Mutation};
# async fn approve(client: &Client, step_up: &str) -> silicon_iam_client::Result<()> {
let app_id = "acme>checkout";
let current = client.applications().webhook(app_id).await?;
let mutation = Mutation::new().step_up(step_up);
let webhook = client.applications()
    .approve_webhook(app_id, current.version, &mutation)
    .await?;
assert!(webhook.active_url.is_some());
# Ok(())
# }

The SDK sends an empty JSON object and returns models::ApplicationWebhook with the new Application aggregate version; no signing secret is returned. Keep the same mutation key and input after an uncertain outcome. A stale version fails its precondition; no pending endpoint or a non-verified Application is a conflict. Test endpoints normally activate immediately, so they do not need approval.

Organization listing and SSO

organizations().list(&paging) returns organizations where the caller has an active membership. Use list_with_status(Some("removed"), &paging) for the caller's removed memberships. That query value describes membership status; the status on each returned Organization still describes the organization itself (active or disabled).

SSO starts locked until a platform administrator grants the organization an entitlement. A Carbon owner or admin with sso.manage can inspect it, obtain a five-minute WorkOS setup link, and test the active connection:

# use silicon_iam_client::{Client, Mutation};
# async fn sso(client: &Client) -> silicon_iam_client::Result<()> {
let configuration = client.sso().get("acme").await?;
let setup = client.sso().setup_link("acme", &Mutation::new()).await?;
let tested = client.sso().test("acme", &Mutation::new()).await?;
# let _ = (configuration, setup, tested);
# Ok(())
# }

Disabling SSO additionally needs the current configuration version and a verified-channel step-up assertion for organization.sso_change bound to the organization's internal UUID:

# use silicon_iam_client::{Client, Mutation};
# async fn disable(client: &Client, version: i64, step_up: &str) -> silicon_iam_client::Result<()> {
client.sso().disable(
    "acme",
    version,
    &Mutation::new().step_up(step_up),
).await?;
# Ok(())
# }

The browser authorization and callback redirects are intentionally not client methods. SSO never creates a Carbon: the person signs up normally first, then begins SSO while authenticated in the same bound browser session.

Testing environments

An environment is the same API against a separate database, starting empty. The lifecycle is controlled from production. A successful creation returns the public UUID and the 32-character root key:

# use silicon_iam_client::{Client, Mutation, models};
# async fn create(production: &Client) -> silicon_iam_client::Result<()> {
let created = production.environments().create(
    "acme",
    &models::TestingEnvironmentCreate {
        name: "checkout-e2e".to_owned(),
        description: Some("CI proof run".to_owned()),
    },
    &Mutation::new(),
).await?;

// Store created.id as the safe selector and created.key in a secret store.
# let _ = created;
# Ok(())
# }

Move a client onto that environment and every ordinary method uses the same route against isolated test data:

# use silicon_iam_client::{Client, EnvironmentKey};
# async fn run(client: &Client, key: &str) -> silicon_iam_client::Result<()> {
let sandbox = client.with_environment(EnvironmentKey::new(key)?);
let organizations = sandbox.organizations().list(&Default::default()).await?;
# let _ = organizations;
# Ok(())
# }

The root key selects the database plane; it does not replace endpoint authentication. A protected call still needs the bearer or Application Basic credential issued inside that environment. Email and SMS delivery are suppressed, and signup, login, invitation and step-up verification accept the fixed code 000000.

Credentials do not cross the boundary in either direction: production access and refresh tokens, short-lived tokens, STKs, Application secrets, sessions, and OBO proofs are refused in a test environment, and test credentials are refused in production. IAM does not currently expose a caller API-key credential; a future API-key surface must retain this same plane binding. Keep one credential store per environment or key it by the environment UUID.

Create or import a test Application

Creating a new test Application uses the ordinary method on the plane-selected client:

# use silicon_iam_client::{Client, Mutation, models};
# async fn create_app(sandbox: &Client) -> silicon_iam_client::Result<()> {
let created = sandbox.applications().create(
    &models::ApplicationCreate {
        app_id: "checkout".to_owned(),
        org_id: "acme".to_owned(),
        app_name: Some("Checkout".to_owned()),
        app_logo: None,
        webhook_url: "https://hooks.example.test/iam".to_owned(),
        webhook_secret: "test-webhook-secret-with-32-characters".to_owned(),
        base_url: "https://checkout.example".to_owned(),
        obo_endpoints: None,
    },
    &Mutation::new(),
).await?;
assert_eq!(created.application.app_id, "acme>checkout");
# Ok(())
# }

The local handle is qualified with the owning organization. A newly created test application cannot claim a canonical ID that already exists in production.

Import copies a production Application into the selected environment. It can also create the corresponding test organization and make the authenticated test Carbon its owner. The response returns a fresh test-only Application secret, but only confirms that the production webhook secret was inherited; it never reveals that secret:

# use silicon_iam_client::{Client, Mutation};
# async fn import(sandbox: &Client) -> silicon_iam_client::Result<()> {
let imported = sandbox
    .applications()
    .import_from_production("google>drive", &Mutation::new())
    .await?;
assert!(imported.webhook_secret_inherited);
// Persist imported.app_secret now. The replay window is ten minutes.
# Ok(())
# }

import_from_production refuses locally when the client has no environment key, before any request is sent.

Discover a base URL in the correct plane

Any authenticated Application may discover any other verified Application, including one outside its organization. Build the caller with its own Basic credential. For a test lookup, keep the environment key on the same client so both caller and target resolve in that environment:

# use silicon_iam_client::{Client, Credential, EnvironmentKey};
# async fn discover(base: &str, key: &str, secret: &str) -> silicon_iam_client::Result<()> {
let caller = Client::new(base)?
    .with_environment(EnvironmentKey::new(key)?)
    .with_credential(Credential::application("acme>checkout", secret));
let target = caller
    .applications()
    .discover_base_url("google>drive")
    .await?;
println!("{}", target.base_url);
# Ok(())
# }

Test webhook delivery is real. Its signed JSON body is wrapped as {"test": {"testing_key": "…", "metadata": {…}, "data": {…}}} rather than using the production top-level metadata and data. Treat testing_key as the root credential it is: never log it, persist it with an event record, or forward it beyond the dedicated test receiver. Verify the signature over the exact body bytes before reading the envelope.

Errors

Error::Api carries the service's envelope, whose code is the stable thing to match on. Error::RateLimited is separate because it is the one failure with a mechanical remedy — wait the stated interval and repeat. Error::Transport means the request never reached a response, so the outcome is genuinely unknown; retry it with the original Mutation rather than a new one.

# use silicon_iam_client::Error;
# fn handle(error: Error) {
if let Some(api) = error.api() {
    if api.is_version_conflict() {
        // Someone changed it first: re-read, then decide.
    } else if api.requires_step_up() {
        // Obtain an assertion and attach it to the mutation.
    }
}
# }

Testing against a real service

The authoritative integration proof is the manual CLI walkthrough in the CLI guide. Run it against an isolated testing environment and inspect each result. At a minimum, prove:

Keep the environment UUID as metadata and its root key in secret storage. Do not substitute mock-only routes: the test plane intentionally uses the same API paths and authorization rules as production.

For crate-development coverage, the repository also contains ignored live integration tests. Point them at a disposable running instance:

SILICON_IAM_LIVE_URL=http://127.0.0.1:8080 \
  cargo test -p silicon-iam-client --test live -- --ignored --test-threads=1

Regenerating the wire types

After changing docs/openapi.yaml:

ruby scripts/generate-client-models.rb

The output is committed as ordinary source, so a contract change shows up as a reviewable diff.

License

Licensed under the Apache License, Version 2.0. See LICENSE.

Copyright 2026 Team of Silicons.

Scope declarations and review discussions

ApplicationCreate.app_scope and ApplicationPatch.app_scope use ApplicationScope { iam, external }. Omitted create scopes default to self.identity.read and self.profile.read. webhook_scope controls event categories (full, membership, updates, trust) independently. Read effective_app_scope for the currently approved configuration and app_scope for the requested configuration.

Use application_scopes().catalog(None) for the IAM catalog or pass an audience app ID for its exposed endpoint scopes. Every OBO endpoint declares critical: true or false. Use applications().update to remove permissions or change noncritical configuration; submit application_scopes().request(app_id, version, &ApplicationScopeRequestCreate, mutation) when requesting critical approval. Supply a detailed initial message. IAM creates one review per target authority. requests(status) lists discussions; get(request_id) returns the messages and whether you can decide. reply and decide both require the current request version and a mutation key. A denial requires a nonempty reason.

The initial critical review blocks first use. An upgrade keeps the previous effective scopes working until the requested additions are approved. Removing scopes does not need approval. No token receives an unapproved scope, and existing user consent never silently expands.

Application bundles

bundles().availability(org_id) reads whether bundle configuration is available to the signed-in Carbon in that organization. It returns only available; it does not expose organization policy settings. Use a direct Carbon IAM credential and an active membership. A successful false response means bundle configuration is unavailable for this caller.

bundles().list/create/get/update/delete manage named groups of applications in one organization. Use list_page(&paging) for all administered organizations or list_for_organization(org_id, &paging) to apply the organization filter before pagination. Both return items and page, including the continuation cursor. Application lists provide applications().list_for_organization(org_id, status, &paging) for the same purpose. Unavailable organizations return 404. Each member remains independently registered, with its own secret.

ApplicationBundleCreate.app_logo accepts an optional HTTPS image URL. For ApplicationBundlePatch.app_logo, None preserves the current logo, Some(Some(url)) replaces it, and Some(None) removes it. Read the saved logo from ApplicationBundle.app_logo; omitting the URL never uploads or invents an image. auth().bundle_login_organizations returns the bundle and member consent views. auth().bundle_short_lived_tokens accepts the complete list of approved member selections and returns one independent SLT per app atomically. Each target exchanges its own SLT normally.

Application testing layer

Authenticate with the production application's Credential::application, then call applications().create_testing_environment(&ApplicationTestingEnvironmentCreate, mutation). Provide name, optional description, and optionally iam_test_key. An existing valid key reuses that exact IAM test environment; an invalid supplied key fails; omitting it creates a new environment. IAM imports the app and all transitive declared external dependencies into that environment, including cyclic/shared dependency graphs without duplicating apps. Only the calling application's test secret is returned; dependency secrets remain inside IAM. applications().testing_environments(Some("all"), &paging) lists active and deleted links, including can_manage, status, version, and purge deadline. The creating production application can use environments().get/update/key/rotate_key/clean/delete/restore for that environment with its production app credential. Imported dependencies do not inherit control-plane authority.

Use the returned iam_test_key with Client::with_environment and the returned test app_secret for subsequent application API calls. The same production routes operate on isolated data, and verification uses 000000. Production and test credentials never mix. For an application receiving a request, the presence of app_secret in the request itself requests test mode: call applications().testing_context() with the test credential and environment key to validate it through IAM and route to the matching IAM environment's isolated application data. Do not select a test database from an unvalidated user flag. All transitive app calls remain in that IAM environment. Inactivity retention defaults to 30 days and is configurable per app through testing_idle_days.

Test webhooks put event fields under test.data and test.metadata and include the IAM testing_key; production events keep data and metadata at the top level. Verify the exact signed outer body, validate the embedded key using verify_testing_environment, and avoid logging it. An imported production webhook signing secret is inherited but never revealed; replace the test destination to install an independent test secret.

Scoped IAM reads and API contracts

An application's user access token carries only approved scopes. Organization lists include only explicitly selected active memberships. Directory lists require the corresponding directory.carbons.read or directory.silicons.read; field scopes independently control profiles, roles, job roles, tags, hierarchy, capabilities, and accessible Silicons. Self permissions never reveal those fields for other members. Email and phone are self-only. Absent fields mean undisclosed and must not be replaced with cached wider permissions.

Rust integrations use client.application_reads().me/organizations/organization/members/member/member_authorization/silicon/tags with Credential::bearer(application_access_token). These methods preserve scope-dependent JSON field omission. Direct IAM management methods retain their full typed response shapes. Use system().contracts() or iam system contracts (alias iam api contracts) to inspect contract versions and compatibility. Breaking changes receive a new major API version; a deprecated version can sunset after seven days without requests.