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

App verification and User verification endpoints

Issue, refresh, verify, revoke and list App verification and User verification proofs, with every request, response and limit.

ReferenceUpdated MarkdownEdit on GitHub
On this page

We make proof tokens, and we check them for the app that receives them. A User verification proof says which account an app is acting for. An App verification proof says which app is calling. Every proof names exactly one receiving app.

What a proof's scopes mean, and which actions they allow, is up to the apps. For the steps, follow Verify a proof, User verification or App verification. How proofs work explains what each app is responsible for.

Here the receiving app verifies a proof token:

Shell
curl -s -X POST "$ACCOUNTS_URL/v1/proofs/verify" -u "briefcase:$BRIEFCASE_SECRET" \
  -H 'Content-Type: application/json' -d '{"proof_token":"'"$PROOF_TOKEN"'"}'
JSON
{
  "valid": true,
  "proof_id": "01a11438-f6ef-75f2-86a0-091d4d1b9b37",
  "kind": "user_verification",
  "expires_at": "2026-10-07T02:47:19.983Z",
  "issuing_app": { "app_id": "dm", "name": "DM" },
  "receiving_app": { "app_id": "briefcase", "name": "Briefcase" },
  "user": { "uuid": "8HV", "id": "c:ada", "kind": "carbon", "membership_id": "dm:8HV" },
  "scopes": ["files.write"]
}

Anything else gets exactly {"valid": false, "expires_at": null}.

Every endpoint takes app auth (-u app_id:app_secret) unless its section says otherwise. Request bodies refuse unknown fields, and proof responses are Cache-Control: no-store.

NumberValue
proof token (sap_…) lifetimeaccess_ttl_seconds, 60 to 1800, default 1800
proof lifetime (its sapr_… refresh token)900 days; a User verification proof never outlives the sign-in it stands on
scopesat most 20 distinct strings, each 1 to 100 characters of A-Z a-z 0-9 _ . : / -, defined by the apps
App verification receiving appsexactly 1 per proof (receiving_app); one proof per app

The issued proof

POST /v1/proofs/user-verification, /app-verification, /refresh and POST /v1/apps/{app_id}/proofs/app-verification all answer with:

JSON
{
  "proof_id": "01a11438-f6ef-75f2-86a0-091d4d1b9b37",
  "kind": "user_verification",
  "proof_token": "sap_ynFCe2dYOohw67CJrGQkC5yFjq4HJxUD2SukHLHZ7J4",
  "expires_at": "2026-10-07T02:47:19.983Z",
  "proof_refresh_token": "sapr_Y06gz3yM8kR4BPPZT_xRDu93d86hGImeipKkaycO2as",
  "refresh_expires_at": "2029-03-25T02:37:19.930Z",
  "issuing_app": "dm",
  "receiving_app": "briefcase",
  "user": { "uuid": "8HV", "id": "c:ada", "kind": "carbon", "membership_id": "dm:8HV" },
  "scopes": ["files.write"]
}

An App verification proof has "kind": "app_verification", its one receiving_app, and "user": null. Give the proof_token to the receiving app and keep the proof_refresh_token yourself. Lifetimes are absolute timestamps (there's no expires_in), so a replayed idempotent response still tells the truth about how much time is left.

POST /v1/proofs/user-verification

Idempotent (10 minutes). Body:

Field
subject_tokenan access token your app received for the account (its aud is your app)
receiving_appthe app that will verify the proof
scopesoptional list of app-defined strings
access_ttl_secondsoptional, 60 to 1800
Shell
curl -s -X POST "$ACCOUNTS_URL/v1/proofs/user-verification" -u "dm:$DM_SECRET" \
  -H 'Content-Type: application/json' -H 'Idempotency-Key: user_verification-briefcase-8HV-1' \
  -d '{"subject_token":"'"$ACCOUNT_ACCESS_TOKEN"'","receiving_app":"briefcase","scopes":["files.write"],"access_ttl_seconds":600}'

Answers 201 with the issued proof. Get the account's consent in your own interface first, because we don't show a consent screen for proofs.

StatusCodeWhy
400invalid_subject_tokennot an access token, bad signature, expired, or its sign-in ended (details.reason: not_an_access_token, invalid, expired, revoked)
403subject_token_wrong_appthe token was issued to another app (details.token_app): an app can only turn its own tokens into proofs
403account_not_activethe account isn't active (details.status)
403membership_inactivethe account has no active membership with your app (details.membership_id)
400unknown_receiving_appno such app (details.app_ids)
400invalid_receiving_appyour own app, or silicon-accounts
403receiving_app_disabledthe receiving app is disabled (details.app_ids)
422validation_failedscopes[i], access_ttl_seconds
JSON
{
  "error": {
    "code": "subject_token_wrong_app",
    "message": "subject_token was issued to the app 'dm', but 'briefcase' is asking for the proof. An app can only turn access tokens it received itself into User verifications.",
    "hint": "Use the access token 'briefcase' received when the account signed into 'briefcase'.",
    "details": { "token_app": "dm" }
  }
}

POST /v1/proofs/app-verification

Idempotent (10 minutes). Send {"receiving_app": "remind", "scopes"?, "access_ttl_seconds"?} and get 201 with the issued App verification proof. An App verification proof is always for exactly one app, so to talk to remind and waveform you issue one proof for each.

Errors:

  • 422 app_verification_single_app: the body has audiences, of any length ("An app verification is for exactly one app; ask for one proof per app.", with details.field: "audiences" and details.apps);
  • 400 unknown_receiving_app;
  • 400 invalid_receiving_app: your own app, or silicon-accounts/developer;
  • 403 receiving_app_disabled;
  • 422 validation_failed: receiving_app, scopes[i], access_ttl_seconds.

POST /v1/apps/{app_id}/proofs/app-verification

The same, for app or author: an app's authors can issue App verification proofs from the app's App verification page on developers.teamofsilicons.com without the app secret. The body and response are the same (including the 422 app_verification_single_app for audiences), and a disabled app gets 403 app_disabled.

POST /v1/proofs/refresh

Only the issuing app can refresh. Send {"proof_refresh_token": "sapr_…", "access_ttl_seconds"?} and get 200 with the issued proof: the same proof_id, a new proof_token and a rotated proof_refresh_token. With an Idempotency-Key, a retried refresh returns the same new tokens.

If a refresh token that was already used is presented again, we revoke the whole proof (400 proof_refresh_token_reused), and later refreshes get 410 proof_revoked:

JSON
{
  "error": {
    "code": "proof_revoked",
    "message": "Proof 01a11438-f6ef-75f2-86a0-091d4d1b9b37 was revoked at 2026-10-07T02:37:31.704Z because one of its proof refresh tokens was presented again after it had been used (refresh_token_reuse), so it can't be refreshed.",
    "hint": "Issue a new proof with POST /v1/proofs/user-verification (the account must still be signed into your app).",
    "details": { "proof_id": "01a11438-f6ef-75f2-86a0-091d4d1b9b37", "reason": "refresh_token_reuse", "revoked_at": "2026-10-07T02:37:31.704Z" }
  }
}

Other errors: 400 invalid_proof_refresh_token (not a sapr_ token, or unknown), 403 not_issuing_app, 410 proof_expired, and 410 proof_revoked (revoked, or a User verification proof whose sign-in ended).

POST /v1/proofs/verify

The app that received the token sends {"proof_token": "sap_…"}. The answer is always 200: valid (the example at the top) or exactly:

JSON
{ "valid": false, "expires_at": null }

A proof is valid only when all of these hold:

  • the token is a known, unexpired proof token;
  • the proof isn't revoked;
  • the calling app is its receiving app;
  • the issuing app is active;
  • for User verification, the account is active, its membership with the issuing app is active, and the sign-in behind the subject token is still live.

Every other case gets the same body, so a caller learns nothing about proofs that aren't theirs. A malformed input (a refresh token, a JWT, an empty string) adds an x-accounts-hint header that describes it:

Text
x-accounts-hint: proof_token must be a proof token (it starts with sap_), but this is a proof refresh token.

user.membership_id is the account's membership with the issuing app (the grant the proof stands on), and user.id is the account's current id. Verifying is one indexed lookup (well under a millisecond of database time), and the app's credentials are checked through a 60-second cache, so verify on every call that needs the proof.

POST /v1/proofs/revoke

Only the issuing app can revoke. Send one of {"proof_id"}, {"proof_token"} or {"proof_refresh_token"} and get 204. Revoking a revoked proof does nothing. Errors: 404 proof_not_found (not one of your proofs; another app's proof id looks unknown to you), 400 invalid_proof_id, 403 not_issuing_app.

GET /v1/apps/{app_id}/proofs

app or author: the proofs your app issued, newest first. Filter with kind (user_verification, app_verification), status (active, revoked, expired), limit and cursor.

JSON
{
  "items": [
    {
      "proof_id": "01a11438-f6ef-75f2-86a0-091d4d1b9b37",
      "kind": "user_verification",
      "receiving_app": "briefcase",
      "user": { "uuid": "8HV", "kind": "carbon", "id": "c:ada", "display_name": "Ada King", "pfp_url": "…", "status": "active" },
      "scopes": ["files.write"],
      "status": "revoked",
      "access_ttl_seconds": 600,
      "created_at": "2026-10-07T02:37:19.983Z",
      "expires_at": "2029-03-25T02:37:19.930Z",
      "token_expires_at": "2026-10-07T02:47:19.983Z",
      "last_refreshed_at": "2026-10-07T02:37:31.553Z",
      "revoked_at": "2026-10-07T02:37:31.704Z",
      "revoke_reason": "refresh_token_reuse"
    }
  ],
  "next_cursor": null
}

expires_at is when the proof ends, and token_expires_at is when its newest token does. For User verification, status is live: a proof whose sign-in was revoked shows revoked with revoke_reason sign_in_revoked. The reasons are revoked_by_app, revoked_by_owner, revoked_by_account, refresh_token_reuse, sign_in_revoked, access_removed, account_deleted, membership_inactive and account_inactive.

DELETE /v1/apps/{app_id}/proofs/{proof_id}

app or author: revokes one of your app's proofs. 204. Errors: 404 proof_not_found, 400 invalid_proof_id.

GET /v1/me/app-verifications

signed-in manager: the App verification records we keep for every app you currently manage, whether they were made in the portal, the CLI or the API. This is the central history the developer portal shows. Filter with app_id or status=active|revoked|expired, and page with limit and the next_cursor we return. Results are newest first. Being the receiving app of a proof doesn't give you access to another app's records.

GET /v1/apps/{app_id}/proofs/{proof_id}/history

signed-in manager: the kept history of one App verification record, meaning when it was issued, refreshed and revoked. We check that you currently manage the issuing app. Historical expiry values show whether they were recorded at the time or derived for older (legacy) records, and missing values stay missing rather than being filled in as facts. No raw proof or refresh token values are returned.

Records and their history stay after the credentials expire or are removed. See central history for the portal flow. In JSON the kinds are app_verification and user_verification, and the issuing routes are /app-verification and /user-verification.

GET /v1/me/proofs

account: the User verification proofs apps issued about you, newest first. Filter with ?status=active|revoked|expired, limit and cursor; unknown query parameters are refused. Each item has proof_id, issuing_app and receiving_app (app summaries), scopes, status, created_at, expires_at, token_expires_at, last_refreshed_at, revoked_at and revoke_reason.

DELETE /v1/me/proofs/{proof_id}

account: revokes a proof about you, and the receiving app's next verification answers valid: false. 204. Errors: 404 proof_not_found, 400 invalid_proof_id.

JSON
{
  "error": {
    "code": "invalid_proof_id",
    "message": "'not-a-uuid' is not a proof id; proof ids are UUIDs like 01928c7e-3b7a-7c4e-9a51-2f3d4c5b6a79.",
    "hint": "Use the proof_id from the issue response or from a proofs listing; to revoke by token send proof_token or proof_refresh_token instead."
  }
}

What ends a proof

Besides revocation and expiry, a User verification proof ends when the grant it stands on ends: the account signs out of the issuing app or removes its access, the sign-in is revoked (an STK rotation, a reused refresh token), or the account is deleted. Verification checks all of this live, so no webhook has to arrive first.

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 · 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 · Start · InstructionsProve 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.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 · 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.

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.