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}/submitSubmit 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 Parameter | Type | Description |
|---|---|---|
downloadType | string | Optional. Describes what was downloaded (e.g. brochure, whitepaper) |
Request Body
Both endpoints accept the same JSON body:
| Field | Type | Required | Description |
|---|---|---|---|
firstName | string | Yes | Contact's first name |
lastName | string | Yes | Contact's last name |
email | string | Yes | Contact's email address |
phone | string | No | Phone number |
companyName | string | No | Company name (B2B) |
companyWebsite | string | No | Company website URL |
industry | string | No | Industry or sector |
interest | string | No | What the contact is interested in |
message | string | No | Free-form message |
website | string | No | Honeypot field — see Spam Protection |
utmSource | string | No | UTM source parameter |
utmMedium | string | No | UTM medium parameter |
utmCampaign | string | No | UTM campaign parameter |
utmContent | string | No | UTM content parameter |
utmTerm | string | No | UTM term parameter |
referrerUrl | string | No | The referring page URL |
pageUrl | string | No | The page URL where the form was submitted |
Response
| Status | Description |
|---|---|
201 Created | Lead was accepted successfully (empty body) |
400 Bad Request | Validation error (missing required fields) |
404 Not Found | Tenant slug not found |
429 Too Many Requests | Rate limit exceeded |
500 Internal Server Error | Unexpected 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 Code | Meaning | Action |
|---|---|---|
201 | Lead created successfully | Show success message |
400 | Missing required fields (firstName, lastName, email) | Show validation errors |
404 | Invalid tenant slug | Check your tenant slug configuration |
429 | Rate limit exceeded (5/min/IP) | Ask user to wait and retry |
500 | Server error | Retry or contact support |