Webhooks & Notifications

Overview

Webhooks allow you to get notifications for events that happen on your Fireblocks workspace such as:

  • Incoming/outgoing transactions
  • Transactions status updates
  • Newly added Vault account, Contract wallet, Internal wallet, or External wallet.

You can 'listen' for events that occur in your workspace at your chosen URL, where all event types will be broadcasted.

Configuring Webhook URLs

To configure URLs for webhook notifications, follow these steps in the Fireblocks Console:

  1. Navigate to Settings > General, then scroll down to the Configure Webhook URL heading and select Manage URLs.
  2. In the Configure Webhook URL window, enter a URL to define the HTTPS endpoint, then press Enter.
  3. Select Save

📘

Webhook URL requirement

Each webhook URL must be a complete, globally available HTTPS address (e.g., https://example.com).

Once your Webhook is connected to your Fireblocks workspace, you will start receiving notifications on events in that workspace.

Receiving Webhook Notifications

Fireblocks sends events to your webhook endpoint URL(s) associated with the workspace, as part of a POST request with a JSON payload.

Validation

You can validate Fireblocks webhook events by validating the signature attached in the request header:

Fireblocks-Signature: Base64(RSA512(_WEBHOOK_PRIVATE_KEY_, SHA512(eventBody)))


To validate the signature in Production workspaces, please use the public key below:

-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0+6wd9OJQpK60ZI7qnZG
jjQ0wNFUHfRv85Tdyek8+ahlg1Ph8uhwl4N6DZw5LwLXhNjzAbQ8LGPxt36RUZl5
YlxTru0jZNKx5lslR+H4i936A4pKBjgiMmSkVwXD9HcfKHTp70GQ812+J0Fvti/v
4nrrUpc011Wo4F6omt1QcYsi4GTI5OsEbeKQ24BtUd6Z1Nm/EP7PfPxeb4CP8KOH
clM8K7OwBUfWrip8Ptljjz9BNOZUF94iyjJ/BIzGJjyCntho64ehpUYP8UJykLVd
CGcu7sVYWnknf1ZGLuqqZQt4qt7cUUhFGielssZP9N9x7wzaAIFcT3yQ+ELDu1SZ
dE4lZsf2uMyfj58V8GDOLLE233+LRsRbJ083x+e2mW5BdAGtGgQBusFfnmv5Bxqd
HgS55hsna5725/44tvxll261TgQvjGrTxwe7e5Ia3d2Syc+e89mXQaI/+cZnylNP
SwCCvx8mOM847T0XkVRX3ZrwXtHIA25uKsPJzUtksDnAowB91j7RJkjXxJcz3Vh1
4k182UFOTPRW9jzdWNSyWQGl/vpe9oQ4c2Ly15+/toBo4YXJeDdDnZ5c/O+KKadc
IMPBpnPrH/0O97uMPuED+nI6ISGOTMLZo35xJ96gPBwyG5s2QxIkKPXIrhgcgUnk
tSM7QYNhlftT4/yVvYnk0YcCAwEAAQ==
-----END PUBLIC KEY-----


To validate the signature in Sandbox workspaces, please use the public key below:

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw+fZuC+0vDYTf8fYnCN6
71iHg98lPHBmafmqZqb+TUexn9sH6qNIBZ5SgYFxFK6dYXIuJ5uoORzihREvZVZP
8DphdeKOMUrMr6b+Cchb2qS8qz8WS7xtyLU9GnBn6M5mWfjkjQr1jbilH15Zvcpz
ECC8aPUAy2EbHpnr10if2IHkIAWLYD+0khpCjpWtsfuX+LxqzlqQVW9xc6z7tshK
eCSEa6Oh8+ia7Zlu0b+2xmy2Arb6xGl+s+Rnof4lsq9tZS6f03huc+XVTmd6H2We
WxFMfGyDCX2akEg2aAvx7231/6S0vBFGiX0C+3GbXlieHDplLGoODHUt5hxbPJnK
IwIDAQAB
-----END PUBLIC KEY-----

Check event objects

Each event is structured as an object with a type and data sub object that holds the transaction id, as well as other related information. Your endpoint must check the event type and parse the payload of each event.

Example: transaction created webhook
{
    "type": "TRANSACTION_CREATED",
    "tenantId”:  ".........-.....-....-....-...........",
    "timestamp": 1679651214621,
    "data": {
        "id": "........-....-....-....-............",
        "createdAt": 1679651104380,
        "lastUpdated": 1679651104380,
        "assetId": "WETH_TEST3",
        "source": {
            "id": "0",
            "type": "VAULT_ACCOUNT",
            "name": "Main",
            "subType": ""
        },
        "destination": {
            "id": "12",
            "type": "VAULT_ACCOUNT",
            "name": "MintBurn",
            "subType": ""
        },
        "amount": 0.001,
        "sourceAddress": "",
        "destinationAddress": "",
        "destinationAddressDescription": "",
        "destinationTag": "",
        "status": "SUBMITTED",
        "txHash": "",
        "subStatus": "",
        "signedBy": [],
        "createdBy": ".........-.....-....-....-...........",
        "rejectedBy": "",
        "amountUSD": null,
        "addressType": "",
        "note": "",
        "exchangeTxId": "",
        "requestedAmount": 0.001,
        "feeCurrency": "ETH_TEST3",
        "operation": "TRANSFER",
        "customerRefId": null,
        "amountInfo": {
            "amount": "0.001",
            "requestedAmount": "0.001"
        },
        "feeInfo": {},
        "destinations": [],
        "externalTxId": null,
        "blockInfo": {},
        "signedMessages": [],
        "assetType": "ERC20"
    }
}

Example: transaction status updated webhook
{
    "type": "TRANSACTION_STATUS_UPDATED",
    "tenantId”:  ".........-.....-....-....-...........",
    "timestamp": 1679651214621,
    "data": {
        "id": "........-....-....-....-............",
        "createdAt": 1680014718734,
        "lastUpdated": 1680014729691,
        "assetId": "TRX_USDT",
        "source": {
            "id": "",
            "type": "UNKNOWN",
            "name": "External",
            "subType": ""
        },
        "destination": {
            "id": "2107",
            "type": "VAULT_ACCOUNT",
            "name": "Main",
            "subType": ""
        },
        "amount": 370,
        "networkFee": 27.2559,
        "netAmount": 370,
        "sourceAddress": "",
        "destinationAddress": "",
        "destinationAddressDescription": "",
        "destinationTag": "",
        "status": "COMPLETED",
        "txHash": "e9e1asdade125be06638c8675fdsfsdc79594dd45ff095b7683c3f03b81a9389684",
        "subStatus": "CONFIRMED",
        "signedBy": [],
        "createdBy": "",
        "rejectedBy": "",
        "amountUSD": 0,
        "addressType": "",
        "note": "",
        "exchangeTxId": "",
        "requestedAmount": 370,
        "feeCurrency": "TRX",
        "operation": "TRANSFER",
        "customerRefId": "........-....-....-....-............",
        "numOfConfirmations": 1,
        "amountInfo": {
            "amount": "370",
            "requestedAmount": "370",
            "netAmount": "370",
            "amountUSD": null
        },
        "feeInfo": {
            "networkFee": "27.2559"
        },
        "destinations": [],
        "externalTxId": null,
        "blockInfo": {
            "blockHeight": "49800684",
            "blockHash": "0000000002f7e5ece07efd8dfnjngfh76dda5f2645a9aba5e6h4534ba1bc7d97a8e2"
        },
        "signedMessages": [],
        "amlScreeningResult": {
            "screeningStatus": "BYPASSED",
            "bypassReason": "PASSED_BY_POLICY",
            "timestamp": 1680014729455
        },
        "assetType": "TRON_TRC20"
    }
}

Response

The Fireblocks server will look for a response to confirm the webhook notification was received. All webhook events should receive an HTTP-200 (OK) response.

If no response is received, Fireblocks will resend the request several more times, the retry schedule (in seconds) is 15, 45, 105, 225, 465, 945, 1905, 3825, 7665, and 15345.

Code Examples

const crypto = require("crypto");
const express = require("express");
const bodyParser = require('body-parser')

const port = 3000;

const publicKey = `-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0+6wd9OJQpK60ZI7qnZG
jjQ0wNFUHfRv85Tdyek8+ahlg1Ph8uhwl4N6DZw5LwLXhNjzAbQ8LGPxt36RUZl5
YlxTru0jZNKx5lslR+H4i936A4pKBjgiMmSkVwXD9HcfKHTp70GQ812+J0Fvti/v
4nrrUpc011Wo4F6omt1QcYsi4GTI5OsEbeKQ24BtUd6Z1Nm/EP7PfPxeb4CP8KOH
clM8K7OwBUfWrip8Ptljjz9BNOZUF94iyjJ/BIzGJjyCntho64ehpUYP8UJykLVd
CGcu7sVYWnknf1ZGLuqqZQt4qt7cUUhFGielssZP9N9x7wzaAIFcT3yQ+ELDu1SZ
dE4lZsf2uMyfj58V8GDOLLE233+LRsRbJ083x+e2mW5BdAGtGgQBusFfnmv5Bxqd
HgS55hsna5725/44tvxll261TgQvjGrTxwe7e5Ia3d2Syc+e89mXQaI/+cZnylNP
SwCCvx8mOM847T0XkVRX3ZrwXtHIA25uKsPJzUtksDnAowB91j7RJkjXxJcz3Vh1
4k182UFOTPRW9jzdWNSyWQGl/vpe9oQ4c2Ly15+/toBo4YXJeDdDnZ5c/O+KKadc
IMPBpnPrH/0O97uMPuED+nI6ISGOTMLZo35xJ96gPBwyG5s2QxIkKPXIrhgcgUnk
tSM7QYNhlftT4/yVvYnk0YcCAwEAAQ==
-----END PUBLIC KEY-----`.replace(/\\n/g, "\n");

const app = express();

app.use(bodyParser.json());

app.post("/webhook", (req, res) => {
    const message = JSON.stringify(req.body);
    const signature = req.headers["fireblocks-signature"];

    const verifier = crypto.createVerify('RSA-SHA512');
    verifier.write(message);
    verifier.end();

    const isVerified = verifier.verify(publicKey, signature, "base64");
    console.log("Verified:", isVerified);

    res.send("ok");
});

app.listen(port, () => {
    console.log(`Webhook running at http://localhost:${port}`);
});
import falcon
import json
import rsa
import base64

FIREBLOCKS_PUBLIC_KEY = """
-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0+6wd9OJQpK60ZI7qnZG
jjQ0wNFUHfRv85Tdyek8+ahlg1Ph8uhwl4N6DZw5LwLXhNjzAbQ8LGPxt36RUZl5
YlxTru0jZNKx5lslR+H4i936A4pKBjgiMmSkVwXD9HcfKHTp70GQ812+J0Fvti/v
4nrrUpc011Wo4F6omt1QcYsi4GTI5OsEbeKQ24BtUd6Z1Nm/EP7PfPxeb4CP8KOH
clM8K7OwBUfWrip8Ptljjz9BNOZUF94iyjJ/BIzGJjyCntho64ehpUYP8UJykLVd
CGcu7sVYWnknf1ZGLuqqZQt4qt7cUUhFGielssZP9N9x7wzaAIFcT3yQ+ELDu1SZ
dE4lZsf2uMyfj58V8GDOLLE233+LRsRbJ083x+e2mW5BdAGtGgQBusFfnmv5Bxqd
HgS55hsna5725/44tvxll261TgQvjGrTxwe7e5Ia3d2Syc+e89mXQaI/+cZnylNP
SwCCvx8mOM847T0XkVRX3ZrwXtHIA25uKsPJzUtksDnAowB91j7RJkjXxJcz3Vh1
4k182UFOTPRW9jzdWNSyWQGl/vpe9oQ4c2Ly15+/toBo4YXJeDdDnZ5c/O+KKadc
IMPBpnPrH/0O97uMPuED+nI6ISGOTMLZo35xJ96gPBwyG5s2QxIkKPXIrhgcgUnk
tSM7QYNhlftT4/yVvYnk0YcCAwEAAQ==
-----END PUBLIC KEY-----
"""

signature_pub_key = rsa.PublicKey.load_pkcs1_openssl_pem(FIREBLOCKS_PUBLIC_KEY)

class RequestBodyMiddleware(object):
    def process_request(self, req, resp):
        req.body = req.bounded_stream.read()

class AuthMiddleware(object):
    def process_request(self, req, resp):
        signature = req.get_header('Fireblocks-Signature')

        if signature is None:
            raise falcon.HTTPUnauthorized('Signature required')

        if not self._signature_is_valid(req.body, signature):
            raise falcon.HTTPUnauthorized('Invalid signature')

    def _signature_is_valid(self,  body, signature):
        try:
            hashing_alg = rsa.verify(body, base64.b64decode(signature), signature_pub_key)
            return hashing_alg == "SHA-512"
        except rsa.pkcs1.VerificationError:
            return False

class DummyRequest(object):
    def on_post(self, req, resp):
        obj = json.loads(req.body.decode("utf-8"))
        print(obj)
        resp.status = falcon.HTTP_201


# Create falcon app
app = falcon.API(
    middleware=[
        RequestBodyMiddleware(),
        AuthMiddleware()
    ]
)

app.add_route('/webhook', DummyRequest())


if __name__ == '__main__':
    from wsgiref import simple_server  # NOQA
    httpd = simple_server.make_server('127.0.0.1', 8000, app)
    httpd.serve_forever()

🚧

Warning - Reference only

These examples are not production-ready and are used only for reference.

Please follow our security guidelines for secure API interaction.

Resending Notifications

Resend all failed webhook notifications

The following command re-sends all the failed webhooks pending the backoff retry mechanism being re-sent.


result = fireblocks.resend_webhooks()


const result = await fireblocks.resendWebhooks();

  • The above command returns webhookCount which is the number of re-sent webhook notifications.

Resend missed notifications for a specific transaction

Use the following command to re-send a missing webhook notification per specific transaction:


result = fireblocks.resend_transaction_webhooks_by_id(txId, resend_created, resend_status_updated)


const result = await fireblocks.resendTransactionWebhooksById(txId, resendCreated, resendStatusUpdated);

Returns 200 on success