Webhook Verification
Verify that webhooks are genuinely from VectorCare and haven’t been tampered with. All webhook payloads are signed using RSA-SHA256.
How It Works
Section titled “How It Works”- VectorCare signs the webhook body with a private key
- The signature is included in the
X-VectorCare-Signatureheader (base64-encoded) - You verify the signature using VectorCare’s public key
- If verification fails, reject the webhook
Getting the Public Key
Section titled “Getting the Public Key”- Log into VectorCare as an organization admin
- Navigate to Settings > Open API
- Copy the RSA public key (PEM format)
Verification Details
Section titled “Verification Details”| Property | Value |
|---|---|
| Algorithm | RSA with SHA-256 |
| Padding | PKCS#1 v1.5 |
| Signature encoding | Base64 |
| Data to verify | Raw request body (bytes) |
The signature must be verified against the raw request body bytes before any JSON parsing.
Python (Flask)
Section titled “Python (Flask)”Using PyCryptodome (pip install pycryptodome):
from base64 import b64decodefrom Crypto.Hash import SHA256from Crypto.PublicKey import RSAfrom Crypto.Signature import PKCS1_v1_5from flask import Flask, request, abort
app = Flask(__name__)
# In production, load from environment variable or secrets managerPUBLIC_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 "", 200Node.js (Express)
Section titled “Node.js (Express)”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 managerconst 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 parsingapp.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();});Go (net/http)
Section titled “Go (net/http)”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 managerconst 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))}Important Notes
Section titled “Important Notes”- Verify before processing - Always verify the signature before trusting the data
- Use raw bytes - Verify against the raw request body, not parsed/re-serialized JSON
- Reject failures - Return
401and don’t process webhooks with invalid signatures - Store keys securely - Don’t hardcode keys in source code; use environment variables or a secrets manager