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 engineering@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.
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.