Multi-Contract Website Lead Intake
This document describes a single API contract through which the site backend sends lead data, the visitor's contact details, and marketing UTM metrics to ReactiveCRM in one request.
1. Purpose
The site integrator does not need to separately create the contact, phone, email, lead, and marketing event. One request must atomically create:
- the contact;
- the contact's primary phone;
- the contact's primary email, if provided;
- the lead;
- the link between the contact and the lead;
- the marketing event and UTM attribution.
If any step fails, the whole request is rolled back and partially created data is not saved.
2. Endpoint
POST /api/site/leads
Authorization: Bearer <CRM_ACCESS_TOKEN>
Content-Type: application/json
The token must be passed only from the site backend or a serverless function. Never place the CRM token in browser JavaScript.
The tenant is determined from the authorization token. All provided UUIDs are validated within this tenant.
3. Full JSON Contract
{
"externalId": "site-form-01J7K9A2M4Y5T6",
"lead": {
"leadSourceId": "7b33b18f-55bd-48a5-a8f0-e450a56dde47",
"quality": "WARM",
"message": "I'd like a consultation on CRM implementation"
},
"contact": {
"firstName": "Ivan",
"lastName": "Ivanov",
"phone": "+79991234567",
"email": "ivan@example.com"
},
"marketing": {
"occurredAt": "2026-09-06T18:00:00Z",
"visitorId": "visitor-8b7f",
"sessionId": "session-31ac",
"source": "website",
"channel": "paid",
"utmSource": "google",
"utmMedium": "cpc",
"utmCampaign": "summer-consulting",
"utmContent": "banner-a",
"utmTerm": "crm consultation",
"gclid": "EAIaIQobChMI-example",
"fbclid": null,
"landingUrl": "https://example.com/consultation",
"referrer": "https://www.google.com/"
}
}
4. Request Fields
4.1. Root fields
| Field | Type | Required | Description |
|---|---|---|---|
externalId | string | recommended | Stable unique ID of the form submission for idempotency |
lead | object | yes | Data of the lead being created |
contact | object | yes | Contact person data |
marketing | object | no | UTM metrics and technical data of the marketing visit |
externalId is generated before the first send attempt. When repeating the request after a timeout or network error, use the same value.
4.2. The lead object
| Field | Type | Required | Description |
|---|---|---|---|
leadSourceId | uuid | yes | ID of the lead source in the current tenant |
quality | string | no | Initial rating: HOT, WARM, COOL or COLD |
message | string | no | Visitor's message or a comment on the submission |
If the site does not perform pre-qualification, it is better not to send the quality field.
4.3. The contact object
| Field | Type | Required | Description |
|---|---|---|---|
firstName | string | yes | Contact person's first name |
lastName | string | no | Contact person's last name |
phone | string | yes | Primary phone; E.164 format recommended |
email | string(email) | no | Contact person's primary email |
The minimum data required to process a submission is firstName and phone.
4.4. The marketing object
| Field | Type | Required | Description |
|---|---|---|---|
occurredAt | date-time | no | Form submission time; CRM time is used when absent |
visitorId | string | no | Anonymous visitor ID |
sessionId | string | no | Site session ID |
source | string | no | Event source, defaults to website |
channel | string | no | Channel: e.g. paid, organic, social, email, direct |
utmSource | string | no | utm_source value |
utmMedium | string | no | utm_medium value |
utmCampaign | string | no | utm_campaign value |
utmContent | string | no | utm_content value |
utmTerm | string | no | utm_term value |
gclid | string | no | Google Click ID |
fbclid | string | no | Meta/Facebook Click ID |
landingUrl | string | no | Landing page URL |
referrer | string | no | Previous page URL |
Personal data must not be duplicated in the marketing object. Name, phone, email and the message text are passed only in contact and lead.
5. Minimal Request
{
"externalId": "site-form-550e8400-e29b-41d4-a716-446655440000",
"lead": {
"leadSourceId": "7b33b18f-55bd-48a5-a8f0-e450a56dde47"
},
"contact": {
"firstName": "Ivan",
"phone": "+79991234567"
}
}
6. Successful Response
First request — 201 Created
{
"leadId": "22222222-2222-2222-2222-222222222222",
"contactId": "33333333-3333-3333-3333-333333333333",
"phoneId": "44444444-4444-4444-4444-444444444444",
"emailId": "55555555-5555-5555-5555-555555555555",
"marketingEventId": "66666666-6666-6666-6666-666666666666",
"duplicate": false,
"createdAt": "2026-09-06T18:00:01Z"
}
If email or marketing data is not provided, the corresponding IDs are returned as null.
Repeat with the same externalId — 200 OK
{
"leadId": "22222222-2222-2222-2222-222222222222",
"contactId": "33333333-3333-3333-3333-333333333333",
"phoneId": "44444444-4444-4444-4444-444444444444",
"emailId": "55555555-5555-5555-5555-555555555555",
"marketingEventId": "66666666-6666-6666-6666-666666666666",
"duplicate": true,
"createdAt": "2026-09-06T18:00:01Z"
}
A repeated request must not create a second lead or contact.
7. cURL Example
curl -X POST "$CRM_BASE_URL/api/site/leads" \
-H "Authorization: Bearer $CRM_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"externalId": "site-form-01J7K9A2M4Y5T6",
"lead": {
"leadSourceId": "7b33b18f-55bd-48a5-a8f0-e450a56dde47",
"quality": "WARM",
"message": "Call me back"
},
"contact": {
"firstName": "Ivan",
"lastName": "Ivanov",
"phone": "+79991234567",
"email": "ivan@example.com"
},
"marketing": {
"utmSource": "google",
"utmMedium": "cpc",
"utmCampaign": "crm-demo",
"landingUrl": "https://example.com/demo"
}
}'
8. TypeScript Interfaces
type LeadQuality = 'HOT' | 'WARM' | 'COOL' | 'COLD';
interface SiteLeadRequest {
externalId?: string;
lead: {
leadSourceId: string;
quality?: LeadQuality;
message?: string;
};
contact: {
firstName: string;
lastName?: string;
phone: string;
email?: string;
};
marketing?: {
occurredAt?: string;
visitorId?: string;
sessionId?: string;
source?: string;
channel?: string;
utmSource?: string;
utmMedium?: string;
utmCampaign?: string;
utmContent?: string;
utmTerm?: string;
gclid?: string;
fbclid?: string;
landingUrl?: string;
referrer?: string;
};
}
interface SiteLeadResponse {
leadId: string;
contactId: string;
phoneId: string;
emailId: string | null;
marketingEventId: string | null;
duplicate: boolean;
createdAt: string;
}
9. Server-Side Example
export async function sendLeadToCrm(payload: SiteLeadRequest): Promise<SiteLeadResponse> {
const response = await fetch(`${process.env.CRM_BASE_URL}/api/site/leads`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.CRM_ACCESS_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});
if (!response.ok) {
const body = await response.text();
throw new Error(`ReactiveCRM returned ${response.status}: ${body}`);
}
return response.json() as Promise<SiteLeadResponse>;
}
10. Errors
| HTTP | Cause | Integrator action |
|---|---|---|
400 | Format error, missing required field, invalid email/phone/quality | Fix the data; do not retry automatically without changes |
401 | Token missing or invalid | Refresh the integration token |
403 | No access to the tenant or operation | Check the integration user and role |
404 | leadSourceId not found in the current tenant | Update the source ID in the site settings |
409 | externalId is already linked to an incompatible request | Check the ID generation and the integration log |
5xx | Transient CRM error | Retry the same request with the same externalId |
Example validation error:
{
"status": 400,
"error": "Bad Request",
"message": "contact.phone must not be blank",
"path": "/api/site/leads"
}
11. Idempotency
externalIdmust be unique within the tenant and thewebsitesource.- Generate
externalIdbefore the first request and store it in the site submission. - On a timeout, retry the request with the same body and the same
externalId. - Do not create a new
externalIdwhen retrying a single submission. - If
duplicate: true, the submission is already processed and is considered successfully delivered.
12. Mini-Guide for the Site Developer
- On first visit, store UTM tags,
gclid,fbclid, the landing URL and the referrer. - When the form is submitted, generate a stable
externalId. - Send the form to the site backend.
- The backend adds the CRM token and calls
POST /api/site/leads. - On
201or200withduplicate: true, consider the submission delivered. - On a timeout or
5xx, retry the request with the sameexternalId. - Never send the CRM token directly from the browser.
13. What the CRM Manager Gets
After a successful request, CRM contains a lead that:
- has the
leadSourceIdsource set; - keeps the visitor's message;
- has the initial quality rating, if the site provided it;
- is linked to a contact with a name and primary phone;
- is linked to a primary email, if provided;
- keeps UTM metrics and advertising identifiers for analytics.
This set is enough for the manager to see the submission, call the contact, and qualify the lead.
Reactive CRM