Hook DocsOpen Hook ↗
SILICON HOOK / DOCUMENTATION

Silicon Hook API documentation#

This document explains every operation in the Silicon Hook OpenAPI contract. The machine-readable contract is in openapi.yaml. The product behavior it implements is UNDERSTANDING.md.

API conventions#

Base URL#

Management, history, and delivery operations use:

https://hook.teamofsilicons.com/api/v1

An authenticated Silicon owns the namespace https://hook.teamofsilicons.com/{silicon_id}/. Each hook it creates receives a public endpoint inside that namespace:

https://hook.teamofsilicons.com/silicon/{silicon_id}/{endpoint_key}

The endpoint key is eight uppercase alphanumeric characters. It routes a request to a hook and is never a credential; authenticity comes from the hook's signature policy.

API version#

Every route belongs to an API major. Before anything else a client sends the unversioned handshake:

GET /api/version
Silicon-Hook-Supported-API-Versions: v1

Hook answers with the highest major both sides support, in the body (service, selected_api_version, supported_api_versions, build, commit) and in Silicon-Hook-API-Version, and varies the response on the advertised list. With no shared major it answers 406 api_version_unsupported. A client then pins the major on every request with Silicon-Hook-API-Version: v1; a pin that disagrees with the route is refused with 400 api_version_mismatch. The official Rust client, silicon-hook-client, performs this handshake on connect.

Authentication#

  • Bearer authentication: the only credential. A Silicon presents the access token Silicon IAM issued it; a Carbon presents the Hook Application token obtained through sign-in or an IAM access token of their own. Hook exposes no OBO endpoints.
  • Provider ingress: Endpoint URLs are publicly reachable; each hook's signature policy decides what is delivered.
  • IAM ingress: POST /iam/events is authenticated by IAM's webhook signature, not a bearer.
  • Organization context: Management requests require X-Org-ID.

Hook verifies every bearer online with Silicon IAM through the official silicon-iam-client crate and IAM's organization directory, and fails closed if IAM cannot make a current decision. Every management response, including errors, carries Cache-Control: private, no-store.

Hooks are for Silicons. A Silicon manages only its own hooks. A Carbon sees the hooks, logs, and streams of every Silicon IAM confirms they can view; organization owners and administrators see and may mutate every Silicon's hooks. Deleting, restoring, enabling, updating, rotating, and connecting the IAM hook require the Silicon itself, an owner, or an administrator.

Idempotency#

Create, restore, secret rotation, endpoint rotation, and the IAM hook connection require an Idempotency-Key of 8–255 visible ASCII characters. Repeating a key in the same operation scope with the same request returns the original result; reusing it with different content returns 409 idempotency_conflict. Secret-bearing responses can be replayed for ten minutes; afterwards, rotate instead. Update and activation express a desired state and need no key.

Errors#

Errors use one stable envelope and include the request correlation ID:

{
  "error": {
    "code": "invalid_signature",
    "message": "The request contains invalid data.",
    "request_id": "0198...",
    "details": "unknown function md5 at byte 0"
  }
}

Authentication failures return 401, authorization failures and blocked addresses return 403, invisible resources return 404, request deadlines return 408, conflicts return 409, expired recovery, expired one-time-secret replay, and retired endpoints return 410, oversized requests return 413, unsupported request media return 415, invalid input returns 422 (with details where a safe explanation exists), internal invariant failures return a redacted 500, and dependency outages return 503.

Hook management#

GET /silicons/{silicon_id}/hooks#

Lists a Silicon's hooks. Each item includes the hook name, its endpoint_url, and last_received_at, the time the provider last reached out with a verified request, plus last_blocked_at and the full signature policy without secret material. include_deleted=true adds hooks still inside their 45-day recovery window. A Silicon can retain at most 1,000 hooks including recoverable deleted ones.

POST /silicons/{silicon_id}/hooks#

Creates a hook.

  • Required header: Idempotency-Key.
  • Input: name (the provider name used in every summary), optional description, optional IANA time_zone (default UTC), and an optional signature policy.
  • Returns: 201 with the hook, its endpoint_url, its endpoint_key, and signing_secret.

Omitting signature produces the Standard Webhooks policy with a generated secret of the form v1. followed by 32 alphanumeric characters. Give that secret to the provider. When the provider issues its own secret, supply it in signature.secret and describe its scheme; the response echoes the supplied secret once. Asymmetric algorithms take public_key instead and return signing_secret: null.

Bring your own secret (BYOS)#

Use signature.secret on creation or PATCH an existing hook at any time. The secret is stored verbatim and encrypted at rest; secret_encoding determines how it becomes verification key bytes. Changing only the secret preserves the URL, algorithm, payload, signature locator, and enabled/required settings.

{"signature":{"secret":"your-provider-secret","secret_encoding":"utf8"}}

To configure the verification scheme in the same request, include the other signature members. You can create first with a generated secret, then set the provider's secret once registration completes. Verification uses the new secret immediately; the old secret stops working. PATCH/read/list responses contain no secret. The creation response returns the supplied or generated secret once.

Omitting secret keeps the current secret on PATCH and generates one on POST for symmetric algorithms. Secrets must contain 1–4096 UTF-8 bytes without control characters and decode to a nonempty key using the selected encoding. Invalid secrets, incompatible encoding changes, or a secret supplied with an asymmetric algorithm return 422. Asymmetric algorithms use public_key instead. Generated/rotated secrets respect the configured encoding: text encodings use v1. plus 32 random alphanumerics; hex/base64/base64url encode those key bytes.

GET /silicons/{silicon_id}/hooks/{hook_id}#

Returns one hook. The signing secret is never returned after creation. An expired soft-deleted hook returns 404.

PATCH /silicons/{silicon_id}/hooks/{hook_id}#

Changes any subset of name, description (null clears it), time_zone, enabled, and signature. Signature members merge onto the current policy, so a single member such as {"signature": {"required": false}} turns verification off while keeping the rest. "public_key": null clears the key. Supplying secret replaces the stored secret. Requiring signatures for a symmetric algorithm needs a stored or supplied secret; otherwise the request fails with 422 invalid_signature and an explanation.

enabled alone uses the hook.hooks.enabled.update action; any other member uses hook.hooks.update. Requesting the current activation state is a successful no-op.

PATCH /silicons/{silicon_id}/hooks#

Enables or disables 1–1,000 unique hooks atomically. An unknown, deleted, cross-Silicon, or unauthorized member makes the whole request fail with no partial change.

DELETE /silicons/{silicon_id}/hooks/{hook_id}#

Soft-deletes a hook. Its endpoint stops accepting requests immediately. The hook, its secret, and its logs remain recoverable for 45 days; repeating the delete is a successful 204.

POST /silicons/{silicon_id}/hooks/{hook_id}/restore#

Restores a deleted hook within its recovery window with the same endpoint and secret. Requires Idempotency-Key.

POST /silicons/{silicon_id}/hooks/{hook_id}/secret/rotate#

Issues a new generated secret and returns it once. The previous secret stops verifying immediately. Not applicable to asymmetric policies. Requires Idempotency-Key.

POST /silicons/{silicon_id}/hooks/{hook_id}/endpoint/rotate#

Replaces the endpoint key with a fresh eight-character key that has never been used for this Silicon, and permanently retires the previous key: it is never reissued for the Silicon, and requests to it receive 410 endpoint_retired for as long as the Silicon exists. Returns the hook with its new endpoint_url. Requires Idempotency-Key.

Signature policy#

A policy has six parts:

Member Meaning Default
required Withhold requests that do not verify true
algorithm HMAC-SHA1, HMAC-SHA256, HMAC-SHA384, HMAC-SHA512, SHA1, SHA256, SHA384, SHA512, Ed25519, ECDSA-SHA256, RSA-SHA1, RSA-SHA256 HMAC-SHA256
payload Expression producing the bytes the provider signed concat(request.headers["webhook-id"], ".", request.headers["webhook-timestamp"], ".", request.raw_body)
signature Expression locating the presented signature request.headers["webhook-signature"]
signature_encoding hex, base64, base64url, or raw base64
secret_encoding How the stored secret text becomes key bytes: utf8, ascii, hex, base64, base64url, raw utf8

Plain SHA* algorithms digest the payload without a key, so the payload must include secret itself, for example concat(secret, request.raw_body). Asymmetric algorithms verify with public_key (PEM SubjectPublicKeyInfo, PEM RSA PUBLIC KEY, or raw hex/base64 key bytes; RSA moduli must be at least 2048 bits) and hold no secret.

The presented signature value is split on whitespace and commas, and a short label= prefix is stripped from each token, so sha256=<hex>, t=<ts>,v1=<hex>, and v1,<base64> v1,<base64> all verify. Symmetric comparisons run in constant time.

With signature_encoding: raw, the expression instead supplies one exact byte sequence. It is never decoded as text, trimmed, split or stripped of labels; for example, signature: request.raw_body_bytes can read a binary signature.

Signature expressions#

Blocks:

request.raw_body            body as UTF-8 text        request.raw_body_bytes   exact bytes
request.body.<json path>    parsed JSON member        request.form.<key>       form field
request.multipart.<key>     multipart part            request.method           uppercase token
request.url  request.scheme  request.authority  request.host  request.hostname  request.port  request.path
request.query_string        raw query                 request.query.<key>      decoded value
request.headers.<name>      case-insensitive          request.cookies.<key>    cookie value
hook.id  hook.url           receiving hook            secret                   decoded key bytes
key.public                  DER SubjectPublicKeyInfo

Functions: concat(...), join(separator: "" | "." | ":" | "," | ";" | "\n" | " ", ...), sort(list, order: asc | desc), sort_keys(object, order: asc | desc), utf8, ascii, url_encode, url_decode, percent_encode, percent_decode, canonicalize_url, canonicalize_query, json_encode, form_encode, sha1, sha256, sha384, sha512, hex, hex_decode, base64, base64_decode, base64url, base64url_decode, lowercase, uppercase, trim.

Bracket syntax addresses names with hyphens or JSON members: request.headers["x-hub-signature-256"], request.body.items[0].id. A missing header evaluates to null and makes the payload unavailable, so a partial payload is never signed. Expressions are limited to 4 KiB, 32 nesting levels, and 512 nodes.

Examples:

GitHub:  payload request.raw_body
         signature request.headers["x-hub-signature-256"]    hex
Stripe:  payload concat(request.headers["stripe-timestamp"], ".", request.raw_body)
         signature request.headers["stripe-signature"]       hex
Shopify: payload request.raw_body
         signature request.headers["x-shopify-hmac-sha256"]  base64

Ingress#

ANY https://hook.teamofsilicons.com/silicon/{silicon_id}/{endpoint_key}#

Receives a provider request. POST is the common case, but every method is captured because some providers verify endpoints with GET. The /api/v1/silicon/... route and an optional trailing slash are aliases.

Processing order:

  1. Resolve the endpoint. Unknown, disabled, and deleted endpoints return 404; a retired key returns 410 endpoint_retired.
  2. Check the client address against the hook's block list. A blocked address receives 403 ip_blocked (with Retry-After for a temporary block) and nothing it sent is stored.
  3. Capture the exact method, URL, headers (at most 128 fields, 64 KiB), and body (at most 1 MiB). Multipart bodies are parsed for expressions.
  4. If the policy requires signatures, verify. A verified request joins the log and the Silicon's delivery stream; an unverified request goes to the blocked log and counts against the address.
  5. Respond 200 {"status":"webhook.ok","receipt_id":"..."}. The response is identical for verified and withheld requests so it cannot be used as a signature oracle.

Behind a load balancer the deployment sets HOOK_TRUSTED_PROXY_HOPS so the blocked address is the real sender rather than the balancer.

Safety#

Twenty unverified requests from one address to one endpoint block that address from the endpoint for one day. Counting restarts after each block. Blocks are per endpoint, so a misconfigured provider cannot lock a Silicon out of its other hooks.

History#

GET /silicons/{silicon_id}/events and GET /silicons/{silicon_id}/hooks/{hook_id}/events#

Return the last n verified requests (limit 1–10,000, default 100) newest first, account-wide or for one hook. Each record contains the stable id, hook_id, provider, the summary line, the delivery_sequence, received_at, and the captured request with its method, URL, headers, content_type, body (text) or body_base64, and remote_ip. A 16 MiB page budget may shorten a page; follow next_cursor. Logs are kept for 14 days.

GET /silicons/{silicon_id}/blocked-requests and GET /silicons/{silicon_id}/hooks/{hook_id}/blocked-requests#

Return withheld requests in the same shape with a reason_code (for example signature_mismatch, signature_missing, payload_unavailable) and a short reason_detail. Kept for 14 days.

Cursors are authenticated and bound to the organization, Silicon, collection, and filter; a cursor from the events list is rejected on the blocked list.

Deliveries#

Every verified request is one position in its Silicon's ordered delivery stream. Each consumer (the authenticated actor) has an acknowledged cursor per Silicon, so a Silicon's own acknowledgments and a Carbon viewer's are independent.

GET /api/v1/ws?silicon_id=...#

WebSocket delivery. Authenticate the upgrade request like a management call (Authorization plus X-Org-ID) and repeat silicon_id for every stream. Frames are JSON text.

Server frames:

{"type":"ready","protocol_version":1,"connection_id":"...","silicon_ids":["cos:tos"],
 "acknowledged_through":{"cos:tos":41},"heartbeat_interval_seconds":30,"heartbeat_timeout_seconds":120}
{"type":"ping","ping_id":"..."}
{"type":"new_event","data":{"sender":"stripe","metadata":{...Event...}}}
{"type":"ack_recorded","silicon_id":"cos:tos","acknowledged_through":42}
{"type":"error","code":"invalid_frame","message":"...","recoverable":true}

Client frames:

{"type":"pong","ping_id":"..."}
{"type":"ack","silicon_id":"cos:tos","through_sequence":42}
{"type":"resume","silicon_id":"cos:tos","after_sequence":40}

Hook event deliveries contain exactly type and data at the top level. data.sender is the provider name recorded at receipt; data.metadata is the complete Event object, including silicon_id, delivery_sequence, summary, receive timestamp and the captured request. The same shape is used by the client/CLI recipient POST and for replayed events. Read ACK positions from data.metadata.delivery_sequence. Heartbeat, ready, ACK and error control frames retain their documented shapes; REST history and polling continue to return Event objects in items.

After ready the server sends every event after the acknowledged cursor, then live events as they arrive. The server sends ping every 30 seconds; the client answers with a pong carrying the same ping_id. If no valid pong arrives for two minutes the server closes with code 4000 and reason heartbeat-timeout. Pings and pongs are never stored, never acknowledged, and never consume sequences. resume replays from a client-held position without changing the cursor.

GET /silicons/{silicon_id}/deliveries#

Polling alternative. Without after_sequence it returns the unacknowledged backlog, oldest first, with the consumer's cursor and the Silicon's latest_sequence.

POST /silicons/{silicon_id}/deliveries/ack#

Acknowledges everything through through_sequence. Cursors never move backwards. The value must be between zero and the stream's latest allocated sequence; acknowledging a future event returns 422 without changing the cursor. The same validation applies to WebSocket ACKs, which return a recoverable invalid_ack.

GET /silicons/{silicon_id}/deliveries/cursor#

Reads the consumer's acknowledged position.

Sign-in#

Both Carbons and Silicons sign in with a short-lived token issued by IAM for tos>hook. Tokens are opaque. Do not infer their validity from a prefix. There is no password, OTP, redirect or callback endpoint in Hook.

POST /auth/login#

JSON {"slt":"<short-lived IAM token>"} with Idempotency-Key. Returns access_token, refresh_token, token_type, expires_in, scopes, actor and org_id. IAM consumes the SLT; retry with the same idempotency key after an uncertain result. The client requires a local recipient URL but never includes it in this backend request.

POST /auth/refresh#

JSON {"refresh_token":"<refresh token>"} with Idempotency-Key. Returns a new token pair. Save both atomically. Reuse the same mutation key when retrying an uncertain refresh. Never refresh the same family concurrently from two stores.

POST /auth/logout#

Empty body and Idempotency-Key. Supply the refresh token as the bearer to revoke the entire refresh-token family, as the CLI does. Supplying an access token revokes that token according to IAM's revocation contract. Returns 204.

All these operations accept X-Hook-Test-Key to select the matching IAM test world. A production SLT cannot authenticate a test session.

Silicon IAM#

POST /silicons/{silicon_id}/hooks/iam#

IAM must authorize the caller to configure the Silicon’s webhook; see IAM authority. The implemented orchestration connects IAM events to the Silicon. Hook finds or creates the Silicon's Silicon IAM hook (restoring a deleted one; there is exactly one per Silicon), registers the hook's endpoint URL as the Silicon's IAM webhook using the caller's own bearer, and stores the signing secret IAM issues. The hook's policy is IAM's own convention: HMAC-SHA-256 over X-Silicon-IAM-Timestamp.body, keyed with the secret's UTF-8 bytes and presented as v1=<hex> in X-Silicon-IAM-Signature. Returns the hook plus iam_webhook.secret_version. Requires Idempotency-Key; a retry with the same key reconciles a partial failure. IAM's own refusals surface as 403, 404, or 409 iam_rejected.

POST /webhook/ (origin-relative; legacy alias /api/v1/iam/events)#

Receives Hook's own Application webhook from IAM. Deliveries are verified with the crate's exact-byte verifier over the configured whs_ keyring before anything is read; unverifiable deliveries receive 403, verified ones 204. See the IAM integration guide.

Complete flows#

Provider event#

Provider POSTs to the endpoint URL
  -> Hook routes the key and checks the address
  -> Hook captures the exact request and verifies the signature policy
  -> Hook answers 200 webhook.ok
  -> verified: appended to the 14-day log and the Silicon's delivery stream
     withheld: appended to the blocked log and counted against the address
  -> connected sessions receive {"type":"new_event","data":{...}} and acknowledge
  -> unacknowledged events replay on the next connection

New Silicon#

Silicon authenticates with IAM and calls POST /silicons/{silicon_id}/hooks/iam
  -> Hook creates the Silicon IAM hook with IAM's signing convention
  -> Hook registers the endpoint as the Silicon's IAM webhook with the Silicon's bearer
  -> IAM issues the signing secret; Hook stores it as the hook's signing secret
  -> IAM signs every Silicon event to that endpoint; verified events flow to the stream

Deliberately deferred operations#

  • No event-detail endpoint by event ID.
  • No explicit replay of an individual event beyond resume.
  • No timestamp-tolerance option in signature policies; a signed timestamp is authenticated but is not checked for freshness by Hook. Providers requiring replay prevention should deduplicate by provider event ID and enforce timestamp freshness at the recipient.
  • No public permanent-purge operation; the worker purges deleted hooks after 45 days and logs after 14 days.
  • No unblock operation for addresses; temporary blocks expire after one day and inactive blocks are forgotten after 30 days.

Testing environments#

Use X-Hook-Test-Key: <32-alphanumeric-root-key> on ordinary API v1 calls. The same bearer authorization, permissions and request shapes then apply inside that environment. See the complete testing API guide for creation, key administration, cleanup, IAM configuration and lifecycle. Public test ingress uses /test/silicon/{silicon_id}/{endpoint_key} and carries no test key. Its endpoint ledger determines the environment.

WebSocket flow control#

At most 32 events per subscribed Silicon are outstanding on a connection. Acknowledging an event releases capacity for the next retained event. Read frames continuously even when processing a slow recipient. Use independent streams when different Silicons need independent processing budgets. A consumer that reads without ACK eventually reaches the window limit; HTTP history remains available for inspection without acknowledging delivery.

Environment reset, root-key rotation, deletion or IAM reconfiguration changes the environment generation. Existing sessions close with 4001 environment-changed; reconnect using the current key/configuration. Live IAM authorization is rechecked every 30 seconds and after verified IAM webhook notifications, which fan out to replicas over PostgreSQL. Loss of authority closes with 4003 authorization-changed.

Login discovery and status#

GET /api/v1/auth/iam needs no bearer. It returns app_id, iam_url, testing, and login_method: "short_lived_token"; app_id is null when only local development auth is configured. It never exposes secrets. Attach x-hook-test-key for the test application's configuration; an invalid or unconfigured environment cannot fall back to the production application.

GET /api/v1/auth/status requires a bearer and x-org-id, plus the test key in a test environment. Hook checks the actor and current membership online through the official IAM client. Success returns authenticated: true, actor: {"type": "carbon" | "silicon", "id": "..."}, and org_id. Invalid/revoked credentials return 401; permission and provider failures retain their normal error responses. Both discovery and status use Cache-Control: no-store.

Local relay destinations are configured after SLT exchange through the client or CLI and never appear in the backend login request. webhook and unhook change local delivery configuration; neither changes a provider hook URL.