Webhooks

Webhooks are a powerful feature that enables you to stay up-to-date with events in Utila without the need for constant polling. Instead, we send data directly to your application when relevant events occur such as deposit to specific wallets, transaction state change, and wallet creation.

Webhook Creation

  1. In the Console navigate to Vault Settings → Webhooks.
  2. Click on Add Webhook.
  3. Provide a display name for your webhook.
  4. Provide the URL where you want to receive webhook data. This is where we'll send notifications when the selected events occur.
    You can test the endpoint by clicking on Test - this will send a test event to your endpoint.
  5. Select the events you want to be notified about. For example, you can choose to receive notifications when a new transaction is created.
  6. Click Add to save the webhook.

Webhook Event Payload

When an event occurs, we'll send you a JSON payload containing the relevant information about the event.
Here are example messages of what you might receive:

Transaction Created

{
  "id": "94ea3ce9-fb6d-41f3-91aa-8c42f17df1f6",
  "vault": "vaults/3bf247bc8ee2c",
  "type": "TRANSACTION_CREATED",
  "resourceType": "TRANSACTION",
  "resource": "vaults/3bf247bc8ee2c/transactions/b6e3cd32e827"
}

Transaction State Updated

{
  "id": "9c683b37-bfb6-42f4-bcaa-1a6f46d3b6f8",
  "vault": "vaults/3bf247bc8ee2c",
  "type": "TRANSACTION_STATE_UPDATED",
  "details": {
    "transactionStateUpdated": {
      "newState": "CONFIRMED"
    }
  },
  "resourceType": "TRANSACTION",
  "resource": "vaults/3bf247bc8ee2c/transactions/b6e3cd2e8217"
}

Wallet Created

{
  "id": "94ea3ce9-fb6d-41f3-91aa-8c42f17df1f6",
  "vault": "vaults/3bf247bc8ee2c",
  "type": "WALLET_CREATED",
  "resourceType": "WALLET",
  "resource": "vaults/3bf247bc8ee2c/wallets/b6e3cd32e827"
}

Wallet Address Created

{
  "id": "94ea3ce9-fb6d-41f3-91aa-8c42f17df1f6",
  "vault": "vaults/3bf247bc8ee2c",
  "type": "WALLET_ADDRESS_CREATED",
  "resourceType": "WALLET_ADDRESS",
  "resource": "vaults/3bf247bc8ee2c/wallets/b6e3cd32e827/addresses/4340c6919558"
}

Webhook Security

Here are some important requirements and guidelines to keep your webhook integration secure:

  • Use HTTPS in your webhook endpoint, to ensure data is encrypted in transit.
  • Validate the events using the signature attached in the request header:
    • A signature on the event payload is attached in a request header named x-utila-signature.
    • The signature is computed using RSA-4096 encryption, SHA512 hash and PSS padding, and encoded in base64.
    • The signature can be validated using Utila's webhook public key:
      -----BEGIN PUBLIC KEY-----
      MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAulI1XPGRDFcymdf2zXvD
      spfdTXA1g0NOavZ50+AtcQP7f+KTpXoO1bkr6x9dO2Jq8FHImRT1sbhKhcNXT4WC
      dLSa/2Zh60QE3tp9d51o1XDSnzRMwcGbFyJ7C30DVpVEIwqD2Z5GRlzXinqIeVdY
      GOubuVol/wOAynS32DX+6y2PiqbYj7P84csBOgpNT27Mc6InEqKb7LWQtU8LPttx
      tfyceOPXE5G4h+UujPsPG6WN5MHHVbP9r6oneEF3knbfL3hCJRjwV9HfTtG6JyYr
      25Dy6SOCphrlEZi8IGcKxL6fEMetDGGVCjm7XfHyt6fYoUonD9lZsvbSyUsRwf/1
      +x77F2LxtzQyvMJR9jD16WUyzm+fUBSVQixxKnKSrVkeLqkmGboDTY5kw3doSVTP
      zcGDzWkzqC3lgwRLnSg4J+koQY+yo9jYBbFdSp+/PfVmp9NEaBuCV63mp/85VWIh
      1FRYe6lEdGZWdmIcbDvNYU/Cui/yGZoID7+sJJq/rWN0Qxx/0skEaT/083+iYLVA
      QNLvWtmQfgNKPm6GeQknRUEWyWUJtq6ANeP/8hGVM1G/edOdLn+KfhXZvw41O5z1
      uKHEqHIV+NaCNnFbDj924bJhA/fWNKxYv7/Nm44Wy1nXlgqdHiFkSqtjUBPmzE/n
      yj92azWBq1RbGHY+9/POguMCAwEAAQ==
      -----END PUBLIC KEY-----
      
    • For more info, see the example below in the Webhook Example section.

Webhook Response

The webhook endpoint is expected to respond with HTTP 200 OK response to confirm the webhook event was received.

Webhook Retries

In case the webhook endpoint does not respond with a successful acknowledgment, we'll make multiple attempts to deliver the event using an exponential back-off mechanism (which means the time between each attempt gradually increases) for up to 24 hours, after which the event will be considered undeliverable and be discarded.

This retry mechanism ensures that even if there are temporary issues on your end, you have a 24-hour window to receive the event data successfully. It's designed to make your webhook integration as reliable as possible.

Webhook Example

The following code snippet is a webhook function configured to parse the event, handle it and return a 200 response upon successful handling.

import base64
from Cryptodome.PublicKey import RSA
from Cryptodome.Signature import pss
from Cryptodome.Hash import SHA512
from flask import Flask, request


app = Flask(__name__)

@app.route('/webhook', methods=['POST'])
def webhook():
    # Check if the request contains JSON data
    if request.is_json:
        data = request.get_json()

        if not verify_signature(request.headers.get('x-utila-signature'), request.data, utila_public_key):
            return 'Invalid signature', 400

        # Check the event type
        event_type = data.get("type")

        if event_type:
            # Process based on the event type
            if event_type == "TRANSACTION_CREATED":
                # handle_transaction_created(data)
            elif event_type == "TRANSACTION_STATE_UPDATED":
                # handle_transaction_state_updated(data)
            else:
                # handle_unknown_event(data)

            # Respond with a success status code (HTTP 200 OK)
            return '', 200

    # If the request doesn't contain valid JSON data or an unknown event type, respond with an error
    return 'Invalid request data or event type', 400

def verify_signature(signature_base64, data, public_key_pem):
    try:
        # Load the public key from its PEM-encoded representation
        public_key = RSA.import_key(public_key_pem)

        # Decode the base64-encoded signature
        signature = base64.b64decode(signature_base64)

        # Create a SHA-512 hash object and update it with the data
        h = SHA512.new(data)

        # Verify the signature
        pss.new(public_key).verify(h, signature)

        return True
    except (ValueError, TypeError, AssertionError) as e:
        print(f"Signature verification failed: {str(e)}")
        return False

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)
import { createVerify, constants } from "crypto";
import express from "express";

const publicKey = `...`;
const app = express();
const port = 5000;

function verifyUtilaSignatureMiddleware(req, res, next) {
  let data = "";

  req.on("data", (chunk) => (data += chunk));

  req.on("end", () => {
    try {
      const isValid = verifySignature(
        req.headers["x-utila-signature"],
        data,
        publicKey
      );

      if (!isValid) {
        throw new Error("Signature verification failed");
      }

      req.body = JSON.parse(data);
      next();
    } catch (e) {
      res.status(401).send({ error: e.message });
    }
  });
}

app.post("/my-webhook", verifyUtilaSignatureMiddleware, (req, res) => {
  console.log(req.body);

  if (req.body.type === "TEST") {
    // Do something when the webhook type is TEST
  }

  return res.json({ success: true });
});

function verifySignature(signatureBase64, data, publicKey) {
  try {
    const signatureBuffer = Buffer.from(signatureBase64, "base64");
    const verifier = createVerify("RSA-SHA512");

    verifier.update(data);

    const isValid = verifier.verify(
      {
        key: publicKey,
        padding: constants.RSA_PKCS1_PSS_PADDING,
        saltLength: constants.RSA_PSS_SALTLEN_DIGEST,
      },
      signatureBuffer
    );

    return isValid;
  } catch (error) {
    return false;
  }
}

app.listen(port, () => {
  console.log(`Webhook server listening at http://localhost:${port}`);
});