Events describe what happened on-chain; channels decide where the alert goes. A subscription selects matching activity, waits for its trigger and sends a notification through its assigned channel. Two kinds are available, and they share one lifecycle.
Webhook + Telegram Signed deliveries One lifecycle Notification history
Use a project key with api:channel:write to create, update and test channels, api:channel:delete to delete them and api:channel:read to read channels and history. Keep a separate Idempotency-Key and the original inputs for each mutation; reuse them after a lost response. See Idempotency.
A webhook posts JSON to your backend for automated processing; a Telegram channel sends a formatted message to a chat for humans to read. Production setups usually run both - one to act, one to watch.
An HTTP POST with the full WebhookV2Payload, signed so you can trust it.
HTTP POST to your endpoint
JSON (WebhookV2Payload)
Automated systems and backends
URL + optional custom headers
A formatted bot message in a group, channel or DM - readable at a glance.
Bot message to a chat
Formatted text
Human monitoring and alerting
Bot token + chat_id
A webhook channel sends an HTTP POST with a JSON payload to your endpoint when an attached subscription's trigger is met. Create one with POST /channels.
Choose kind: webhook, set the receiver URL and any dedicated headers. Select payload_version: v2 for structured events; webhook payload versions are chosen independently of the HTTP API version. The create-channel page above contains request and response schemas and language examples.
Headers are stored and returned unmasked to authorized channel readers. If your receiver needs authentication, use a dedicated receiver credential and restrict channel read access. Never put a Vilna API key there. Verify the webhook signature as well.
Save webhook_secret from the creation response in a secrets manager. Retrying that creation with the same key and body can recover it for 60 seconds from completion, under current access and only while the secret remains current. Retries do not extend the window; a later GET returns metadata, not plaintext. Recover or rotate deliberately if the secret was lost. Use it to verify X-Webhook-Signature; your receiver’s credentials and Telegram bot token are separate from the Vilna API key.
A webhook is just an HTTP POST, so one channel can fan out to wherever your architecture needs it. Point the URL at a thin proxy and forward from there:
write the raw event
SQS, RabbitMQ, Pub/Sub
human-readable alerts
escalate high-value events
any HTTP endpoint
For a specific integration, contact support@vilna.io.
A Telegram channel sends formatted messages to a chat through the Bot API. Set the bot up first, then create the channel:
Create a bot with @BotFather and save the bot token.
Add the bot to the target chat - a group, channel or direct message.
Get the chat ID with @userinfobot or the Telegram getUpdates API.
Use Create channel with kind: telegram, the bot token and chat_id. Choose the message language; set thread_id for a forum topic, or leave it at 0. The operation page contains the configuration schema and request examples.
Both kinds follow the same five-step lifecycle. Only the config differs - the endpoints are identical.
With api:subscription:write, create a subscription for this channel. It selects which transactions trigger notifications.
max_attempts (1-10) bounds the automatic delivery attempts for one notification, counting the first. Failed attempts are retried on a fixed schedule with a growing delay; ten attempts stretch over about a day. When the budget runs out, the channel is disabled with disabled_reason: attempts_exhausted. A destination the service has confirmed as unusable disables the channel at once with channel_error. There is no switch to keep a channel on regardless; the attempt budget is the knob.
Disabling a channel, automatically or by hand, cancels every notification still waiting on it: they end with status: canceled and reason: channel_disabled. Re-enabling admits only new transactions, not the cancelled backlog. A manual resend and a test never count against the budget.
Raise the budget with PATCH /channels/{channel_id}. Raising it, or repairing the destination, does not enable a disabled channel by itself; set is_enabled explicitly, in the same request.
To detect a disabled channel, poll GET /channels and watch for is_enabled: false, or list GET /notifications with status=canceled and the channel's id to see what was dropped and why.
Editing a webhook's URL or headers, or a Telegram chat, cancels the automatic retries of deliveries that already made at least one attempt; rotating the webhook signing secret does not.
A disabled channel does not lose a deposit. The transaction, the activity rows and the balance are recorded whether or not a notification went out; what is cancelled is the delivery. Three habits keep the gap small:
Set max_attempts: 10 on channels used for payment monitoring. That is about a day of automatic retries before the channel gives up.
Create a second subscription with the same filters on a second channel, such as a Telegram chat or another webhook. A subscription is bound to one channel, so disabling one leaves the other delivering.
After re-enabling, reconcile dropped notifications and movements during the outage.
Reconciliation also requires api:activity:read:
- Get the
chain_gidandtxidof dropped notices fromGET /notifications?status=canceled&channel_id=…. Collect every page ofGET /activitywithdate_fromanddate_tofor the outage window. - Resend notices whose payload is still retained; see resend responses. On
notification.payload_unavailable, reconcile from the transaction instead. Onnotification.send_in_progress, wait for the queued or running attempt. - Group all activity rows by
chain_gid,txid(hashwithpayload_version: v1),addressandasset_gid, and aggregate signeddelta.basevalues across incoming and outgoing effects. Several Bitcoin outputs to one address belong to one group. Apply your own deposit eligibility and confirmation rules before crediting; gross incoming value alone is not a credit rule. Save the group key and accounting result atomically.
event_id identifies a notification, not a transfer: another subscription can report the same deposit under a different event_id. Use it to deduplicate deliveries of one notification, and the shared credit key to reconcile across subscriptions and API polling.
After creation, use Test channel. Select only a transaction family, such as evm; you cannot choose a transaction, recipient or event kind. Use the operation page for the request schema and examples.
The call answers 202 with a notification_id. A test works on a disabled channel and never changes its state; it sends one built-in sample for the given family, not live data, in a single attempt with no automatic retries. A test that has not gone out within five minutes ends with reason test_expired. Inspect its notification before deliberately starting a new test with a new intent key; retrying the old key only replays its accepted result.
Every notification and each delivery attempt behind it is kept as history. Use it to confirm a test landed and to debug failures. List notifications from GET /notifications, newest first, and open GET /notifications/{notification_id}/logs to see every attempt for one of them.
Start with channel_id and status; separate tests with is_test and bound the window with date_from and date_to. A txid filter also requires chain_gid. Read every result page. The notification-list operation above defines all filters, response schemas and request examples.
Each attempt in the log carries the HTTP status and error your endpoint returned. When alerts stop arriving, query with status=failed and read the last attempt.
Once the endpoint is fixed, use POST /notifications/{notification_id}/actions/resend. Supply a separate Idempotency-Key for a fresh manual resend; retry the same key after a lost response.
| Response | Next action |
|---|---|
202 | The resend is queued with the same body and event_id; deduplicate the delivery by event_id. |
429 notification.resend_delayed | Wait for Retry-After, covering the delivery pause or cooldown. |
409 notification.send_in_progress | Wait for the existing attempt. |
409 notification.payload_unavailable | Reconcile the transaction instead. |
A resend uses the channel’s current destination, headers and signing credentials. The retained body does not preserve the old channel configuration or update transaction facts. Read the invoice again before treating a resend as completion. The payload is kept for 30 days after a notification finishes, then is unavailable for resend.
Follow the Webhooks reference for signature verification, deduplication and response deadlines. Use separate channels per environment and purpose, keeping critical alerts apart from routine notifications.