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.
Send your first lead in 90 seconds
Three steps: get approved, grab your API key, send a POST. No SDK install required.
- 1. Apply for publisher access. Admin approves, you receive your API key by email.
- 2. Set it as an env var:
export CONNEXIS_API_KEY=pk_live_… - 3. POST your first lead with the sample below — accepted leads land in your dashboard immediately.
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_PRODUCTIONcurl -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 }
}'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);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()){ "ok": true, "lead_id": "lead_…", "status": "accepted" | "queued" | "rejected", "reason": null }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.
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
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.
| Method | POST |
| URL | https://connex.is/api/v1/leads/ingest |
| Auth | X-API-Key: <your_key> |
| Content-Type | application/json |
| Idempotency | Dedupe by email+phone (30-day window) |
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.
| Field | Type | Required | Description |
|---|---|---|---|
| sector | string | Yes | One of the published sector keys (see Sector pricing below). |
| first_name | string | Yes | End-consumer first name. |
| last_name | string | Yes | End-consumer last name. |
| string | Yes | Validated email address (RFC 5322). | |
| phone | string | Yes | E.164 format preferred (e.g. +447700900123). |
| city | string | — | Optional — used for routing weighting. |
| postcode | string | — | UK format (e.g. SW1A 1AA). Lowercased server-side. |
| consent_token | string | — | Token from your consent capture flow. Stored alongside the lead in the audit trail. |
| consent_at | ISO-8601 | — | Timestamp consent was captured. UTC recommended. |
| ip | string | — | Original click/submit IP. Used for fraud + dedupe. |
| user_agent | string | — | Browser user-agent string at submit time. |
| source_sub_id | string | — | Your own A/B variant or campaign ID — round-tripped in webhooks. |
| extra_fields | object | — | Sector-specific structured fields (mortgage value, loan amount, debt total, etc.). |
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.
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.purchasedA 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.rejectedYour 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"
}X-Connexis-Signature: t=<unix_ts>,v1=<hex_sha256>. Verify before processing — see next section for working snippets.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.
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));
}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)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.
| Status | Reason | Notes |
|---|---|---|
| 401 | Invalid or missing X-API-Key | Header missing, key revoked, or doesn't exist. |
| 403 | Publisher account is paused | Contact your account manager. Existing keys continue to validate but ingestion is blocked. |
| 422 | Validation error | Pydantic schema failure — typically a required field is missing or malformed. |
| 409 | Duplicate lead | Same email or phone within the 30-day dedupe window. |
| 429 | Rate limited | Per-key throughput exceeded. See Rate limits. |
| 500 | Internal error | Unexpected — auto-retried by your client? Email ops@connex.is with the request id. |
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.
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.
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);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.
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.
<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.
- 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.