Developer documentation · v1

Build on the routing fabric.

Submit leads, receive signed webhooks, reconcile payouts. The same API our own publishers and partner stacks run on — production-grade, versioned, and stable.

Stable v1 since 2025 HMAC-signed webhooks (Stripe-style) 99.95% rolling uptime
01 · Quickstart

Send your first lead in 90 seconds

Three steps: get approved, grab your API key, send a POST. No SDK install required.

  1. 1. Apply for publisher access. Admin approves, you receive your API key by email.
  2. 2. Set it as an env var: export CONNEXIS_API_KEY=pk_live_…
  3. 3. POST your first lead with the sample below — accepted leads land in your dashboard immediately.
Try it now — no account required.

Use the public sandbox key below to send a test lead and see the exact response shape. Sandbox requests are never stored, routed, billed, or surfaced to buyers — they're purely for client integration testing.

pk_test_sandbox_DO_NOT_USE_IN_PRODUCTION
cURL
bash
curl -X POST https://connex.is/api/v1/leads/ingest \
  -H "X-API-Key: $CONNEXIS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "sector": "mortgages",
    "first_name": "James",
    "last_name": "Patel",
    "email": "j.patel@example.co.uk",
    "phone": "+447712345678",
    "postcode": "SW1A 1AA",
    "consent_token": "ct_abc123",
    "consent_at": "2026-02-09T07:42:18Z",
    "ip": "203.0.113.42",
    "extra_fields": { "property_value_gbp": 350000, "deposit_gbp": 70000 }
  }'
Node.js
javascript
import fetch from "node-fetch";

const res = await fetch("https://connex.is/api/v1/leads/ingest", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.CONNEXIS_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    sector: "mortgages",
    first_name: "James",
    last_name: "Patel",
    email: "j.patel@example.co.uk",
    phone: "+447712345678",
    postcode: "SW1A 1AA",
    consent_token: "ct_abc123",
    consent_at: new Date().toISOString(),
    extra_fields: { property_value_gbp: 350000, deposit_gbp: 70000 },
  }),
});

const result = await res.json();
console.log(result);
Python
python
import os, requests

r = requests.post(
    "https://connex.is/api/v1/leads/ingest",
    headers={"X-API-Key": os.environ["CONNEXIS_API_KEY"]},
    json={
        "sector": "mortgages",
        "first_name": "James",
        "last_name":  "Patel",
        "email":      "j.patel@example.co.uk",
        "phone":      "+447712345678",
        "postcode":   "SW1A 1AA",
        "consent_token": "ct_abc123",
        "consent_at":    "2026-02-09T07:42:18Z",
        "extra_fields": {"property_value_gbp": 350000, "deposit_gbp": 70000},
    },
    timeout=10,
)
r.raise_for_status()
print(r.json())
Successful response: { "ok": true, "lead_id": "lead_…", "status": "accepted" | "queued" | "rejected", "reason": null }
02 · Authentication

Single header. No OAuth dance.

Every request must carry your secret API key in the X-API-Key header. Keys are issued per publisher account; rotate from your publisher dashboard.

bash
X-API-Key: cnx_live_a1b2c3d4e5f6g7h8…
  • · Treat the key like a password — never commit it, never paste into client-side JS
  • · Rotate immediately if exposed; old key is revoked the moment a new one is generated
  • · Bearer / Authorization headers are not accepted on ingest — must be X-API-Key
03 · Endpoint reference

POST · /api/v1/leads/ingest

The headline endpoint. Submits a single end-consumer lead into the Connexis routing fabric. Returns synchronously with an accepted / queued / rejected verdict in < 500ms typical.

MethodPOST
URLhttps://connex.is/api/v1/leads/ingest
AuthX-API-Key: <your_key>
Content-Typeapplication/json
IdempotencyDedupe by email+phone (30-day window)
04 · Lead schema

Request body fields

Required fields are bolded. Anything not listed below should go inside extra_fields — we round-trip the full object back to buyers, so sector-specific data is preserved.

FieldTypeRequiredDescription
sectorstringYesOne of the published sector keys (see Sector pricing below).
first_namestringYesEnd-consumer first name.
last_namestringYesEnd-consumer last name.
emailstringYesValidated email address (RFC 5322).
phonestringYesE.164 format preferred (e.g. +447700900123).
citystringOptional — used for routing weighting.
postcodestringUK format (e.g. SW1A 1AA). Lowercased server-side.
consent_tokenstringToken from your consent capture flow. Stored alongside the lead in the audit trail.
consent_atISO-8601Timestamp consent was captured. UTC recommended.
ipstringOriginal click/submit IP. Used for fraud + dedupe.
user_agentstringBrowser user-agent string at submit time.
source_sub_idstringYour own A/B variant or campaign ID — round-tripped in webhooks.
extra_fieldsobjectSector-specific structured fields (mortgage value, loan amount, debt total, etc.).
05 · Sector pricing

Base CPLs by sector

These are the default minimum prices we route at. Actual paid CPL can be higher when a buyer holds a premium rule for the geo/score/source combination.

Investor Leads
investor-leads
£120
Wealth Management
wealth-management
£95
Commercial Finance
commercial-finance
£145
Property Investment
property-investment
£88
Business Finance
business-finance
£52
Claims Management
claims-management
£42
Mortgages
mortgages
£38
Life Insurance
life-insurance
£34
Debt Help
debt-help
£28
Payday Loans
payday-loans
£14
06 · Webhooks

Server-to-server event delivery

Configure a webhook URL on your publisher account and we'll POST signed JSON to it when downstream events fire. Stripe-style retry schedule: 30s → 2m → 10m → 1h → 4h, 5 attempts before giving up.

lead.purchased

A buyer purchases your lead from the marketplace.

{
  "event": "lead.purchased",
  "id":    "lead_8f3b…",
  "sector":"mortgages",
  "buyer": { "id":"buy_…", "company":"…" },
  "price_gbp": 38.00,
  "purchased_at": "2026-02-09T07:42:18Z",
  "source_sub_id": "ab-variant-3"
}
lead.rejected

Your lead failed routing acceptance (validation, dedupe, suppression, or no-buyer-match).

{
  "event": "lead.rejected",
  "id":    "lead_8f3b…",
  "reason":"dedupe_30d",
  "rejected_at": "2026-02-09T07:42:18Z"
}
Signature header: X-Connexis-Signature: t=<unix_ts>,v1=<hex_sha256>. Verify before processing — see next section for working snippets.
07 · Verifying signatures

Drop-in code (no SDK required)

We sign the request body with HMAC-SHA256 against your per-publisher webhook_secret (rotate from your dashboard). Reject anything with a stale timestamp (> 5 minutes) to defeat replay attacks.

Node.js
javascript
import crypto from "node:crypto";

// Verify an inbound Connexis webhook (Stripe-style HMAC).
function verifyConnexisSignature(secret, rawBody, header, toleranceSec = 300) {
  if (!header) return false;
  const parts = Object.fromEntries(header.split(",").map(kv => kv.split("=")));
  if (!parts.t || !parts.v1) return false;
  // Reject anything older than tolerance — defeats replays
  if (Math.abs(Math.floor(Date.now() / 1000) - Number(parts.t)) > toleranceSec) return false;
  const expected = crypto.createHmac("sha256", secret)
    .update(`${parts.t}.${rawBody}`).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}
Python
python
import hmac, hashlib, time

def verify_connexis_signature(secret: str, raw_body: bytes, header: str, tolerance_sec: int = 300) -> bool:
    """Verify Connexis webhook signature (Stripe-style)."""
    if not header:
        return False
    parts = dict(kv.split("=", 1) for kv in header.split(",") if "=" in kv)
    t, v1 = parts.get("t"), parts.get("v1")
    if not t or not v1:
        return False
    if abs(int(time.time()) - int(t)) > tolerance_sec:
        return False  # stale or replay
    signing_input = f"{t}.".encode() + raw_body
    expected = hmac.new(secret.encode(), signing_input, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, v1)
08 · Errors

HTTP status reference

Every non-2xx response carries a JSON body with detail set to a human-readable explanation. Treat 5xx as transient — retry with backoff. Treat 4xx as a client bug to fix.

StatusReasonNotes
401Invalid or missing X-API-KeyHeader missing, key revoked, or doesn't exist.
403Publisher account is pausedContact your account manager. Existing keys continue to validate but ingestion is blocked.
422Validation errorPydantic schema failure — typically a required field is missing or malformed.
409Duplicate leadSame email or phone within the 30-day dedupe window.
429Rate limitedPer-key throughput exceeded. See Rate limits.
500Internal errorUnexpected — auto-retried by your client? Email engineering@connex.is with the request id.
09 · Rate limits & idempotency

What's enforced

Defaults are generous; bespoke ceilings available on request. If you're bursting, space submissions out evenly rather than batching — the routing fabric prefers a steady stream.

  • 120 req/min per API key on the ingest endpoint. 429 returned when exceeded.
  • 30-day dedupe window by (email OR phone) within your publisher account. A retry of an already-accepted lead returns 409.
  • Webhook retries: 30s → 2m → 10m → 1h → 4h, 5 attempts.
10 · SDKs

On the roadmap

The API is small enough that no SDK is strictly needed — 5 lines of HTTP and you're submitting leads. Official Node, Python and Ruby wrappers are tracked on the public roadmap. If you need one urgently, tell us at engineering@connex.is.

Ready to start?

Apply for publisher access — admin reviews within one business day.

Get an API key