Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.offthehook.dev/llms.txt

Use this file to discover all available pages before exploring further.

Every webhook delivery from Off the Hook is a Standard Webhooks-compliant HTTP POST to your destination URL. The body is a JSON envelope with a consistent shape across all event types: a unique event ID, a kind string that identifies what happened, a timestamp, and a data object containing the chain-specific transfer details. Your endpoint can use these fields to identify the event, verify it hasn’t been processed before, and extract the transfer information you need.

Payload structure

{
  "id": "evt_8xN9kP2QbA...",
  "kind": "wallet.transfer.broadcasted",
  "date": "2026-05-08T12:34:56Z",
  "data": {
    "chainId": "tron:mainnet",
    "blockNumber": 78912345,
    "blockHash": "00000000...",
    "blockTimestamp": 1730000000000,
    "txId": "abc123...",
    "logIndex": 0,
    "from": "TLsV52sRDL79HXGGm9yzwKibb6BeruhUzy",
    "to": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
    "amount": "1500000000",
    "asset": {
      "type": "fungible",
      "contract": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
      "symbol": "USDT",
      "decimals": 6
    },
    "matchedAddresses": [
      {
        "chainId": "tron:mainnet",
        "address": "TLsV52sRDL79HXGGm9yzwKibb6BeruhUzy",
        "role": "from"
      }
    ]
  }
}

Envelope fields

id
string
required
Unique event identifier, prefixed evt_. This ID is stable across retries — if Off the Hook retries a failed delivery, the same id appears in every attempt. Use it as your idempotency key to avoid processing the same transfer twice.
kind
string
required
The event type string, such as "wallet.transfer.broadcasted". Your subscription only receives events whose kinds appear in its events array. See event types for all supported values.
date
string
required
ISO 8601 timestamp of when the event was generated.

Transfer data fields

data.chainId
string
required
CAIP-2 chain identifier for the network where the transfer occurred, such as "tron:mainnet". See supported networks.
data.blockNumber
number
required
The block number that included this transfer.
data.blockHash
string
required
The hash of the block that included this transfer.
data.blockTimestamp
number
required
Unix timestamp of the block in milliseconds.
data.txId
string
required
The TRON transaction ID.
data.logIndex
number
required
Position of this transfer within the block. For TRC-20 tokens this is the index of the Transfer log event (0-based). For native TRX transfers this is always -1 — a deliberate sentinel that avoids colliding with the first TRC-20 log in the same transaction.
data.from
string
required
Sender address in canonical TRON base58check format (starts with T).
data.to
string
required
Recipient address in canonical TRON base58check format.
data.amount
string
required
Raw integer transfer amount as a string. Divide by 10^asset.decimals to get the human-readable value. For example, "1500000000" with decimals: 6 is 1,500 USDT.
data.asset
object
required
Describes the transferred token.
data.matchedAddresses
object[]
required
The addresses from your subscription’s filter list that caused this event to fire. Each entry includes the chain, the address, and the role it played in the transfer: "from" if the address sent the funds, or "to" if it received them. A single transfer can match the same address in both roles if your filter includes both sender and recipient.

Deduplication

Off the Hook retries deliveries on 5xx, 408, 429, and network errors using a backoff schedule of 200ms, 1s, 5s, 1min, 5min, 30min, and 2h. Every attempt — original and retries — carries the same webhook-id header, which matches the id field in the payload body. To avoid processing the same transfer twice, record the webhook-id of every delivery you successfully handle and skip any delivery whose ID you’ve already seen.
app.post('/webhooks', async (req, res) => {
  const eventId = req.headers['webhook-id'];

  if (await db.eventAlreadyProcessed(eventId)) {
    return res.status(200).send('already processed');
  }

  // handle the event
  await processTransfer(req.body);
  await db.markEventProcessed(eventId);

  res.status(200).send('ok');
});

Delivery headers

Each POST request Off the Hook sends includes the following headers:
HeaderValue
content-typeapplication/json
webhook-idEvent ID (e.g. evt_8xN9kP2QbA...)
webhook-timestampUnix timestamp (seconds)
webhook-signaturev1,<base64-hmac-sha256>
x-oth-subscription-idYour subscription ID
user-agentOffTheHook/0.1
During a secret rotation grace window, the webhook-signature header contains multiple v1, values separated by spaces — one for each active secret. A valid signature from any of them means the delivery is authentic.

Next steps