Webhooks
Webhooks let your servers react to AltFiScore events the moment they happen — without polling. Configure an endpoint, save the signing secret, and we'll POST every relevant event to your URL with a cryptographic signature you can verify.
Why webhooks
Most lender integrations need to know when a decision is finalized so they can move the application forward in their LOS, notify the applicant, provision a loan, or kick off a funding workflow. Without webhooks, your systems have to poll the AltFiScore API repeatedly asking "is anything new?" — wasting bandwidth, adding latency, and making real-time UX impossible.
Webhooks invert that flow. We push events to you the instant they happen, usually within a few hundred milliseconds. Your servers stay idle until there's something real to act on.
Webhooks complement, not replace, the API
Quickstart
Three steps to receiving your first webhook:
1. Create an endpoint in the lender portal
Open the lender portal Webhooks page and click New endpoint. Provide:
- The HTTPS URL where AltFiScore should POST events
- An optional description to help your team identify it later
- Which environment this endpoint serves (
sandboxorlive) - Which events you want delivered (specific types or all events)
2. Save the signing secret
On endpoint creation we show you a signing secret once. Store it in your secrets manager or environment variables immediately — if you lose it, you'll need to rotate to get a new one.
signing secret format
whsec_aBc7xYzPq3rStUvWxYz123456789abcDeFgHiJkLmNoPqRs3. Verify signatures on every request
Every webhook carries an AltFi-Signature header. Verify the signature before trusting the payload — see signature verification below for Node and Python examples.
Payload structure
Every webhook POSTs a JSON envelope with a consistent shape. The data field varies by event type; everything else is the same across all events.
POST /your-endpoint
{
"id": "ab9b0f02-7944-4070-849c-9486a0a60a0d",
"type": "decision.finalized",
"created": 1763500000,
"attempt": 1,
"data": {
"decision_id": "f4b9c1d2-8e3a-4567-89b0-1234567890ab",
"application_id": "c8d3e4f5-6a7b-8c9d-0e1f-2a3b4c5d6e7f",
"outcome": "approved",
"score": 742,
"confidence": 0.86,
"recommended_amount": 12500.00,
"recommended_rate_pct": 8.5,
"risk_grade": "B",
"reason_codes": ["RC001", "RC042"],
"engine_version": "v2.3"
}
}id— UUID for this event. Use it to deduplicate if your endpoint receives the same event twice.type— Event type. See the event catalog.created— Unix timestamp (seconds) when the event was generated.attempt— Which retry attempt this is.1for the first try.data— Event-specific payload. Shape varies bytype.
Event types
AltFiScore currently emits the following event types. We add new ones over time — selecting All events when configuring your endpoint ensures you'll receive future event types automatically.
Decision lifecycle
decision.finalized— A decision was finalized (approved, declined, or referred). Fires for both engine-path and policy-path decisions.decision.adjusted— An underwriter manually overrode a referred decision via the lender portal.decision.created— A new decision record was created (before final outcome). Useful for tracking applications in flight.
Application lifecycle
application.created— A new application entered the pipeline.application.kyc_completed— KYC verification succeeded for an applicant.application.kyc_failed— KYC verification failed, requiring lender review.application.documents_requested— An underwriter requested additional documents from the applicant.application.condition_added— An underwriter added an approval condition.
Score lifecycle
score.completed— A scoring run completed for an applicant.score.errored— A scoring run failed due to data source unavailability or another non-recoverable error.
Lender events
dealer.added— A dealer was added to the lender's network (auto-loan tenants).dealer.removed— A dealer was soft-disabled.
System
webhook.test— Sent when you click "Send test event" in the lender portal. Useful for verifying your endpoint configuration end-to-end.
Signature verification
Every webhook includes an AltFi-Signature header containing a timestamp and HMAC-SHA256 signature:
HTTP headers
AltFi-Signature: t=1763500000,v1=5257a869e7ecebeda32affa62cdca3fab9c1d2...
AltFi-Webhook-Id: ab9b0f02-7944-4070-849c-9486a0a60a0d
AltFi-Webhook-Type: decision.finalized
Content-Type: application/json
User-Agent: AltFiScore-Webhooks/1.0To verify a request really came from AltFiScore, compute HMAC-SHA256 of {timestamp}.{request_body} using your endpoint's signing secret, then compare against the v1 value using a constant-time comparison function.
Always use timing-safe comparison
crypto.timingSafeEqual in Node or hmac.compare_digest in Python. A regular == comparison leaks timing information and can be exploited.Node.js
verify-webhook.js
const crypto = require('crypto');
function verifyAltFi(secret, rawBody, signatureHeader) {
// Header format: t=1763500000,v1=hex-signature
const parts = Object.fromEntries(
signatureHeader.split(',').map(p => p.split('='))
);
const timestamp = parts.t;
const provided = parts.v1;
// Reject requests older than 5 minutes (replay protection)
const age = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10);
if (age > 300) {
throw new Error('Webhook timestamp too old');
}
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected, 'hex'),
Buffer.from(provided, 'hex')
);
}
// Express example
app.post('/altfi-webhook', express.raw({ type: 'application/json' }), (req, res) => {
const ok = verifyAltFi(
process.env.ALTFI_SIGNING_SECRET,
req.body.toString('utf8'),
req.headers['altfi-signature']
);
if (!ok) return res.status(401).send('Invalid signature');
const event = JSON.parse(req.body);
// ... handle event.type ...
res.status(200).send('ok');
});Python
verify_webhook.py
import hmac
import hashlib
import time
def verify_altfi(secret: str, raw_body: bytes, signature_header: str) -> bool:
# Header format: t=1763500000,v1=hex-signature
parts = dict(p.split('=', 1) for p in signature_header.split(','))
timestamp = parts['t']
provided = parts['v1']
# Reject requests older than 5 minutes (replay protection)
age = int(time.time()) - int(timestamp)
if age > 300:
return False
payload = f"{timestamp}.{raw_body.decode('utf-8')}".encode()
expected = hmac.new(
secret.encode(),
payload,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, provided)
# Flask example
from flask import Flask, request, abort
import os, json
app = Flask(__name__)
@app.route('/altfi-webhook', methods=['POST'])
def handle_webhook():
if not verify_altfi(
os.environ['ALTFI_SIGNING_SECRET'],
request.get_data(),
request.headers.get('AltFi-Signature', ''),
):
abort(401, 'Invalid signature')
event = json.loads(request.get_data())
# ... handle event['type'] ...
return 'ok', 200Reliability and retries
AltFiScore is committed to delivering every event. If your endpoint is temporarily unavailable, we retry with exponential backoff.
Retry schedule
Each delivery is attempted up to 8 times over roughly 3.5 days:
- Attempt 1 — immediate
- Attempt 2 — 1 minute later
- Attempt 3 — 5 minutes later
- Attempt 4 — 30 minutes later
- Attempt 5 — 2 hours later
- Attempt 6 — 6 hours later
- Attempt 7 — 24 hours later
- Attempt 8 — 72 hours later (final)
We consider a delivery successful if your endpoint returns any HTTP status code in the 2xx range within 10 seconds. Any other response (including timeouts, 4xx, and 5xx) is treated as a failure and triggers the next retry.
Auto-disable
If an endpoint fails 50 consecutive deliveries, we auto-disable it to stop sending. You'll see an auto-disabled badge in the lender portal with the reason logged. Re-enable the endpoint manually once you've fixed the underlying issue.
Idempotency
Design for at-least-once delivery
id on your side. Treating webhooks as idempotent is the difference between a reliable integration and a brittle one.Testing your integration
Three ways to verify your webhook handler works correctly:
1. Use the "Send test event" button
Every endpoint detail page in the lender portal has a Send test event button. Clicking it queues a webhook.test event delivered to your URL in seconds. The delivery shows up in the endpoint's delivery history with the full request, response code, and timing — useful for debugging.
2. Use webhook.site for early development
Before you've written a handler, point your endpoint at webhook.site — a free service that shows you exactly what we send, headers and all. Useful for understanding the payload shape and signature header before writing verification code.
3. Trigger real events in sandbox
Once your handler is wired up, point a sandbox endpoint at your local dev server (use ngrok or similar to expose localhost), then trigger real flows: create an application, run a scoring task, override a referred decision. All sandbox events fire the same webhooks as production — perfect for end-to-end testing.
Security best practices
- Always verify signatures. Without verification, anyone who guesses your endpoint URL can forge events. Treating an unverified request as authentic is the single most common webhook mistake.
- Use HTTPS only. AltFiScore refuses to send to HTTP endpoints. Don't disable this — the signing secret in a plaintext HTTP body is sniffable.
- Store the signing secret securely. Treat it like an API key: secrets manager, env vars, never in git.
- Reject old timestamps. The verification code above rejects requests older than 5 minutes — this prevents replay attacks if an attacker captures a webhook in transit.
- Return 2xx as fast as possible. Process events asynchronously after acknowledging. Long-running handlers risk timeouts and unnecessary retries.
Rotating the signing secret
If you suspect your signing secret has been compromised, rotate it from the endpoint detail page in the lender portal. Note that rotation is immediate — any webhook signed with the old secret that arrives at your server after you've updated your code will fail verification. Plan a brief overlap window where your handler accepts either the old or new secret during cutover.
Next steps
- Configure your first endpoint in the lender portal.
- Review API authentication for the inverse direction — your code calling AltFiScore.
- Handle errors consistently across your API calls and webhook handlers.