Lead Import API
Bulk import leads from CSV files with field mapping and deduplication. The flow is preview → start → check status:
POST /leads/import/preview— Upload CSV, getfile_idand column headersPOST /leads/import/start— Start import with field mappings usingfile_idGET /leads/import/jobsandGET /leads/import/jobs/{jobId}— Track progress
POST /leads/import/preview
Upload a CSV file to preview headers and sample rows before importing.
Request
Multipart form data:
| Parameter | Type | Required | Description |
|---|---|---|---|
file | file | Yes | CSV file with header row |
curl -X POST "https://api.kenbun.io/leads/import/preview" \
-H "Authorization: Bearer <token>" \
-F "file=@leads.csv"
Response
200 OK
{
"file_id": "preview_abc123",
"file_name": "leads.csv",
"headers": ["Email", "First Name", "Last Name", "Company", "Phone"],
"sample_rows": [
["jane@example.com", "Jane", "Doe", "Example Inc", "+1-555-0100"],
["john@example.com", "John", "Smith", "Acme Corp", "+1-555-0101"]
],
"row_count": 1000,
"auto_detected_identifiers": {
"email_columns": ["Email"],
"phone_columns": ["Phone"]
},
"expires_at": "2025-01-15T11:00:00Z"
}
| Field | Type | Description |
|---|---|---|
file_id | string | Unique identifier for cached file (valid for 1 hour) |
file_name | string | Original filename |
headers | array | Column names from CSV header row |
sample_rows | array | First 5 rows of data for preview |
row_count | integer | Total rows excluding header |
auto_detected_identifiers | object | Columns that look like identifiers |
expires_at | timestamp | When the cached file expires |
Common Errors
| Status | Meaning | Solution |
|---|---|---|
| 400 | Bad Request | File missing or invalid CSV format |
| 413 | Payload Too Large | File exceeds 10 MB limit |
| 415 | Unsupported Media Type | File must be CSV |
POST /leads/import/start
Start an import job with field mappings.
Request
| Parameter | Type | Required | Description |
|---|---|---|---|
file_id | string | Yes | File ID from preview response |
primary_identifier.column | string | Yes | CSV column name for identifier |
primary_identifier.kind | string | Yes | email, phone, cookie, external_id, or social |
field_mappings | object | No | Map CSV columns to profile fields |
alias_mappings | object | No | Map CSV columns to additional aliases |
Alias mapping keys use the _alias_ prefix: _alias_email, _alias_phone, _alias_cookie, _alias_external_id, _alias_social.
{
"file_id": "preview_abc123",
"primary_identifier": {
"column": "Email",
"kind": "email"
},
"field_mappings": {
"First Name": "first_name",
"Last Name": "last_name",
"Company": "company",
"Job Title": "title"
},
"alias_mappings": {
"Phone": "_alias_phone",
"LinkedIn": "_alias_social"
}
}
Response
202 Accepted
{
"job_id": "job_abc123",
"status": "queued",
"message": "Import job started",
"file_name": "leads.csv",
"total_rows": 1000
}
Common Errors
| Status | Meaning | Solution |
|---|---|---|
| 400 | Bad Request | Missing required fields or invalid mappings |
| 404 | Not Found | File ID expired or not found (previews expire after 1 hour) |
GET /leads/import/jobs
List all import jobs for the current OU.
Request
| Parameter | Type | Required | Description |
|---|---|---|---|
page | integer | No | Page number (default: 1) |
limit | integer | No | Results per page (default: 25, max: 100) |
curl -X GET "https://api.kenbun.io/leads/import/jobs?page=1&limit=25" \
-H "Authorization: Bearer <token>"
Response
200 OK
{
"jobs": [
{
"job_id": "job_abc123",
"status": "completed",
"file_name": "leads.csv",
"created_at": "2025-01-15T10:00:00Z",
"completed_at": "2025-01-15T10:05:00Z",
"total_rows": 1000,
"created_count": 750,
"updated_count": 230,
"failed_count": 20
}
],
"total": 1,
"page": 1,
"limit": 25
}
Job Status Values
| Status | Description |
|---|---|
queued | Job is waiting to start |
in_progress | Currently processing rows |
completed | All rows processed |
failed | Job failed to complete |
cancelled | User cancelled the job |
GET /leads/import/jobs/{jobId}
Get detailed results for a specific import job.
Request
| Parameter | Type | Required | Description |
|---|---|---|---|
jobId | string | Yes | Import job ID (path parameter) |
curl -X GET "https://api.kenbun.io/leads/import/jobs/job_abc123" \
-H "Authorization: Bearer <token>"
Response
200 OK
{
"job_id": "job_abc123",
"status": "completed",
"created_at": "2025-01-15T10:00:00Z",
"started_at": "2025-01-15T10:00:05Z",
"completed_at": "2025-01-15T10:05:00Z",
"file_name": "leads.csv",
"total_rows": 1000,
"created_count": 750,
"updated_count": 230,
"failed_count": 20,
"primary_identifier": {
"column": "Email",
"kind": "email"
},
"field_mappings": {
"First Name": "first_name",
"Last Name": "last_name",
"Company": "company"
},
"errors": [
{"row": 45, "error": "Invalid email format", "value": "not-an-email"},
{"row": 102, "error": "Missing required identifier", "value": ""}
]
}
| Field | Type | Description |
|---|---|---|
job_id | string | Job identifier |
status | string | Current status |
created_at | timestamp | When job was created |
started_at | timestamp | When processing began |
completed_at | timestamp | When processing finished (null if in progress) |
total_rows | integer | Rows excluding header |
created_count | integer | New leads created |
updated_count | integer | Existing leads updated |
failed_count | integer | Rows that failed |
errors | array | Details for failed rows (row number, error, value) |
Common Errors
| Status | Meaning | Solution |
|---|---|---|
| 404 | Not Found | Job ID does not exist or not accessible |
Import Behavior
Deduplication
For each row, kenbun looks up the lead by primary identifier:
- No match → Creates a new lead
- Match → Updates the existing lead's profile with new values
- Aliases → Added to existing leads (not replaced)
Example: CSV row has email=jane@example.com, phone=555-0100. Existing lead has email=jane@example.com, phone=555-0200. Result: lead now has both phone numbers as aliases.
Field Mapping Rules
- Empty CSV cells are skipped (won't overwrite existing data)
- Non-empty values update the profile field
- Unknown field names are ignored
- Aliases are added (not replaced); duplicates are ignored
- All profile fields stored as strings (no automatic type conversion)
- Identifier validation runs (e.g., email format)
Error Handling
Failed rows don't stop the import — other rows continue processing. Errors are recorded with row number and reason.
| Error | Cause |
|---|---|
| Invalid email format | Email doesn't match pattern |
| Invalid phone format | Phone contains invalid characters |
| Missing required identifier | Identifier column is empty |
| Server error | Unexpected storage failure |
Use Cases
Migrate Contacts from Another System
# 1. Preview the CSV
curl -X POST "https://api.kenbun.io/leads/import/preview" \
-H "Authorization: Bearer <token>" \
-F "file=@contacts.csv"
# 2. Start import with mappings
curl -X POST "https://api.kenbun.io/leads/import/start" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"file_id": "preview_abc123",
"primary_identifier": {"column": "Email", "kind": "email"},
"field_mappings": {
"FirstName": "first_name",
"LastName": "last_name",
"Company": "company",
"Title": "title"
}
}'
# 3. Monitor progress
curl -X GET "https://api.kenbun.io/leads/import/jobs/job_abc123" \
-H "Authorization: Bearer <token>"
Import Trade Show Leads with Multiple Identifiers
curl -X POST "https://api.kenbun.io/leads/import/start" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"file_id": "preview_xyz789",
"primary_identifier": {"column": "Email", "kind": "email"},
"field_mappings": {
"Name": "first_name",
"Company": "company",
"Booth Notes": "notes"
},
"alias_mappings": {
"Mobile": "_alias_phone",
"Badge ID": "_alias_external_id"
}
}'
Bulk Update Existing Leads
When the primary identifier matches existing leads, their profiles are updated with new field values. Empty cells preserve existing data.
Notes
- CSV files must have a header row with column names
- Maximum file size: 10 MB
- File previews are cached for 1 hour (then must be re-uploaded)
- Imports run as background jobs (non-blocking)
- Large imports (10,000+ rows) may take several minutes
- Import jobs are OU-scoped
- Empty CSV cells don't overwrite existing lead data
- UTF-8 encoding recommended for special characters
- Profile field names are case-insensitive in mappings