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.99% uptime SLA
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 ops@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

Drop-in client libraries

Tiny, single-file SDKs with retries, validation, and typed errors. MIT-licensed, dependency-free (Node uses built-in fetch, Python uses requests). Drop the file into your project and you're submitting leads in 5 lines.

Node.js
v1.0.0

Works on Node 18+ and modern browsers. CommonJS + ESM dual export.

const { Connexis } = require("./connexis-node.js");

const client = new Connexis({ apiKey: process.env.CONNEXIS_API_KEY });
const res = await client.ingest({
  sector: "mortgages", first_name: "Olivia", last_name: "Hughes",
  email: "olivia@example.com", phone: "+447900654321",
});
console.log(res.lead_id, res.price_gbp);
Python
v1.0.0

Single file, requires only requests. Compatible with Python 3.8+.

from connexis_sdk import Connexis

client = Connexis(api_key=os.environ["CONNEXIS_API_KEY"])
res = client.ingest({
    "sector": "mortgages", "first_name": "Olivia", "last_name": "Hughes",
    "email": "olivia@example.com", "phone": "+447900654321",
})
print(res["lead_id"], res["price_gbp"])

Official npm & PyPI packages land Q2 2026. Until then, the source files above are versioned & production-ready — you can vendor them into your repo or wrap them in your own internal package. Bugs? ops@connex.is.

11 · Trust widget

Show off live market activity

Anonymous, zero-PII trust badge built on top of the same routing engine. Embed it on your own marketing site, publisher portal, or partner landing pages to prove the Connexis market is alive. Updates every 60 seconds.

Live preview
Open ↗
Embed code
<iframe src="https://connex.is/embed/sector-stats" width="100%" height="480" frameborder="0" title="Connexis live market" style="border:1px solid #e2e8f0;border-radius:12px;background:#fff;max-width:680px;"></iframe>

Drop into any iframe-friendly surface — your marketing site, partner page, Confluence, Notion, etc. Updates every 60 seconds. Zero-PII.

What's shown
  • Leads · 24h — total accepted across all sectors
  • Active publishers — distinct sources sending in the last 24h
  • Fill rate — accepted / (accepted + matched-but-rejected)
  • Per-sector base price & volume — sorted by busiest first

Ready to start?

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

Get an API key