Lead Hub
API Docs

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:

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

FieldTypeDefaultDescription
user_iduuidSupabase auth user id — primary key of public.members (required)
displayNamestringOptional display name

Required

FieldTypeDefaultDescription
sourcestringOriginating site/form (e.g. "weekly-investor")
emailstringValid email address

Core

FieldTypeDefaultDescription
phonestringPhone number — used for XVerify
firstNamestring
lastNamestring

Consent

FieldTypeDefaultDescription
marketing_consentbooleanfalse
sms_consentbooleanfalse
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

FieldTypeDefaultDescription
universal_leadidstringTrustedForm / Universal LeadId
affstringAffiliate ID
s1 – s20stringSub-ID / click-tracking params (s1 = primary transaction ID)

UTM

FieldTypeDefaultDescription
utm_sourcestring
utm_mediumstring
utm_campaignstring
utm_termstring
utm_contentstring

browserData (object, optional)

FieldTypeDefaultDescription
deviceTypestring
operatingSystemstring
browserstring
browserVersionstring
screenResolutionstring
viewportSizestring
languagestring
timezonestring
timezoneOffsetnumber
connectionTypestring
pageUrlstring
pageTitlestring
landingPagestring
referrerstring
sessionIdstring
isReturningVisitorboolean
timeOnPagenumberseconds
formCompletionTimenumberseconds

locationData (object, optional)

FieldTypeDefaultDescription
countrystring
regionstring
citystring
postalstring
latitudenumber
longitudenumber

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

FieldTypeDefaultDescription
GET /api/cron/aweber-members-retryEvery 15 minRetries 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=true to 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_id or email is rejected with 409 (also enforced by unique index on lower(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.