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.funded",
  "version": "3.0",
  "eventId": "112dff51-8275-4d60-9cd4-ad9aeb930478",
  "sign": "c580cd5259a8d2289a22ca6f97af56ed5ebd8a7a783bf56636761ef9d59b1830",
  "data": {
    "cardId": "c78041e26160072b02e04e855ae8d6e5b5dedfe5b3c9edc9cd",
    "cardholderId": "2d35aecc059dc46b68bdee8b3d009fe789a0",
    "reference": "333550a7-aea3-4cfd-b250-6eacd18828fa",
    "amount": 5,
    "fee": 0,
    "currency": "USD",
    "appId": "F3R0Q8D1Z5B8O6F8",
    "timestamp": "2026-05-10T23:18:45+00:00"
  }
}
The sign is computed over the raw data value 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 = raw JSON bytes of the "data" value  // exactly as received — do not re-serialize
)
Critical details:
  • Sign only the data value — not event, version, eventId, or sign itself
  • Use the exact bytes from the HTTP body for the data value — do not parse and re-serialize it
  • Always use constant-time comparison — never === or ==
Do not re-serialize data through a dictionary or map. Most JSON libraries sort map keys when encoding, which produces different bytes than the original and causes signature mismatch. The examples below all preserve the raw bytes.

Test Your Implementation

Use these known-good values to verify your implementation before going live.
FieldValue
Webhook secret975127f2e7165836d99f54cf9c298da5b8bd43060bc0634e8cb3774e8bd6db4c
Expected signc580cd5259a8d2289a22ca6f97af56ed5ebd8a7a783bf56636761ef9d59b1830
Full payload to feed into your handler:
{"event":"card.funded","version":"3.0","eventId":"112dff51-8275-4d60-9cd4-ad9aeb930478","sign":"c580cd5259a8d2289a22ca6f97af56ed5ebd8a7a783bf56636761ef9d59b1830","data":{"cardId":"c78041e26160072b02e04e855ae8d6e5b5dedfe5b3c9edc9cd","cardholderId":"2d35aecc059dc46b68bdee8b3d009fe789a0","reference":"333550a7-aea3-4cfd-b250-6eacd18828fa","amount":5,"fee":0,"currency":"USD","appId":"F3R0Q8D1Z5B8O6F8","timestamp":"2026-05-10T23:18:45+00:00"}}
Your verifySignature function should return true when given this payload and secret. If it returns false, your implementation has a bug — the most common cause is re-serializing data instead of using the raw bytes.

Verification Examples

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

// Use raw body middleware — required to extract the exact data bytes
app.use(express.raw({ type: 'application/json' }));

function verifySignature(rawBody, secret) {
  // Parse the envelope fields but sign the raw data bytes from the original body
  const body = rawBody.toString('utf8');
  const payload = JSON.parse(body);
  const { sign, data } = payload;

  if (!sign || !data) return { valid: false, payload: null };

  // Extract the raw "data" value as it appears in the body — key order preserved
  const dataStart = body.indexOf('"data"');
  const dataJson = body.slice(body.indexOf('{', dataStart));
  // Simpler: re-stringify works in Node.js since V8 preserves key order after JSON.parse
  const rawData = JSON.stringify(data);

  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawData)
    .digest('hex');

  const valid = crypto.timingSafeEqual(
    Buffer.from(sign, 'hex'),
    Buffer.from(expected, 'hex')
  );

  return { valid, payload };
}

app.post('/webhooks/fyatu', (req, res) => {
  const secret = process.env.FYATU_WEBHOOK_SECRET;
  const { valid, payload } = verifySignature(req.body, secret);

  if (!valid) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const { event, data } = payload;

  switch (event) {
    case 'card.funded':
      // handle funded card
      break;
    case 'card.transaction.approved':
      // handle approved 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 eventId or reference 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.

Never re-serialize through a map

Sign the raw data bytes as received. Re-encoding through a dictionary can change key order, producing a different HMAC. The Go example uses json.RawMessage to avoid this.

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.