Required headers
Content-Typeapplication/jsonX-Lead-Source-Codewebsite or partner codeX-Lead-Source-Keysource secret keyIdempotency-Keystable external order ID
Contractor menu
Lead quality, purchase formats and app access.
Integration docs
Server-to-server intake for construction orders, lead forms, supplier CRM payloads and source-specific mappings.
Production endpoint
POST https://contractorleads.app/api/v1/lead-intake
Endpoint
Send requests from your backend. Never expose the source key in public form JavaScript.
Content-Type application/jsonX-Lead-Source-Code website or partner codeX-Lead-Source-Key source secret keyIdempotency-Key stable external order IDKeep the same idempotency key when retrying the same order. This prevents duplicate intake requests and duplicate leads.
Payload
The endpoint stores raw JSON first. Use a native payload when possible, mapped JSON for stable supplier fields, or AI normalization for arbitrary CRM JSON.
Native fields when the partner can send our expected JSON shape.
Deterministic source mapping when supplier keys differ but remain stable.
AI normalization for arbitrary CRM JSON that cannot be controlled upstream.
Work category: roofing, remodeling, exterior, and so on.
What the customer wants done.
Customer name.
Customer phone number.
Project location.
Customer consent to share the request. Must be true.
{
"external_order_id": "partner-order-123",
"source_site": "partner-site.com",
"landing_page": "/roofing/seattle-wa",
"service_category": "roofing",
"project_type": "residential",
"project_description": "Need a roof replacement estimate after storm damage.",
"budget_min": 15000,
"budget_max": 25000,
"timeline": "Within 30 days",
"customer_name": "Jane Customer",
"customer_phone": "(206) 555-0101",
"customer_email": "[email protected]",
"preferred_contact_method": "phone",
"address_line1": "1200 1st Ave",
"city": "Seattle",
"state": "WA",
"zip_code": "98101",
"country": "US",
"consent": true,
"utm": {
"utm_source": "partner-site",
"utm_campaign": "roofing-seattle"
},
"tracking": {
"click_id": "click-123"
},
"answers": {
"roof_type": "asphalt",
"project_stage": "ready_to_hire"
},
"files": [
{
"url": "https://partner-site.com/uploads/roof-photo.jpg",
"name": "roof-photo.jpg",
"mime_type": "image/jpeg"
}
]
}
Processing
Incoming requests are stored with status=received. Cron then leases queued rows, normalizes payloads, creates lead records and starts the admin price-estimation workflow.
Authenticate source headers, keep the original JSON, and return a request ID.
Cron leases queued rows and converts each request into canonical lead fields.
Accepted leads enter price estimation and the admin review workflow.
Run once
php yii cron/run
Cron
* * * * * cd /path/to/contractorleads && php yii cron/run >> runtime/logs/project-cron.log 2>&1
Processor result
{
"processed_count": 1,
"accepted_count": 1,
"failed_count": 0,
"items": [
{
"request_id": 9102,
"status": "accepted",
"lead_id": 5832,
"error_message": null,
"duplicate_candidate": false,
"duplicate_candidate_count": 0
}
]
}
AI payloads
Configure a source as openai_json_v1 when the supplier cannot send canonical fields. The AI result still passes backend validation before a lead is created.
openai_json_v1, and keep or adjust the OpenAI hints JSON.
Create source
php yii lead-source/create suppliercrm "Supplier CRM" "replace-with-source-api-key" "" openai_json_v1 docs/api/openai-json-lead-source.example.json
Supplier JSON example
{
"supplier": "supplier.example",
"order": {
"id": "supplier-123",
"trade": "Roofing",
"property_type": "Residential",
"details": "Need roof replacement after wind damage.",
"budget": 18000,
"timeline": "Within 30 days",
"priority": "high"
},
"customer": {
"name": "Jane Customer",
"phone": "(206) 555-0101",
"email": "[email protected]"
},
"job_location": {
"address": "1200 1st Ave",
"city": "Seattle",
"state": "WA",
"zip": "98101"
},
"customer_consent": true
}
Responses
Raw JSON was stored and will be processed by cron.
The same idempotency key was replayed; no duplicate request or lead was created.
JSON is invalid, too large, or required headers are missing.
Source code, key, or source status is invalid.
{
"request_id": 9102,
"lead_id": null,
"status": "received",
"duplicate": false,
"queued": true,
"duplicate_candidate": false,
"duplicate_candidate_count": 0
}
lead_id is filled after cron normalizes the queued request.
php yii lead-source/failed-requests 20 suppliercrm 1.
Example
curl -X POST https://contractorleads.app/api/v1/lead-intake \
-H "Content-Type: application/json" \
-H "X-Lead-Source-Code: partner-site" \
-H "X-Lead-Source-Key: replace-with-source-api-key" \
-H "Idempotency-Key: partner-order-123" \
-d @order.json
Backend
const response = await fetch('https://contractorleads.app/api/v1/lead-intake', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Lead-Source-Code': process.env.CONTRACTOR_LEADS_SOURCE_CODE,
'X-Lead-Source-Key': process.env.CONTRACTOR_LEADS_SOURCE_KEY,
'Idempotency-Key': order.id
},
body: JSON.stringify(order)
});
if (!response.ok) {
throw new Error(await response.text());
}
const result = await response.json();
Source onboarding
Separate sources let you disable one partner, change mapping rules and track lead quality by project.