Skip to content
Last updated

Authentication

This guide covers how to authenticate with the Vilna API, verify webhook signatures, and keep your credentials secure.

API key authentication

Every request to the Vilna API must include an API key in the X-Api-Key header.

Getting an API key

  1. Sign up and create your account.
  2. Create a workspace and a project.
  3. Generate an API key in the project settings with the permissions you need.

API keys are project-scoped. RPC keys and management keys are workspace-scoped - create them in workspace settings. See the Dashboard for details.

Using the key in requests

curl "https://api.vilna.io/v1/blockchains" \
  -H "X-Api-Key: your-api-key"

If the key is missing or invalid, the API returns an RFC 7807 error:

{
  "type": "https://docs.vilna.io/errors/unauthorized",
  "title": "Unauthorized",
  "status": 401,
  "detail": "Invalid or missing API key."
}

API key types

Vilna uses three types of API keys, each designed for a specific service and scope.

TypePrefixScopeUsed for
API keyvilna_api_ProjectPlatform API access (addresses, transactions, balances)
RPC keyvilna_rpc_WorkspaceBlockchain node access via Vilna RPC
Management keyvilna_mgt_WorkspaceManagement API (workspaces, projects, members, keys)

API keys are project-scoped. Each key is bound to a single project within a workspace and can only access resources that belong to that project. If you have multiple projects, you need a separate API key for each one.

RPC keys and management keys are workspace-scoped. They operate across all projects in the workspace. An RPC key grants access to blockchain node endpoints, while a management key allows you to manage workspaces, projects, members, and other keys programmatically.

Each key type carries its own set of permissions that control which operations the key can perform. For example, an API key may have api:address:read but not api:address:write. See the authorization reference for the full permission matrix.

All three key types are passed in the same X-Api-Key header. The gateway identifies the key type from its prefix and routes the request to the correct service automatically.

A key is 40 characters long: the type prefix, 24 random base62 characters, and 6 CRC32 check characters. For example:

vilna_api_aBcDeFgHiJkLmNoPqRsTuVwX123456

You can create and manage keys through the Management API or the Vilna dashboard.

Webhook signature verification

When Vilna delivers a webhook event, the request includes headers that let you verify authenticity, prevent replay attacks, and deduplicate retries.

Webhook headers

HeaderDescription
X-Webhook-SignatureStripe-format HMAC-SHA256 signature: t=<unix-seconds>,v1=<hex> — the timestamp is inside this header as t=
X-Webhook-EventEvent name in dot notation: transaction.detected or transaction.confirmed. Test deliveries reuse the same value — check is_test_message in the body to distinguish them.
X-Webhook-Event-IdUUIDv5 — stable across retries, use for deduplication
X-Webhook-Delivery-IdUUID — changes on every attempt, use for support correlation only

Your webhook signing secret

When you create a webhook notification channel via POST /channels, the response includes a webhook_secret field — a 42-character plaintext string of the form vilna_whsec_ followed by 30 base62 characters. This is your signing secret — store it immediately in a secure secrets manager. It is only returned once on creation and once when you rotate it via POST /channels/{channel_id}/webhook-secrets/actions/rotate.

Secret shown only once

The webhook secret is unique per notification channel and is only returned at creation time and on explicit rotation. It is never shown in subsequent reads. Store it in a secure secrets manager right away.

Two read-only endpoints let you inspect and manage the channel's secrets without exposing plaintext:

Verifying the signature

The X-Webhook-Signature header has the format:

t=1714262800,v1=5257a8696bc5...

During secret rotation, two v1= entries are present simultaneously:

t=1714262800,v1=5257a8696bc5...,v1=8a7d4f3e1c9b...

The signed payload is ${t}.${rawBody} — the timestamp value from inside the header, a literal dot, and the raw request body bytes.

import crypto from "node:crypto";

function verifyWebhookSignature(
  rawBody: Buffer,
  signatureHeader: string,
  secret: string
): boolean {
  // Parse the header: t=...,v1=...[,v1=...]
  const parts = signatureHeader.split(",");
  const tEntry = parts.find((p) => p.startsWith("t="));
  const v1Values = parts
    .filter((p) => p.startsWith("v1="))
    .map((p) => p.slice(3));

  if (!tEntry || v1Values.length === 0) return false;

  const t = tEntry.slice(2);

  // Reject events outside a 5-minute window (anti-replay)
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(t, 10)) > 300) return false;

  // Compute expected HMAC over raw bytes — never re-serialize parsed JSON
  const payload = Buffer.concat([
    Buffer.from(t),
    Buffer.from("."),
    rawBody,
  ]);
  const expected = crypto
    .createHmac("sha256", secret)
    .update(payload)
    .digest("hex");

  const expectedBuf = Buffer.from(expected, "utf8");

  // Accept if any v1= value matches (covers dual-secret rotation window)
  return v1Values.some((v1) => {
    if (v1.length !== expected.length) return false;
    return crypto.timingSafeEqual(Buffer.from(v1, "utf8"), expectedBuf);
  });
}

Deduplicating events

event_id appears in both the body and the X-Webhook-Event-Id header. The body copy is covered by the HMAC, so it is tamper-evident; the header copy is a convenience for routing and filtering without parsing the body. Either works as your dedup key.

// After verifying the signature, parse the body
const payload = JSON.parse(rawBody.toString("utf8"));
const eventId = payload.event_id; // same as X-Webhook-Event-Id header

if (await db.webhookEvents.exists(eventId)) {
  return res.status(200).send("duplicate, skipped");
}

await db.webhookEvents.insert(eventId);
// ... process the event

Required client behaviors

The verification function above covers most of these inline; this checklist exists so you can audit your integration against the full list.

  1. Use a timing-safe comparison (crypto.timingSafeEqual, hmac.compare_digest, or equivalent). Plain === leaks how many characters matched.
  2. Sign the raw request bytes. In Express that means express.raw({ type: "application/json" }); in other frameworks read the body buffer before any JSON parsing. A re-serialized parsed object will not byte-match.
  3. Deduplicate by event_id. Retries reuse the same value.
  4. Check the timestamp window. Reject events outside ±300 seconds. Do not set tolerance to zero — clock skew between servers will cost you legitimate deliveries.
  5. Ignore unknown signature versions. Only act on v1=. Future versions (v2= etc.) will arrive alongside v1=, not instead of it. Fail the delivery only when no known version validates.
  6. Rotate when team access changes. When someone with access to the secret leaves, rotate immediately — the 24-hour overlap lets you redeploy without downtime, and you can end it early once every receiver has the new secret. See the endpoints listed in Your webhook signing secret above.

Why HMAC and not asymmetric signatures?

A common question: why does Vilna use a shared HMAC secret instead of signing webhooks with an asymmetric private key (Ed25519, RSA) and giving you a public key to verify?

HMAC is the industry standard for webhook authentication — Stripe, GitHub, Twilio, Slack, and the Standard Webhooks specification all default to HMAC. The reasons are practical:

  • Performance. HMAC-SHA256 is 10–100× faster than Ed25519 or RSA signing. At webhook-delivery scale this difference is meaningful and translates directly into lower latency and infrastructure cost.
  • Operational simplicity. A shared secret has no key-distribution problem, no public-key discovery endpoint, and no certificate-style rotation choreography. You receive one string at channel creation and store it.
  • Developer familiarity. The verification code is the same pattern your team already uses for other webhook providers — no new cryptographic primitives to learn.

The trade-off is that HMAC does not provide non-repudiation: anyone who possesses the shared secret can produce a valid signature, so the signature alone cannot prove that Vilna — and only Vilna — sent a given request. This matters in exactly one scenario: a malicious insider on your side (for example, a former employee who retained the secret) sends a forged request to your own webhook endpoint and you treat it as authoritative.

The correct defense against that scenario is never to treat a webhook as the source of truth. Webhooks are notifications — your handler should re-verify the underlying event by querying the Vilna API or the blockchain directly before taking any irreversible action (releasing funds, marking an invoice paid, granting access). With that pattern in place, a forged webhook fails verification at the next step regardless of who produced its signature.

If your use case requires cryptographic non-repudiation (regulated financial settlement, formal arbitration, signed delivery receipts), let us know — we can add Ed25519 as an opt-in second signature scheme alongside v1= (as v2= in the same X-Webhook-Signature header) without breaking the HMAC path.

Security best practices

Never expose keys in frontend code. API keys grant full access to your project resources. Keep them on the server side only.

Use environment variables. Store credentials in environment variables or a secrets manager, not in source code.

export VILNA_API_KEY="your-api-key"
const client = createVilnaClient({
  apiKey: process.env.VILNA_API_KEY!,
});

Rotate keys periodically. If a key is compromised, revoke it immediately through the dashboard or Management API and create a new one.

Permissions are fixed at creation. Each key is issued with a frozen permission set. There is no endpoint to change a key's permissions after the fact — if you need to broaden or narrow access, revoke the key and create a new one with the desired permissions.

Use separate keys per environment. Maintain different API keys for development, staging, and production so that a leak in one environment does not affect the others.

Restrict network access. Where possible, allow-list the IP addresses that your server uses to call the Vilna API.

Error responses

All authentication and authorization errors follow the RFC 7807 application/problem+json format:

StatusMeaning
401Missing or invalid API key
403Valid key but insufficient permissions
{
  "type": "https://docs.vilna.io/errors/forbidden",
  "title": "Forbidden",
  "status": 403,
  "detail": "Your API key does not have access to this resource."
}

For full details on error handling, see the Platform API.

Further reading