# Guide to Utila Webhooks: Features and Best Practices

Utila webhooks notify your systems about digital-asset activity: a transaction is created or changes state, a wallet or wallet address is created, an AML screening result is ready. If you're building on Utila's custody/wallet platform, webhooks are how you react to on-chain and account events without polling.

This guide covers how Utila webhooks work, the events you'll handle, how to verify the RSA signature (asymmetric crypto, not HMAC), the thin payloads, and the best practices for production.

## What are Utila webhooks?

Utila webhooks are HTTP POSTs delivered to endpoints you configure in the Console. Each is signed with an asymmetric RSA signature, not an HMAC. The `x-utila-signature` header carries an RSA-4096 signature computed with SHA-512 and PSS padding, base64-encoded, which you verify with Utila's PEM-encoded RSA public key. There's no shared secret and no timestamp header (so no built-in replay protection).

## Utila webhook features

| Feature | Details |
| --- | --- |
| Configuration | Console > Vault Settings > Webhooks |
| Signature header | `x-utila-signature` (base64) |
| Signature scheme | RSA-4096, SHA-512, PSS padding, over the raw body |
| Verification key | Utila's PEM RSA-4096 public key (from the Console); no shared secret |
| Replay protection | None (no timestamp header) |
| Payload | Thin, fetch full detail via the Stream/API |
| Retries | Exponential backoff up to 24h, then discarded; return HTTP 200 |
| SDK | `@utila/api` is documented but does not currently resolve on the public registry |

## Common events

Utila has exactly five events, in SCREAMING_SNAKE_CASE:

| Event | Fires when |
| --- | --- |
| `TRANSACTION_CREATED` | A transaction is created |
| `TRANSACTION_STATE_UPDATED` | A transaction changes state |
| `WALLET_CREATED` | A wallet is created |
| `WALLET_ADDRESS_CREATED` | A wallet address is created |
| `TRANSACTION_AML_SCREENING_RESULT_READY` | An AML screening result is ready |

Payloads are thin, use the identifiers to fetch full detail via the Utila Stream/API.

## Setting up Utila webhooks

Create webhooks in the Console under Vault Settings > Webhooks, and copy Utila's PEM-encoded RSA-4096 public key from the webhook settings, that's what you verify with. There's no shared secret to store.

## Securing Utila webhooks

The `x-utila-signature` header is a base64 RSA signature over the raw body, computed with RSA-4096, SHA-512, and PSS padding. Verify it with Utila's public key.

```javascript
const crypto = require("crypto");

const PUBLIC_KEY = process.env.UTILA_PUBLIC_KEY; // PEM RSA-4096 from the Console

function verify(rawBody, signatureB64) {
  return crypto.verify(
    "sha512",
    rawBody, // raw body bytes
    {
      key: PUBLIC_KEY,
      padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
      saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST,
    },
    Buffer.from(signatureB64 || "", "base64")
  );
}

app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
  if (!verify(req.body, req.headers["x-utila-signature"])) {
    return res.sendStatus(401);
  }

  res.sendStatus(200); // acknowledge fast
  processQueue.add(JSON.parse(req.body)); // fetch full detail via API, async
});

```

The same check in Python (using `pycryptodome`):

```python
import base64
import os
from Crypto.PublicKey import RSA
from Crypto.Signature import pss
from Crypto.Hash import SHA512

PUBLIC_KEY = RSA.import_key(os.environ["UTILA_PUBLIC_KEY"])

def verify(raw_body: bytes, signature_b64: str) -> bool:
    signature = base64.b64decode(signature_b64)
    h = SHA512.new(raw_body)
    try:
        pss.new(PUBLIC_KEY).verify(h, signature)
        return True
    except (ValueError, TypeError):
        return False

```

## Utila webhook limitations and pain points

### RSA-PSS with SHA-512, not HMAC

The Problem: Verification is asymmetric (RSA-4096, SHA-512, PSS padding) against Utila's public key. There's no shared secret, so HMAC logic doesn't apply, and PSS padding must be set explicitly (the default RSA padding won't verify).

Why It Happens: Utila signs with an RSA private key and PSS padding.

Workarounds:

* Verify with the public key, SHA-512, and PSS padding (salt length = digest length), over the raw body.

How Hookdeck Can Help: Hookdeck verifies the RSA-PSS signature at the edge, so your application receives pre-verified events without embedding asymmetric crypto and padding config in your handler.

### No timestamp, so no replay window

The Problem: There's no timestamp header, so a captured, validly signed payload can be replayed and it will still verify.

Why It Happens: Utila's scheme signs the body without a timestamp.

Workarounds:

* Make processing idempotent and dedupe on a transaction/event identifier.

How Hookdeck Can Help: Hookdeck deduplicates deliveries at the edge, filtering replays. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

### Thin payloads

The Problem: Payloads are minimal, so you fetch full detail from the Stream/API for anything useful.

Why It Happens: Utila keeps payloads small.

Workarounds:

* Acknowledge fast, then fetch detail via the API off a queue.

How Hookdeck Can Help: Hookdeck durably queues each event so your worker fetches detail at a controlled rate.

### Failure means discard

The Problem: Failed deliveries retry with exponential backoff up to 24 hours, then are discarded. A longer outage loses events.

Why It Happens: Utila bounds retries at 24 hours.

Workarounds:

* Keep the endpoint healthy and reconcile via the API for anything critical.

How Hookdeck Can Help: Hookdeck accepts and durably stores deliveries, retrying to your downstream on a schedule you control, so a 24-hour ceiling isn't your only safety net.

## Best practices

### Verify RSA-PSS/SHA-512 with the public key

Verify the base64 `x-utila-signature` with Utila's public key, SHA-512, and PSS padding over the raw body.

### Dedupe (there's no replay window)

Since there's no timestamp, dedupe on a transaction/event id so replays are no-ops.

### Acknowledge fast, fetch via the API

Return 200 immediately and fetch full detail off a queue. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### Reconcile for critical events

Reconcile via the API since deliveries are discarded after 24 hours of failure.

## Conclusion

Utila webhooks are signed with RSA-4096, SHA-512, and PSS padding, verified against Utila's public key over the raw body, with no shared secret and no timestamp (so no replay window). Set PSS padding explicitly, dedupe to guard against replays, acknowledge fast, and reconcile via the API for critical events.

[Hookdeck](https://hookdeck.com) verifies the RSA-PSS signature, deduplicates, and durably queues every event at the edge, so your app only ever processes verified, unique events.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Utila webhooks reliably in minutes.