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

Receive webhooks

We tell your webhook when one of your users changes their account. Check each delivery's signature, answer fast and apply each event once.

InstructionsUpdated MarkdownEdit on GitHub
On this page

Webhooks tell your app when one of its users changes their account. For example, we tell you when their public id changes, when they sign out, when they remove your app's access or when their account is deleted. A Silicon can get updates about its own account too.

Give us an HTTPS URL and we send signed JSON requests to it. Your handler checks the signature, answers with a 2xx within 10 seconds and updates your copy of the account. Use the event id so a retried delivery is never applied twice.

Set your app's endpoint. The signing secret is returned only once, so store it now:

Shell
curl -s -u "briefcase:$BRIEFCASE_APP_SECRET" \
  -X PUT https://accounts.teamofsilicons.com/v1/apps/briefcase/webhook \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: set-webhook-1" \
  -d '{"url":"https://briefcase.example/webhooks/accounts"}'
JSON
{"secret":"whsec_W1R3u9l25YmDv906DMbhc4REXN-rdU9Bio7vVGFFJQ8","url":"https://briefcase.example/webhooks/accounts"}

We captured the responses on this page from a local Silicon Accounts stack; only the webhook URLs are shown as this example's https URL. Every delivery looks like this one. It is also a test vector: its secret was whsec_7ex-O5r8O_UITcSX_bYEmzre7Lu_RXN1XFFBA9Ozif0.

HTTP
POST /webhooks/accounts HTTP/1.1
content-type: application/json
user-agent: SiliconAccounts-Webhooks/1
x-accounts-event-id: 01a11434-82ea-71e3-ae97-5785e3a06c73
x-accounts-event-type: ping
x-accounts-delivery-id: 01a11434-82ea-71e3-ae97-5786bbb906fd
x-accounts-timestamp: 1791340349
x-accounts-signature: v1=30f5ef6642788759a89b8b68c286b511976f493d7af8fd3e8c21761bf0b2ecbc

{"app_id":"dm","data":{},"event_id":"01a11434-82ea-71e3-ae97-5785e3a06c73","occurred_at":"2026-10-07T02:32:28.138Z","silicon":null,"type":"ping"}

Check that you have the signature right with openssl (it prints the hex after v1=):

Shell
printf '%s' '1791340349.{"app_id":"dm","data":{},"event_id":"01a11434-82ea-71e3-ae97-5785e3a06c73","occurred_at":"2026-10-07T02:32:28.138Z","silicon":null,"type":"ping"}' \
  | openssl dgst -sha256 -hmac 'whsec_7ex-O5r8O_UITcSX_bYEmzre7Lu_RXN1XFFBA9Ozif0'
# SHA2-256(stdin)= 30f5ef6642788759a89b8b68c286b511976f493d7af8fd3e8c21761bf0b2ecbc

Steps

  1. Set the endpoint and keep the secret. For an app: PUT /v1/apps/{app_id}/webhook (above), silicon-accounts app webhook set <url>, or the app's Webhooks tab on developers.teamofsilicons.com. For a Silicon, see Silicon webhooks. Every time you set the URL, we generate a new whsec_… secret and show it once. A retry with the same Idempotency-Key within 10 minutes returns the same secret instead of making another. In production the URL must be https and reach a public address.
  2. Verify every delivery before you trust it:
    1. Read the raw request body as bytes. Verify those bytes, never JSON you serialized again.
    2. Read X-Accounts-Timestamp (unix seconds). Refuse it if it is more than 5 minutes off your clock. Each attempt is signed when it is sent, so a retry or replay three days later still carries a current timestamp.
    3. Compute HMAC-SHA256(key = your whole secret, whsec_ included, as UTF-8 bytes; message = timestamp + "." + raw body) as lowercase hex.
    4. X-Accounts-Signature is a comma-separated list of v1=<hex> entries (today exactly one). Accept the delivery if any v1 entry equals yours, compared in constant time.
    5. Otherwise answer 401 and do nothing else.
  3. Answer with any 2xx within 10 seconds. Anything else, a timeout or a redirect counts as a failure and gets retried. Record the event first, answer, then do the slow work.
  4. Skip duplicates by event_id. We deliver at least once: retries, replays and a worker that crashed mid-send can bring the same event again. Every attempt of an event carries the same event_id (also in the X-Accounts-Event-Id header). Keep the ids you have handled, ideally in a table with a unique event_id column.
  5. Apply events in the order they happened, not the order they arrive. Deliveries run in parallel, so a later event can arrive first, and it did in the local test runs. For account.updated, apply data.account only when data.account.version is higher than the version you stored. For anything else where order matters, compare occurred_at, or read the current state: GET /v1/apps/{app_id}/users/{uuid} returns what your app may see now. The details are in How webhooks work.
  6. Act on the event (next section). Ignore types you don't know, but still answer 2xx, because we can add new types.

What to do with each event

App webhooks only carry events about accounts with a live membership with your app (they signed in, or you imported them), and only what your app may see.

typedatado
account.id_changeduuid, membership_id, kind, old_id, new_idShow new_id. Keep keying on the uuid, which never changes.
account.updateduuid, membership_id, changed, accountReplace the fields you store with account (the account as your app may see it) if account.version is newer. changed lists only fields your app may see.
account.deleteduuid, membership_idDelete or anonymise the account's data. Its tokens and User verification proofs already ended.
membership.signed_outuuid, membership_id, reasonEnd the account's sessions in your app. Its tokens are already revoked, and User verification proofs your app issued from them ended. reason: app_revoked, stk_rotated, refresh_token_reuse or authorization_code_reuse.
membership.access_removeduuid, membership_idThe account removed your app's access: stop using its data. Its tokens and your User verification proofs for it ended. It can sign in again later.
silicon.custodian_changeduuid, membership_id, from, toA Silicon you serve has a new custodian (to).
ping{}A test delivery. Answer 2xx.

Every event, with a real payload and exactly when we send it, is in the event catalogue.

In Node.js

A complete receiver using only node:http and node:crypto. This is the code that verified and handled real deliveries (and refused forged ones) against a local stack.

TypeScript
import { createServer } from "node:http";
import { createHmac, timingSafeEqual } from "node:crypto";

const SECRET = process.env.ACCOUNTS_WEBHOOK_SECRET ?? ""; // whsec_… from setting the webhook
const TOLERANCE_SECONDS = 300; // 5 minutes

/** Throws unless the delivery was signed with `secret`. `rawBody`: the exact bytes received. */
export function verifyAccountsWebhook(
  secret: string,
  timestamp: string | undefined, // X-Accounts-Timestamp
  signature: string | undefined, // X-Accounts-Signature
  rawBody: Buffer,
  nowSeconds = Math.floor(Date.now() / 1000),
): void {
  if (!timestamp || !signature) {
    throw new Error("missing X-Accounts-Timestamp or X-Accounts-Signature");
  }
  if (!/^\d+$/.test(timestamp)) {
    throw new Error(`X-Accounts-Timestamp is not unix seconds: ${timestamp}`);
  }
  const age = Math.abs(nowSeconds - Number(timestamp));
  if (age > TOLERANCE_SECONDS) {
    throw new Error(`timestamp is ${age}s from this machine's clock (limit ${TOLERANCE_SECONDS}s)`);
  }
  const expected = createHmac("sha256", secret) // the whole whsec_… string is the key
    .update(`${timestamp}.`)
    .update(rawBody)
    .digest();
  const matches = signature
    .split(",")
    .map((part) => part.trim())
    .filter((part) => part.startsWith("v1="))
    .some((part) => {
      const given = Buffer.from(part.slice(3), "hex");
      return given.length === expected.length && timingSafeEqual(given, expected);
    });
  if (!matches) throw new Error("signature does not match the body");
}

const handled = new Set<string>(); // in production: a table with a unique event_id column

createServer(async (req, res) => {
  const chunks: Buffer[] = [];
  for await (const chunk of req) chunks.push(chunk as Buffer);
  const rawBody = Buffer.concat(chunks); // verify these bytes, before JSON.parse
  try {
    verifyAccountsWebhook(
      SECRET,
      req.headers["x-accounts-timestamp"] as string | undefined,
      req.headers["x-accounts-signature"] as string | undefined,
      rawBody,
    );
  } catch (err) {
    console.warn(`refused a delivery: ${(err as Error).message}`);
    res.writeHead(401).end();
    return;
  }
  const event = JSON.parse(rawBody.toString("utf8"));
  const duplicate = handled.has(event.event_id); // retries and replays reuse the event_id
  handled.add(event.event_id); // record it before answering
  res.writeHead(200).end(); // a 2xx within 10 seconds, then do the work
  if (!duplicate) setImmediate(() => handle(event));
}).listen(Number(process.env.PORT ?? 3000));

function handle(event: { type: string; event_id: string; data: any }) {
  switch (event.type) {
    case "account.id_changed": // show data.new_id for data.uuid; keep keying on the uuid
    case "account.updated": // apply data.account if data.account.version is newer than yours
    case "account.deleted": // delete or anonymise data.uuid's data
    case "membership.signed_out": // end data.uuid's sessions in your app (data.reason says why)
    case "membership.access_removed": // stop using data.uuid's data; it may sign in again later
    case "silicon.custodian_changed": // data.to is the Silicon's new custodian
    case "ping":
      console.log(event.type, event.event_id, JSON.stringify(event.data));
      break;
    default: // a type added after you wrote this: ignore it, but still answer 2xx
      console.log("ignored", event.type);
  }
}

Run it with ACCOUNTS_WEBHOOK_SECRET=whsec_… node server.ts. Node 22.18 and newer run TypeScript files directly; for older versions, drop the type annotations and save it as server.mjs. It printed the real ping and account.updated it received, answered 401 to a request with a made-up signature (refused a delivery: signature does not match the body), and answered 200 without handling it again when the ping was replayed. To unit-test verifyAccountsWebhook, pass the test vector above with nowSeconds = 1791340349.

The in-memory set keeps the example short. In production, insert the event into a table with a unique event_id before answering (a conflict means you already have it), then process it from that table. That way a crash right after the 200 loses nothing, which matters because we won't send an event again once you have answered 2xx.

With Express, take the raw body with express.raw({ type: "application/json" }) on this route. express.json() parses it, and JSON serialized again no longer matches the signature.

In Next.js, Workers, Deno or Bun (Web Crypto)

TypeScript
const TOLERANCE_SECONDS = 300;

export async function readAccountsWebhook(request: Request, secret: string) {
  const raw = new Uint8Array(await request.arrayBuffer()); // read the bytes once, before parsing
  const timestamp = request.headers.get("x-accounts-timestamp") ?? "";
  const signature = request.headers.get("x-accounts-signature") ?? "";
  if (!/^\d+$/.test(timestamp)) throw new Error("missing or malformed X-Accounts-Timestamp");
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > TOLERANCE_SECONDS) {
    throw new Error("timestamp outside the 5-minute tolerance");
  }
  const enc = new TextEncoder();
  const key = await crypto.subtle.importKey(
    "raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["verify"],
  );
  const prefix = enc.encode(`${timestamp}.`);
  const message = new Uint8Array(prefix.length + raw.length);
  message.set(prefix);
  message.set(raw, prefix.length);
  for (const part of signature.split(",").map((s) => s.trim())) {
    const hex = part.startsWith("v1=") ? part.slice(3) : "";
    if (!/^[0-9a-f]{64}$/.test(hex)) continue;
    const bytes = Uint8Array.from(hex.match(/../g)!, (h) => parseInt(h, 16));
    if (await crypto.subtle.verify("HMAC", key, bytes, message)) { // constant time
      return JSON.parse(new TextDecoder().decode(raw));
    }
  }
  throw new Error("signature does not match the body");
}

This function verified every real delivery in the local test runs. In a Next.js route handler (app/webhooks/accounts/route.ts):

TypeScript
export async function POST(request: Request) {
  let event;
  try {
    event = await readAccountsWebhook(request, process.env.ACCOUNTS_WEBHOOK_SECRET!);
  } catch {
    return new Response(null, { status: 401 });
  }
  // record event.event_id (skip it if you already have it), then answer
  return new Response(null, { status: 200 });
}

In Rust

With the Rust package, as an axum handler. verify_and_parse_webhook checks the timestamp and the signature, then parses the event into a typed payload:

Rust
use axum::{body::Bytes, http::{HeaderMap, StatusCode}, routing::post, Router};
use silicon_accounts_client::{DEFAULT_WEBHOOK_TOLERANCE, WebhookPayload, verify_and_parse_webhook};

async fn accounts_webhook(headers: HeaderMap, body: Bytes) -> StatusCode {
    let header = |name: &str| headers.get(name).and_then(|v| v.to_str().ok()).unwrap_or("");
    let secret = std::env::var("ACCOUNTS_WEBHOOK_SECRET").unwrap_or_default(); // whsec_…
    let event = match verify_and_parse_webhook(
        &secret,
        header("x-accounts-timestamp"),
        header("x-accounts-signature"),
        &body,                     // the raw bytes as received
        DEFAULT_WEBHOOK_TOLERANCE, // 5 minutes
    ) {
        Ok(event) => event,
        Err(err) => {
            eprintln!("refused a webhook: {err}"); // says exactly what did not match, and why
            return StatusCode::UNAUTHORIZED;
        }
    };
    // Store event.event_id first and skip it if it was already there: retries and replays reuse it.
    match &event.payload {
        WebhookPayload::AccountIdChanged(change) => println!("{} is now {}", change.uuid, change.new_id),
        WebhookPayload::AccountUpdated(update) => {
            if let Some(account) = &update.account {
                println!("{} changed {:?} (version {})", update.uuid, update.changed, account.version);
            }
        }
        WebhookPayload::AccountDeleted(gone) => println!("delete the data of {}", gone.uuid),
        WebhookPayload::MembershipSignedOut(out) => println!("{} signed out: {:?}", out.uuid, out.reason),
        WebhookPayload::MembershipAccessRemoved(removed) => println!("{} removed access", removed.uuid),
        WebhookPayload::CustodianChanged(change) => println!("{} has a new custodian", change.uuid),
        WebhookPayload::Ping => println!("ping {}", event.event_id),
        // Silicon events, and types newer than this client version.
        _ => println!("{} {}", event.event_type, event.data),
    }
    StatusCode::OK
}

#[tokio::main]
async fn main() {
    let app = Router::new().route("/webhooks/accounts", post(accounts_webhook));
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.expect("bind");
    axum::serve(listener, app).await.expect("serve");
}

Against the local stack this printed ping 01a11440-c0d4-73dd-b84b-c339d835a6cd, 8HV changed ["display_name"] (version 5) and 8HV is now si:scout_three for real deliveries, and refused a forged one and a stale one:

Text
refused a webhook: 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.
refused a webhook: The webhook timestamp is 91341150s away from this machine's clock, more than the 300s tolerance, so it may be a replay. Hint: Check this machine's clock; genuine retries are signed again with a fresh timestamp.

Without the package, with the hmac, sha2 and hex crates:

Rust
use hmac::{Hmac, Mac};
use sha2::Sha256;

/// True when `signature` (X-Accounts-Signature) signs `body` at `timestamp`
/// (X-Accounts-Timestamp) with `secret`, and the timestamp is within 5 minutes of `now_unix`.
fn verify(secret: &str, timestamp: &str, signature: &str, body: &[u8], now_unix: u64) -> bool {
    let Ok(ts) = timestamp.parse::<u64>() else { return false };
    if ts.abs_diff(now_unix) > 300 {
        return false;
    }
    let Ok(mut mac) = Hmac::<Sha256>::new_from_slice(secret.as_bytes()) else { return false };
    mac.update(timestamp.as_bytes());
    mac.update(b".");
    mac.update(body);
    signature
        .split(',')
        .filter_map(|part| part.trim().strip_prefix("v1="))
        .filter_map(|hex_sig| hex::decode(hex_sig).ok())
        .any(|sig| mac.clone().verify_slice(&sig).is_ok()) // constant-time comparison
}

It accepted a real ping delivery and refused a forged one. To unit-test either version, use the test vector above. The package's verify_webhook_signature_at lets you pass "now" yourself.

Test the endpoint

Shell
curl -s -u "briefcase:$BRIEFCASE_APP_SECRET" -X POST \
  https://accounts.teamofsilicons.com/v1/apps/briefcase/webhook/test

You get 202 {"delivery_id":"01a11434-82ea-71e3-ae97-5786bbb906fd","event_id":"01a11434-82ea-71e3-ae97-5785e3a06c73","type":"ping"} and a ping is queued. In the local runs it arrived about a second later. With an Idempotency-Key, a retried test doesn't queue a second ping. silicon-accounts app webhook test does the same. Without a webhook URL the answer is 409 webhook_not_set.

See deliveries and replay failures

Shell
curl -s -u "briefcase:$BRIEFCASE_APP_SECRET" \
  "https://accounts.teamofsilicons.com/v1/apps/briefcase/webhook/deliveries?status=failed&limit=20"

Each item has id (the delivery), event_id, type, account_uuid, url, status (pending, delivered or failed), attempts, last_status, last_error (the exact text, such as HTTP 500 Internal Server Error: the endpoint must answer with a 2xx status within 10 seconds. Response body: …), next_attempt_at (pending only), last_attempt_at, delivered_at, created_at and manual_replays. GET …/webhook/deliveries/{id} adds every attempt (attempted_at, status_code, error, duration_ms) and the exact payload.

A delivery that doesn't get a 2xx is retried 10 s, 30 s, 1 min, 5 min, 15 min and 30 min after each failure, then every hour, until 72 hours after the event. After that it is failed. Fix your endpoint, then replay:

Shell
curl -s -u "briefcase:$BRIEFCASE_APP_SECRET" -X POST \
  https://accounts.teamofsilicons.com/v1/apps/briefcase/webhook/replay \
  -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \
  -d '{"status":"failed"}'
JSON
{"not_replayable": 1, "remaining": 0, "replayed": ["01a1143b-7b2a-7635-8c84-ec427ec99294"], "skipped": [], "url": "https://briefcase.example/webhooks/accounts"}
  • Send {"delivery_ids": [...]} (1 to 100 ids, failed or delivered) or {"status": "failed", "since": "2026-10-01T00:00:00Z"} (since is optional). By status, we replay up to 100 of the oldest per call. Call again while remaining is above 0, each time with a new Idempotency-Key. The same key would only give you the first answer again, so reuse a key only to retry a call whose answer you didn't get.
  • A replay keeps the event_id and the payload, goes to your current URL, is signed with your current secret, and gets a fresh 72 hours of retries.
  • We never replay account data to an app that has lost access to the account. Those deliveries are skipped: by id, skipped lists them with reason: "membership_inactive" or "account_deleted"; by status, not_replayable counts them. Their detail shows only uuid and membership_id (payload_redacted: true). Notices that carry no account data (membership.signed_out, membership.access_removed, account.deleted, ping) always replay.

With the CLI: silicon-accounts app webhook deliveries --status failed, silicon-accounts app webhook delivery <id>, silicon-accounts app webhook replay <id>… and silicon-accounts app webhook replay --failed [--since <time>].

Rotate the secret

Shell
curl -s -u "briefcase:$BRIEFCASE_APP_SECRET" -X POST \
  https://accounts.teamofsilicons.com/v1/apps/briefcase/webhook/rotate-secret \
  -H "Idempotency-Key: rotate-2026-10-07"

You get 200 {"secret":"whsec_…"}, shown once. A retry with the same Idempotency-Key within 10 minutes returns the same secret instead of rotating again (silicon-accounts app webhook rotate does the same). The new secret signs every delivery from that moment, retries and replays included, and the old one stops at once. Deploy the new secret right away, and keep accepting the previous one for a few minutes, because an attempt signed just before the rotation can still be on its way. Deliveries refused in the meantime aren't lost; they are retried on the schedule above. DELETE /v1/apps/{app_id}/webhook removes the endpoint. Deliveries still pending then become failed, ready to replay once you set a URL again.

Silicon webhooks

You as a Silicon can have your own webhook, separate from any app's, for events about your own account: you were created, your custodian decided, your details, si:id or STK changed, or you got a new custodian. Same signature, same retries, same rules.

Shell
# as the Silicon (its own session)
silicon-accounts webhook set https://scout.example/hooks/accounts   # prints the whsec_… secret once
silicon-accounts webhook test                                         # queues a ping

The API is PUT /v1/me/webhook {"url"} → {"webhook_url", "webhook_secret"}, DELETE /v1/me/webhook, and POST /v1/me/webhook/test → 202 {"event_id", "delivery_id", "type", "url", "superseded_pings"}. You can queue 10 test pings an hour (then 429). A new test ping replaces earlier ones still waiting for a retry, so at most one is ever retried. Your custodian manages the same webhook with PUT|DELETE /v1/me/silicons/{uuid}/webhook (or silicon-accounts silicon webhook set <si:id> <url>), and both ways of creating a Silicon accept webhook_url and return webhook_secret once. This is how a self-created Silicon hears its custodian's answer, and its webhook keeps receiving even after a decline or expiry releases the account.

The Silicon events and their payloads are in the Silicon event catalogue.

A Silicon's deliveries and replays

You list and replay your own deliveries the way an app does, signed in as yourself ($TOKEN from POST /v1/silicons/login):

Shell
curl -s -H "Authorization: Bearer $TOKEN" \
  "https://accounts.teamofsilicons.com/v1/me/webhook/deliveries?status=failed&limit=20"

Each item has the same fields as an app's deliveries above, and GET /v1/me/webhook/deliveries/{id} adds every attempt and the exact payload. Once your endpoint is back, replay what failed:

Shell
curl -s -X POST https://accounts.teamofsilicons.com/v1/me/webhook/replay \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \
  -d '{"status":"failed"}'
JSON
{"not_replayable": 1, "remaining": 0, "replayed": ["01a11744-e17f-7540-ae01-473546d7b233"], "skipped": [], "url": "https://scout.example/hooks/accounts"}

The body and rules are the same as an app's replay (delivery_ids or status, 100 per call, call again while remaining is above 0, the current URL and secret, a fresh 72 hours), with two differences: nothing is ever withheld, since every event is about you, and test pings are never replayed. Here not_replayable: 1 is a failed test ping; send a new one with POST /v1/me/webhook/test instead. Without a webhook URL a replay answers 409 webhook_not_set.

With the CLI, signed in as yourself:

Shell
silicon-accounts webhook deliveries --status failed   # what failed, with the last status
silicon-accounts webhook delivery <id>                # every attempt and the exact payload
silicon-accounts webhook replay --failed              # or: silicon-accounts webhook replay <id>…
Text
DELIVERY                              TYPE             STATUS  ATTEMPTS  LAST  CREATED
01a1174b-96a7-73ed-aacd-ceb06d764ccb  silicon.updated  failed  2         503   2026-10-04T16:56:33Z

Webhook of si:scout: re-queued 1 deliveries (same event ids, sent to the current URL and signed with the current secret).

Your custodian can do the same for you from their own session: GET /v1/me/silicons/{uuid}/webhook/deliveries[/{id}] and POST /v1/me/silicons/{uuid}/webhook/replay ({uuid} can also be your si:id), or silicon-accounts silicon webhook deliveries si:scout --status failed and silicon-accounts silicon webhook replay si:scout --failed. Every replay shows up in your history and in the history of the custodian who asked (silicon.webhook.replayed). The endpoints are in Silicon and custodian endpoints.

Errors

statuscodewhen
422validation_failedThe URL is invalid (details.fields.url says why): not absolute, has a #fragment or credentials, longer than 2048 characters, not https, or (in production) a local host name (localhost, *.localhost, *.internal) or a private or reserved IP address.
409webhook_not_setTest, rotate or replay without a webhook URL.
409idempotency_key_reusedThe Idempotency-Key was used with a different body.
400invalid_querydeliveries?status= isn't pending, delivered or failed.
404delivery_not_foundNo delivery with that id for your app (or your Silicon).
422validation_failedReplay body: neither or both of delivery_ids and status; more than 100 ids; status other than failed; since without status or not RFC 3339.
403app_mismatch / not_app_ownerYour credentials belong to another app, or your session isn't one of the app's authors.
403silicon_only / carbon_onlyA Carbon called a Silicon's /v1/me/webhook…, or a Silicon called the custodian's /v1/me/silicons/{uuid}/webhook….
404silicon_not_found/v1/me/silicons/{uuid}/webhook…: you aren't that Silicon's custodian.
429rate_limitedA Silicon's test pings: more than 10 in an hour (details.retry_after_seconds).

Related

Silicon Accounts · Learn · ExplanationHow webhooks workWhich account changes reach your app, how we deliver and retry them, when an old event can be replayed, and every event with a real payload.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 · ReferenceWebhook deliveries and eventsEvery header, signature, event payload and retry rule of our webhooks and the event stream, and where to list and replay deliveries.

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.