Using Webhooks
Add a new Secure Webhook
To create a new Secure Webhook go to Blockfrost Dashboard and click on Add webhook button. In the webhook settings, fill in the webhook name, select the trigger event your app needs, and create the necessary Trigger conditions. Do not worry about not getting everything right, you can update the settings anytime.
The webhook endpoint URL should be set to a publicly accessible HTTPS URL, where you will process the events sent via webhook request. If you do not have your backend exposed to the web, follow the steps in Test your webhook to test with a web server running on your local machine.
Rollbacks and a required number of confirmations
Events are sent to your webhook endpoint after reaching the number of confirmations you specified in Secure Webhook Settings.
During a rollback, multiple blocks may be invalidated and replaced simultaneously. This can result in a flurry of events being dispatched to your webhook endpoint in an unpredictable order. Developers should account for this behavior by implementing logic to handle out-of-order delivery of events.
For instance, a notification about a block at a higher height might arrive before one for a lower height due to the asynchronous nature of these requests.
Due to a rollback, you may receive the same event multiple times.
Let's say you set up a Secure Webhook for a transaction event with the number of required confirmations set to 0. The transaction you are interested in gets included in a block so you will be notified about the transaction. Then the block is rolled back and the same transaction is included in a new block. You will receive another transaction event with the same transaction hash, but the hash of the block in which the transaction was included will differ from the first event.
We recommend verifying that the event you received has not been rolled back or increasing the number of required confirmations before the event is sent to your endpoint.
Process a webhook request
Create an HTTP endpoint on your backend where you will process incoming events and trigger actions you want to execute.
The endpoint must accept POST
requests with data in JSON format and return a successful status code (2xx
).
Each request body is an object with several fields. The event type is stored in type
field. The event itself is included in payload
field. For the complete structure of a webhook request, see Events overview.
Don't forget to verify that the event originated from Blockfrost, not a third party just pretending to be Blockfrost. See Check signatures.
Here is a quick example of a web application that implements /webhook
endpoint and leverages the Blockfrost SDK to verify the webhook signature:
Earn a bounty for contributing examples in your favorite language. Everyone can participate and earn rewards for contributions. Join the Blockfrost Bounty Program now!
- Javascript (Node.js)
- Python
- Go
// Example of Node.js Express app with /webhook endpoint
// for processing events sent by Blockfrost Secure Webhooks
const express = require("express");
const blockfrost = require("@blockfrost/blockfrost-js");
const { verifyWebhookSignature, SignatureVerificationError } = blockfrost;
// You will find the webhook secret auth token in the Webhook settings in the Blockfrost Dashboard
const SECRET_AUTH_TOKEN = "WEBHOOK_AUTH_TOKEN";
const app = express();
// Define endpoint /webhook that accepts POST requests
app.post(
"/webhook",
express.json({ type: "application/json" }),
(request, response) => {
const signatureHeader = request.headers["blockfrost-signature"];
// Make sure that Blockfrost-Signature header exists
if (!signatureHeader) {
console.log("The request is missing Blockfrost-Signature header");
return response.status(400).send(`Missing signature header`);
}
// Validate the webhook signature
try {
const event = verifyWebhookSignature(
JSON.stringify(request.body), // Stringified request.body (Note: In AWS Lambda you don't need to call JSON.stringify as event.body is already stringified)
signatureHeader,
SECRET_AUTH_TOKEN,
600 // optional param to customize maximum allowed age of the webhook event, defaults to 600s
);
// Signature is valid
const type = request.body.type;
const payload = request.body.payload;
// Process the incoming event
switch (type) {
case "transaction":
// process Transaction event
console.log(`Received ${payload.length} transactions`);
// loop through the payload (payload is an array of Transaction events)
for (const transaction of payload) {
console.log(`Transaction ${transaction.tx.hash}`);
}
break;
case "block":
// process Block event
console.log(`Received block hash ${payload.hash}`);
break;
case "delegation":
// process Delegation event
console.log(`Received ${payload.length} delegations`);
// loop through the payload, payload is an array of objects with fields: "tx" (an object) and "delegations" (an array)
for (const transaction of payload) {
for (const delegation of transaction.delegations) {
console.log(
`Delegation from address ${delegation.address} included in tx ${transaction.tx.hash}`
);
}
}
break;
case "epoch":
// process Epoch event
console.log(
`Epoch switch from ${payload.previous_epoch.epoch} to ${payload.current_epoch.epoch}`
);
break;
default:
console.warn(`Unexpected event type ${type}`);
break;
}
// Return status code 2xx
response.json({ processed: true });
} catch (error) {
// In case of invalid signature verifyWebhookSignature will throw SignatureVerificationError
// for easier debugging you can access passed signatureHeader and webhookPayload values (error.detail.signatureHeader, error.detail.webhookPayload)
console.error(error);
return response.status(400).send("Signature is not valid!");
}
}
);
app.listen(6666, () => console.log("Running on port 6666"));
# Example of Python Flask app with /webhook endpoint
# for processing events sent by Blockfrost Secure Webhooks
from flask import Flask, request, json
from blockfrost import verify_webhook_signature
# You will find your webhook secret auth token in your webhook settings in the Blockfrost Dashboard
SECRET_AUTH_TOKEN = "SECRET-WEBHOOK-AUTH-TOKEN"
app = Flask(__name__)
# Define endpoint /webhook that accepts POST requests
@app.route('/webhook', methods=['POST'])
def webhook():
if request.method == 'POST':
# Validate webhook signature
request_bytes = request.get_data()
try:
verify_webhook_signature(
request_bytes, request.headers['Blockfrost-Signature'], SECRET_AUTH_TOKEN)
except SignatureVerificationError as e:
# In case of invalid signature verify_webhook_signature will raise SignatureVerificationError
# for easier debugging you can access passed header and request_body values (e.header, e.request_body)
print('Webhook signature is invalid.', e)
return 'Invalid signature', 403
# Get the payload as JSON
event = request.json
print('Received request id {}, webhook_id: {}'.format(
event['id'], event['webhook_id']))
if event['type'] == "transaction":
# process Transaction event
print('Received {} transactions'.format(len(event['payload'])))
# loop through the payload (payload is an array of Transaction events)
for transaction in event['payload']:
print('Transaction id: {}, block: {} ({})'.format(
transaction['tx']['hash'], transaction['tx']['block'], transaction['tx']['block_height']))
elif event['type'] == "block":
# process Block event
print('Received block hash {}'.format(event['payload']['hash']))
elif event['type'] == "delegation":
# process Delegation event
print('Received {} delegations'.format(len(event['payload'])))
# loop through the payload, payload is an array of objects with fields: "tx" (an object) and "delegations" (an array)
for transaction in event['payload']:
for delegation in transaction['delegations']:
print('Delegation from an address {} included in tx {}'.format(
delegation['address'], transaction['tx']['hash'])
)
elif event['type'] == "epoch":
# process Epoch event
print('Epoch switch from {} to {}'.format(
event['payload']['previous_epoch']['epoch'], event['payload']['current_epoch']['epoch']))
else:
# Unexpected event type
print('Unexpected event type {}'.format(event['type']))
return 'Webhook received', 200
else:
return 'POST Method not supported', 405
if __name__ == "__main__":
app.run(host='0.0.0.0', port=6666)
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"github.com/blockfrost/blockfrost-go"
)
const SECRET_AUTH_TOKEN string = "SECRET-WEBHOOK-AUTH-TOKEN"
func main() {
http.HandleFunc("/webhook", func(w http.ResponseWriter, req *http.Request) {
const MaxBodyBytes = int64(524288)
req.Body = http.MaxBytesReader(w, req.Body, MaxBodyBytes)
payload, err := io.ReadAll(req.Body)
fmt.Printf("Received webhook request.\n")
if err != nil {
fmt.Fprintf(os.Stderr, "Error reading request body: %v\n", err)
w.WriteHeader(http.StatusServiceUnavailable)
return
}
event, err := blockfrost.VerifyWebhookSignature(payload, req.Header.Get("Blockfrost-Signature"), SECRET_AUTH_TOKEN)
if err != nil {
fmt.Fprintf(os.Stderr, "Error verifying webhook signature: %v\n", err)
w.WriteHeader(http.StatusBadRequest)
return
}
// Unmarshal the event data into an appropriate struct depending on its Type
switch event.Type {
case "block":
var blockEvent blockfrost.WebhookEventBlock
err := json.Unmarshal(payload, &blockEvent)
if err != nil {
fmt.Fprintf(os.Stderr, "Error parsing webhook JSON: %v\n", err)
w.WriteHeader(http.StatusBadRequest)
return
}
fmt.Printf("Received block event: %v\n", blockEvent)
case "transaction":
var transactionEvent blockfrost.WebhookEventTransaction
err := json.Unmarshal(payload, &transactionEvent)
if err != nil {
fmt.Fprintf(os.Stderr, "Error parsing webhook JSON: %v\n", err)
w.WriteHeader(http.StatusBadRequest)
return
}
fmt.Printf("Received tx event: %v\n", transactionEvent)
case "epoch":
var epochEvent blockfrost.WebhookEventEpoch
err := json.Unmarshal(payload, &epochEvent)
if err != nil {
fmt.Fprintf(os.Stderr, "Error parsing webhook JSON: %v\n", err)
w.WriteHeader(http.StatusBadRequest)
return
}
fmt.Printf("Received epoch event: %v\n", epochEvent)
case "delegation":
var delegationEvent blockfrost.WebhookEventDelegation
err := json.Unmarshal(payload, &delegationEvent)
if err != nil {
fmt.Fprintf(os.Stderr, "Error parsing webhook JSON: %v\n", err)
w.WriteHeader(http.StatusBadRequest)
return
}
fmt.Printf("Received delegation event: %v\n", delegationEvent)
default:
fmt.Fprintf(os.Stderr, "Unhandled webhook type: %s\n", event.Type)
}
w.WriteHeader(http.StatusOK)
})
fmt.Println("Server is starting on port 8080...")
err := http.ListenAndServe(":8080", nil)
if err != nil {
fmt.Fprintf(os.Stderr, "Server failed to start: %v\n", err)
}
}
Retries
Blockfrost will attempt to resend the webhook request up to two additional times (making a total of three attempts, including the initial try) if your endpoint returns any of the following HTTP error status codes:
- 400 Bad Request
- 408 Request Timeout
- 413 Payload Too Large
- 429 Too Many Requests
- 500 Internal Server Error
- 502 Bad Gateway
- 503 Service Unavailable
- 504 Gateway Timeout
- 521 Web Server Is Down
- 522 Connection Timed Out
- 524 A Timeout Occurred
Blockfrost employs a backoff strategy, waiting a few seconds before attempting to resend the request. Body of the request is the same between retries, the ID of the retried request will be the same as the ID of the original request that failed.
To help you with troubleshooting problems with your endpoint, check the list of recent failed requests in Secure Webhook settings in Blockfrost Dashboard. However, it's worth noting that if any of the retry attempts succeed, the operation is considered successful. Request is logged as failed only if all attempts to deliver the event fail.
If your webhook endpoint doesn't respond with a successful status code (2xx
) within few seconds then the request will time out.
To prevent unexpected issues, your application should be prepared for a scenario in which the webhook request never arrives. To keep your app fully functioning and still provide a good user experience, you can manually fetch the data via the traditional Blockfrost API.
Test your webhook
Webhook.site
The easiest way to explore the data sent via a webhook is to point it to a service such as Webhook.site. It generates a random URL and displays every request sent to this address and its body.
Local web server
In order to test Secure Webhooks with your endpoint implementation running on your local HTTP dev server, you need to expose it to the web. Fortunately, services like Loophole.cloud will set it up for you.
Follow Loophole documentation to download the CLI or the Desktop GUI app, create an account and expose your local HTTP server.
To expose a local web server running on port 6666 via CLI, run the command loophole http 6666
.
In the Webhook settings, set the webhook endpoint to the URL you will see in the output (if you are using the example do not forget to append /webhook
at the end).
$ loophole http 6666
Loophole - End to end TLS encrypted TCP communication between you and your clients
Registering your domain... Success!
Starting local proxy server... Success!
Initializing secure tunnel... Success!
Forwarding https://1a4ab363d5cfd01cccc9fba73777770c.loophole.site -> http://127.0.0.1:6666