{"templateId":"markdown","versions":[{"version":"v2","label":"v2 (current)","link":"/apis/platform/webhooks","default":true,"active":true,"folderId":"c55bad3c"},{"version":"v1","label":"v1 (deprecated)","link":"/apis/platform/v1/webhooks","default":false,"active":false,"folderId":"c55bad3c"}],"sharedDataIds":{"sidebar":"sidebar-apis/sidebars.yaml"},"props":{"metadata":{"markdoc":{"tagList":["quickfacts","fact","tabs","tab","format","part","flow","step","admonition"]},"type":"markdown"},"seo":{"title":"Webhooks - delivery, payload & signing","projectTitle":"Vilna Docs","description":"How Vilna delivers webhooks - event types, payload schema, HMAC-SHA256 signature verification, retries, and testing for the Platform API.","siteUrl":"https://docs.vilna.io","lang":"en-US","image":"https://cdn.vilna.io/docs/og/26103121/en/apis-platform-webhooks.png","meta":[{"name":"og:type","content":"website"},{"name":"og:site_name","content":"Vilna Documentation"},{"name":"og:image:width","content":"1200"},{"name":"og:image:height","content":"630"},{"name":"twitter:card","content":"summary_large_image"},{"name":"robots","content":"max-image-preview:large"}],"llmstxt":{"title":"Vilna API Documentation","description":"Vilna Platform API monitors public blockchain activity, balances and invoices and delivers signed webhooks. It does not sign or broadcast transactions. VKS separately coordinates account setup, signer approvals and signing, retains signed results and can submit a complete transaction to a node. Node acceptance is not blockchain confirmation. Each API uses its own scoped credential in X-Api-Key. Workspace setup and VKS-key issuance start in the Dashboard.\n","details":{"content":"**Platform monitoring.** Platform observes merchant-controlled addresses,\nsupplied directly or derived from the merchant's extended public key (xPub).\nInvoices report incoming on-chain activity; they do not initiate transfers.\n\n**APIs and their credentials.**\n\n- Platform API (api.vilna.io) - monitor addresses and extended public keys, read\n  balances and transactions, reconcile incoming payments, create invoices, and\n  receive events.\n- Blockchain RPC (rpc.vilna.io) - JSON-RPC and gRPC node access across every\n  supported network behind one key.\n- Management API (mgmt.vilna.io) - published workspace, project, member and key\n  operations, with Dashboard bootstrap and credential grant limits.\n- VKS API (vks.vilna.io) - workspace signers, vaults, accounts, signing requests,\n  signed results and optional asynchronous send. Uses a Dashboard-issued VKS key\n  with vks:access; the public Management API has no VKS-key issuance route.\n\n**Pages with no markdown source**, so they cannot be linked below:\n\n- Product overview: https://docs.vilna.io/\n- All guides: https://docs.vilna.io/guides\n"},"excludeFiles":["legal/terms.md","legal/privacy.md","legal/cookies.md","@l10n/ru/legal/terms.md","@l10n/ru/legal/privacy.md","@l10n/ru/legal/cookies.md"],"sections":[{"title":"Getting Started","description":"What the platform is, how to make a first call, and answers to common questions","includeFiles":["get-started/quickstart.md","get-started/dashboard.md","get-started/core-concepts.md","get-started/members.md","get-started/faq.md"]},{"title":"API Reference","description":"Authentication, permissions, idempotency, errors, standards, and per-service endpoint documentation","includeFiles":["apis/authentication.md","apis/permissions.md","apis/idempotency.md","apis/errors.md","apis/standards.md","apis/networks.md","apis/platform/@v2/index.md","apis/platform/@v2/webhooks.md","apis/platform/@v2/api.yaml","apis/mgmt/index.md","apis/mgmt/api.yaml","apis/vks/index.md","apis/vks/api.yaml","apis/rpc/index.md","apis/rpc/json-rpc.md","apis/rpc/grpc.md"]},{"title":"Developer Guides","description":"Integration patterns, events, notifications, and tooling","includeFiles":["guides/events.md","guides/channels.md","guides/tokens.md","guides/integration-patterns.md","guides/mcp.md","guides/simulation.md","guides/invoices.md","guides/vks.md"]},{"title":"Trust and custody","description":"The non-custodial model in plain terms","includeFiles":["legal/non-custodial.md"]},{"title":"Optional","description":"Background reading and release history","includeFiles":["get-started/blockchain-basics.md","changelog.md"]}]},"priority":0.7,"jsonLd":{"@context":"https://schema.org","@graph":[{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://docs.vilna.io/"},{"@type":"ListItem","position":2,"name":"Platform API","item":"https://docs.vilna.io/apis/platform"},{"@type":"ListItem","position":3,"name":"Webhooks"}]},{"@type":"TechArticle","headline":"Webhooks - delivery, payload & signing","description":"How Vilna delivers webhooks - event types, payload schema, HMAC-SHA256 signature verification, retries, and testing for the Platform API.","url":"https://docs.vilna.io/apis/platform/webhooks","datePublished":"2026-06-08","dateModified":"2026-11-01","proficiencyLevel":"Intermediate","author":{"@id":"https://vilna.io/#organization"},"publisher":{"@id":"https://vilna.io/#organization"}}]}},"dynamicMarkdocComponents":[],"compilationErrors":[],"ast":{"$$mdtype":"Tag","name":"article","attributes":{},"children":[{"$$mdtype":"Tag","name":"Heading","attributes":{"level":1,"id":"webhooks","__idx":0},"children":["Webhooks"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Webhooks are how the Platform API pushes blockchain activity to your backend. When a monitored address sees a transaction, Vilna sends a signed HTTP ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["POST"]}," to your webhook ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/guides/channels"},"children":["notification channel"]}," - no polling."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["This page is the delivery contract: the event types, the payload, the headers, how to verify a delivery is authentic, and how retries and testing work. To create or manage a channel, see ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/guides/channels"},"children":["Notification channels"]},"; for the event model itself, see ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/guides/events"},"children":["Events & monitoring"]},"."]},{"$$mdtype":"Tag","name":"QuickFacts","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"Fact","attributes":{"icon":"webhook"},"children":["Signed HTTP POST"]}," ",{"$$mdtype":"Tag","name":"Fact","attributes":{"icon":"lock"},"children":["HMAC-SHA256"]}," ",{"$$mdtype":"Tag","name":"Fact","attributes":{"icon":"check-double"},"children":["Dedup by ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["event_id"]}]}," ",{"$$mdtype":"Tag","name":"Fact","attributes":{"icon":"clock"},"children":["Ack within 10s"]}]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"id":"versions","level":2,"__idx":1},"children":["Payload versions "]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Every webhook channel states a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["payload_version"]}," - ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["v1"]}," or ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["v2"]}," - when you create it. The version freezes into each notification the first time it is sent; changing it later, through a channel ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["PATCH"]},", only applies to notifications sent from that point on. After you switch a channel to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["v2"]},", retries of notifications already frozen on ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["v1"]}," keep going out in the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["v1"]}," shape, alongside new ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["v2"]}," sends. A channel created before the v2 launch stays on ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["v1"]}," until you switch it."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"id":"events","level":2,"__idx":2},"children":["Event types "]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["A channel's ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["payload_version"]}," decides which event name it receives."]},{"$$mdtype":"Tag","name":"Tabs","attributes":{},"children":[{"$$mdtype":"Tag","name":"Tab","attributes":{"label":"v2"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["A ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["payload_version: v2"]}," channel receives one event for every subscribed transaction:"]},{"$$mdtype":"Tag","name":"div","attributes":{"className":"md-table-wrapper"},"children":[{"$$mdtype":"Tag","name":"table","attributes":{"className":"md"},"children":[{"$$mdtype":"Tag","name":"thead","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Event"},"children":["Event"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"When it fires"},"children":["When it fires"]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["transaction.notification"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["A subscription's trigger condition was met."]}]}]}]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["A test delivery reuses the same event name - check ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["is_test"]}," in the body to tell it apart from a real one."]}]},{"$$mdtype":"Tag","name":"Tab","attributes":{"label":"v1"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["A ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["payload_version: v1"]}," channel receives one of two events. It supports only ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["block"]}," triggers with ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["after_blocks: 0"]}," and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["network"]}," triggers; ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["mempool"]}," requires ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["v2"]},"."]},{"$$mdtype":"Tag","name":"div","attributes":{"className":"md-table-wrapper"},"children":[{"$$mdtype":"Tag","name":"table","attributes":{"className":"md"},"children":[{"$$mdtype":"Tag","name":"thead","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Event"},"children":["Event"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"When it fires"},"children":["When it fires"]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["transaction.detected"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Included in a block: a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["block"]}," trigger with ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["after_blocks: 0"]}," was met. An early signal, not finality."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["transaction.confirmed"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Reached the chain's confirmation policy. This does not guarantee irreversibility; reconcile current transaction state before acting."]}]}]}]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["A test delivery reuses these event names too - check ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["is_test_message"]}," in the body."]}]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The event name arrives in the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Webhook-Event"]}," header."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"id":"delivery","level":2,"__idx":3},"children":["The delivery "]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Each delivery is an HTTP ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["POST"]}," with a JSON body and signed headers."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"headers","__idx":4},"children":["Headers"]},{"$$mdtype":"Tag","name":"div","attributes":{"className":"md-table-wrapper"},"children":[{"$$mdtype":"Tag","name":"table","attributes":{"className":"md"},"children":[{"$$mdtype":"Tag","name":"thead","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Header"},"children":["Header"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"What it carries"},"children":["What it carries"]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Webhook-Signature"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["The HMAC-SHA256 signature, ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["t=<unix>,v1=<hex>"]}," - see ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"#verify"},"children":["verifying"]}]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Webhook-Event"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["v2"]},": always ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["transaction.notification"]}," - a test carries the same name, check ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["is_test"]}," in the body. ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["v1"]},": ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["transaction.detected"]}," or ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["transaction.confirmed"]}," - a test reuses these, check ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["is_test_message"]}," in the body"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Webhook-Version"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["The channel's ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["payload_version"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["v1"]}," or ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["v2"]},". Equal to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["version"]}," in a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["v2"]}," body; the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["v1"]}," body carries no such field"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Webhook-Event-Id"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["UUIDv5, stable across retries - your dedup key"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Webhook-Delivery-Id"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["UUID, changes on every attempt - for support correlation"]}]}]}]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"payload","__idx":5},"children":["Payload"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Every delivery is a JSON body, in the shape your channel's ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["payload_version"]}," picked:"]},{"$$mdtype":"Tag","name":"Tabs","attributes":{},"children":[{"$$mdtype":"Tag","name":"Tab","attributes":{"label":"v2"},"children":[{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"json","header":{"controls":{"copy":{}}},"source":"{\n  \"version\": \"v2\",\n  \"event_id\": \"550e8400-e29b-41d4-a716-446655440020\",\n  \"subscription_id\": \"550e8400-e29b-41d4-a716-446655440002\",\n  \"is_test\": false,\n  \"item\": {\n    \"chain_gid\": \"eip155:1\",\n    \"txid\": \"0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef\",\n    \"created_at\": \"2024-01-15T10:30:00Z\",\n    \"activity\": [\n      {\n        \"chain_gid\": \"eip155:1\",\n        \"txid\": \"0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef\",\n        \"address\": \"0x742d35cc6634c0532925a3b8d1e1d78d8E15e5c4\",\n        \"asset_gid\": \"eip155:1/erc20:0xdAC17F958D2ee523a2206206994597C13D831ec7\",\n        \"direction\": \"out\",\n        \"delta\": { \"base\": \"-1000000\", \"formatted\": \"-1.0\" },\n        \"created_at\": \"2024-01-15T10:30:00Z\",\n        \"entry_kind\": \"token_transfer\"\n      }\n    ],\n    \"details\": {\n      \"family\": \"evm\",\n      \"events\": [\n        {\n          \"event_index\": 0,\n          \"kind\": \"transfer\",\n          \"asset_gid\": \"eip155:1/erc20:0xdAC17F958D2ee523a2206206994597C13D831ec7\",\n          \"amount\": { \"base\": \"1000000\", \"formatted\": \"1.0\" },\n          \"operation\": \"transfer\",\n          \"from_address\": \"0x742d35cc6634c0532925a3b8d1e1d78d8E15e5c4\",\n          \"to_address\": \"0x95212290dd7278aa3ddd389cc1e1d165cc4bafe5\"\n        }\n      ],\n      \"is_success\": true,\n      \"fee\": { \"base\": \"21000000000000000\", \"formatted\": \"0.021\" },\n      \"block\": {\n        \"block_hash\": \"0x9f1c2a3b4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f8\",\n        \"block_height\": 18500000\n      },\n      \"transaction_index\": 42,\n      \"from_address\": \"0x742d35cc6634c0532925a3b8d1e1d78d8E15e5c4\",\n      \"to_address\": \"0xdAC17F958D2ee523a2206206994597C13D831ec7\",\n      \"gas_used\": 65000\n    }\n  },\n  \"references\": {\n    \"tokens\": {\n      \"eip155:1/erc20:0xdAC17F958D2ee523a2206206994597C13D831ec7\": {\n        \"gid\": \"eip155:1/erc20:0xdAC17F958D2ee523a2206206994597C13D831ec7\",\n        \"symbol\": \"USDT\",\n        \"name\": \"Tether USD\",\n        \"decimals\": 6\n      }\n    },\n    \"blockchains\": {\n      \"eip155:1\": {\n        \"gid\": \"eip155:1\",\n        \"name\": \"ethereum\",\n        \"short_name\": \"eth\",\n        \"image_url\": \"https://raw.githubusercontent.com/vilna-io/icons/refs/heads/main/chains/ethereum.svg\"\n      }\n    },\n    \"addresses\": {\n      \"0x742d35cc6634c0532925a3b8d1e1d78d8E15e5c4\": \"Hot wallet\"\n    }\n  }\n}\n","lang":"json"},"children":[]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["version"]}]}," - the frozen webhook body version, ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["v2"]},". Independent of the HTTP API version."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["event_id"]}]}," - identity of this notification. Your dedup key on retries and manual resends."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["subscription_id"]}]}," - the subscription that selected this transaction. Null only for a channel test."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["is_test"]}]}," - ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["true"]}," for a channel test, ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["false"]}," for a real transaction."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["item"]}]}," - the transaction itself, with its parsed ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/guides/events"},"children":["events"]}," and per-address activity."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["references"]}]}," - lookup maps for the tokens, chains and addresses the transaction touches, so you rarely need another call to resolve names and symbols."]}]}]},{"$$mdtype":"Tag","name":"Tab","attributes":{"label":"v1"},"children":[{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"json","header":{"controls":{"copy":{}}},"source":"{\n  \"event_id\": \"f47ac10b-58cc-5372-a567-0e02b2c3d479\",\n  \"item\": { \"...\": \"the Transaction object, with its events and per-address activity\" },\n  \"references\": {\n    \"tokens\": { \"...\": \"token GID -> details\" },\n    \"blockchains\": { \"...\": \"chain GID -> details\" },\n    \"addresses\": { \"...\": \"address -> label\" }\n  },\n  \"is_test_message\": false\n}\n","lang":"json"},"children":[]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["event_id"]}]}," - UUIDv5, matches the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Webhook-Event-Id"]}," header. It is part of the signed body, so it is tamper-evident. Your dedup key."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["item"]}]}," - the full transaction, including its parsed ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/guides/events"},"children":["events"]}," (transfers, fees, approvals) and per-address deltas."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["references"]}]}," - lookup maps for the tokens, chains and addresses in the transaction, so you don't need extra calls to resolve names and symbols."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["is_test_message"]}]}," - ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["true"]}," for test deliveries, ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["false"]}," for real blockchain events."]}]}]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"id":"verify","level":2,"__idx":6},"children":["Verifying the signature "]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["A signature proves a delivery came from Vilna and was not tampered with. Verify every delivery before acting on it. The ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Webhook-Signature"]}," header has two parts:"]},{"$$mdtype":"Tag","name":"Format","attributes":{"line":"t=<timestamp>,v1=<signature>"},"children":[{"$$mdtype":"Tag","name":"Part","attributes":{"token":"t=<timestamp>","eg":"t=1714262800"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Timestamp"]}," - Unix seconds when Vilna signed the delivery. It drives the replay window."]}]},{"$$mdtype":"Tag","name":"Part","attributes":{"token":"v1=<signature>","eg":"v1=5257a8696bc5…"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Signature"]}," - the hex HMAC-SHA256 over the signed payload. During a secret rotation two ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["v1="]}," entries appear, so a receiver holding either secret can validate."]}]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["signed payload"]}," is the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["t"]}," value, a literal dot, then the raw request-body bytes: ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["{t}.{rawBody}"]},". Verify it in five steps - the same in any language:"]},{"$$mdtype":"Tag","name":"Flow","attributes":{},"children":[{"$$mdtype":"Tag","name":"Step","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Parse the header"]}," into its ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["t"]}," value and one or more ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["v1"]}," values. Reject a missing or malformed timestamp. Each ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["v1"]}," signature must contain exactly 64 hexadecimal characters; skip malformed candidates."]}]},{"$$mdtype":"Tag","name":"Step","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Check the timestamp."]}," Reject the delivery if ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["t"]}," is more than 5 minutes from now - this blocks replays. Don't set the tolerance to zero; clock skew between servers will drop legitimate deliveries."]}]},{"$$mdtype":"Tag","name":"Step","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Rebuild the signed payload"]}," - the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["t"]}," value, a dot, then the ",{"$$mdtype":"Tag","name":"em","attributes":{},"children":["raw"]}," body bytes. Never re-serialize parsed JSON; it will not byte-match."]}]},{"$$mdtype":"Tag","name":"Step","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Compute HMAC-SHA256"]}," of that payload with your ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["webhook_secret"]},". Decode the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["v1"]}," candidates from hex into 32-byte values for comparison."]}]},{"$$mdtype":"Tag","name":"Step","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Compare"]}," your result against each ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["v1"]}," with a constant-time comparison. Accept if any one matches - that covers the rotation window."]}]}]},{"$$mdtype":"Tag","name":"Admonition","attributes":{"type":"warning","name":"Three things people get wrong"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Compare in constant time."]}," Use a cryptographic byte-comparison function. A malformed signature must fail verification rather than raise a handler error."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Sign the raw bytes."]}," Hash the body exactly as received, before any JSON parsing - a re-serialized object will not byte-match."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Accept any known ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["v1="]},"."]}," Ignore unknown signature schemes and accept only when a supported signature verifies."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"id":"secret","level":2,"__idx":7},"children":["The signing secret "]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The signing secret is unique to the channel. Store it immediately after channel creation or secret rotation."]},{"$$mdtype":"Tag","name":"Admonition","attributes":{"type":"warning","name":"Bounded secret recovery"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Repeat the original request to create a channel or rotate its secret with the same ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Idempotency-Key"]}," to recover the response for 60 seconds from successful completion, under current authorization and only while the secret remains current. Retries do not extend the deadline. Later GETs return masks and timestamps, never plaintext. After expiry or unavailable replay, inspect channel and secret metadata before deliberately rotating; a fresh key can create another channel or rotate again."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Rotation keeps deliveries verifiable with zero downtime:"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Rotate"]}," to issue a new secret. For 24 hours both the old and new secret sign each delivery (two ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["v1="]}," entries), so you can redeploy receivers gradually."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["End the overlap early"]}," once every receiver has the new secret - after that, deliveries are signed with the current secret only."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["A ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["read"]}," endpoint returns the current secret's mask and timestamps (and the previous secret's metadata during the overlap) - never the plaintext."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Rotate whenever someone with access to the secret leaves the team."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"id":"trust","level":2,"__idx":8},"children":["Treat webhooks as notifications, not truth "]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["A valid HMAC signature proves that a holder of the shared secret signed the payload. Before an irreversible action, verify the transaction or ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/guides/invoices"},"children":["invoice"]}," state through the Platform API or the chain."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"id":"retries","level":2,"__idx":9},"children":["Delivery & retries "]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Acknowledge fast."]}," Return an HTTP ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["2xx"]}," within 10 seconds; do the real work afterwards."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Failures are retried."]}," A non-",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["2xx"]}," response or a timeout is retried with backoff. Retries reuse the same ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["event_id"]},", so your dedup logic makes them safe."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Deduplicate by ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["event_id"]},"."]}," It is stable across retries and present in both the header and the signed body - skip anything you have already processed."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["One delivery per subscription and transaction."]}," A subscription notifies a given transaction at most once; re-inclusion or a later content revision never repeats or corrects a notice already sent. The one exception is a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["mempool"]}," subscription, which can notify a second time if the transaction leaves the mempool for over an hour and reappears."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["No notice for a reorg."]}," If a transaction you were told about later drops out of the chain, there is no follow-up delivery - reconcile through the API if you need certainty. See ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/guides/events#triggers"},"children":["triggers"]}," for the full model."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["max_attempts"]}," caps the automatic attempts, counting the first."]}," They run on a fixed schedule set by the service, about a day across ten attempts (1-10). A channel disables itself once they're exhausted or on a confirmed bad destination; see ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/guides/channels#disabled"},"children":["when a channel switches itself off"]},"."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Inspect the ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/guides/channels#logs"},"children":["notification history"]},"."]}," Every attempt records the HTTP status your endpoint returned."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"id":"testing","level":2,"__idx":10},"children":["Testing "]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Send a sample delivery to a channel at any time with the test action - it produces a real signed request:"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Open ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/apis/platform/api/channel/test-channel"},"children":["Test a channel"]},", select ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["family: evm"]}," and supply a separate ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Idempotency-Key"]},". The operation page provides request examples and response details."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["It answers ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["202"]}," with a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["notification_id"]},"."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Use it to validate your signature checking and endpoint wiring before relying on the channel in production. Test deliveries appear in the ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/guides/channels#logs"},"children":["notification history"]}," with ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["is_test: true"]},"."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Keep the original ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Idempotency-Key"]}," unchanged with the same body if the HTTP response is lost. The accepted response replays for 24 hours after completion, subject to current access; it does not send another test. Inspect notification history before intentionally starting another test with a new key. The key remains bound after replay expiry. HTTP mutation replay, signature timestamp checks and deduplicating webhook ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["event_id"]}," solve separate problems."]}]},"headings":[{"value":"Webhooks","id":"webhooks","depth":1},{"value":"Payload versions","id":"versions","depth":2},{"value":"Event types","id":"events","depth":2},{"value":"The delivery","id":"delivery","depth":2},{"value":"Headers","id":"headers","depth":3},{"value":"Payload","id":"payload","depth":3},{"value":"Verifying the signature","id":"verify","depth":2},{"value":"The signing secret","id":"secret","depth":2},{"value":"Treat webhooks as notifications, not truth","id":"trust","depth":2},{"value":"Delivery & retries","id":"retries","depth":2},{"value":"Testing","id":"testing","depth":2}],"frontmatter":{"seo":{"title":"Webhooks - delivery, payload & signing","description":"How Vilna delivers webhooks - event types, payload schema, HMAC-SHA256 signature verification, retries, and testing for the Platform API.","priority":0.7,"image":"https://cdn.vilna.io/docs/og/26103121/en/apis-platform-webhooks.png","jsonLd":{"@context":"https://schema.org","@graph":[{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://docs.vilna.io/"},{"@type":"ListItem","position":2,"name":"Platform API","item":"https://docs.vilna.io/apis/platform"},{"@type":"ListItem","position":3,"name":"Webhooks"}]},{"@type":"TechArticle","headline":"Webhooks - delivery, payload & signing","description":"How Vilna delivers webhooks - event types, payload schema, HMAC-SHA256 signature verification, retries, and testing for the Platform API.","url":"https://docs.vilna.io/apis/platform/webhooks","datePublished":"2026-06-08","dateModified":"2026-11-01","proficiencyLevel":"Intermediate","author":{"@id":"https://vilna.io/#organization"},"publisher":{"@id":"https://vilna.io/#organization"}}]}}},"lastModified":"2026-10-31T22:57:01.000Z","pagePropGetterError":{"message":"","name":""}},"slug":"/apis/platform/webhooks","userData":{"isAuthenticated":false,"teams":["anonymous"]},"isPublic":true}