API ReferenceLead Ingestion

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 SettingsAPI 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

FieldTypeRequiredDescription
emailstringYesLead’s email address
first_namestringNoFirst name
last_namestringNoLast name
phonestringNoPhone number (any format)
program_intereststringNoProgram of interest
sourcestringNoLead source identifier
ip_addressstringNoLead’s IP address
user_agentstringNoBrowser user agent
landing_pagestringNoLanding page URL
referrerstringNoReferrer URL
utm_sourcestringNoUTM source parameter
utm_mediumstringNoUTM medium parameter
utm_campaignstringNoUTM campaign parameter
utm_termstringNoUTM term parameter
utm_contentstringNoUTM content parameter
custom_fieldsobjectNoCustom 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:

  1. Syntax - Valid RFC 5322 format
  2. Domain - Valid MX records
  3. Disposable - Blocked disposable email providers (configurable)
  4. 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
5551234567

International 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

TypeExample ValueDescription
string"Fall 2025"Text value
number5Numeric value
booleantrueTrue/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

ParameterDescriptionExample
utm_sourceTraffic sourcegoogle, facebook, email
utm_mediumMarketing mediumcpc, email, social
utm_campaignCampaign namespring-enrollment-2025
utm_termPaid keywordsonline mba programs
utm_contentAd variationad-variant-a, banner-blue

Error Handling

HTTP Status Codes

CodeMeaningAction
201Lead created successfullyContinue
400Bad request (validation error)Fix data and retry
401Unauthorized (invalid API key)Check API key
409Conflict (duplicate lead)Don’t retry
422Unprocessable (rejected by rules)Don’t retry
429Too many requests (rate limited)Wait and retry
500Server errorRetry with backoff
503Service unavailableRetry 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

EnvironmentEndpointLimitWindow
ProductionSingle ingest500 requests1 minute
ProductionBulk ingest50 requests1 minute
SandboxSingle ingest100 requests1 minute
SandboxBulk ingest10 requests1 minute

Rate Limit Headers

Check rate limit status in response headers:

X-RateLimit-Limit: 500
X-RateLimit-Remaining: 487
X-RateLimit-Reset: 1703505660

Exceeding 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: 1703505660

Testing 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

  1. Test validation - Submit invalid data to verify error handling
  2. Test duplicates - Submit the same lead twice to verify deduplication
  3. Test rate limits - Ensure your code handles 429 responses
  4. Test custom fields - Verify all custom fields are accepted
  5. 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_abc123xyz

Use 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}/status

Response:

{
  "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 ingested
  • lead.rejected - Lead rejected by validation
  • lead.duplicate - Duplicate lead detected
  • lead.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);
}