Integration Guide
Scrape B2B contacts from Apollo, Google Maps, or the Lead Database and push them into HubSpot as contacts with verified emails, phone numbers, job titles, and company data. HubSpot deduplicates by email automatically, so you can run imports repeatedly without creating duplicates.
The HubSpot Contacts API lets you create, update, and enrich contact records programmatically. Combined with ScraperCity, you can build fully automated lead generation pipelines that pull fresh B2B data and push it directly into your CRM - no manual CSV imports, no copy-pasting.
In HubSpot, go to Settings, then Integrations, then Private Apps. Create a new private app and grant it the following scopes:
crm.objects.contacts.write - required to create and update contactscrm.objects.contacts.read - needed if you want to check for existing contacts before creatingCopy the access token from the Private App detail page. This is your YOUR_HUBSPOT_TOKEN for all API requests below.
Log in to your ScraperCity dashboard and go to API Docs at app.scrapercity.com/dashboard/api-docs. Copy your Bearer token. All plans ($49/mo, $149/mo, $649/mo) include full API access.
Query any ScraperCity endpoint for contacts matching your ICP. This example pulls VP of Marketing contacts from the SaaS industry with verified emails:
curl -X GET "https://app.scrapercity.com/api/v1/database/leads?title=VP%20of%20Marketing&industry=computer%20software&hasEmail=true&limit=50" \
-H "Authorization: Bearer YOUR_SCRAPERCITY_KEY"The Lead Database endpoint requires the $649/mo plan and returns up to 100 leads per page with a 100,000 lead/day limit. For Apollo scrapes, delivery is 11-48+ hours. For Google Maps and most other scrapers, results arrive in minutes.
POST each lead to the HubSpot Contacts API. Include all the fields ScraperCity returns:
curl -X POST "https://api.hubapi.com/crm/v3/objects/contacts" \
-H "Authorization: Bearer YOUR_HUBSPOT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"properties": {
"email": "[email protected]",
"firstname": "Jane",
"lastname": "Smith",
"phone": "+15551234567",
"jobtitle": "VP of Marketing",
"company": "Acme Corp",
"website": "https://acmecorp.com",
"hs_lead_status": "NEW",
"lifecyclestage": "lead"
}
}'HubSpot deduplicates by email. If the contact already exists, the API returns a 409 Conflict with the existing contact ID in the response body. Catch that response and switch to a PATCH update request instead.
For bulk imports, use the batch create endpoint to push up to 100 contacts per request instead of making individual calls. This dramatically reduces the number of API calls and keeps you inside HubSpot rate limits:
curl -X POST "https://api.hubapi.com/crm/v3/objects/contacts/batch/create" \
-H "Authorization: Bearer YOUR_HUBSPOT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"inputs": [
{
"properties": {
"email": "[email protected]",
"firstname": "Jane",
"lastname": "Smith",
"jobtitle": "VP of Marketing",
"company": "Acme Corp",
"phone": "+15551234567",
"hs_lead_status": "NEW",
"lifecyclestage": "lead"
}
},
{
"properties": {
"email": "[email protected]",
"firstname": "Mark",
"lastname": "Jones",
"jobtitle": "Head of Growth",
"company": "Startup IO",
"phone": "+15559876543",
"hs_lead_status": "NEW",
"lifecyclestage": "lead"
}
}
]
}'Batch requests are limited to 100 records per request. If you have 500 contacts, split them into 5 separate batch calls. A 409 in a batch means one or more contacts already exist - parse the error to identify which emails conflicted, remove them, and retry the remaining contacts.
When a contact already exists and you want to enrich their record (for example, adding a mobile number from the ScraperCity Mobile Finder), use a PATCH request to the update endpoint:
curl -X PATCH "https://api.hubapi.com/crm/v3/objects/contacts/CONTACT_ID" \
-H "Authorization: Bearer YOUR_HUBSPOT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"properties": {
"phone": "+15551112222",
"mobilephone": "+15553334444"
}
}'Replace CONTACT_ID with the HubSpot numeric ID from the original create response or the 409 conflict response body. Only properties included in the PATCH body will be updated - existing properties are not overwritten.
ScraperCity returns standardized field names across all scrapers. The table below shows how to map them to HubSpot contact properties for the API. To retrieve the full list of available properties in your HubSpot account, make a GET request to /crm/v3/properties/0-1.
| ScraperCity Field | HubSpot Property Name | Notes |
|---|---|---|
email | email | Primary deduplication key in HubSpot |
firstName | firstname | Lowercase, no camelCase |
lastName | lastname | Lowercase, no camelCase |
title | jobtitle | Job title / position |
company | company | Company name as a string |
phone | phone | Work / office phone |
mobilePhone | mobilephone | From Mobile Finder enrichment |
linkedinUrl | hs_linkedin_ad_handles or custom | Store as a custom text property if needed |
website | website | Company website domain |
city / state | city / state | Contact location fields |
industry | industry | Use HubSpot enumeration value |
| (auto-set) | hs_lead_status | Set to NEW for fresh imports |
| (auto-set) | lifecyclestage | Set to lead for new prospects |
When using enumeration properties, you must use the internal value name, not the display label. Internal names stay the same even if you rename the label in HubSpot settings.
Scrape Apollo for your ideal customer profile - title, industry, company size, location. Push results into HubSpot as leads and segment them into smart lists for targeted outreach.
Scrape Google Maps for businesses in a specific city and industry. Push contacts into HubSpot with the business name, phone, email, and rating as properties.
Export HubSpot contacts missing phone numbers. Run them through ScraperCity Mobile Finder ($0.25/input) and update the records with direct dial numbers via PATCH.
Use n8n or Zapier to query ScraperCity daily for new leads matching your criteria and create them in HubSpot automatically. Fresh prospects in your CRM every morning.
Scrape Shopify or WooCommerce stores via Store Leads ($0.0039/lead) and push store owners into HubSpot with company URL and estimated revenue for targeted outreach.
Use the ScraperCity People Finder or Email Finder to resolve LinkedIn profiles to verified emails, then create the contacts in HubSpot with full contact details ready for sequences.
The script below pulls leads from ScraperCity and pushes them into HubSpot in batches of 100, with basic 409 conflict handling. Run it with node import.js after setting your API keys as environment variables.
// import.js
const SCRAPERCITY_KEY = process.env.SCRAPERCITY_KEY
const HUBSPOT_TOKEN = process.env.HUBSPOT_TOKEN
const BATCH_SIZE = 100
async function fetchLeads() {
const url =
'https://app.scrapercity.com/api/v1/database/leads' +
'?title=VP%20of%20Marketing&industry=computer%20software' +
'&hasEmail=true&limit=100'
const res = await fetch(url, {
headers: { Authorization: `Bearer ${SCRAPERCITY_KEY}` },
})
const data = await res.json()
return data.leads || []
}
function mapLead(lead) {
return {
properties: {
email: lead.email,
firstname: lead.firstName,
lastname: lead.lastName,
jobtitle: lead.title,
company: lead.company,
phone: lead.phone || '',
website: lead.website || '',
hs_lead_status: 'NEW',
lifecyclestage: 'lead',
},
}
}
async function batchCreate(inputs) {
const res = await fetch(
'https://api.hubapi.com/crm/v3/objects/contacts/batch/create',
{
method: 'POST',
headers: {
Authorization: `Bearer ${HUBSPOT_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ inputs }),
}
)
// 207 means partial success, 201 means all created
if (res.status === 409) {
console.warn('Conflict in batch - some contacts may already exist')
}
return res.json()
}
async function main() {
const leads = await fetchLeads()
console.log(`Fetched ${leads.length} leads from ScraperCity`)
for (let i = 0; i < leads.length; i += BATCH_SIZE) {
const chunk = leads.slice(i, i + BATCH_SIZE).map(mapLead)
const result = await batchCreate(chunk)
console.log(`Batch ${Math.floor(i / BATCH_SIZE) + 1}: status ${result.status || 'ok'}`)
// Respect HubSpot rate limits - pause 500ms between batches
await new Promise((r) => setTimeout(r, 500))
}
}
main().catch(console.error)Why it happens: Your HubSpot access token is missing, expired, or malformed.
How to fix: Check that you are passing the token in the Authorization header as Bearer YOUR_TOKEN with no extra spaces. Tokens generated by Private Apps do not expire by default, but if you regenerated the app token the old one is immediately invalid. Verify the token in Settings - Integrations - Private Apps.
Why it happens: Your Private App does not have the required scope.
How to fix: Go to Settings - Integrations - Private Apps, edit your app, and confirm that crm.objects.contacts.write is enabled. After adding a scope, you must click Update and confirm the change - the existing token automatically inherits the new permissions.
Why it happens: A contact with the submitted email address already exists in your HubSpot portal.
How to fix: The 409 response body includes the existing contact ID. Parse the response, extract the ID, and issue a PATCH request to /crm/v3/objects/contacts/{id} with the new properties instead. For batch creates, a conflict in one record can cause the full batch to return 409 - remove the conflicting email and retry the remaining contacts.
Why it happens: You have exceeded HubSpot's rate limit - typically 100-190 requests per 10 seconds depending on your subscription tier.
How to fix: Add a delay between batch requests (500ms-1s is usually sufficient). The batch endpoint lets you send 100 contacts per call instead of one, so 500 contacts requires only 5 requests rather than 500. Watch the X-HubSpot-RateLimit-Remaining response header to monitor remaining capacity before each call.
Why it happens: Invalid property name, malformed JSON, or an unrecognized enumeration value.
How to fix: Check that your property names exactly match HubSpot internal names (lowercase, no spaces). Enumeration properties like lifecyclestage and hs_lead_status require specific internal values - for example, lifecyclestage accepts lead, marketingqualifiedlead, salesqualifiedlead, opportunity, customer. Retrieve the full enumeration options via GET /crm/v3/properties/0-1/{propertyName}.
Why it happens: HubSpot's Search API indexes new records with a short delay after creation.
How to fix: There can be a delay of 5-10 seconds before a newly created contact is available via the Search API. If you need to look up a contact immediately after creating it, use the direct GET /crm/v3/objects/contacts/{id} endpoint with the ID returned from the create response instead of searching by email.
When importing thousands of contacts, a few patterns keep your pipeline fast and reliable without hitting HubSpot rate limits.
The batch create endpoint (/crm/v3/objects/contacts/batch/create) accepts up to 100 records per request. A list of 10,000 contacts requires 100 batch calls instead of 10,000 individual calls. This alone can cut import time from hours to minutes and keeps you well inside rate limits.
Scrape results may occasionally include duplicate email addresses across pages. Deduplicate your input array by email before building your batch payload. This avoids unnecessary 409 conflicts and ensures each batch call succeeds cleanly.
A 500ms pause between each 100-contact batch request keeps your request rate well below the HubSpot burst limit. For very large imports (50,000+ contacts), increase the delay to 1 second per batch to stay safe across different subscription tiers.
When reading contacts back from HubSpot to check for existing records, pass a properties query parameter with only the fields you need. The default response only returns a small subset of properties - requesting all properties for every record adds unnecessary payload size and latency.
When querying the Lead Database or Apollo scraper, filter with hasEmail=true so ScraperCity only returns contacts with verified emails. Contacts without emails cannot be created in HubSpot (email is the primary deduplication key), so filtering at the source saves quota on both platforms.
If you prefer no-code or low-code automation rather than writing a script, all three major workflow tools connect to both ScraperCity and HubSpot via their HTTP request nodes or native integrations.
Use the HTTP Request node to call ScraperCity, then the HubSpot node or a second HTTP Request to create contacts. n8n self-hosted gives you unlimited executions and is well-suited for scheduled lead generation pipelines.
Trigger a Zap on a schedule (or webhook), call the ScraperCity API with a Webhooks by Zapier step, then use the HubSpot - Create Contact action. Good for simple one-at-a-time flows. Use Formatter steps to map ScraperCity fields to HubSpot property names.
Use a Node.js code step to call ScraperCity and loop over results, then HubSpot's built-in Pipedream app to create each contact. Pipedream's free tier is generous for low-volume daily runs and the code steps handle batch logic cleanly.