API Reference
update Changelog

Signature Verification

All webhooks include an Araucaria-Signature header that you must verify to ensure the request came from Araucaria and hasn't been tampered with.

Signature Format

text
Araucaria-Signature: t=1705760400,v1=5d4f8a3c9e7b2a1d...

The signature header contains:

  • t: Unix timestamp when the webhook was sent
  • v1: HMAC-SHA256 signature (hex-encoded)

Verification Algorithm

  1. Extract the timestamp (t) and signature (v1) from the header
  2. Concatenate the timestamp and raw request body with a period: {timestamp}.{body}
  3. Compute HMAC-SHA256 of this string using your webhook secret
  4. Compare the computed signature to v1 using constant-time comparison
  5. Verify the timestamp is within 5 minutes of current time (replay protection)

Node.js Example

javascript
const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secret, toleranceSec = 300) {
  // Parse signature header
  const parts = signature.split(',');
  const timestampPart = parts.find(p => p.startsWith('t='));
  const signaturePart = parts.find(p => p.startsWith('v1='));

  if (!timestampPart || !signaturePart) {
    return { valid: false, error: 'Invalid signature format' };
  }

  const timestamp = parseInt(timestampPart.slice(2), 10);
  const expectedSignature = signaturePart.slice(3);

  // Check timestamp tolerance (replay protection)
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - timestamp) > toleranceSec) {
    return { valid: false, error: 'Timestamp outside tolerance' };
  }

  // Compute expected signature
  const signedPayload = `${timestamp}.${payload}`;
  const computedSignature = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');

  // Constant-time comparison
  const valid = crypto.timingSafeEqual(
    Buffer.from(expectedSignature),
    Buffer.from(computedSignature)
  );

  return { valid };
}

Express.js Middleware

javascript
app.post('/webhooks/araucaria',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const signature = req.headers['araucaria-signature'];
    const payload = req.body.toString();

    const result = verifyWebhookSignature(
      payload,
      signature,
      process.env.WEBHOOK_SECRET
    );

    if (!result.valid) {
      console.error('Invalid signature:', result.error);
      return res.status(401).send('Invalid signature');
    }

    const event = JSON.parse(payload);

    // Process the event
    switch (event.type) {
      case 'connection.connected':
        handleConnectionConnected(event.data);
        break;
      case 'accounts.updated':
        handleAccountsUpdated(event.data);
        break;
    }

    res.status(200).send('OK');
  }
);

Python Example

python
import hmac
import hashlib
import time

def verify_webhook_signature(payload, signature, secret, tolerance=300):
    parts = signature.split(',')
    timestamp_part = next((p for p in parts if p.startswith('t=')), None)
    signature_part = next((p for p in parts if p.startswith('v1=')), None)

    if not timestamp_part or not signature_part:
        return False, 'Invalid signature format'

    timestamp = int(timestamp_part[2:])
    expected_sig = signature_part[3:]

    # Check timestamp tolerance
    now = int(time.time())
    if abs(now - timestamp) > tolerance:
        return False, 'Timestamp outside tolerance'

    # Compute signature
    signed_payload = f'{timestamp}.{payload}'
    computed_sig = hmac.new(
        secret.encode(),
        signed_payload.encode(),
        hashlib.sha256
    ).hexdigest()

    # Constant-time comparison
    return hmac.compare_digest(expected_sig, computed_sig), None
⚠️ Important
Use the raw request body for verification. Do not parse and re-serialize the JSON, as this may change whitespace and break the signature.