Silicon Hook DocsOpen Hook ↗
Silicon Hook documentation

Receiving through Ting#

When the Hook server delivers through Ting, every verified provider request reaches its Silicon (and Carbons who subscribed) as a compact reference. The app that hosts the Silicon owns its Ting destination, the callback listener, durable acceptance and token refresh; Hook's Rust client starts none of these. The Silicon and its custodian never set anything up by hand.

Delivery through Ting is optional on the server. When the operator has not configured it, Hook still receives, verifies and stores every event, queues nothing, and says so: client.delivery_status() returns enabled: false, register_recipient and subscribe answer delivery_disabled, and publication reports state: delivery_disabled. Read events with client.events(..) instead.

Setup#

  1. Sign the receiving account in to Hook (see Sign in) and keep its tokens.
  2. client.register_recipient() enrols the account with Ting. Hook proves the account's agreement to Ting with a Silicon Accounts User verification proof made from the account's own Hook token; nothing else is needed.
  3. A Carbon with access to a Silicon may call client.subscribe(silicon) to receive copies of its future events (no backfill). A Silicon receives its own events without subscribing.
  4. Register the receiving destination with Ting and keep its destination id and a high-entropy bearer secret (32 characters or more). The receiving URL stays in Ting's configuration.
  5. Build the receiver from the trusted context, never from a callback:
use silicon_hook_client::{Client, Secret, delivery::Receiver};

# async fn example(client: &Client) -> silicon_hook_client::Result<()> {
let context = client.delivery_context().await?; // Hook's app id + the account's uuid and id
let receiver = Receiver::new(
    context,
    "destination-id",
    Secret::new("a-host-generated-bearer-secret-at-least-32-characters"),
)?;
# let _ = receiver;
# Ok(()) }

Callback acceptance#

Ting sends {"tings": [...]}. Each Hook notification has the type hook.webhook.received, the producer key hook:{event id}:{sha256(recipient uuid)}, and inside data the envelope {"type": "new_event", "data": {"sender", "metadata"}} where metadata is the reference:

{
  "id": "0198c21a-6330-7000-8000-000000000001",
  "silicon": {"uuid": "Sx1", "id": "si:scout"},
  "hook_id": "0198c21a-6330-7000-8000-000000000002",
  "delivery_sequence": 42,
  "received_at": "2026-09-22T10:00:00Z",
  "summary": "stripe triggered at 10:00:00 22-09-2026 UTC"
}

silicon.uuid is permanent; fetch with it. silicon.id is the id the Silicon had when the event arrived (display only). Provider headers and bodies stay in Hook. A record's for, when Ting includes it, may name the recipient by uuid, by id or as {uuid, id}; it must be the receiver's account.

For each callback, the host:

  1. Requires exactly one Authorization and one Ting-Webhook-Id header and gives their values and the unmodified body to Receiver::decode.
  2. Calls Receiver::resolve(&client, &notifications) with a current Hook token. Every reference is checked before any event is fetched, and Hook checks the account's access on each lookup.
  3. Durably accepts and deduplicates the available events, keyed by Ting id and event id, and records each Unavailable result separately (it is not completed work).
  4. Answers HTTP 204 only after the whole batch is accepted.
# async fn decode_and_resolve(
#     receiver: &silicon_hook_client::delivery::Receiver,
#     client: &silicon_hook_client::Client,
#     authorization: &str,
#     webhook_id: &str,
#     body: &[u8],
# ) -> silicon_hook_client::Result<()> {
let notifications = receiver.decode(authorization, webhook_id, body)?;
let outcomes = receiver.resolve(client, &notifications).await?;
// Persist and deduplicate every Event or Unavailable outcome, then answer 204.
# let _ = outcomes;
# Ok(()) }

The SDK never marks a fetched event as accepted, keeps no deduplication state and never acknowledges Ting. Only Hook's 404 not_found after validation becomes Unavailable (Hook keeps events 14 days; Ting may keep the reference longer). hydrate is the strict variant that fails on any missing event. Invalid, unauthorized, transient and protocol failures reject the whole batch: never acknowledge a valid subset. References that carry fields this version does not know (for example those queued before Hook 1.0) are rejected. A shared callback host routes other apps' notifications itself; this receiver rejects other types. Batches hold at most 100 items and 2 MiB.

The hydrated Event is the original provider request. Binary bodies use request.body_base64. delivery_sequence identifies the event within its hook's stream; Ting does not promise order.

Tokens and delivery status#

Refreshing tokens is the host's job: refresh one at a time, store the new pair, and build the next Client with the new access token.

publication(silicon, event_id) separates pending, accepted_by_ting and accepted_silently (ordinary delivery while notifications are muted), and shows Ting's receipt when available. not_queued means the event arrived while delivery was off; not_delivered_legacy marks sends queued before Hook 1.0 that Ting never accepted. A destination's 204 confirms its acceptance, not that the application finished its work.

unsubscribe(silicon) stops only the current Carbon's copies; the Silicon's own deliveries continue. A notification already accepted by Ting may still arrive, and hydrating it checks access again.