Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.fyatu.com/llms.txt

Use this file to discover all available pages before exploring further.

Overview

Every webhook event Fyatu delivers includes a sign field in the JSON body. This is an HMAC-SHA256 signature you must verify before processing the event.
{
  "event": "card.transaction.approved",
  "version": "2.0",
  "sign": "3a5b8c2d1e4f9a0b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b",
  "data": {
    "cardId": "crd_9x8y7z6w5v4u",
    "cardholderId": "CH-ABC123DEF456",
    "amount": 29.99,
    "currency": "USD",
    "status": "APPROVED",
    "timestamp": "2026-04-30T10:35:00+00:00"
  }
}
The sign is computed over the data object only — not the full envelope. Your endpoint must recompute the same HMAC and compare it against sign before trusting anything in the payload.
Your webhookSecret is generated when you call POST /webhooks/secret/regenerate. It is shown once and never returned again. Store it securely in an environment variable — never in code or version control.

Signature Algorithm

sign = HMAC-SHA256(
  key     = webhookSecret,
  message = JSON.stringify(data)   // the "data" object, encoded with no unicode escaping
)
Critical details:
  • Sign only the data object — not event, version, or sign itself
  • Encode data as JSON with no unicode escaping and no slash escaping (equivalent to PHP’s JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
  • Always use constant-time comparison when checking the result — never === or ==

Verification Examples

const crypto = require('crypto');
const express = require('express');
const app = express();

app.use(express.json());

function verifySignature(data, sign, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(JSON.stringify(data)) // sign the data object only
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(sign, 'hex'),
    Buffer.from(expected, 'hex')
  );
}

app.post('/webhooks/fyatu', (req, res) => {
  const { sign, data, event } = req.body;
  const secret = process.env.FYATU_WEBHOOK_SECRET;

  if (!sign || !data) {
    return res.status(400).json({ error: 'Missing required fields' });
  }

  if (!verifySignature(data, sign, secret)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // Signature valid — process the event
  switch (event) {
    case 'card.transaction.approved':
      // handle approved transaction
      break;
    case 'card.transaction.declined':
      // handle declined transaction
      break;
    // ... other events
  }

  res.status(200).json({ received: true });
});

Best Practices

Respond quickly

Return 200 within 10 seconds. Acknowledge first and process asynchronously if needed. Fyatu retries timed-out deliveries.

Make handlers idempotent

The same event may be delivered more than once. Use reference or another unique field to deduplicate — store processed event identifiers in your database.

Use constant-time comparison

Always use timing-safe functions (timingSafeEqual, hash_equals, hmac.Equal). Variable-time === comparisons are vulnerable to timing attacks.

Re-serialize data carefully

When recomputing the signature, re-serialize the data object from the parsed JSON — don’t use the raw body substring. Use the same encoding settings as the examples above.

Retry Behavior

If your endpoint returns a non-2xx status or doesn’t respond within 10 seconds, Fyatu retries with exponential backoff:
AttemptDelay
1st retry1 minute
2nd retry5 minutes
3rd retry30 minutes
After 3 failed attempts the event is marked as undelivered. Use the Test Webhook endpoint to replay events during development.

Rotating Your Secret

If your webhookSecret is compromised, regenerate it immediately:
curl -X POST https://api.fyatu.com/api/v3/webhooks/secret/regenerate \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
The new secret is returned once in the response and takes effect immediately. Update your environment variable before the old secret is invalidated.