Granite Upgrade Activates in00d:00h:00m:00sLearn more

Webhook Signature

Webhook Signature for the Webhook API

To make your webhooks extra secure, you can verify that they originated from our side by generating an HMAC SHA-256 hash code using your Authentication Token and request body. You can get the signing secret through the AvaCloud portal or Glacier API.

Find your signing secret

Using the portal
Navigate to the webhook section and click on Generate Signing Secret. Create the secret and copy it to your code.

Using Data API
The following endpoint retrieves a shared secret:

curl --location 'https://glacier-api.avax.network/v1/webhooks:getSharedSecret' \
--header 'x-glacier-api-key: <YOUR_API_KEY>' \

Validate the signature received

Every outbound request will include an authentication signature in the header. This signature is generated by:

  1. Canonicalizing the JSON Payload: This means arranging the JSON data in a standard format.
  2. Generating a Hash: Using the HMAC SHA256 hash algorithm to create a hash of the canonicalized JSON payload.

To verify that the signature is from us, follow these steps:

  1. Generate the HMAC SHA256 hash of the received JSON payload.
  2. Compare this generated hash with the signature in the request header. This process, known as verifying the digital signature, ensures the authenticity and integrity of the request.

Example Request Header

Content-Type: application/json;
x-signature: your-hashed-signature

Example Signature Validation Function

This Node.js code sets up an HTTP server using the Express framework. It listens for POST requests sent to the /callback endpoint. Upon receiving a request, it validates the signature of the request against a predefined signingSecret. If the signature is valid, it logs match; otherwise, it logs no match. The server responds with a JSON object indicating that the request was received.

Node (JavaScript)

  const express = require('express');
  const crypto = require('crypto');
  const { canonicalize } = require('json-canonicalize');

  const app = express();
  app.use(express.json({limit: '50mb'}));

  const signingSecret = 'c13cc017c4ed63bcc842c8edfb49df37512280326a32826de3b885340b8a3d53';

  function isValidSignature(signingSecret, signature, payload) {
  const canonicalizedPayload = canonicalize(payload);
  const hmac = crypto.createHmac('sha256', Buffer.from(signingSecret, 'hex'));
  const digest = hmac.update(canonicalizedPayload).digest('base64');
  console.log("signature: ", signature);
  console.log("digest", digest);
  return signature === digest;
  }

  app.post('/callback', express.json({ type: 'application/json' }), (request, response) => {
  const { body, headers } = request;
  const signature = headers['x-signature'];
  // Handle the event
  switch (body.evenType) {
  case 'address\*activity':
  console.log("\*\** Address*activity \*\*\*");
  console.log(body);
  if (isValidSignature(signingSecret, signature, body)) {
  console.log("match");
  } else {
  console.log("no match");
  }
  break;
  // ... handle other event types
  default:
  console.log(`Unhandled event type ${body}`);
  }
  // Return a response to acknowledge receipt of the event
  response.json({ received: true });
  });

  const PORT = 8000;
  app.listen(PORT, () => console.log(`Running on port ${PORT}`));

Python (Flask)

  from flask import Flask, request, jsonify
  import hmac
  import hashlib
  import base64
  import json

  app = Flask(__name__)

  SIGNING_SECRET = 'c13cc017c4ed63bcc842c8edfb49df37512280326a32826de3b885340b8a3d53'

  def canonicalize(payload):
      """Function to canonicalize JSON payload"""
      # In Python, canonicalization can be achieved by using sort_keys=True in json.dumps
      return json.dumps(payload, separators=(',', ':'), sort_keys=True)

  def is_valid_signature(signing_secret, signature, payload):
      canonicalized_payload = canonicalize(payload)
      hmac_obj = hmac.new(bytes.fromhex(signing_secret), canonicalized_payload.encode('utf-8'), hashlib.sha256)
      digest = base64.b64encode(hmac_obj.digest()).decode('utf-8')
      print("signature:", signature)
      print("digest:", digest)
      return signature == digest

  @app.route('/callback', methods=['POST'])
  def callback_handler():
      body = request.json
      signature = request.headers.get('x-signature')

      # Handle the event
      if body.get('eventType') == 'address_activity':
          print("*** Address_activity ***")
          print(body)
          if is_valid_signature(SIGNING_SECRET, signature, body):
              print("match")
          else:
              print("no match")
      else:
          print(f"Unhandled event type {body}")

      # Return a response to acknowledge receipt of the event
      return jsonify({"received": True})

  if __name__ == '__main__':
      PORT = 8000
      print(f"Running on port {PORT}")
      app.run(port=PORT)

Go (net/http)

  package main

  import (
  	"crypto/hmac"
  	"crypto/sha256"
  	"encoding/base64"
  	"encoding/hex"
  	"encoding/json"
  	"fmt"
  	"net/http"
  	"sort"
  	"strings"
  )

  const signingSecret = "c13cc017c4ed63bcc842c8edfb49df37512280326a32826de3b885340b8a3d53"

  // Canonicalize function sorts the JSON keys and produces a canonicalized string
  func Canonicalize(payload map[string]interface{}) (string, error) {
  	var sb strings.Builder
  	var keys []string
  	for k := range payload {
  		keys = append(keys, k)
  	}
  	sort.Strings(keys)
  	sb.WriteString("{")
  	for i, k := range keys {
  		v, err := json.Marshal(payload[k])
  		if err != nil {
  			return "", err
  		}
  		sb.WriteString(fmt.Sprintf("\"%s\":%s", k, v))
  		if i < len(keys)-1 {
  			sb.WriteString(",")
  		}
  	}
  	sb.WriteString("}")
  	return sb.String(), nil
  }

  func isValidSignature(signingSecret, signature string, payload map[string]interface{}) bool {
  	canonicalizedPayload, err := Canonicalize(payload)
  	if err != nil {
  		fmt.Println("Error canonicalizing payload:", err)
  		return false
  	}

  	key, err := hex.DecodeString(signingSecret)
  	if err != nil {
  		fmt.Println("Error decoding signing secret:", err)
  		return false
  	}

  	h := hmac.New(sha256.New, key)
  	h.Write([]byte(canonicalizedPayload))
  	digest := h.Sum(nil)
  	encodedDigest := base64.StdEncoding.EncodeToString(digest)

  	fmt.Println("signature:", signature)
  	fmt.Println("digest:", encodedDigest)

  	return signature == encodedDigest
  }

  func callbackHandler(w http.ResponseWriter, r *http.Request) {
  	var body map[string]interface{}
  	err := json.NewDecoder(r.Body).Decode(&body)
  	if err != nil {
  		fmt.Println("Error decoding body:", err)
  		http.Error(w, "Invalid request body", http.StatusBadRequest)
  		return
  	}

  	signature := r.Header.Get("x-signature")
  	eventType, ok := body["eventType"].(string)
  	if !ok {
  		fmt.Println("Error parsing eventType")
  		http.Error(w, "Invalid event type", http.StatusBadRequest)
  		return
  	}

  	switch eventType {
  	case "address_activity":
  		fmt.Println("*** Address_activity ***")
  		fmt.Println(body)
  		if isValidSignature(signingSecret, signature, body) {
  			fmt.Println("match")
  		} else {
  			fmt.Println("no match")
  		}
  	default:
  		fmt.Printf("Unhandled event type %s\n", eventType)
  	}

  	w.Header().Set("Content-Type", "application/json")
  	json.NewEncoder(w).Encode(map[string]bool{"received": true})
  }

  func main() {
  	http.HandleFunc("/callback", callbackHandler)
  	fmt.Println("Running on port 8000")
  	http.ListenAndServe(":8000", nil)
  }

Rust (actix-web)

  use actix_web::{web, App, HttpServer, HttpResponse, Responder, post};
  use serde::Deserialize;
  use hmac::{Hmac, Mac};
  use sha2::Sha256;
  use base64::encode;
  use std::collections::BTreeMap;

  type HmacSha256 = Hmac<Sha256>;

  const SIGNING_SECRET: &str = "c13cc017c4ed63bcc842c8edfb49df37512280326a32826de3b885340b8a3d53";

  #[derive(Deserialize)]
  struct EventPayload {
      eventType: String,
      // Add other fields as necessary
  }

  // Canonicalize the JSON payload by sorting keys
  fn canonicalize(payload: &BTreeMap<String, serde_json::Value>) -> String {
      serde_json::to_string(payload).unwrap()
  }

  fn is_valid_signature(signing_secret: &str, signature: &str, payload: &BTreeMap<String, serde_json::Value>) -> bool {
      let canonicalized_payload = canonicalize(payload);

      let mut mac = HmacSha256::new_from_slice(signing_secret.as_bytes())
          .expect("HMAC can take key of any size");
      mac.update(canonicalized_payload.as_bytes());

      let result = mac.finalize();
      let digest = encode(result.into_bytes());

      println!("signature: {}", signature);
      println!("digest: {}", digest);

      digest == signature
  }

  #[post("/callback")]
  async fn callback(body: web::Json<BTreeMap<String, serde_json::Value>>, req: web::HttpRequest) -> impl Responder {
      let signature = req.headers().get("x-signature").unwrap().to_str().unwrap();

      if let Some(event_type) = body.get("eventType").and_then(|v| v.as_str()) {
          match event_type {
              "address_activity" => {
                  println!("*** Address_activity ***");
                  println!("{:?}", body);

                  if is_valid_signature(SIGNING_SECRET, signature, &body) {
                      println!("match");
                  } else {
                      println!("no match");
                  }
              }
              _ => {
                  println!("Unhandled event type: {}", event_type);
              }
          }
      } else {
          println!("Error parsing eventType");
          return HttpResponse::BadRequest().finish();
      }

      HttpResponse::Ok().json(serde_json::json!({ "received": true }))
  }

  #[actix_web::main]
  async fn main() -> std::io::Result<()> {
      HttpServer::new(|| {
          App::new()
              .service(callback)
      })
      .bind("0.0.0.0:8000")?
      .run()
      .await
  }

TypeScript (ChainKit SDK)

  import { isValidSignature } from '@avalanche-sdk/chainkit/utils';
  import express from 'express';

  const app = express();
  app.use(express.json());

  const signingSecret = 'your-signing-secret'; // Replace with your signing secret

  app.post('/webhook', (req, res) => {
      const signature = req.headers['x-signature'];
      const payload = req.body;

      if (isValidSignature(signingSecret, signature, payload)) {
          console.log('Valid signature');
          // Process the request
      } else {
          console.log('Invalid signature');
      }

      res.json({ received: true });
  });

  app.listen(8000, () => console.log('Server running on port 8000'));

Is this guide helpful?