Lead Ingestion API
The Lead Ingestion API is designed for vendors and external systems to submit leads to Ledly. This API uses API key authentication and provides specialized endpoints for high-volume lead submission.
Base URL: https://api.ledly.io/api/v1
Authentication: API Key (see Authentication)
Quick Start
Get Your API Key
Generate an API key in Settings → API Keys
Submit Your First Lead
curl -X POST https://api.ledly.io/api/leads/inbound \
-H "Authorization: Bearer vk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"first_name": "John",
"last_name": "Doe",
"program_interest": "MBA"
}'Check Response
{
"success": true,
"lead_id": "lead_abc123",
"message": "Lead accepted"
}Single Lead Submission
Submit one lead at a time.
Endpoint: POST /api/leads/inbound
Request
curl -X POST https://api.ledly.io/api/leads/inbound \
-H "Authorization: Bearer vk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"first_name": "Jane",
"last_name": "Doe",
"phone": "+1-555-123-4567",
"program_interest": "MBA Online",
"source": "landing-page-a",
"utm_source": "google",
"utm_medium": "cpc",
"utm_campaign": "spring-2025",
"custom_fields": {
"preferred_start": "Fall 2025",
"time_preference": "Evening",
"years_experience": 5
}
}'Request Body Fields
| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | Lead’s email address |
first_name | string | No | First name |
last_name | string | No | Last name |
phone | string | No | Phone number (any format) |
program_interest | string | No | Program of interest |
source | string | No | Lead source identifier |
ip_address | string | No | Lead’s IP address |
user_agent | string | No | Browser user agent |
landing_page | string | No | Landing page URL |
referrer | string | No | Referrer URL |
utm_source | string | No | UTM source parameter |
utm_medium | string | No | UTM medium parameter |
utm_campaign | string | No | UTM campaign parameter |
utm_term | string | No | UTM term parameter |
utm_content | string | No | UTM content parameter |
custom_fields | object | No | Custom field values |
Response (201 Created)
{
"success": true,
"lead_id": "lead_abc123xyz",
"message": "Lead accepted",
"timestamp": "2024-12-25T10:30:00Z"
}Error Responses
Validation Error (400 Bad Request):
{
"success": false,
"error": "Validation failed",
"details": [
{
"field": "email",
"message": "Invalid email format"
},
{
"field": "phone",
"message": "Phone number must be in E.164 format"
}
]
}Duplicate Lead (409 Conflict):
{
"success": false,
"error": "Duplicate lead detected",
"duplicate_of": "lead_existing123",
"similarity_score": 95,
"matched_fields": ["email"]
}Rejected by Rules (422 Unprocessable Entity):
{
"success": false,
"error": "Lead rejected",
"reason": "Disposable email address not allowed",
"rule_id": "rule_reject_disposable"
}Rate Limited (429 Too Many Requests):
{
"success": false,
"error": "Rate limit exceeded",
"retry_after": 60,
"limit": 500,
"window": "1 minute"
}Bulk Lead Submission
Submit up to 100 leads in a single request for improved efficiency.
Endpoint: POST /api/leads/inbound/batch
Request
curl -X POST https://api.ledly.io/api/leads/inbound/batch \
-H "Authorization: Bearer vk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"leads": [
{
"email": "[email protected]",
"first_name": "John",
"last_name": "Smith",
"program_interest": "MBA"
},
{
"email": "[email protected]",
"first_name": "Jane",
"last_name": "Doe",
"program_interest": "Nursing"
},
{
"email": "[email protected]",
"first_name": "Bob",
"last_name": "Johnson",
"program_interest": "Computer Science"
}
]
}'Response (200 OK)
{
"success": true,
"total": 3,
"accepted": 2,
"rejected": 1,
"results": [
{
"index": 0,
"success": true,
"lead_id": "lead_abc123",
"email": "[email protected]"
},
{
"index": 1,
"success": true,
"lead_id": "lead_def456",
"email": "[email protected]"
},
{
"index": 2,
"success": false,
"email": "[email protected]",
"error": "Duplicate lead detected",
"duplicate_of": "lead_existing789"
}
],
"timestamp": "2024-12-25T10:30:00Z"
}Bulk ingestion processes all leads independently. Some may succeed while others fail.
Field Validation
Email Validation
Emails are validated for:
- Syntax - Valid RFC 5322 format
- Domain - Valid MX records
- Disposable - Blocked disposable email providers (configurable)
- Typos - Common domain typos flagged
Valid Examples:
Invalid Examples:
not-an-email
@example.com
user@
[email protected]Phone Number Formatting
Phone numbers are automatically normalized to E.164 format. All these are equivalent:
+1-555-123-4567
(555) 123-4567
555.123.4567
5551234567International Numbers:
+44 20 7123 4567 (UK)
+49 30 12345678 (Germany)
+61 2 1234 5678 (Australia)
+86 10 1234 5678 (China)Always include country code for international numbers to ensure proper formatting.
Custom Fields
Custom fields allow you to capture additional data beyond standard fields.
Defining Custom Fields
Custom fields must be configured by your organization administrator before use. Contact your admin to set up:
- Field name and type
- Validation rules
- Default values
- Required/optional status
Using Custom Fields
Pass custom fields in the custom_fields object:
{
"email": "[email protected]",
"first_name": "John",
"custom_fields": {
"preferred_start": "Fall 2025",
"education_level": "Bachelor's Degree",
"years_experience": 5,
"interested_in_scholarship": true,
"referrer_name": "Dr. Smith",
"notes": "Interested in evening classes"
}
}Custom Field Types
| Type | Example Value | Description |
|---|---|---|
string | "Fall 2025" | Text value |
number | 5 | Numeric value |
boolean | true | True/false |
date | "2025-09-01" | ISO 8601 date |
array | ["MBA", "Finance"] | List of values |
object | {"key": "value"} | Nested object |
UTM Parameters and Attribution
Track marketing attribution by including UTM parameters:
{
"email": "[email protected]",
"utm_source": "google",
"utm_medium": "cpc",
"utm_campaign": "spring-enrollment-2025",
"utm_term": "online mba programs",
"utm_content": "ad-variant-a"
}UTM Parameter Reference
| Parameter | Description | Example |
|---|---|---|
utm_source | Traffic source | google, facebook, email |
utm_medium | Marketing medium | cpc, email, social |
utm_campaign | Campaign name | spring-enrollment-2025 |
utm_term | Paid keywords | online mba programs |
utm_content | Ad variation | ad-variant-a, banner-blue |
Error Handling
HTTP Status Codes
| Code | Meaning | Action |
|---|---|---|
| 201 | Lead created successfully | Continue |
| 400 | Bad request (validation error) | Fix data and retry |
| 401 | Unauthorized (invalid API key) | Check API key |
| 409 | Conflict (duplicate lead) | Don’t retry |
| 422 | Unprocessable (rejected by rules) | Don’t retry |
| 429 | Too many requests (rate limited) | Wait and retry |
| 500 | Server error | Retry with backoff |
| 503 | Service unavailable | Retry with backoff |
Retry Strategy
Implement exponential backoff for server errors and rate limits:
async function submitLeadWithRetry(leadData, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch('https://api.ledly.io/api/leads/inbound', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.LEDLY_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(leadData),
});
// Success
if (response.status === 201) {
return await response.json();
}
// Rate limited - use Retry-After header
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After') || 60;
await sleep(retryAfter * 1000);
continue;
}
// Server error - retry with exponential backoff
if (response.status >= 500) {
await sleep(Math.pow(2, attempt) * 1000);
continue;
}
// Client error - don't retry
if (response.status >= 400) {
const error = await response.json();
throw new Error(`Lead submission failed: ${error.error}`);
}
} catch (error) {
if (attempt === maxRetries - 1) throw error;
await sleep(Math.pow(2, attempt) * 1000);
}
}
throw new Error('Max retries exceeded');
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}Rate Limits
Limits by Environment
| Environment | Endpoint | Limit | Window |
|---|---|---|---|
| Production | Single ingest | 500 requests | 1 minute |
| Production | Bulk ingest | 50 requests | 1 minute |
| Sandbox | Single ingest | 100 requests | 1 minute |
| Sandbox | Bulk ingest | 10 requests | 1 minute |
Rate Limit Headers
Check rate limit status in response headers:
X-RateLimit-Limit: 500
X-RateLimit-Remaining: 487
X-RateLimit-Reset: 1703505660Exceeding Rate Limits
When rate limited, you’ll receive:
{
"success": false,
"error": "Rate limit exceeded",
"retry_after": 45,
"limit": 500,
"window": "1 minute"
}And response headers:
Retry-After: 45
X-RateLimit-Limit: 500
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1703505660Testing and Sandbox
Sandbox Environment
Test your integration without affecting production data:
Endpoint: https://sandbox.api.ledly.io/api/leads/inbound
API Key: vk_test_... (different from production)Sandbox Behavior:
- All validation and processing rules apply
- Leads are automatically deleted after 24 hours
- Lower rate limits than production
- No CRM sync occurs
- Webhooks sent to sandbox URLs only
Testing Best Practices
- Test validation - Submit invalid data to verify error handling
- Test duplicates - Submit the same lead twice to verify deduplication
- Test rate limits - Ensure your code handles 429 responses
- Test custom fields - Verify all custom fields are accepted
- Monitor acceptance rate - Track success vs. rejection rates
Tracking and Monitoring
Request ID
Each request receives a unique ID in response headers:
X-Request-ID: req_abc123xyzUse Cases:
- Debug specific submissions
- Track lead through processing pipeline
- Include in support requests
Checking Lead Status
After submission, check lead processing status:
GET /api/leads/{lead_id}/statusResponse:
{
"id": "lead_abc123",
"status": "accepted",
"submitted_at": "2024-12-25T10:30:00Z",
"processed_at": "2024-12-25T10:30:01Z",
"validation_status": "passed",
"enrichment_status": "completed",
"crm_sync_status": "synced",
"crm_id": "00Q1234567890ABC"
}Webhooks
Configure webhooks to receive notifications about lead processing:
Available Events:
lead.accepted- Lead successfully ingestedlead.rejected- Lead rejected by validationlead.duplicate- Duplicate lead detectedlead.crm_synced- Lead synced to CRM
See Webhooks API for configuration.
Best Practices
1. Validate Before Submitting
Validate data client-side before API calls:
function validateLead(lead) {
const errors = [];
// Email validation
if (!lead.email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(lead.email)) {
errors.push('Invalid email format');
}
// Phone validation (if provided)
if (lead.phone && !/^\+?[1-9]\d{1,14}$/.test(lead.phone.replace(/[\s()-]/g, ''))) {
errors.push('Invalid phone number');
}
return errors;
}2. Include Source Tracking
Always include source and UTM parameters:
{
email: '[email protected]',
source: 'landing-page-mba',
utm_source: 'google',
utm_medium: 'cpc',
utm_campaign: 'spring-2025'
}3. Use Bulk Endpoint for Batches
For multiple leads, use bulk ingestion:
// Good - Bulk submission
await bulkIngest([lead1, lead2, lead3]);
// Bad - Individual submissions
await ingestLead(lead1);
await ingestLead(lead2);
await ingestLead(lead3);4. Handle Errors Gracefully
Don’t retry 4xx errors (fix the data first):
if (response.status >= 400 && response.status < 500) {
// Client error - log and don't retry
console.error('Lead submission failed:', await response.json());
return;
}
if (response.status >= 500) {
// Server error - retry with backoff
await retryWithBackoff();
}5. Monitor Acceptance Rates
Track your lead acceptance rate:
const stats = {
submitted: 0,
accepted: 0,
rejected: 0,
duplicates: 0
};
// Alert if acceptance rate drops below 90%
if (stats.accepted / stats.submitted < 0.9) {
alertDevTeam('Low lead acceptance rate');
}6. Store Request IDs
Log request IDs for debugging:
const response = await submitLead(leadData);
const requestId = response.headers.get('X-Request-ID');
// Store for debugging
await logSubmission({
leadEmail: leadData.email,
requestId,
timestamp: new Date(),
success: response.status === 201
});Examples
Complete Integration Example
class LedlyClient {
constructor(apiKey, environment = 'production') {
this.apiKey = apiKey;
this.baseUrl = environment === 'production'
? 'https://api.ledly.io/api/v1'
: 'https://sandbox.api.ledly.io/api/v1';
}
async submitLead(leadData) {
const response = await fetch(`${this.baseUrl}/leads/ingest`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(leadData),
});
const requestId = response.headers.get('X-Request-ID');
if (!response.ok) {
const error = await response.json();
throw new LeadSubmissionError(error, requestId);
}
const result = await response.json();
return { ...result, requestId };
}
async bulkSubmit(leads) {
const response = await fetch(`${this.baseUrl}/leads/bulk-ingest`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ leads }),
});
return await response.json();
}
}
// Usage
const client = new LedlyClient(process.env.LEDLY_API_KEY);
try {
const result = await client.submitLead({
email: '[email protected]',
first_name: 'John',
last_name: 'Doe',
program_interest: 'MBA',
utm_source: 'google'
});
console.log('Lead submitted:', result.lead_id);
} catch (error) {
console.error('Submission failed:', error.message);
}Related Resources
- Authentication - API key generation and management
- Webhooks - Receive lead notifications
- Vendor Portal - Monitor submitted leads
- Validation Guide - Understanding validation rules