CRM Data Export
📋 Overview
The CRM Data Export API allows you to retrieve contacts, companies, opportunities (deals), and notes from your Keap account. All exports support pagination and automatic token refresh through middleware.
📤 Export Endpoint
Endpoint
GET /v1/e/keap/export/:type
Authentication
Authorization: Bearer {jwt_token}
Required Scopes
['contacts', 'contacts.external'];
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
type | String | ✅ Yes | Data type to export: contacts, companies, opportunities, or notes |
Query Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
limit | Integer | ❌ No | 10 | Number of records per page (1-200) |
page | Integer | ❌ No | 1 | Page number (1-indexed) |
Request Example
curl -X GET "https://api.dashclicks.com/v1/e/keap/export/contacts?limit=100&page=1" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
Response Format
{
"success": true,
"message": "Data exported",
"data": {
"contacts": [
{
"id": 12345,
"given_name": "John",
"family_name": "Doe",
"email_addresses": [
{
"email": "john.doe@example.com",
"field": "EMAIL1"
}
],
"phone_numbers": [
{
"number": "+1-555-123-4567",
"field": "PHONE1"
}
]
}
],
"count": 250
},
"pagination": {
"page": 1,
"limit": 100,
"total": 3,
"next_page": 2,
"pre_page": null
}
}
🔄 How It Works
1. Token Refresh (Automatic)
The getToken middleware runs before the export controller:
// Middleware automatically:
// 1. Retrieves stored token from MongoDB
// 2. Refreshes token with Keap API
// 3. Updates stored token with fresh credentials
// 4. Attaches fresh access_token to req.access_token
Developer Note: You don't need to manually refresh tokens - the middleware handles this automatically.
2. Calculate Pagination
let limit = parseInt(req.query.limit) || 10;
let page = parseInt(req.query.page) || 1;
// Convert page number to offset (0-indexed for Keap API)
const offset = page ? (page - 1) * limit : 0;
// Keap API expects offset/limit parameters
const query = {
limit: limit,
offset: offset,
};
Example: Page 3 with limit 100 → offset = 200
3. Call Keap API
const typeData = await keapProvider.exportType(
req.access_token, // Fresh token from middleware
getType, // 'contacts', 'companies', etc.
query, // { limit, offset }
);
Keap API Request:
GET https://api.infusionsoft.com/crm/rest/v1/contacts?limit=100&offset=0
Authorization: Bearer {fresh_access_token}
4. Transform Response
// Keap returns: { contacts: [...], count: 250, next: "...", previous: "..." }
const { count, next, previous, ...responseData } = typeData;
// Calculate pagination metadata
const pagination = utilities.generatePagination(limit, page, count);
// Response structure
{
success: true,
message: 'Data exported',
data: responseData, // { contacts: [...] }
pagination: {
page: 1,
limit: 100,
total: 3, // Total pages = Math.ceil(count / limit)
next_page: 2, // null if last page
pre_page: null // null if first page
}
}
📊 Export Types
1. Contacts
Endpoint
GET /v1/e/keap/export/contacts?limit=100&page=1
Keap API Reference
GET https://api.infusionsoft.com/crm/rest/v1/contacts
Response Fields
| Field | Type | Description |
|---|---|---|
id | Integer | Contact ID |
given_name | String | First name |
family_name | String | Last name |
middle_name | String | Middle name |
email_addresses | Array | Email addresses with field labels |
phone_numbers | Array | Phone numbers with field labels |
addresses | Array | Physical addresses |
company | Object | Associated company (if any) |
tag_ids | Array | Applied tag IDs |
custom_fields | Array | Custom field values |
date_created | String | ISO 8601 datetime |
last_updated | String | ISO 8601 datetime |
owner_id | Integer | Owner user ID |
Example Response
{
"success": true,
"message": "Data exported",
"data": {
"contacts": [
{
"id": 12345,
"given_name": "John",
"family_name": "Doe",
"email_addresses": [
{
"email": "john.doe@example.com",
"field": "EMAIL1"
}
],
"phone_numbers": [
{
"number": "+1-555-123-4567",
"field": "PHONE1",
"type": "Phone1"
}
],
"addresses": [
{
"line1": "123 Main St",
"locality": "Los Angeles",
"region": "CA",
"zip_code": "90001",
"country_code": "US",
"field": "BILLING"
}
],
"company": {
"id": 678
},
"tag_ids": [123, 456],
"date_created": "2025-01-15T10:30:00.000Z",
"last_updated": "2025-10-10T08:15:00.000Z"
}
],
"count": 250
},
"pagination": {
"page": 1,
"limit": 100,
"total": 3,
"next_page": 2,
"pre_page": null
}
}
2. Companies
Endpoint
GET /v1/e/keap/export/companies?limit=100&page=1
Keap API Reference
GET https://api.infusionsoft.com/crm/rest/v1/companies
Response Fields
| Field | Type | Description |
|---|---|---|
id | Integer | Company ID |
company_name | String | Company name |
email_address | String | Primary email |
phone_number | Object | Phone number with extension |
address | Object | Physical address |
website | String | Company website URL |
fax_number | Object | Fax number |
notes | String | Company notes |
custom_fields | Array | Custom field values |
date_created | String | ISO 8601 datetime |
last_updated | String | ISO 8601 datetime |
Example Response
{
"success": true,
"message": "Data exported",
"data": {
"companies": [
{
"id": 678,
"company_name": "Acme Corporation",
"email_address": "contact@acme.com",
"phone_number": {
"number": "+1-555-987-6543",
"extension": "101"
},
"address": {
"line1": "456 Business Ave",
"locality": "New York",
"region": "NY",
"zip_code": "10001",
"country_code": "US"
},
"website": "https://www.acme.com",
"notes": "Key client - priority support",
"date_created": "2024-06-10T14:20:00.000Z",
"last_updated": "2025-09-15T09:45:00.000Z"
}
],
"count": 45
},
"pagination": {
"page": 1,
"limit": 100,
"total": 1,
"next_page": null,
"pre_page": null
}
}
3. Opportunities
Endpoint
GET /v1/e/keap/export/opportunities?limit=100&page=1
Keap API Reference
GET https://api.infusionsoft.com/crm/rest/v1/opportunities
Note: In Keap, opportunities are also called "deals" or "sales".
Response Fields
| Field | Type | Description |
|---|---|---|
id | Integer | Opportunity ID |
opportunity_title | String | Opportunity name |
contact | Object | Associated contact |
stage | Object | Current pipeline stage |
projected_revenue_high | Number | High revenue estimate |
projected_revenue_low | Number | Low revenue estimate |
probability | Integer | Win probability (0-100) |
estimated_close_date | String | Expected close date |
next_action_date | String | Next follow-up date |
next_action_notes | String | Follow-up notes |
custom_fields | Array | Custom field values |
date_created | String | ISO 8601 datetime |
last_updated | String | ISO 8601 datetime |
Example Response
{
"success": true,
"message": "Data exported",
"data": {
"opportunities": [
{
"id": 9012,
"opportunity_title": "Q4 Enterprise License",
"contact": {
"id": 12345
},
"stage": {
"id": 5,
"name": "Proposal Sent",
"target_num_days": 14
},
"projected_revenue_high": 50000,
"projected_revenue_low": 40000,
"probability": 75,
"estimated_close_date": "2025-12-31",
"next_action_date": "2025-10-15",
"next_action_notes": "Follow up on pricing questions",
"date_created": "2025-09-01T08:00:00.000Z",
"last_updated": "2025-10-09T16:30:00.000Z"
}
],
"count": 128
},
"pagination": {
"page": 1,
"limit": 100,
"total": 2,
"next_page": 2,
"pre_page": null
}
}
4. Notes
Endpoint
GET /v1/e/keap/export/notes?limit=100&page=1
Keap API Reference
GET https://api.infusionsoft.com/crm/rest/v1/notes
Response Fields
| Field | Type | Description |
|---|---|---|
id | Integer | Note ID |
title | String | Note title |
body | String | Note content (HTML) |
contact_id | Integer | Associated contact ID |
user_id | Integer | Creator user ID |
type | String | Note type |
date_created | String | ISO 8601 datetime |
last_updated | String | ISO 8601 datetime |
Example Response
{
"success": true,
"message": "Data exported",
"data": {
"notes": [
{
"id": 3456,
"title": "Sales Call Follow-up",
"body": "<p>Discussed pricing options and timeline. Client interested in annual contract.</p>",
"contact_id": 12345,
"user_id": 789,
"type": "Appointment",
"date_created": "2025-10-08T14:15:00.000Z",
"last_updated": "2025-10-08T14:15:00.000Z"
}
],
"count": 87
},
"pagination": {
"page": 1,
"limit": 100,
"total": 1,
"next_page": null,
"pre_page": null
}
}
🎯 Use Cases
1. Export All Contacts
// Fetch all contacts with pagination
async function getAllContacts(jwtToken) {
let allContacts = [];
let page = 1;
let hasMorePages = true;
while (hasMorePages) {
const response = await axios.get(`/v1/e/keap/export/contacts?limit=100&page=${page}`, {
headers: { Authorization: `Bearer ${jwtToken}` },
});
allContacts = [...allContacts, ...response.data.data.contacts];
if (response.data.pagination.next_page) {
page = response.data.pagination.next_page;
} else {
hasMorePages = false;
}
console.log(`Fetched page ${page - 1}, total so far: ${allContacts.length}`);
}
return allContacts;
}
// Usage
const contacts = await getAllContacts(jwt_token);
console.log(`Total contacts exported: ${contacts.length}`);
2. Export Multiple Types in Parallel
// Export contacts, companies, and opportunities simultaneously
async function exportAllCRMData(jwtToken) {
const types = ['contacts', 'companies', 'opportunities', 'notes'];
const exportType = async type => {
const response = await axios.get(`/v1/e/keap/export/${type}?limit=200&page=1`, {
headers: { Authorization: `Bearer ${jwtToken}` },
});
return {
type,
data: response.data.data[type],
count: response.data.data.count,
pagination: response.data.pagination,
};
};
// Fetch all types in parallel
const results = await Promise.all(types.map(exportType));
return results.reduce((acc, result) => {
acc[result.type] = {
data: result.data,
count: result.count,
totalPages: result.pagination.total,
};
return acc;
}, {});
}
// Usage
const crmData = await exportAllCRMData(jwt_token);
console.log(`Contacts: ${crmData.contacts.count}`);
console.log(`Companies: ${crmData.companies.count}`);
console.log(`Opportunities: ${crmData.opportunities.count}`);
console.log(`Notes: ${crmData.notes.count}`);
3. Sync Contacts to Local Database
// Sync Keap contacts to your database
async function syncContacts(accountId, jwtToken) {
let page = 1;
let syncedCount = 0;
while (true) {
const response = await axios.get(`/v1/e/keap/export/contacts?limit=100&page=${page}`, {
headers: { Authorization: `Bearer ${jwtToken}` },
});
const contacts = response.data.data.contacts;
// Store in local database
for (const contact of contacts) {
await ContactModel.findOneAndUpdate(
{
accountId: accountId,
keapContactId: contact.id,
},
{
accountId: accountId,
keapContactId: contact.id,
firstName: contact.given_name,
lastName: contact.family_name,
email: contact.email_addresses?.[0]?.email,
phone: contact.phone_numbers?.[0]?.number,
lastSynced: new Date(),
rawData: contact,
},
{ upsert: true },
);
syncedCount++;
}
console.log(`Synced page ${page}, total: ${syncedCount}`);
if (!response.data.pagination.next_page) {
break;
}
page = response.data.pagination.next_page;
}
return syncedCount;
}
// Usage
const syncedCount = await syncContacts(accountId, jwt_token);
console.log(`Synced ${syncedCount} contacts from Keap`);
4. Find Contacts by Email
// Export contacts and filter by email domain
async function findContactsByDomain(domain, jwtToken) {
let matchingContacts = [];
let page = 1;
while (true) {
const response = await axios.get(`/v1/e/keap/export/contacts?limit=100&page=${page}`, {
headers: { Authorization: `Bearer ${jwtToken}` },
});
const contacts = response.data.data.contacts;
// Filter by email domain
const filtered = contacts.filter(contact => {
return contact.email_addresses?.some(email => email.email.endsWith(`@${domain}`));
});
matchingContacts = [...matchingContacts, ...filtered];
if (!response.data.pagination.next_page) {
break;
}
page = response.data.pagination.next_page;
}
return matchingContacts;
}
// Usage
const gmailContacts = await findContactsByDomain('gmail.com', jwt_token);
console.log(`Found ${gmailContacts.length} Gmail contacts`);
5. Export Opportunities by Stage
// Export opportunities and group by stage
async function getOpportunitiesByStage(jwtToken) {
const response = await axios.get('/v1/e/keap/export/opportunities?limit=1000&page=1', {
headers: { Authorization: `Bearer ${jwtToken}` },
});
const opportunities = response.data.data.opportunities;
// Group by stage
const byStage = opportunities.reduce((acc, opp) => {
const stageName = opp.stage?.name || 'Unknown';
if (!acc[stageName]) {
acc[stageName] = [];
}
acc[stageName].push(opp);
return acc;
}, {});
// Calculate totals per stage
const summary = Object.entries(byStage).map(([stage, opps]) => ({
stage,
count: opps.length,
totalRevenue: opps.reduce((sum, o) => sum + (o.projected_revenue_high || 0), 0),
avgProbability: Math.round(
opps.reduce((sum, o) => sum + (o.probability || 0), 0) / opps.length,
),
}));
return summary;
}
// Usage
const stageSummary = await getOpportunitiesByStage(jwt_token);
stageSummary.forEach(stage => {
console.log(
`${stage.stage}: ${stage.count} opportunities, $${stage.totalRevenue} revenue, ${stage.avgProbability}% avg probability`,
);
});
⚠️ Error Handling
1. Invalid Export Type
Request:
GET /v1/e/keap/export/invalid_type
Keap API Response: 404 Not Found
Solution: Use valid types: contacts, companies, opportunities, notes
2. Token Not Found
Response:
{
"message": "User oauth token not found. Please redirect to login"
}
HTTP Status: 401 Unauthorized
Solution: Redirect to /auth/login to authenticate
3. Token Invalidated
Response:
{
"success": false,
"errno": 400,
"message": "TOKEN_INVALIDATED"
}
Solution: Re-authenticate via /auth/login
4. Pagination Out of Range
Request: ?page=999 (beyond available pages)
Response: Empty data array
{
"success": true,
"message": "Data exported",
"data": {
"contacts": []
},
"pagination": {
"page": 999,
"limit": 100,
"total": 3,
"next_page": null,
"pre_page": 3
}
}
Handling: Check if data array is empty to detect end of results
5. Rate Limit Exceeded
Keap API Response: 429 Too Many Requests
Solution:
- Implement exponential backoff
- Reduce request frequency
- Cache exported data
📊 Pagination Best Practices
1. Start with Reasonable Limits
// Good: 100 records per page
const limit = 100;
// Too small: Many API calls required
const limit = 10;
// Too large: May hit API timeouts
const limit = 1000;
2. Check for More Pages
// Always check next_page before continuing
if (response.data.pagination.next_page) {
page = response.data.pagination.next_page;
// Fetch next page
} else {
// No more pages - done
}
3. Handle Empty Results
// Check if data array is empty
if (response.data.data.contacts.length === 0) {
console.log('No more contacts found');
break;
}
4. Show Progress
// Display progress during large exports
const total = response.data.pagination.total;
const current = response.data.pagination.page;
const progress = Math.round((current / total) * 100);
console.log(`Progress: ${progress}% (Page ${current} of ${total})`);
💡 Performance Optimization
1. Use Parallel Requests for Different Types
// Faster: Fetch multiple types simultaneously
const [contacts, companies] = await Promise.all([
axios.get('/v1/e/keap/export/contacts'),
axios.get('/v1/e/keap/export/companies'),
]);
2. Implement Caching
// Cache exported data (valid for 1 hour)
const cacheKey = `keap_${accountId}_contacts`;
const cached = await redis.get(cacheKey);
if (cached && Date.now() - cached.timestamp < 3600000) {
return JSON.parse(cached.data);
}
// Fetch fresh data
const data = await exportContacts();
await redis.set(
cacheKey,
JSON.stringify({
data,
timestamp: Date.now(),
}),
);
3. Use Appropriate Limits
// Balance between API calls and response size
const optimalLimit = 100; // Sweet spot for most use cases
4. Implement Retry Logic
// Retry on transient errors
async function exportWithRetry(type, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await axios.get(`/v1/e/keap/export/${type}`);
} catch (error) {
if (i === maxRetries - 1) throw error;
await new Promise(r => setTimeout(r, 1000 * Math.pow(2, i)));
}
}
}