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)
displayNamestring—Optional display name

Required

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

Core

FieldTypeDefaultDescription
phonestring—Phone 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_leadidstring—TrustedForm / Universal LeadId
affstring—Affiliate ID
s1 – s20string—Sub-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—
timeOnPagenumber—seconds
formCompletionTimenumber—seconds

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 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=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.