Octopus Cards

Signature Verification

Verify webhook authenticity using HMAC-SHA256 signatures

Every webhook request includes an X-Signature header containing an HMAC-SHA256 signature of the request body. Verify this signature to ensure the webhook was sent by Octopus and has not been tampered with.

How Signing Works

  1. Octopus computes HMAC-SHA256(request_body, your_secret)
  2. The result is hex-encoded and sent in the X-Signature header
  3. You compute the same HMAC on the received body using your secret
  4. If the signatures match, the webhook is authentic

Verification Headers

HeaderPurpose
X-SignatureHMAC-SHA256 hex digest of the raw request body
X-OCTOPUS-WEBHOOK-TOKENYour shared secret (for quick lookup)
X-TimestampUnix timestamp — use to reject stale webhooks
X-Event-IDUnique event ID — use for idempotency

Implementation Examples

package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "io"
    "net/http"
)

func webhookHandler(w http.ResponseWriter, r *http.Request) {
    // Read the raw body
    body, err := io.ReadAll(r.Body)
    if err != nil {
        http.Error(w, "Bad request", 400)
        return
    }

    // Get the signature from the header
    receivedSig := r.Header.Get("X-Signature")
    secret := "your_webhook_secret"

    // Compute expected signature
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(body)
    expectedSig := hex.EncodeToString(mac.Sum(nil))

    // Constant-time comparison to prevent timing attacks
    if !hmac.Equal([]byte(receivedSig), []byte(expectedSig)) {
        http.Error(w, "Invalid signature", 401)
        return
    }

    // Signature valid — process the event
    w.WriteHeader(http.StatusOK)
}
const crypto = require('crypto');
const express = require('express');
const app = express();

app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
  const receivedSig = req.headers['x-signature'];
  const secret = 'your_webhook_secret';

  // Compute expected signature
  const expectedSig = crypto
    .createHmac('sha256', secret)
    .update(req.body)
    .digest('hex');

  // Constant-time comparison
  if (!crypto.timingSafeEqual(
    Buffer.from(receivedSig, 'hex'),
    Buffer.from(expectedSig, 'hex')
  )) {
    return res.status(401).send('Invalid signature');
  }

  // Signature valid — process the event
  const event = JSON.parse(req.body);
  console.log(`Received ${event.type}: ${event.id}`);

  res.sendStatus(200);
});
import hmac
import hashlib
from flask import Flask, request, abort

app = Flask(__name__)

@app.route('/webhooks', methods=['POST'])
def webhook():
    received_sig = request.headers.get('X-Signature')
    secret = 'your_webhook_secret'
    body = request.get_data()

    # Compute expected signature
    expected_sig = hmac.new(
        secret.encode(),
        body,
        hashlib.sha256
    ).hexdigest()

    # Constant-time comparison
    if not hmac.compare_digest(received_sig, expected_sig):
        abort(401)

    # Signature valid — process the event
    event = request.get_json()
    print(f"Received {event['type']}: {event['id']}")

    return '', 200

Timestamp Validation

Use the X-Timestamp header to reject stale or replayed webhooks. A common approach is to reject webhooks older than 5 minutes:

timestamp := r.Header.Get("X-Timestamp")
ts, _ := strconv.ParseInt(timestamp, 10, 64)
age := time.Since(time.Unix(ts, 0))

if age > 5*time.Minute {
    http.Error(w, "Webhook too old", 401)
    return
}

On this page