Webhooks

Configure webhooks in Engine to notify your backend server of transaction or backend wallet events.

Supported events

Transactions

Handle when a blockchain transaction is sent and mined onchain.

EventDescription
sent_transactionA transaction is submitted to RPC. A transaction hash is provided, but it may not be mined onchain yet.
mined_transactionA transaction is mined on the blockchain. Note: The transaction may have reverted onchain. Check the onchainStatus field to confirm if the transaction was successful.
errored_transactionA transaction is unable to be submitted. There may be an error in the transaction params, backend wallet, or server.
all_transactionAll the above events.
Webhooks lifecycle

The transaction payload contains a status field which is one of: sent, mined, errored

Depending on the transaction, your backend will receive one of these webhook sequences:

Transaction statusesDescription
sent + minedThe transaction was sent and mined onchain.
erroredThe transaction errored before being sent. Common reasons: The transaction failed simulation, the backend wallet is out of funds, the network is down, or another internal error, transaction with a gas ceiling timed out. The errorMessage field will contain further details.
sent + erroredThe transaction was sent but was not mined onchain after some duration. Common reasons: The transaction was dropped from RPC mempool, another transaction was mined with the same nonce, or the nonce was too far ahead of the onchain nonce.
cancelledThe transaction was in queue (not sent yet) and cancelled.
sent + cancelledThe transaction was sent and waiting to be mined, but cancelled.

Note: Webhooks may come out of order. Treat later statuses as higher priority than earlier ones. For example if your backend receives a sent webhook after a mined webhook, treat this transaction as mined.

Wallets

EventDescription
backend_wallet_balanceA backend wallet's balance is below minWalletBalance. To read or update this value, call GET/POST /configuration/backend-wallet-balance.

Setup

Create a webhook

  • Visit the Engine dashboard and select your Engine.
  • Select the Configuration tab.
  • Select Create Webhook.

Webhook URLs must start with https://.

Payload format

Method: POST

Headers:

  • Content-Type: application/json
  • X-Engine-Signature: <payload signature>
  • X-Engine-Timestamp: <Unix timestamp in seconds>

Since any outside origin can call your webhook endpoint, it is recommended to verify the webhook signature to ensure a request comes from your Engine instance.

Check the signature

The payload body is signed with the webhook secret and provided in the X-Engine-Signature request header.

Get the webhook secret for your webhook endpoint from the dashboard.

This code example checks if the signature is valid:

const generateSignature = (
body: string,
timestamp: string,
secret: string,
): string => {
const payload = `${timestamp}.${body}`;
return crypto
.createHmac("sha256", secret)
.update(payload)
.digest("hex");
};
const isValidSignature = (
body: string,
timestamp: string,
signature: string,
secret: string,
): boolean => {
const expectedSignature = generateSignature(
body,
timestamp,
secret,
);
return crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(signature),
);
};

Check the timestamp

The event timestamp is provided in the X-Engine-Timestamp request header.

This code example checks if the event exceeds a given expiration duration:

export const isExpired = (
timestamp: string,
expirationInSeconds: number,
): boolean => {
const currentTime = Math.floor(Date.now() / 1000);
return currentTime - parseInt(timestamp) > expirationInSeconds;
};

Example webhook endpoint

This NodeJS code example listens for event notifications on the /webhook endpoint:

import express from "express";
import bodyParser from "body-parser";
import { isValidSignature, isExpired } from "./webhookHelper";
const app = express();
const port = 3000;
const WEBHOOK_SECRET = "<your_webhook_auth_secret>";
app.use(bodyParser.text());
app.post("/webhook", (req, res) => {
const signatureFromHeader = req.header("X-Engine-Signature");
const timestampFromHeader = req.header("X-Engine-Timestamp");
if (!signatureFromHeader || !timestampFromHeader) {
return res
.status(401)
.send("Missing signature or timestamp header");
}
if (
!isValidSignature(
req.body,
timestampFromHeader,
signatureFromHeader,
WEBHOOK_SECRET,
)
) {
return res.status(401).send("Invalid signature");
}
if (isExpired(timestampFromHeader, 300)) {
// Assuming expiration time is 5 minutes (300 seconds)
return res.status(401).send("Request has expired");
}
// Process the request
res.status(200).send("Webhook received!");
});
app.listen(port, () => {
console.log(`Server started on http://localhost:${port}`);
});