Iron Front Leads API
REST endpoints + signed outbound webhooks. Authenticated via per-userifl_live_*tokens. Mint a key at /dashboard/credits (requires a customer account).
Authentication
Every request to /api/v1/leads/* requires an API key in one of two header shapes (Stripe-compatible):
Authorization: Bearer ifl_live_8a2c4f6e1d0b3a9c5e7f2a8b4d6c9e1f
or:
x-api-key: ifl_live_8a2c4f6e1d0b3a9c5e7f2a8b4d6c9e1f
Tokens are shown once at mint time + then SHA-256 hashed before storage. Lose your token and you rotate to a new one. The prefix is displayed in your dashboard for identifying multiple keys at a glance:ifl_live_8a2c4f6e…
Revoke a key by clicking Revoke next to it at /dashboard/credits. Revoked keys return 401 immediately on subsequent calls.
REST endpoints
Base URL: https://ironfrontdigital.com
GET/api/v1/leads/balance
Current credit balance + lifetime totals for the authenticated user.
curl https://ironfrontdigital.com/api/v1/leads/balance \ -H "Authorization: Bearer ifl_live_..."
{
"balance": 73,
"lifetime_purchased": 100,
"lifetime_consumed": 27,
"last_topped_up_at": "2026-05-16T05:19:42.000Z",
"last_consumed_at": "2026-05-22T14:00:00.000Z"
}GET/api/v1/leads/deliveries
Most-recent-first list of delivered batches. Excludes the leads payload — use the per-id endpoint or /csv to fetch actual lead rows.
{
"deliveries": [
{
"id": "01H5K8...",
"batch_id": "james-2026-05-22",
"delivered_at": "2026-05-22T14:00:00.000Z",
"lead_count": 10,
"credits_charged": 10,
"email_sent_at": "2026-05-22T14:00:03.000Z",
"downloaded_at": null,
"notes": null,
"download_url": "https://ironfrontdigital.com/api/v1/leads/deliveries/01H5K8.../csv"
}
]
}GET/api/v1/leads/deliveries/{id}
Single delivery batch with the leads array as JSON. Use this for programmatic ingest into your CRM. 404 (not 403) if the delivery doesn't belong to your account — we don't reveal existence to non-owners.
{
"id": "01H5K8...",
"batch_id": "james-2026-05-22",
"delivered_at": "2026-05-22T14:00:00.000Z",
"lead_count": 10,
"credits_charged": 10,
"leads": [
{
"name": "Acme Commercial Plumbing",
"phone": "(555) 123-4567",
"email": "owner@acmeplumbing.com",
"website": "https://acmeplumbing.com",
"address": "123 Main St",
"city": "Tampa",
"state": "FL",
"rating": 4.6,
"reviewCount": 142,
"industry": "Commercial plumbing",
"auditScore": 58,
"notes": "Missing schema, unclaimed GBP"
}
]
}GET/api/v1/leads/deliveries/{id}/csv
Same batch as text/csv. Suitable forcurl -o batch.csv redirects + spreadsheet imports. Marks the delivery as downloaded on first hit (idempotent).
curl https://ironfrontdigital.com/api/v1/leads/deliveries/01H5K8.../csv \ -H "Authorization: Bearer ifl_live_..." \ -o batch.csv
Outbound webhooks
Configure a webhook URL at /dashboard/credits and Iron Front will POST a signed JSON payload on every delivery to your account. No polling required.
Event types
leads.delivery.created— new batch landed in your account.
Headers
Content-Type: application/json User-Agent: IronFrontDigital-Webhooks/1 x-webhook-event: leads.delivery.created x-webhook-delivery-id: 01H5K8XJ2WQRZ9N3MPVB6T7Y4F x-webhook-signature: sha256=4f8a2c... x-webhook-attempt: 1
x-webhook-attempt increments on retry — use it with x-webhook-delivery-id for idempotency in your handler.
Payload schema
{
"event": "leads.delivery.created",
"delivery_id": "01H5K8XJ2WQRZ9N3MPVB6T7Y4F",
"user_id": "01H5K8Y7N4XJ8K2WMPVR3T9Q6E",
"batch_id": "james-2026-05-22",
"delivered_at": "2026-05-22T14:00:00.000Z",
"lead_count": 10,
"credits_charged": 10,
"fetch_url": "https://ironfrontdigital.com/api/v1/leads/deliveries/01H5K8.../",
"csv_url": "https://ironfrontdigital.com/api/v1/leads/deliveries/01H5K8.../csv"
}The fetch_url + csv_url both require your API key (Bearer auth). Use them inside your handler to pull the actual leads after verifying the signature.
Retry behavior
Non-2xx responses trigger automatic retry with exponential backoff:
- Attempt 1 failed → retry after 2 hours
- Attempt 2 failed → retry after 4 hours
- Attempt 3 failed → retry after 8 hours
- Attempt 4 failed → retry after 16 hours
- Attempt 5+ → permanent failure; pull from
/api/v1/leads/deliveriesas fallback
Your endpoint should respond with 200-299 within 10 seconds; longer responses are aborted as timeouts. Idempotency is your responsibility — see x-webhook-delivery-id above.
Signature verification
Every webhook POST includes an x-webhook-signature: sha256=<hex> header. Compute the same hash on your side over the raw request body bytes using your webhook secret (shown once when you set your URL — store it in your env). Reject any request whose computed hash doesn't match.
Node.js (Express)
const crypto = require('crypto')
const express = require('express')
const app = express()
// CRITICAL: read raw body bytes BEFORE any JSON parsing. The signature
// is computed over the exact bytes POSTed; JSON.parse + re-stringify
// will reorder keys and break the hash.
app.post(
'/webhooks/iron-front-leads',
express.raw({ type: 'application/json' }),
(req, res) => {
const sigHeader = req.header('x-webhook-signature') || ''
const expected = sigHeader.replace(/^sha256=/, '')
const computed = crypto
.createHmac('sha256', process.env.IFL_WEBHOOK_SECRET)
.update(req.body) // Buffer
.digest('hex')
// Constant-time compare to defend against timing attacks.
if (
expected.length !== computed.length ||
!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(computed))
) {
return res.status(401).send('Invalid signature')
}
const payload = JSON.parse(req.body.toString('utf8'))
// ... handle payload.event === 'leads.delivery.created' ...
res.status(200).send('ok')
}
)Python (Flask)
import hmac
import hashlib
import os
from flask import Flask, request, abort
app = Flask(__name__)
@app.route('/webhooks/iron-front-leads', methods=['POST'])
def iron_front_webhook():
# CRITICAL: use request.get_data() (raw bytes), NOT request.get_json()
# which re-parses and would silently break the signature check.
raw_body = request.get_data()
sig_header = request.headers.get('x-webhook-signature', '')
expected = sig_header.removeprefix('sha256=')
computed = hmac.new(
os.environ['IFL_WEBHOOK_SECRET'].encode(),
raw_body,
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(expected, computed):
abort(401, 'Invalid signature')
payload = request.get_json()
# ... handle payload['event'] == 'leads.delivery.created' ...
return 'ok', 200Rate limits
60 requests per minute, per API key. Different keys on the same account are rate-limited independently — split your workloads across keys if you need more throughput for, e.g., a CSV-pull cron + a balance-poll dashboard.
Every response carries standard headers:
X-RateLimit-Limit: 60 X-RateLimit-Remaining: 57 X-RateLimit-Reset: 1716396000 # unix-seconds until window reset X-RateLimit-Status: ok # or "healed" / "failopen"
When the limit is exceeded, responses return 429 Too Many Requestswith the same headers + a Retry-After header.
Error responses
Errors return JSON with a stable shape:
{
"error": "unauthorized",
"message": "Provide a valid Authorization: Bearer ifl_live_... header. Get a key at /dashboard/credits."
}{ "error": "not_found" }{
"error": "rate_limited",
"message": "60 requests per minute. Retry after 38s.",
"retryAfter": 38
}Versioning
All endpoints are versioned under /api/v1/leads/*. Breaking changes ship under /api/v2/leads/*; v1 stays available for at least 12 months after v2 launches.
Webhook payload schemas are additive — we add fields, never remove or rename them. The event string is the contract; new event types ship at new event-string values, not by changing existing payloads.
Ready to wire it up?
Buy a pack, mint an API key, set your webhook URL. ~10 minutes total.