Developer documentation

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):

HTTP
Authorization: Bearer ifl_live_8a2c4f6e1d0b3a9c5e7f2a8b4d6c9e1f

or:

HTTP
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
curl https://ironfrontdigital.com/api/v1/leads/balance \
  -H "Authorization: Bearer ifl_live_..."
200 response
{
  "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.

200 response
{
  "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.

200 response
{
  "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
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

HTTP
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

JSON
{
  "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/deliveries as 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)

JavaScript
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)

Python
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', 200
Rotate your secret if you suspect it's been leaked. Click Update URL + rotate secret at /dashboard/credits — the old secret stops being valid immediately. We'll show the new plaintext once; update your env var before the next delivery fires.

Rate 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:

HTTP response 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:

401 Unauthorized
{
  "error": "unauthorized",
  "message": "Provide a valid Authorization: Bearer ifl_live_... header. Get a key at /dashboard/credits."
}
404 Not Found
{ "error": "not_found" }
429 Too Many Requests
{
  "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.

Iron Front Leads API — Developer documentation — Iron Front Digital