v1.0

Webhooks Guide

How to receive and verify webhook events

Webhooks notify your server in real-time when events occur in SmartMCA.

How It Works

  1. You create a webhook subscription with a URL and event types
  2. When an event occurs, SmartMCA sends a POST request to your URL
  3. Your server verifies the signature and processes the event

Webhook Payload

{
  "eventId": "evt_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "eventType": "deal.funded",
  "createdAt": "2026-03-04T12:00:00.000Z",
  "data": {
    "id": "clx...",
    "dealId": "N-1001-DEM-A01",
    "merchantName": "Acme Coffee",
    "fundedAmount": 10000,
    "status": "active"
  }
}

Headers

HeaderDescription
X-SmartMCA-Signaturesha256={hmac} — HMAC-SHA256 of timestamp.body
X-SmartMCA-TimestampUnix timestamp (seconds) when the event was sent
X-SmartMCA-Event-IdUnique event ID for deduplication
Content-Typeapplication/json

Verifying Signatures

Every webhook includes an HMAC-SHA256 signature. The signature is computed over {timestamp}.{body} — the timestamp header value, a literal dot, and the raw request body — signed with your webhook secret.

Always verify signatures to ensure the request came from SmartMCA and wasn’t tampered with.

TypeScript / Node.js

import crypto from 'crypto';

function verifyWebhook(
  rawBody: string,
  signatureHeader: string,
  timestampHeader: string,
  secret: string,
): boolean {
  // 1. Reconstruct the signed payload
  const signedPayload = `${timestampHeader}.${rawBody}`;

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

  // 3. Constant-time comparison
  return crypto.timingSafeEqual(
    Buffer.from(signatureHeader),
    Buffer.from(expected),
  );
}

// Express example
app.post('/webhooks/smartmca', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-smartmca-signature'] as string;
  const timestamp = req.headers['x-smartmca-timestamp'] as string;
  const body = req.body.toString();

  if (!verifyWebhook(body, signature, timestamp, process.env.WEBHOOK_SECRET!)) {
    return res.status(401).send('Invalid signature');
  }

  const event = JSON.parse(body);
  console.log(`Received ${event.eventType}: ${event.eventId}`);

  // Process asynchronously, respond immediately
  res.status(200).send('OK');
});

Python

import hmac
import hashlib

def verify_webhook(raw_body: bytes, signature_header: str, timestamp_header: str, secret: str) -> bool:
    """Verify SmartMCA webhook signature."""
    signed_payload = f"{timestamp_header}.{raw_body.decode('utf-8')}"
    expected = "sha256=" + hmac.new(
        secret.encode("utf-8"),
        signed_payload.encode("utf-8"),
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(signature_header, expected)

# Flask example
from flask import Flask, request, abort

app = Flask(__name__)

@app.route("/webhooks/smartmca", methods=["POST"])
def handle_webhook():
    signature = request.headers.get("X-SmartMCA-Signature", "")
    timestamp = request.headers.get("X-SmartMCA-Timestamp", "")

    if not verify_webhook(request.data, signature, timestamp, WEBHOOK_SECRET):
        abort(401)

    event = request.get_json()
    print(f"Received {event['eventType']}: {event['eventId']}")
    return "OK", 200

C#

using System.Security.Cryptography;
using System.Text;

public static bool VerifyWebhook(string rawBody, string signatureHeader, string timestampHeader, string secret)
{
    var signedPayload = $"{timestampHeader}.{rawBody}";
    using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
    var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(signedPayload));
    var expected = $"sha256={Convert.ToHexString(hash).ToLowerInvariant()}";
    return CryptographicOperations.FixedTimeEquals(
        Encoding.UTF8.GetBytes(signatureHeader),
        Encoding.UTF8.GetBytes(expected));
}

// ASP.NET Core example
[HttpPost("/webhooks/smartmca")]
public async Task<IActionResult> HandleWebhook()
{
    using var reader = new StreamReader(Request.Body);
    var body = await reader.ReadToEndAsync();

    var signature = Request.Headers["X-SmartMCA-Signature"].ToString();
    var timestamp = Request.Headers["X-SmartMCA-Timestamp"].ToString();

    if (!VerifyWebhook(body, signature, timestamp, _webhookSecret))
        return Unauthorized();

    var evt = JsonSerializer.Deserialize<WebhookEvent>(body);
    _logger.LogInformation("Received {EventType}: {EventId}", evt.EventType, evt.EventId);
    return Ok();
}

bash (testing)

# Verify a webhook signature manually
TIMESTAMP="1709553600"
BODY='{"eventId":"evt_abc","eventType":"deal.funded","createdAt":"2026-03-04T12:00:00Z","data":{}}'
SECRET="whsec_your_secret_here"

EXPECTED=$(echo -n "${TIMESTAMP}.${BODY}" | openssl dgst -sha256 -hmac "${SECRET}" | awk '{print $2}')
echo "sha256=${EXPECTED}"
# Compare with the X-SmartMCA-Signature header value

Responding

  • Return a 2xx status code within 10 seconds to acknowledge receipt
  • Non-2xx responses or timeouts are treated as failures
  • Failed deliveries are retried with exponential backoff

Retry Policy

AttemptDelay
1st retry5 minutes
2nd retry30 minutes
3rd retry2 hours
4th retry12 hours

After 4 retries (5 total attempts), the subscription is automatically paused. Check the delivery logs endpoint to diagnose failures.

Best Practices

  1. Respond quickly — Do heavy processing asynchronously. Return 200 immediately, then process the event in a background job.
  2. Verify signatures — Always validate the HMAC using the X-SmartMCA-Timestamp and raw body.
  3. Check timestamps — Reject events with timestamps older than 5 minutes to prevent replay attacks.
  4. Handle duplicates — Use eventId for idempotent processing. The same event may be delivered more than once.
  5. Monitor delivery logs — Use GET /webhooks/{id}/deliveries to check for failures.
  6. Use HTTPS — Always use HTTPS endpoints for webhook URLs.