Lead Hub API
Internal API for ingesting leads and enrolling authenticated site members.
POST /api/leads
Accepts a lead, verifies contact info via XVerify, saves to Supabase, and syncs to AWeber. Beehiiv sync is currently disabled — set BEEHIIV_ENABLED=true to resume.
POST /api/members
Enrolls an authenticated reader of the weekly-investor site as a member. Same pipeline as /api/leads (auth → validate → dedupe → XVerify → save) with these differences:
- Body = lead payload plus
user_id(required Supabase auth uuid) and optionaldisplayName. - Saves to
membersinstead ofleads.tieris alwaysfree— never accepted from callers. - Subscriber syncs to the dedicated AWeber members list (
AWEBER_MEMBERS_LIST_ID), taggedmember_signup. - Dedupe — same
user_idor email returns409permanently (no 5-minute window).
Trust model: user_id must be a real auth.users row (FK enforced). Email in the body must match that auth user's email — the weekly-investor site bridge enforces this via auth.admin.getUserById.
Authentication
All requests must include:
x-api-key: <LEAD_HUB_API_KEY>Missing or incorrect key returns 401 Unauthorized.
Request Fields
Content-Type: application/json
Shared by both routes unless noted. /api/members does not accept double_opt_override.
Members only
| Field | Type | Default | Description |
|---|---|---|---|
| user_id | uuid | Supabase auth user id — primary key of public.members (required) | |
| displayName | string | — | Optional display name |
Required
| Field | Type | Default | Description |
|---|---|---|---|
| source | string | Originating site/form (e.g. "weekly-investor") | |
| string | Valid email address |
Core
| Field | Type | Default | Description |
|---|---|---|---|
| phone | string | — | Phone number — used for XVerify |
| firstName | string | — | |
| lastName | string | — |
Consent
| Field | Type | Default | Description |
|---|---|---|---|
| marketing_consent | boolean | false | |
| sms_consent | boolean | false | |
| double_opt_override | "on" | "off" | "not_set" | "not_set" | Leads only. Overrides Beehiiv double opt-in — currently inert (Beehiiv sync disabled); only applies when BEEHIIV_ENABLED=true. |
Tracking
| Field | Type | Default | Description |
|---|---|---|---|
| universal_leadid | string | — | TrustedForm / Universal LeadId |
| aff | string | — | Affiliate ID |
| s1 – s20 | string | — | Sub-ID / click-tracking params (s1 = primary transaction ID) |
UTM
| Field | Type | Default | Description |
|---|---|---|---|
| utm_source | string | — | |
| utm_medium | string | — | |
| utm_campaign | string | — | |
| utm_term | string | — | |
| utm_content | string | — |
browserData (object, optional)
| Field | Type | Default | Description |
|---|---|---|---|
| deviceType | string | — | |
| operatingSystem | string | — | |
| browser | string | — | |
| browserVersion | string | — | |
| screenResolution | string | — | |
| viewportSize | string | — | |
| language | string | — | |
| timezone | string | — | |
| timezoneOffset | number | — | |
| connectionType | string | — | |
| pageUrl | string | — | |
| pageTitle | string | — | |
| landingPage | string | — | |
| referrer | string | — | |
| sessionId | string | — | |
| isReturningVisitor | boolean | — | |
| timeOnPage | number | — | seconds |
| formCompletionTime | number | — | seconds |
locationData (object, optional)
| Field | Type | Default | Description |
|---|---|---|---|
| country | string | — | |
| region | string | — | |
| city | string | — | |
| postal | string | — | |
| latitude | number | — | |
| longitude | number | — |
Example Request — /api/leads
curl -X POST https://your-domain.com/api/leads \
-H "Content-Type: application/json" \
-H "x-api-key: your_api_key_here" \
-d '{
"source": "weekly-investor",
"email": "jane@example.com",
"phone": "5551234567",
"firstName": "Jane",
"lastName": "Doe",
"marketing_consent": true,
"sms_consent": false,
"double_opt_override": "off",
"aff": "aff123",
"s1": "click_abc",
"utm_source": "facebook",
"utm_medium": "cpc",
"utm_campaign": "q1-promo"
}'Example Request — /api/members
curl -X POST https://your-domain.com/api/members \
-H "Content-Type: application/json" \
-H "x-api-key: your_api_key_here" \
-d '{
"source": "the-weekly-investor-site",
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"email": "jane@example.com",
"firstName": "Jane",
"lastName": "Doe",
"marketing_consent": true
}'Responses
200 OK — /api/leads
{
"success": true,
"leadId": "uuid-string",
"source": "weekly-investor",
"verified": { "email": true, "phone": true }
}Use leadId for downstream monetization calls.
200 OK — /api/members
{
"success": true,
"memberId": "550e8400-e29b-41d4-a716-446655440000",
"source": "the-weekly-investor-site",
"verified": { "email": true, "phone": false }
}If AWEBER_MEMBERS_LIST_ID is missing, enrollment still succeeds but the response includes a warnings array.
400 — Validation failed
{ "error": "Validation failed", "details": { ... } }Also returned when XVerify marks the contact as invalid (production only).
401 — Unauthorized
{ "error": "Unauthorized" }409 — Duplicate (/api/leads)
{ "error": "Already submitted. Check your email for confirmation.", "isDuplicate": true }Same email or phone + source submitted within 5 minutes.
409 — Duplicate (/api/members)
{ "error": "Already a member.", "isDuplicate": true }Existing row with the same user_id or email (permanent, no time window).
503 — Verification unavailable
{ "error": "Verification service unavailable. Please try again." }500 — Internal error
{ "error": "Internal server error" }Cron Jobs
| Field | Type | Default | Description |
|---|---|---|---|
| GET /api/cron/aweber-members-retry | Every 15 min | — | Retries members where aweber_synced = false |
Both require Authorization: Bearer <CRON_SECRET>.
Behavior Notes
/api/leads
- Deduplication — same email or phone + source within 5 minutes is rejected with 409.
- Verification — email and phone checked via XVerify before saving. Invalid contacts rejected in production. Set
XVERIFY_SKIP_ON_ERROR=trueto bypass on errors. - AWeber sync — happens asynchronously after the response. Beehiiv sync is disabled (gated on
BEEHIIV_ENABLED; the beehiiv-retry cron is unscheduled).
/api/members
- Deduplication — same
user_idor email is rejected with 409 (also enforced by unique index onlower(email)). - Verification — same XVerify rules as leads.
- AWeber sync — happens asynchronously to
AWEBER_MEMBERS_LIST_ID. Unsynced members are retried by/api/cron/aweber-members-retry.
Both routes
- IP / User-Agent — captured automatically from request headers.