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

Prove your app to other apps (App verification)

Show another app that a call really comes from your app. You get one App verification proof for each app you talk to.

InstructionsUpdated MarkdownEdit on GitHub
On this page

Use App verification when your app calls another app as itself. Say commit tells remind and waveform that a build finished. Each of them needs a way to check that the call really came from commit.

commit asks us for one proof for remind and another for waveform, and sends each app the token made for it. The receiving app then verifies the token with us. A proof is always for exactly one receiving app.

Shell
curl -s -u "commit:$COMMIT_APP_SECRET" \
  -X POST https://accounts.teamofsilicons.com/v1/proofs/app-verification \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: app_verification-remind-1" \
  -d '{"receiving_app":"remind","scopes":["notify"],"access_ttl_seconds":300}'

201 Created (a real response from a local stack, like every response on this page):

JSON
{
  "expires_at": "2026-10-07T12:41:09.361Z",
  "issuing_app": "commit",
  "kind": "app_verification",
  "proof_id": "01a1165d-3411-7233-aa94-3be86373c4cc",
  "proof_refresh_token": "sapr_WJYywoB9Cu5Na1of_mPjwTyxR-N0oBeYW5bp_Py7GLU",
  "proof_token": "sap_mpIk5D-xnK9HebJzGmvlkD_ONRKsQUq6ddI9SBUYsa8",
  "receiving_app": "remind",
  "refresh_expires_at": "2029-03-25T12:36:09.361Z",
  "scopes": ["notify"],
  "user": null
}

When remind verifies the token with its own credentials:

JSON
{
  "valid": true,
  "proof_id": "01a1165d-3411-7233-aa94-3be86373c4cc",
  "kind": "app_verification",
  "expires_at": "2026-10-07T12:41:09.361Z",
  "issuing_app": { "app_id": "commit", "name": "Commit" },
  "receiving_app": { "app_id": "remind", "name": "Remind" },
  "user": null,
  "scopes": ["notify"]
}

waveform, which this proof is not for, gets {"valid": false, "expires_at": null} for the same token. It verifies the proof commit issued for it instead, {"receiving_app": "waveform", …}.

Asking for several apps at once is refused, so a proof can never be replayed from one receiving app to another:

JSON
{
  "error": {
    "code": "app_verification_single_app",
    "message": "An App verification is for exactly one app; ask for one proof per app.",
    "hint": "Send {\"receiving_app\": \"remind\"} to POST /v1/proofs/app-verification instead of \"audiences\", and call it once for every app that should verify a proof from you; each app verifies its own proof.",
    "details": { "field": "audiences", "apps": ["remind", "waveform"] }
  }
}

App verification or User verification

  • Use App verification when the call is about your app itself: notifications, syncing, one service calling another. The receiving app learns which app is calling, and nothing about any account.
  • Use User verification when you act for an account. The receiving app then learns which account, and the proof ends by itself when the account signs out of your app, removes its access or is deleted.

Don't stretch App verification to act for accounts by putting an account id in your own payload. The receiving app couldn't tell whether the account agreed or still uses your app, and the account couldn't see or revoke it. That is exactly what User verification is for.

1. Issue the proof

With your app's credentials: POST /v1/proofs/app-verification.

fieldrequiredrules
receiving_appyesThe one app id that may verify the proof: 3 to 30 characters of a-z, 0-9, - and _ as Silicon Apps creates them, or an older id of 2 to 40 characters of a-z, 0-9 and - starting with a letter (trimmed and lowercased). Not your own app, and not Silicon Accounts itself (silicon-accounts, developer). It must exist and be active. A body with audiences (any length) is 422 app_verification_single_app.
scopesnoUp to 20 distinct strings, each 1 to 100 characters of A-Z a-z 0-9 _ . : / -.
access_ttl_secondsnoHow long each proof token lives: 60 to 1800 seconds, default 1800.

Send an Idempotency-Key. A retry with the same key and body within 10 minutes returns the same proof instead of a second one.

The answer has the same fields as a User verification proof, with user: null. refresh_expires_at is 900 days after issuing. Keep proof_refresh_token on your side, and send proof_token to the apps.

As one of the app's authors. Every app has an App verification page on developers.teamofsilicons.com (/apps/<app_id>/app-verification) where its owner makes, sees and revokes App verification proofs, one app at a time. Behind it is the owner endpoint. It takes the owner's session instead of the app secret, with the same body and the same answer:

Shell
curl -s -X POST https://accounts.teamofsilicons.com/v1/apps/commit/proofs/app-verification \
  -H "Authorization: Bearer $AUTHOR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: app_verification-page-1" \
  -d '{"receiving_app":"remind","scopes":["notify"]}'

$AUTHOR_ACCESS_TOKEN belongs to the author's Silicon Accounts session and has the audience silicon-accounts. You get it through code login (POST /v1/cli/login/start, then POST /v1/cli/login/verify) or the device flow.

The CLI does this for you. Sign in as an author with silicon-accounts login, then run silicon-accounts app --app-id commit proof app-verification … without an app secret (example below). On the developer portal, the App verification page uses your developer session, whose audience is developer. A Carbon who isn't one of the app's authors gets 403 not_app_owner.

The proof you get belongs to the app, so refreshing it still needs the app's credentials. Pass the refresh token to your app's server, or create the proof from that server in the first place.

Central history for apps you manage

Open App verification on the developer portal to see every App verification record we still keep for the apps you manage today. Records made on the portal, with the CLI and through the API show up together, whether they are active, expired or revoked. Filter by issuing app or status, and load more pages to see older records.

Expand a record to see when it was issued, every token refresh and its revocation. The proof family's expiry and each token's expiry are separate: an active family can be refreshed even after its current proof token has expired. The history marks which expiry times were recorded and which were derived for older records, and anything we don't have stays shown as unavailable.

Proof and refresh token values are shown only once, when they are generated. The history keeps the records after the credentials expire or are removed, but it never brings back raw secrets. Every list and history request checks that you still manage the app, and receiving a proof doesn't give the receiving app access to the issuing app's history.

The App verification tab of each app lives at /apps/<app_id>/app-verification and links to this central history with that app already selected. API integrations use GET /v1/me/app-verifications and GET /v1/apps/{app_id}/proofs/{proof_id}/history (reference). Proofs are issued with /v1/proofs/app-verification and /v1/proofs/user-verification. The JSON kinds are app_verification and user_verification.

2. Send the token with each call

Send proof_token to the one app it is for, for example as Authorization: Proof sap_…, and keep using it until shortly before expires_at. The receiving app verifies it with POST /v1/proofs/verify and checks kind: "app_verification", issuing_app.app_id and scopes.

3. Refresh, revoke, list

These work exactly as for User verification proofs, with your app's credentials:

  • POST /v1/proofs/refresh {"proof_refresh_token": "sapr_…"} gives you a new proof token and a new refresh token. Never present the used refresh token again, because that revokes the proof. Without access_ttl_seconds, the new token gets the proof's own lifetime. In a local test, refreshing a 60-second proof gave another 60-second token, which verified, while the expired first token answered {"valid": false, "expires_at": null}. See Refresh before the proof token expires.
  • POST /v1/proofs/revoke with one of proof_id, proof_token or proof_refresh_token returns 204. An author can use DELETE /v1/apps/{app_id}/proofs/{proof_id} from their session.
  • GET /v1/apps/{app_id}/proofs?kind=app_verification lists them, newest first:
JSON
{
  "items": [
    {
      "proof_id": "01a1165d-3411-7233-aa94-3be86373c4cc",
      "kind": "app_verification",
      "receiving_app": "remind",
      "user": null,
      "scopes": ["notify"],
      "status": "active",
      "access_ttl_seconds": 300,
      "created_at": "2026-10-07T12:36:09.361Z",
      "expires_at": "2029-03-25T12:36:09.361Z",
      "token_expires_at": "2026-10-07T12:41:09.361Z",
      "last_refreshed_at": null,
      "revoked_at": null,
      "revoke_reason": null
    }
  ],
  "next_cursor": null
}

An App verification proof ends only when it is revoked or reaches refresh_expires_at. While your app is disabled, its proofs don't verify and it can't issue new ones (403 app_disabled).

In Rust

Rust
use silicon_accounts_client::{AccountsClient, IssueAppVerification, ProofVerification};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let url = std::env::var("ACCOUNTS_URL").unwrap_or_else(|_| "https://accounts.teamofsilicons.com".into());
    let client = AccountsClient::new(url)?;

    // commit: one proof per receiving app.
    let commit = client.as_app("commit", std::env::var("COMMIT_APP_SECRET")?);
    let mut proofs = Vec::new();
    for app in ["remind", "waveform"] {
        let proof = commit
            .issue_app_verification(
                &IssueAppVerification {
                    receiving_app: app.into(),
                    scopes: vec!["notify".into()],
                    access_ttl_seconds: Some(300),
                },
                Some(&format!("app_verification-notify-batch-0193-{app}")),
            )
            .await?;
        println!("App verification proof {} for {}", proof.proof_id, proof.receiving_app.as_deref().unwrap_or("?"));
        proofs.push(proof);
    }
    let proof = &proofs[0]; // remind's

    // remind: is this call really from commit?
    let remind = client.as_app("remind", std::env::var("REMIND_APP_SECRET")?);
    match remind.verify_proof(proof.proof_token.expose()).await? {
        ProofVerification::Valid(p) if p.issuing_app.app_id == "commit" && p.scopes.iter().any(|s| s == "notify") => {
            println!("remind: accepted, from {} until {}", p.issuing_app.app_id, p.expires_at);
        }
        _ => println!("remind: refused"),
    }
    Ok(())
}
Text
App verification proof 01a1165d-… for remind
App verification proof 01a1165d-… for waveform
remind: accepted, from commit until 2026-10-07 12:41:09.361 +00:00:00

issue_app_verification calls POST /v1/proofs/app-verification with app credentials, and refuses a receiving_app that names several apps before sending anything. When the client acts as the app's owner (client.with_token(owner_token).app("commit")), it calls the owner endpoint instead. refresh_proof and revoke_proof work as shown in the User verification example.

With the CLI

Text
$ export ACCOUNTS_APP_ID=commit ACCOUNTS_APP_SECRET=…
$ silicon-accounts app proof app-verification --to waveform --scope notify --ttl 300
App verification proof 01a1165d-49c5-72e3-a5e1-c38f003d30d7 from commit for waveform.
proof token    sap_MJMgDB69Mp_WgDKGdoR_OxnDs8Nf0eg-KmDcCGZM48Q
expires        2026-10-07T12:41:14Z (in 4m)
refresh token  sapr_umG_g5wmVYV48Yp2uXeHS36ya-E2u0yMPsLVqE2xFcM
refresh until  2029-03-25T12:36:14Z (in 899d)
scopes         notify

--to takes exactly one app. A list is refused before anything is sent, and the hint gives you one command per app:

Text
$ silicon-accounts app proof app-verification --to remind,waveform
error: An App verification proof is for exactly one app, but --to names 2: remind, waveform.
hint: Issue one proof per app; each app verifies its own: silicon-accounts app proof app-verification --to remind ; silicon-accounts app proof app-verification --to waveform

Signed in as one of the app's authors (silicon-accounts login), you can run silicon-accounts app --app-id commit proof app-verification --to remind --scope notify without the secret, through the owner endpoint. silicon-accounts app proof list --kind app_verification, refresh and revoke work as for User verification. Refreshing and verifying need the app's own credentials.

Errors

statuscodewhen
422app_verification_single_appThe body has audiences (any length): ask for one proof per app with receiving_app. details.apps lists the valid app ids that were sent.
400invalid_receiving_appreceiving_app is your own app ("An app can't issue a proof to itself: …") or Silicon Accounts itself (silicon-accounts, developer).
400unknown_receiving_appThe app doesn't exist.
403receiving_app_disabledThe receiving app is disabled.
403app_disabledYour app is disabled, so it can't issue proofs.
422validation_failedEvery field problem at once in details.fields, for example {"receiving_app": "…"} or {"access_ttl_seconds": "must be between 60 and 1800 seconds; got 30", "scopes[1]": "'has space' contains ' '; …", "scopes[2]": "must not be empty; …"}.
403not_app_owner / app_mismatchAuthor endpoint: you aren't one of the app's authors, or your app credentials belong to another app than the one in the URL.
409idempotency_key_reusedThe Idempotency-Key was used with a different body.

Refresh and revoke errors are the same as for User verification: see the User verification errors.

Related

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 · InstructionsAct for an account at another app (User verification)When your app needs to do something at another app for one of its users, get their agreement, ask us for a User verification proof and send it with your call.Silicon Accounts · Learn · ExplanationHow App verification and User verification workWhy App verification and User verification work the way they do, who each proof names, how its tokens are checked and what ends it.Silicon Accounts · Reference · ReferenceApp verification and User verification endpointsIssue, refresh, verify, revoke and list App verification and User verification proofs, with every request, response and limit.

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.