Href Creative Docs
Lead Intake API

POST /api/leads/intake

Push a new lead into a tenant. The only public write endpoint in v1.

This is the endpoint HREF developers wire client lead forms into. Every successful POST creates a lead, fires a realtime broadcast to the tenant's CRM, and (eventually) emails every member of the tenant.

Endpoint

POST https://<tenant>.hrefcreative.co/api/leads/intake

<tenant> is the slug assigned at provisioning (e.g. acme). In local development, replace the host with <tenant>.localtest.me:3001.

Headers

HeaderRequiredNotes
Content-TypeyesMust be application/json
X-API-KeyyesThe tenant's API key, in the form hcrm_<43 chars>

Request body

A JSON object. Every field is optional individually, but at least one of email or phone must be present. Unknown top-level fields are stripped.

FieldTypeNotes
first_name, last_namestringtrimmed, max 200 chars each
emailstringvalid email format, lowercased and trimmed, max 320 chars
phonestringtrimmed, max 50 chars (no format enforcement)
street, city, state, zipstringtrimmed, max varies
sourcestringfree text, max 200 chars; surfaces in the inbox row, kanban card, and notification email
utm_source ... utm_contentstringfull UTM set, max 200 each
metadataobjectopen-ended JSON object; arbitrary extras (loan amount, case type, anything your form sends) are preserved here

The metadata blob is the v1 escape hatch for industry-specific fields: a divorce attorney's case_type, a mortgage broker's loan_amount, etc. Send anything; it's stored as-is and surfaces in the lead detail view as a key/value list.

Response

201 Created

{
  "lead_id": "79732b65-ec7d-4e9e-91f7-90bc42a0f763",
  "created_at": "2026-05-10T16:42:11.834Z"
}

400 Bad Request

The body did not parse as JSON, or it failed validation.

{
  "error": "invalid_request",
  "message": "Body did not pass validation.",
  "details": [
    { "path": "email", "message": "At least one of email or phone is required" }
  ]
}

401 Unauthorized

Missing or invalid X-API-Key.

{ "error": "invalid_api_key", "message": "API key is invalid for this tenant." }

404 Not Found

The subdomain does not match a known tenant.

{ "error": "tenant_not_found", "message": "No tenant matches this subdomain." }

5xx Internal Error

{ "error": "internal_error", "message": "Failed to create lead." }

Safe to retry: leads are not deduplicated in v1, but the bigger concern is Resend retries on the email side. Idempotency keys land in a future phase if double-submits become a real problem.

Examples

curl

curl -X POST https://acme.hrefcreative.co/api/leads/intake \
  -H "Content-Type: application/json" \
  -H "X-API-Key: hcrm_AJoMq2B4SvJDvshNJqruPrZOIudmVh1r1QkwNuYJU4M" \
  -d '{
    "first_name": "Jane",
    "last_name": "Doe",
    "email": "jane@example.com",
    "phone": "+15551234567",
    "source": "website-contact-form",
    "utm_source": "google",
    "utm_medium": "cpc",
    "utm_campaign": "spring-2026",
    "metadata": {
      "case_type": "divorce",
      "estimated_assets": 450000
    }
  }'

Node 22+ (fetch)

const res = await fetch("https://acme.hrefcreative.co/api/leads/intake", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-API-Key": process.env.ACME_CRM_KEY!,
  },
  body: JSON.stringify({
    first_name: "Jane",
    email: "jane@example.com",
    source: "website-contact-form",
    metadata: { case_type: "divorce" },
  }),
});

if (!res.ok) {
  const err = await res.json();
  throw new Error(`intake failed: ${err.error} ${err.message}`);
}

const { lead_id } = await res.json();

Python (requests)

import os, requests

resp = requests.post(
    "https://acme.hrefcreative.co/api/leads/intake",
    headers={
        "Content-Type": "application/json",
        "X-API-Key": os.environ["ACME_CRM_KEY"],
    },
    json={
        "first_name": "Jane",
        "email": "jane@example.com",
        "source": "website-contact-form",
        "metadata": {"case_type": "divorce"},
    },
    timeout=10,
)

resp.raise_for_status()
print(resp.json()["lead_id"])

What happens server-side

  1. The proxy resolves the tenant from the host header and stamps x-tenant-slug on the request.
  2. The route handler resolves the tenant (cached for 60 seconds) and returns 404 if it does not exist.
  3. The provided X-API-Key is hashed with SHA-256 and compared in constant time against the stored tenants.api_key_hash. Mismatches return 401.
  4. The body is parsed and validated with Zod. Failures return 400 with field-level details.
  5. The shared createLead() helper:
    • Looks up the tenant's first stage_type='active' pipeline stage and lands the lead there.
    • Inserts the leads row via service role with status='new'.
    • Inserts a lead_activities.created row.
    • Fires-and-forgets a Resend email to every active member of the tenant.
    • Supabase Realtime auto-broadcasts the insert on the tenant:<id> channel.
  6. The handler returns 201 with the new lead_id and created_at.

The same createLead() helper is used by the manual-entry form in the CRM UI, so the API and UI cannot drift.

What's deliberately not in v1

  • Rate limiting. HREF controls every form posting in. If a runaway form floods a tenant, we'll see it and add per-key throttling at that point.
  • Idempotency keys. Skipped until double-submits become a real problem.
  • Outbound webhooks. No "lead created → POST anywhere" feature yet.
  • Bulk insert. One lead per request.

On this page