# Salesforce Pagination Guide ## Overview Salesforce uses a cursor-based pagination system with `nextRecordsUrl` instead of traditional offset/limit pagination. ## Salesforce Response Structure ### First Request Response ```json { "totalSize": 7530, "done": false, "nextRecordsUrl": "/services/data/v61.0/query/01gxx0000034XYZAAA-2000", "records": [ { "attributes": { "type": "Lead", "url": "/services/data/v59.0/sobjects/Lead/00QdN00000AZGH4UAP" }, "Id": "00QdN00000AZGH4UAP", "FirstName": "John", "LastName": "Steele (Sample)", "Company": "BigLife Inc.", "Email": "info@salesforce.com", "Status": "Working" } // ... more records ] } ``` ### Key Fields - `totalSize`: Total number of records matching the query - `done`: Boolean indicating if there are more records - `nextRecordsUrl`: URL path to fetch the next batch (only present when `done` is `false`) - `records`: Array of record objects ## Using Pagination in Your Backend ### First Request - Get Initial Records ```http GET /api/v1/n8n/salesforce/crm/leads?limit=200 Authorization: Bearer ``` **Response:** ```json { "status": "success", "message": "salesforce crm leads data fetched successfully", "data": { "success": true, "data": [ { "Id": "00QdN00000AZGH4UAP", "FirstName": "John", "LastName": "Steele", ... } ], "count": 200, "metadata": { "totalSize": 7530, "done": false, "nextRecordsUrl": "/services/data/v61.0/query/01gxx0000034XYZAAA-2000" } }, "timestamp": "2025-10-09T12:00:00.000Z" } ``` ### Subsequent Requests - Get Next Pages Use the `nextRecordsUrl` from the previous response: ```http GET /api/v1/n8n/salesforce/crm/leads?nextRecordsUrl=/services/data/v61.0/query/01gxx0000034XYZAAA-2000 Authorization: Bearer ``` **Response:** ```json { "status": "success", "message": "salesforce crm leads data fetched successfully", "data": { "success": true, "data": [ // Next 200 records ], "count": 200, "metadata": { "totalSize": 7530, "done": false, "nextRecordsUrl": "/services/data/v61.0/query/01gxx0000034XYZAAA-4000" } }, "timestamp": "2025-10-09T12:00:00.000Z" } ``` ### Last Page When you reach the last page, `done` will be `true` and `nextRecordsUrl` will be `null`: ```json { "metadata": { "totalSize": 7530, "done": true, "nextRecordsUrl": null } } ``` ## Frontend Implementation ### JavaScript Example - Fetch All Pages ```javascript async function fetchAllSalesforceLeads(token) { const API_URL = 'http://localhost:3000/api/v1'; let allRecords = []; let nextRecordsUrl = null; let hasMore = true; while (hasMore) { // Build URL const url = nextRecordsUrl ? `${API_URL}/n8n/salesforce/crm/leads?nextRecordsUrl=${encodeURIComponent(nextRecordsUrl)}` : `${API_URL}/n8n/salesforce/crm/leads?limit=200`; // Fetch data const response = await fetch(url, { headers: { 'Authorization': `Bearer ${token}` } }); const result = await response.json(); // Add records to collection allRecords = allRecords.concat(result.data.data); // Check if there are more pages hasMore = !result.data.metadata.done; nextRecordsUrl = result.data.metadata.nextRecordsUrl; console.log(`Fetched ${result.data.count} records. Total so far: ${allRecords.length}`); } console.log(`Completed! Total records: ${allRecords.length}`); return allRecords; } // Usage const token = 'your_jwt_token'; const allLeads = await fetchAllSalesforceLeads(token); ``` ### React Example - Paginated Table ```javascript import React, { useState, useEffect } from 'react'; import axios from 'axios'; function SalesforceLeadsTable() { const [leads, setLeads] = useState([]); const [loading, setLoading] = useState(false); const [nextRecordsUrl, setNextRecordsUrl] = useState(null); const [hasMore, setHasMore] = useState(true); const [totalSize, setTotalSize] = useState(0); const API_URL = 'http://localhost:3000/api/v1'; const token = localStorage.getItem('jwt_token'); const fetchLeads = async (nextUrl = null) => { setLoading(true); try { const url = nextUrl ? `${API_URL}/n8n/salesforce/crm/leads?nextRecordsUrl=${encodeURIComponent(nextUrl)}` : `${API_URL}/n8n/salesforce/crm/leads?limit=50`; const response = await axios.get(url, { headers: { Authorization: `Bearer ${token}` } }); const { data, metadata } = response.data.data; setLeads(prevLeads => [...prevLeads, ...data]); setNextRecordsUrl(metadata.nextRecordsUrl); setHasMore(!metadata.done); setTotalSize(metadata.totalSize); } catch (error) { console.error('Error fetching leads:', error); } finally { setLoading(false); } }; useEffect(() => { fetchLeads(); }, []); const loadMore = () => { if (nextRecordsUrl && hasMore) { fetchLeads(nextRecordsUrl); } }; return (

Salesforce Leads ({leads.length} of {totalSize})

{leads.map(lead => ( ))}
Name Company Email Status
{lead.FirstName} {lead.LastName} {lead.Company} {lead.Email} {lead.Status}
{hasMore && ( )}
); } export default SalesforceLeadsTable; ``` ## Important Notes ### 1. URL Encoding Always URL-encode the `nextRecordsUrl` when passing it as a query parameter: ```javascript const encodedUrl = encodeURIComponent(nextRecordsUrl); ``` ### 2. Cursor Expiration Salesforce cursors (nextRecordsUrl) expire after a certain time. If you get an error, start from the beginning. ### 3. Limit Parameter The `limit` parameter only affects the first request. Subsequent requests using `nextRecordsUrl` return the same batch size as the first request. ### 4. Don't Mix Pagination Methods Once you start using `nextRecordsUrl`, don't try to use `offset` for the same query sequence. ### 5. Performance - Fetching all records at once can be slow for large datasets - Consider implementing lazy loading or virtual scrolling - Use web workers for processing large datasets ## Comparison with Zoho Pagination | Feature | Salesforce | Zoho | |---------|-----------|------| | Method | Cursor-based | Page-based | | Parameter | `nextRecordsUrl` | `page` | | Total Count | `totalSize` | Varies | | Has More | `done` boolean | Page calculation | | URL | Changes each page | Increments page number | ## Troubleshooting ### "Invalid nextRecordsUrl" - The cursor might have expired - Start a new query from the beginning ### "Missing records" - Always use the `nextRecordsUrl` from the response - Don't manually construct the URL ### "Slow performance" - Reduce the initial limit - Implement caching on the frontend - Use pagination instead of loading all records ## API Reference ### Endpoint ``` GET /api/v1/n8n/salesforce/:service/:module ``` ### Query Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `limit` | number | No | Number of records (first request only, default: 200) | | `nextRecordsUrl` | string | No | URL for next page (from previous response) | ### Response Format ```json { "status": "success", "message": "...", "data": { "success": true, "data": [...], "count": 200, "metadata": { "totalSize": 7530, "done": false, "nextRecordsUrl": "/services/data/v61.0/query/..." } }, "timestamp": "..." } ``` ## Examples ### cURL - First Page ```bash curl -X GET "http://localhost:3000/api/v1/n8n/salesforce/crm/leads?limit=100" \ -H "Authorization: Bearer $TOKEN" ``` ### cURL - Next Page ```bash NEXT_URL="/services/data/v61.0/query/01gxx0000034XYZAAA-2000" curl -X GET "http://localhost:3000/api/v1/n8n/salesforce/crm/leads?nextRecordsUrl=$(echo $NEXT_URL | jq -sRr @uri)" \ -H "Authorization: Bearer $TOKEN" ``` ### Python Example ```python import requests API_URL = "http://localhost:3000/api/v1" token = "your_jwt_token" headers = {"Authorization": f"Bearer {token}"} def fetch_all_leads(): all_records = [] url = f"{API_URL}/n8n/salesforce/crm/leads?limit=200" while url: response = requests.get(url, headers=headers) data = response.json() records = data['data']['data'] metadata = data['data']['metadata'] all_records.extend(records) # Check for next page if not metadata['done'] and metadata['nextRecordsUrl']: next_url = metadata['nextRecordsUrl'] url = f"{API_URL}/n8n/salesforce/crm/leads?nextRecordsUrl={requests.utils.quote(next_url)}" else: url = None print(f"Fetched {len(records)} records. Total: {len(all_records)}") return all_records leads = fetch_all_leads() print(f"Total leads: {len(leads)}") ``` --- **Last Updated**: October 9, 2025 **Version**: 1.0.0