Skip to content

Webhook Verification

Verify that webhooks are genuinely from VectorCare and haven’t been tampered with. All webhook payloads are signed using RSA-SHA256.

  1. VectorCare signs the webhook body with a private key
  2. The signature is included in the X-VectorCare-Signature header (base64-encoded)
  3. You verify the signature using VectorCare’s public key
  4. If verification fails, reject the webhook
  1. Log into VectorCare as an organization admin
  2. Navigate to Settings > Open API
  3. Copy the RSA public key (PEM format)
PropertyValue
AlgorithmRSA with SHA-256
PaddingPKCS#1 v1.5
Signature encodingBase64
Data to verifyRaw request body (bytes)

The signature must be verified against the raw request body bytes before any JSON parsing.

Using PyCryptodome (pip install pycryptodome):

from base64 import b64decode
from Crypto.Hash import SHA256
from Crypto.PublicKey import RSA
from Crypto.Signature import PKCS1_v1_5
from flask import Flask, request, abort
app = Flask(__name__)
# In production, load from environment variable or secrets manager
PUBLIC_KEY = """-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...
-----END PUBLIC KEY-----"""
def verify_signature(public_key: str, signature: str, data: bytes) -> bool:
try:
rsakey = RSA.importKey(public_key)
verifier = PKCS1_v1_5.new(rsakey)
digest = SHA256.new()
digest.update(data)
return verifier.verify(digest, b64decode(signature))
except (ValueError, TypeError):
return False
@app.route("/webhook", methods=["POST"])
def webhook():
signature = request.headers.get("X-VectorCare-Signature")
if not signature or not verify_signature(PUBLIC_KEY, signature, request.data):
abort(401)
data = request.get_json()
# Process webhook...
return "", 200

Using the built-in crypto module:

const crypto = require('crypto');
const express = require('express');
const app = express();
// In production, load from environment variable or secrets manager
const PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...
-----END PUBLIC KEY-----`;
function verifySignature(publicKey, signature, data) {
try {
const verifier = crypto.createVerify('RSA-SHA256');
verifier.update(data);
return verifier.verify(publicKey, signature, 'base64');
} catch (error) {
return false;
}
}
// Use raw body parser to get bytes before JSON parsing
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-vectorcare-signature'];
if (!signature || !verifySignature(PUBLIC_KEY, signature, req.body)) {
return res.status(401).send('Invalid signature');
}
const data = JSON.parse(req.body);
// Process webhook...
res.status(200).send();
});

Using the standard library:

package main
import (
"crypto"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"io"
"log"
"net/http"
)
// In production, load from environment variable or secrets manager
const publicKey = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...
-----END PUBLIC KEY-----`
func verifySignature(publicKeyPEM string, signature string, data []byte) error {
block, _ := pem.Decode([]byte(publicKeyPEM))
if block == nil {
return errors.New("failed to parse PEM block")
}
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return err
}
rsaPub, ok := pub.(*rsa.PublicKey)
if !ok {
return errors.New("not an RSA public key")
}
sigBytes, err := base64.StdEncoding.DecodeString(signature)
if err != nil {
return err
}
hash := sha256.Sum256(data)
return rsa.VerifyPKCS1v15(rsaPub, crypto.SHA256, hash[:], sigBytes)
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read body", http.StatusBadRequest)
return
}
signature := r.Header.Get("X-VectorCare-Signature")
if err := verifySignature(publicKey, signature, body); err != nil {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
// Process webhook...
w.WriteHeader(http.StatusOK)
}
func main() {
http.HandleFunc("/webhook", webhookHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
  1. Verify before processing - Always verify the signature before trusting the data
  2. Use raw bytes - Verify against the raw request body, not parsed/re-serialized JSON
  3. Reject failures - Return 401 and don’t process webhooks with invalid signatures
  4. Store keys securely - Don’t hardcode keys in source code; use environment variables or a secrets manager