Documentation

Webhook Signatures

Learn how to verify webhook authenticity and protect against spoofing attacks

Overview

Every webhook request includes a signature in the header that you can verify to ensure the request genuinely came from Yolfi and wasn't tampered with during transit.

Always verify webhook signatures before processing any webhook event. Failing to do so could allow attackers to send fake payment notifications to your system.


Why Verify Signatures?

Signature verification provides two key security guarantees:

  1. Authenticity - Confirms the webhook was sent by Yolfi, not an imposter
  2. Integrity - Ensures the payload wasn't modified in transit Replay and deduplication are application concerns. Use the signed payload id with your idempotency pattern to avoid duplicate side effects. X-Yolfi-Event-ID mirrors the same value for routing convenience, but the header is not part of the HMAC.

YOLFI_API_KEY authenticates requests to the Yolfi management API. It must not be used as a webhook signing secret. Each webhook endpoint has its own signingSecret for X-Yolfi-Signature verification.


How It Works

Signature Generation

When we send a webhook, we:

  1. Create a JSON payload with the event data
  2. Generate an HMAC-SHA256 signature using that endpoint's signing secret
  3. Encode the signature in Base64
  4. Send it in the request headers

Headers

Each webhook request includes these headers:

HeaderDescription
Content-TypeAlways application/json
X-Yolfi-SignatureBase64-encoded HMAC-SHA256 signature
X-Yolfi-Event-IDConvenience copy of the signed payload id

Verification Process

To verify a webhook signature:

Extract the X-Yolfi-Signature header from the request.

Retrieve the signing secret returned when this webhook endpoint was created or last rotated. Store each endpoint's secret separately.

Calculate HMAC-SHA256 of the raw request body using the endpoint signing secret.

Use constant-time comparison to compare your computed signature with the one in the header.


Code Examples

const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, webhookSecret) {
  if (!signature || !webhookSecret) {
    return false;
  }

  const expected = crypto
    .createHmac('sha256', webhookSecret)
    .update(payload, 'utf8')
    .digest('base64');

  try {
    return crypto.timingSafeEqual(
      Buffer.from(signature, 'base64'),
      Buffer.from(expected, 'base64')
    );
  } catch (e) {
    return false;
  }
}

// Express.js example
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-yolfi-signature'];
  const payload = req.body.toString(); // Keep raw body as string
  const webhookSecret = process.env.YOLFI_WEBHOOK_SECRET;

  const isValid = verifyWebhookSignature(payload, signature, webhookSecret);

  if (!isValid) {
    return res.status(400).send('Invalid signature');
  }

  const event = JSON.parse(payload);
  console.log('Received webhook:', event.type);

  // Process the event
  res.status(200).send('OK');
});
import hmac
import hashlib
import base64
import os

def verify_signature(payload: str, signature: str, webhook_secret: str) -> bool:
    if not signature or not webhook_secret:
        return False

    expected = hmac.new(
        webhook_secret.encode('utf-8'),
        payload.encode('utf-8'),
        hashlib.sha256
    ).digest()

    return hmac.compare_digest(
        base64.b64decode(signature),
        expected
    )

# Flask example
@app.route('/webhook', methods=['POST'])
def webhook():
    signature = request.headers.get('X-Yolfi-Signature')
    payload = request.get_data(as_text=True)
    webhook_secret = os.environ.get('YOLFI_WEBHOOK_SECRET')

    if not verify_signature(payload, signature, webhook_secret):
        return 'Invalid signature', 400

    event = request.get_json()
    print(f"Received webhook: {event['type']}")
    return 'OK', 200
import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/base64"
    "os"
)

func verifySignature(payload string, signature string, webhookSecret string) bool {
    if signature == "" || webhookSecret == "" {
        return false
    }

    mac := hmac.New(sha256.New, []byte(webhookSecret))
    mac.Write([]byte(payload))
    expected := mac.Sum(nil)

    sig, err := base64.StdEncoding.DecodeString(signature)
    if err != nil {
        return false
    }

    // hmac.Equal is constant-time.
    return hmac.Equal(expected, sig)
}

// Echo framework example
e.POST("/webhook", func(c echo.Context) error {
    signature := c.Request().Header.Get("X-Yolfi-Signature")
    payload, _ := io.ReadAll(c.Request().Body)
    webhookSecret := os.Getenv("YOLFI_WEBHOOK_SECRET")

    if !verifySignature(string(payload), signature, webhookSecret) {
        return c.String(400, "Invalid signature")
    }

    var event map[string]interface{}
    json.Unmarshal(payload, &event)
    fmt.Printf("Received webhook: %v\n", event["type"])

    return c.String(200, "OK")
})

Security Best Practices

Always use constant-time comparison functions (crypto.timingSafeEqual in Node.js, hmac.compare_digest in Python) to prevent timing attacks.

Keep endpoint signing secrets in environment variables or a secure secrets manager. Never commit them to version control, and never substitute the organization API key.

Rotation affects new deliveries. A delivery queued before rotation can still be retried with the previous secret, so accept both secrets until pre-rotation deliveries have drained.

Compute the signature over the raw request body, not a parsed/re-serialized version.

Verify the signature and return 200 OK immediately. Process the event asynchronously afterward.

Webhooks may be retried on failure. Design your handler to be idempotent and deduplicate using the signed payload id. If you use X-Yolfi-Event-ID, first require it to match the payload value.

On this page