Webhooks
Slate notifies you of important events via webhooks. You configure your endpoints and subscribed events in the dashboard. Every event uses the same payload shape:
{
"event_type": "credit-line.statement-ready",
"data": { "id": "123e4567-e89b-12d3-a456-426614174000" },
"timestamp": "2026-01-01T00:00:00Z"
}
data.id is the id of the object the event refers to (see the tables below). Webhooks are notifications, not payloads — always fetch the current state from the API after receiving one.
Verifying Webhooks
All webhook events are delivered using Svix and each request is cryptographically signed. You must verify signatures to ensure requests are authentic. Failing to verify webhook signatures may expose your system to spoofed or replayed requests.
Overview
Each webhook call includes three headers with additional information that are used for verification:
| Header | Description |
|---|---|
svix-id |
The unique message identifier for the webhook message. This identifier is unique across all messages, but will be the same when the same webhook is being resent |
svix-timestamp |
Unix timestamp (seconds) |
svix-signature |
Base64 encoded list of signatures (space delimited) |
Each webhook endpoint has a signing secret, which looks like: whsec_XXXXXXXXXXXXXXXXXXXXXXXXXXXX. You can get it from your webhook information on the dashboard.
.
How Signatures Are Generated
Webhooks are signed using:
- Algorithm: HMAC
- Hash: SHA-256
- Encoding: Base64
The signature is computed over the following string:
${svix_id}.${svix_timestamp}.${body};
The HMAC key is the Base64-decoded portion of your signing secret
(the part after the whsec_ prefix).
Signature Verification Steps
1. Read Required Headers
From the incoming request, extract:
svix-id
svix-timestamp
svix-signature
Also read the raw request body exactly as received.
⚠️ The body must not be modified (no JSON reformatting, whitespace changes, etc.).
2. Construct the Signed Content
{svix-id}.{svix-timestamp}.{raw_body}
Example:
msg_2N3R9J... . 1704839123 . {"event":"payment.completed","data":{...}}
3. Decode the Signing Secret
If your secret is:
whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw
Use only the part after whsec_ and Base64-decode it.
4. Compute the Expected Signature
Generate an HMAC-SHA256 signature and Base64-encode it.
5. Compare Against the svix-signature Header
The svix-signature header may contain multiple signatures, for example:
v1,abc123= v1,def456=
Rules:
- Split by spaces.
- Remove the version prefix (v1,).
- Compare each signature.
- If any signature matches, the webhook is valid.
6. Use Constant-Time Comparison
Always compare signatures using a constant-time comparison to prevent timing attacks.
Node.js Verification Example
import crypto from "crypto";
function verifyWebhook({
payload,
headers,
signingSecret,
}) {
const svixId = headers["svix-id"];
const svixTimestamp = headers["svix-timestamp"];
const svixSignature = headers["svix-signature"];
if (!svixId || !svixTimestamp || !svixSignature) {
throw new Error("Missing Svix headers");
}
const signedContent = `${svixId}.${svixTimestamp}.${payload}`;
// Remove "whsec_" prefix and base64-decode the secret
const secret = signingSecret.split("_")[1];
const secretBytes = Buffer.from(secret, "base64");
const expectedSignature = crypto
.createHmac("sha256", secretBytes)
.update(signedContent)
.digest("base64");
// svix-signature can contain multiple signatures
const signatures = svixSignature.split(" ");
for (const sig of signatures) {
const [, signature] = sig.split(",");
if (
crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
)
) {
return true;
}
}
return false;
}
Resources
- [Verifying Webhooks Manually][https://docs.svix.com/receiving/verifying-payloads/how-manual]
- [How to Verify Webhooks with the Svix Libraries][https://docs.svix.com/receiving/verifying-payloads/how]
Event catalog
Customers & financial data
| Event | Fired when | data.id |
|---|---|---|
customer.created |
A new customer is created | Customer |
customer.updated |
A customer is updated | Customer |
business.created |
A new business is created | Business |
business.updated |
A business is updated | Business |
business.financial-data.attached |
Financial data is attached to a business | Business |
Pre-approvals
| Event | Fired when | data.id |
|---|---|---|
preapproval.created |
A new pre-approval (offer) is created | Pre-approval |
preapproval.expired |
A pre-approval expires | Pre-approval |
preapproval.consumed |
A pre-approval is consumed by an application | Pre-approval |
Applications
| Event | Fired when | data.id |
|---|---|---|
application.created |
A new application is created | Application |
application.submitted |
An application is submitted | Application |
application.bank.verified |
The application's bank account is verified | Application |
application.ready-to-accept |
The application is ready for the customer to accept and sign | Application |
application.approved |
An application is approved | Application |
application.rejected |
An application is rejected | Application |
Financing agreements
| Event | Fired when | data.id |
|---|---|---|
financing-agreement.created |
A new financing agreement is created | Financing Agreement |
financing-agreement.disbursed |
The agreement's funds are disbursed | Financing Agreement |
financing-agreement.repayment |
The agreement receives a repayment | Financing Agreement |
financing-agreement.completed |
The agreement is fully repaid | Financing Agreement |
financing-agreement.defaulted |
The agreement is defaulted | Financing Agreement |
Credit lines
| Event | Fired when | data.id |
Type |
|---|---|---|---|
credit-line.approved |
A credit line application is approved with a maximum facility limit | Application | both |
credit-line.created |
A credit line is created (and, for revolving lines, its first period opens) | Credit Line | both |
credit-line.suspended |
The line is suspended — an unpaid minimum on a revolving line, or an unpaid draw past its final payment date. Block the card / stop allowing draws | Credit Line | both |
credit-line.reinstated |
A suspended line is reactivated — the card can be re-enabled and draws are accepted again | Credit Line | both |
credit-line.statement-ready |
A billing statement is generated for a period | Credit Line | revolving |
credit-line.draw.created |
A draw was created against the line. financingAgreementId carries the draw's agreement |
Credit Line | draws |
credit-line.draw.funded |
A draw's funds were transferred to the partner | Credit Line | draws |
credit-line.draw.repaid |
A draw was fully repaid within its window | Credit Line | draws |
Draws of a credit line are financing agreements, so they also emit the financing-agreement.* events above — created, disbursed, repayment and completed.
See the Revolving guide or the Draws guide for what to do on each event.
Finance requests (Early Wage Access)
| Event | Fired when | data.id |
|---|---|---|
finance-request.created |
A new finance request is created | Finance Request |
finance-request.in-progress |
The worker started the application | Finance Request |
finance-request.submitted |
The application is submitted | Finance Request |
finance-request.approved |
The request is approved | Finance Request |
finance-request.declined |
The request is declined | Finance Request |
finance-request.canceled |
The request is canceled | Finance Request |
finance-request.expired |
The request expires | Finance Request |
See the Early Wage Access guide for the full flow.