AlphaAlpha Docs

Lead Capture API

Integrate your website forms with the Alpha CRM to automatically capture leads

Lead Capture API

The Lead Capture API allows you to submit leads from any external website or application directly into the Alpha CRM. No authentication is required — these are public endpoints designed for use in client-side forms.

Quick Start

Submit a lead with a simple fetch call:

await fetch("https://your-alpha-instance.com/api/v1/public/leads/your-tenant-slug/submit", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    firstName: "Jane",
    lastName: "Doe",
    email: "jane@example.com",
    message: "Interested in your services",
  }),
});

Finding Your Tenant Slug

Your tenant slug is the unique code for your organization. You can find it in Settings > Organization > Tenant Code.

This slug is used in the API URL path: /api/v1/public/leads/{tenantSlug}/submit


API Reference

Submit Contact Form

Creates a new lead with source WEBSITE_CONTACT.

POST /api/v1/public/leads/{tenantSlug}/submit

Submit Download Request

Creates a new lead with source WEBSITE_DOWNLOAD. Use this for brochure or content download forms.

POST /api/v1/public/leads/{tenantSlug}/download?downloadType=brochure
Query ParameterTypeDescription
downloadTypestringOptional. Describes what was downloaded (e.g. brochure, whitepaper)

Request Body

Both endpoints accept the same JSON body:

FieldTypeRequiredDescription
firstNamestringYesContact's first name
lastNamestringYesContact's last name
emailstringYesContact's email address
phonestringNoPhone number
companyNamestringNoCompany name (B2B)
companyWebsitestringNoCompany website URL
industrystringNoIndustry or sector
intereststringNoWhat the contact is interested in
messagestringNoFree-form message
websitestringNoHoneypot field — see Spam Protection
utmSourcestringNoUTM source parameter
utmMediumstringNoUTM medium parameter
utmCampaignstringNoUTM campaign parameter
utmContentstringNoUTM content parameter
utmTermstringNoUTM term parameter
referrerUrlstringNoThe referring page URL
pageUrlstringNoThe page URL where the form was submitted

Response

StatusDescription
201 CreatedLead was accepted successfully (empty body)
400 Bad RequestValidation error (missing required fields)
404 Not FoundTenant slug not found
429 Too Many RequestsRate limit exceeded
500 Internal Server ErrorUnexpected server error

Spam Protection (Honeypot)

The API supports a honeypot field called website. This field should be included in your form but hidden from real users using CSS. Bots that auto-fill all fields will populate it, and the submission will be silently discarded.

<form id="contact-form">
  <input type="text" name="firstName" required />
  <input type="text" name="lastName" required />
  <input type="email" name="email" required />
  <textarea name="message"></textarea>

  <!-- Honeypot: hidden from humans, bots will fill it -->
  <div style="position: absolute; left: -9999px;" aria-hidden="true">
    <input type="text" name="website" tabindex="-1" autocomplete="off" />
  </div>

  <button type="submit">Send</button>
</form>

When the website field contains any value, the API returns 201 Created as usual (so bots believe it worked), but no lead is created.


Rate Limits

Public lead endpoints are rate-limited to 5 requests per minute per IP address per tenant. This prevents abuse while allowing normal form submissions.

When the limit is exceeded, the API returns:

{
  "status": 429,
  "error": "Too Many Requests",
  "message": "Rate limit exceeded. Please try again later."
}

Handle this in your frontend by showing a friendly message:

const response = await fetch(url, { method: "POST", ... });

if (response.status === 429) {
  alert("Too many submissions. Please wait a minute and try again.");
  return;
}

UTM Campaign Tracking

Pass UTM parameters to track which marketing campaigns generate leads. These values appear in the CRM lead detail view.

Extracting UTM Parameters from the URL

function getUtmParams() {
  const params = new URLSearchParams(window.location.search);
  return {
    utmSource: params.get("utm_source"),
    utmMedium: params.get("utm_medium"),
    utmCampaign: params.get("utm_campaign"),
    utmContent: params.get("utm_content"),
    utmTerm: params.get("utm_term"),
    referrerUrl: document.referrer || null,
    pageUrl: window.location.href,
  };
}

Including UTM in Submission

const formData = {
  firstName: "Jane",
  lastName: "Doe",
  email: "jane@example.com",
  message: "Interested in your services",
  ...getUtmParams(),
};

await fetch(`${baseUrl}/api/v1/public/leads/${tenantSlug}/submit`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify(formData),
});

Code Examples

Plain HTML + JavaScript

<form id="contact-form">
  <input type="text" name="firstName" placeholder="First name" required />
  <input type="text" name="lastName" placeholder="Last name" required />
  <input type="email" name="email" placeholder="Email" required />
  <input type="tel" name="phone" placeholder="Phone" />
  <textarea name="message" placeholder="Your message"></textarea>

  <!-- Honeypot -->
  <div style="position: absolute; left: -9999px;" aria-hidden="true">
    <input type="text" name="website" tabindex="-1" autocomplete="off" />
  </div>

  <button type="submit">Submit</button>
</form>

<script>
  const TENANT_SLUG = "your-tenant-slug";
  const API_URL = `https://your-alpha-instance.com/api/v1/public/leads/${TENANT_SLUG}/submit`;

  document.getElementById("contact-form").addEventListener("submit", async (e) => {
    e.preventDefault();
    const form = e.target;
    const data = Object.fromEntries(new FormData(form));

    // Add UTM params
    const params = new URLSearchParams(window.location.search);
    data.utmSource = params.get("utm_source");
    data.utmMedium = params.get("utm_medium");
    data.utmCampaign = params.get("utm_campaign");
    data.referrerUrl = document.referrer;
    data.pageUrl = window.location.href;

    try {
      const res = await fetch(API_URL, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(data),
      });

      if (res.status === 201) {
        form.reset();
        alert("Thank you! We'll be in touch.");
      } else if (res.status === 429) {
        alert("Too many submissions. Please wait a minute.");
      } else {
        alert("Something went wrong. Please try again.");
      }
    } catch {
      alert("Network error. Please check your connection.");
    }
  });
</script>

React

import { useState, FormEvent } from "react";

const TENANT_SLUG = "your-tenant-slug";
const API_URL = `https://your-alpha-instance.com/api/v1/public/leads/${TENANT_SLUG}/submit`;

export function ContactForm() {
  const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle");

  async function handleSubmit(e: FormEvent<HTMLFormElement>) {
    e.preventDefault();
    setStatus("loading");

    const form = new FormData(e.currentTarget);
    const params = new URLSearchParams(window.location.search);

    const data = {
      firstName: form.get("firstName"),
      lastName: form.get("lastName"),
      email: form.get("email"),
      message: form.get("message"),
      website: form.get("website"), // honeypot
      utmSource: params.get("utm_source"),
      utmMedium: params.get("utm_medium"),
      utmCampaign: params.get("utm_campaign"),
      referrerUrl: document.referrer,
      pageUrl: window.location.href,
    };

    try {
      const res = await fetch(API_URL, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(data),
      });

      setStatus(res.status === 201 ? "success" : "error");
    } catch {
      setStatus("error");
    }
  }

  if (status === "success") return <p>Thank you! We'll be in touch.</p>;

  return (
    <form onSubmit={handleSubmit}>
      <input name="firstName" placeholder="First name" required />
      <input name="lastName" placeholder="Last name" required />
      <input name="email" type="email" placeholder="Email" required />
      <textarea name="message" placeholder="Your message" />

      {/* Honeypot - hidden from users */}
      <div style={{ position: "absolute", left: "-9999px" }} aria-hidden="true">
        <input name="website" tabIndex={-1} autoComplete="off" />
      </div>

      <button type="submit" disabled={status === "loading"}>
        {status === "loading" ? "Sending..." : "Submit"}
      </button>
      {status === "error" && <p>Something went wrong. Please try again.</p>}
    </form>
  );
}

Next.js (Server Action)

"use server";

const TENANT_SLUG = process.env.ALPHA_TENANT_SLUG!;
const API_URL = `${process.env.ALPHA_API_URL}/api/v1/public/leads/${TENANT_SLUG}/submit`;

export async function submitLead(formData: FormData) {
  const data = {
    firstName: formData.get("firstName"),
    lastName: formData.get("lastName"),
    email: formData.get("email"),
    message: formData.get("message"),
    website: formData.get("website"),
    utmSource: formData.get("utmSource"),
    utmMedium: formData.get("utmMedium"),
    utmCampaign: formData.get("utmCampaign"),
    referrerUrl: formData.get("referrerUrl"),
    pageUrl: formData.get("pageUrl"),
  };

  const res = await fetch(API_URL, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(data),
  });

  return { success: res.status === 201 };
}

Error Handling

Status CodeMeaningAction
201Lead created successfullyShow success message
400Missing required fields (firstName, lastName, email)Show validation errors
404Invalid tenant slugCheck your tenant slug configuration
429Rate limit exceeded (5/min/IP)Ask user to wait and retry
500Server errorRetry or contact support

On this page