BanklyzeBanklyze/Developer Docs
Sign In

Webhooks

Webhooks let your application receive real-time HTTP notifications when events occur in Banklyze. Instead of polling the API, configure a webhook URL and Banklyze will send a POST request to your endpoint whenever a subscribed event fires.

Each organization has a single webhook configuration. Use PUT /v1/webhooks/config to set it up.

Available Events

NameTypeRequiredDescription
deal.createdeventOptionalA new deal was created. Fires after POST /deals.
deal.updatedeventOptionalDeal details or status changed. Fires after PATCH /deals/{id} or when processing updates the deal.
deal.decision_changedeventOptionalA deal was approved, declined, or funded. Fires after POST /deals/{id}/decision.
document.uploadedeventOptionalA new document was uploaded. Fires after POST /deals/{id}/documents.
document.processedeventOptionalA document was successfully processed through the analysis pipeline.
document.failedeventOptionalDocument processing failed due to an unrecoverable error.
recommendation.generatedeventOptionalAn underwriting recommendation was generated for a deal.
deal.recommendation_readyeventOptionalAll documents processed and the deal is ready for review with a recommendation.

Payload Structure

Every webhook delivery is a POST request with a JSON body. All payloads share this structure:

NameTypeRequiredDescription
eventstringOptionalEvent type (e.g. deal.created)
event_idstringOptionalUnique event ID for idempotency
timestampstringOptionalISO 8601 timestamp
api_versionstringOptionalAPI version (e.g. 2026-02-15)
dataobjectOptionalEvent-specific payload data

deal.created

deal.created payload
{
  "event": "deal.created",
  "event_id": "evt_a1b2c3d4e5f6",
  "timestamp": "2026-02-15T14:35:00Z",
  "api_version": "2026-02-15",
  "data": {
    "id": 42,
    "business_name": "Acme Corp",
    "status": "new",
    "funding_amount_requested": 50000.00,
    "created_at": "2026-02-15T14:35:00Z"
  }
}

deal.updated

deal.updated payload
{
  "event": "deal.updated",
  "event_id": "evt_u1p2d3a4t5e6",
  "timestamp": "2026-02-15T15:00:00Z",
  "api_version": "2026-02-15",
  "data": {
    "id": 42,
    "business_name": "Acme Corp",
    "status": "ready",
    "health_score": 72.5,
    "health_grade": "B",
    "document_count": 3
  }
}

deal.decision_changed

deal.decision_changed payload
{
  "event": "deal.decision_changed",
  "event_id": "evt_d1e2c3i4s5n6",
  "timestamp": "2026-02-16T10:00:00Z",
  "api_version": "2026-02-15",
  "data": {
    "id": 42,
    "business_name": "Acme Corp",
    "decision": "approve",
    "decided_by": "Jane Doe"
  }
}

document.processed

document.processed payload
{
  "event": "document.processed",
  "event_id": "evt_x9y8z7w6v5u4",
  "timestamp": "2026-02-15T14:30:45Z",
  "api_version": "2026-02-15",
  "data": {
    "id": 15,
    "deal_id": 42,
    "filename": "acme_jan_2026.pdf",
    "document_type": "bank_statement",
    "status": "completed",
    "bank_name": "Chase",
    "transaction_count": 87
  }
}

document.failed

document.failed payload
{
  "event": "document.failed",
  "event_id": "evt_f1a2i3l4e5d6",
  "timestamp": "2026-02-15T14:31:00Z",
  "api_version": "2026-02-15",
  "data": {
    "id": 16,
    "deal_id": 42,
    "filename": "corrupted_file.pdf",
    "status": "failed",
    "error_message": "Unable to extract text from PDF"
  }
}

recommendation.generated

recommendation.generated payload
{
  "event": "recommendation.generated",
  "event_id": "evt_r1e2c3o4m5m6",
  "timestamp": "2026-02-15T14:35:00Z",
  "api_version": "2026-02-15",
  "data": {
    "deal_id": 42,
    "decision": "approve",
    "total_score": 86.7,
    "health_grade": "B",
    "max_advance": 37629.00,
    "cfcr": 1.45
  }
}

Webhook Headers

Each webhook request includes the following headers:

NameTypeRequiredDescription
Content-TypeheaderRequiredAlways application/json.
X-Webhook-SignatureheaderOptionalHMAC-SHA256 signature in the format sha256={hex_digest}. Present only if a secret was configured.

Configuring a Webhook

Set up your webhook endpoint via the API. You must provide an HTTPS URL and optionally specify which events to subscribe to:

Configure webhook
curl -X PUT https://api.banklyze.com/v1/webhooks/config \
  -H "X-API-Key: your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yourapp.com/webhooks/banklyze",
    "events": ["deal.created", "deal.updated", "document.processed", "document.failed", "recommendation.generated"],
    "secret": "whsec_your_signing_secret"
  }'

The secret field is optional but strongly recommended. It is used to generate the X-Webhook-Signature header so you can verify that incoming webhooks are genuinely from Banklyze.

Always provide a signing secret when configuring a webhook. This lets you verify that every incoming payload is genuinely from Banklyze by checking the X-Webhook-Signature header.

Testing Webhooks

After configuration, send a test event to verify your endpoint is reachable and your handler works correctly:

Send a test event
curl -X POST https://api.banklyze.com/v1/webhooks/test \
  -H "X-API-Key: your_api_key_here"

The test endpoint sends a synthetic webhook.test event with sample data. Your endpoint should return a 2xx response to indicate success.

Signature Verification

If you configured a secret, Banklyze signs every payload using HMAC-SHA256. The signature is included in the X-Webhook-Signature header with a sha256= prefix.

To verify a webhook:

  1. Read the raw request body (before parsing JSON).
  2. Compute the HMAC-SHA256 of the raw body using your shared secret.
  3. Compare the computed hex digest with the value after sha256= in the X-Webhook-Signature header, using a timing-safe comparison.
  4. Reject the request if the signatures do not match.

TypeScript verification example:

TypeScript signature verification
import { createHmac, timingSafeEqual } from "crypto";

function verifyWebhookSignature(
  rawBody: string,
  signatureHeader: string,
  secret: string
): boolean {
  // Header format: "sha256={hex_digest}"
  const signature = signatureHeader.replace("sha256=", "");

  const expected = createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");

  return timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

Python verification example:

Python signature verification
import hmac
import hashlib

def verify_webhook_signature(
    raw_body: bytes,
    signature_header: str,
    secret: str,
) -> bool:
    # Header format: "sha256={hex_digest}"
    signature = signature_header.removeprefix("sha256=")

    expected = hmac.new(
        secret.encode(),
        raw_body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(signature, expected)

Retry Policy

If your endpoint does not return a 2xx status code (or does not respond within 10 seconds), Banklyze retries the delivery up to 3 times with exponential backoff:

NameTypeRequiredDescription
1st retry5s delayOptionalFirst retry fires 5 seconds after the initial failure.
2nd retry30s delayOptionalSecond retry fires 30 seconds after the first retry.
3rd retry2m delayOptionalFinal retry fires 2 minutes after the second retry.

After 3 failed retries, the delivery is marked as failed. The same event_id is used across all retry attempts for idempotency.

Circuit Breaker

If your endpoint fails 10 consecutive deliveries, the webhook circuit breaker opens and deliveries are paused. The circuit breaker resets after 5 minutes, at which point deliveries resume. You can monitor webhook health in the dashboard.

Use the event_id field for idempotent processing. Retries reuse the same event ID, so store it and deduplicate on your end to avoid handling the same event twice.

Best Practices

  • Respond quickly. Return a 200 response immediately and process the event asynchronously. Long-running handlers may cause timeouts and trigger retries.
  • Handle duplicates. Use the event_id to deduplicate events. Retries send the same event ID.
  • Verify signatures. Always validate the X-Webhook-Signature header to ensure payloads are authentic.
  • Use HTTPS. Webhook URLs must use HTTPS. HTTP endpoints are rejected during configuration.
  • Log deliveries. Store incoming webhook payloads for debugging and auditing.

Example Webhook Handler

A complete Express.js webhook handler with signature verification:

Express.js webhook handler
import express from "express";
import { createHmac, timingSafeEqual } from "crypto";

const app = express();

const WEBHOOK_SECRET = process.env.BANKLYZE_WEBHOOK_SECRET!;

// Use raw body for signature verification
app.post(
  "/webhooks/banklyze",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const signatureHeader = req.headers["x-webhook-signature"] as string;
    const rawBody = req.body.toString();

    // 1. Verify signature
    if (signatureHeader) {
      const signature = signatureHeader.replace("sha256=", "");
      const expected = createHmac("sha256", WEBHOOK_SECRET)
        .update(rawBody)
        .digest("hex");

      if (
        !timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
      ) {
        console.error("Invalid webhook signature");
        return res.status(401).json({ error: "Invalid signature" });
      }
    }

    // 2. Parse the event
    const event = JSON.parse(rawBody);
    const { event: eventType, event_id, data } = event;

    console.log(`Received ${eventType} (event: ${event_id})`);

    // 3. Handle the event
    switch (eventType) {
      case "deal.created":
        console.log(`Deal ${data.id} created: ${data.business_name}`);
        break;

      case "deal.updated":
        console.log(
          `Deal ${data.id} updated — Grade: ${data.health_grade}`
        );
        break;

      case "document.processed":
        console.log(
          `Document ${data.id} processed for deal ${data.deal_id}`
        );
        break;

      case "document.failed":
        console.error(
          `Document ${data.id} failed: ${data.error_message}`
        );
        break;

      case "recommendation.generated":
        console.log(
          `Recommendation for deal ${data.deal_id}: ${data.decision}`
        );
        break;

      default:
        console.log(`Unhandled event type: ${eventType}`);
    }

    // 4. Respond immediately
    res.status(200).json({ received: true });
  }
);

app.listen(3000, () => {
  console.log("Webhook handler listening on port 3000");
});