Skip to main content

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

ParameterTypeRequiredDescription
typeString✅ YesData type to export: contacts, companies, opportunities, or notes

Query Parameters

ParameterTypeRequiredDefaultDescription
limitInteger❌ No10Number of records per page (1-200)
pageInteger❌ No1Page 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

FieldTypeDescription
idIntegerContact ID
given_nameStringFirst name
family_nameStringLast name
middle_nameStringMiddle name
email_addressesArrayEmail addresses with field labels
phone_numbersArrayPhone numbers with field labels
addressesArrayPhysical addresses
companyObjectAssociated company (if any)
tag_idsArrayApplied tag IDs
custom_fieldsArrayCustom field values
date_createdStringISO 8601 datetime
last_updatedStringISO 8601 datetime
owner_idIntegerOwner 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

FieldTypeDescription
idIntegerCompany ID
company_nameStringCompany name
email_addressStringPrimary email
phone_numberObjectPhone number with extension
addressObjectPhysical address
websiteStringCompany website URL
fax_numberObjectFax number
notesStringCompany notes
custom_fieldsArrayCustom field values
date_createdStringISO 8601 datetime
last_updatedStringISO 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

FieldTypeDescription
idIntegerOpportunity ID
opportunity_titleStringOpportunity name
contactObjectAssociated contact
stageObjectCurrent pipeline stage
projected_revenue_highNumberHigh revenue estimate
projected_revenue_lowNumberLow revenue estimate
probabilityIntegerWin probability (0-100)
estimated_close_dateStringExpected close date
next_action_dateStringNext follow-up date
next_action_notesStringFollow-up notes
custom_fieldsArrayCustom field values
date_createdStringISO 8601 datetime
last_updatedStringISO 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

FieldTypeDescription
idIntegerNote ID
titleStringNote title
bodyStringNote content (HTML)
contact_idIntegerAssociated contact ID
user_idIntegerCreator user ID
typeStringNote type
date_createdStringISO 8601 datetime
last_updatedStringISO 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)));
}
}
}
💬

Documentation Assistant

Ask me anything about the docs

Hi! I'm your documentation assistant. Ask me anything about the docs!

I can help you with:
- Code examples
- Configuration details
- Troubleshooting
- Best practices

Try asking: How do I configure the API?
09:30 AM