How to send website leads to HubSpot with the API
Create a HubSpot private app, then upsert each website lead as a contact by email. The exact request, the scopes you need and how to avoid duplicates.

On this page
To send website leads to HubSpot, create a private app with the crm.objects.contacts.write scope, then POST each lead to HubSpot’s contacts batch upsert endpoint using email as the ID property. HubSpot creates the contact if the email is new and updates it if it isn’t, so returning customers don’t turn into duplicates.
Step 1: create a private app
- In HubSpot, open Settings → Integrations → Private Apps (newer accounts show this under Development).
- Create an app and name it after your website form.
- Under Scopes, tick
crm.objects.contacts.write, andcrm.objects.contacts.readif you want to check the key works. - Create the app, open Authentication and copy the access token.
The token works like a password for your CRM. Keep it on your server, never in website JavaScript.
Step 2: send the lead
HubSpot now uses date versions in its API paths. The current contacts path looks like this:
POST https://api.hubapi.com/crm/objects/2026-09/contacts/batch/upsert
Authorization: Bearer <your private app token>
Content-Type: application/json
{
"inputs": [
{
"id": "[email protected]",
"idProperty": "email",
"properties": {
"email": "[email protected]",
"firstname": "Amira",
"lastname": "Khan",
"phone": "+447700900456",
"zip": "M20 2LS"
}
}
]
}
If you have older code using /crm/v3/objects/contacts, it keeps working until HubSpot’s end-of-life date for it, but new connections should use the latest date version.
Step 3: record where the lead came from
Out of the box, HubSpot fills Original source from its own tracking code. Leads sent by the API show as “Offline sources”. To keep the detail, add a custom contact property such as lead_source_detail (single-line text) and send it with each lead: “Website estimator”, “QR code on van”, “Google Ads”. Then build a list or report on it. Our guide to tracking lead sources explains which details are worth saving.
Step 4: let HubSpot do the follow-up
With leads arriving as contacts, use a HubSpot workflow (on paid tiers) enrolling contacts when lead_source_detail is known, to:
- create a deal in your sales pipeline,
- assign the contact to whoever handles quotes,
- send the customer a short “thanks, we’ll call you” email.
On HubSpot’s free tools, you can still filter contacts by the property and work through them each morning.
Common mistakes
- Using
/contacts(create) instead of upsert. A second enquiry from the same email returns a 409 conflict and the new details are lost. - Putting the token in the browser. Anyone can read it from the page. Send leads from a server, a form tool or an integration that keeps the token secret.
- Ignoring 429 replies. HubSpot limits how fast you can call it. Wait and retry instead of giving up.
- Mixing up
zipand a custom postcode field. HubSpot’s built-in property iszip, whatever your country calls it.
Doing it in RoughFigure
In RoughFigure we’ve made HubSpot a built-in connection, so you don’t write any of this. Go to Integrations → Send leads to your CRM, choose HubSpot, paste your private app token and click Connect. We check the token with HubSpot, and from then on every estimator lead is upserted as a contact with their name, email, phone and postcode.
If HubSpot is down, we keep trying for about four hours, and any lead that fails shows the reason and a Resend button. See the HubSpot integration.
Questions
Does this work on HubSpot’s free plan?
Yes. Private apps and the contacts API are available on free HubSpot accounts. Workflows that act on new contacts need a paid tier.
Why do my API leads show as “Offline sources”?
HubSpot sets original source from its own website tracking. Contacts created through the API are labelled offline. Save your own source in a custom property.
Can I create a deal as well as a contact?
Yes, with a second request to the deals endpoint and an association to the contact. A workflow is often easier to maintain.


