Looking for a cleaner pattern for HubSpot contact upsert in n8n when the email might be brand new.
Current flow I have:
HTTP Request (search by email) → Code (decide create vs update) → IF → two HTTP branches (create or update). The native HubSpot node 422s on unknown emails so I dropped down to raw HTTP.
It works, but feels heavy for what’s a fundamentally idempotent operation. HubSpot’s API actually exposes a real upsert via:
text
PATCH /crm/v3/objects/contacts/{email}?idProperty=email
```
— one call, no search-then-branch. But I can't find a community node that surfaces this cleanly. Has anyone wrapped it in a custom node, or do you just drop the PATCH into a Code node and call it a day? Genuinely curious how others are handling this in production.
Context: missed-call rescue workflow pulling contacts off voice-agent transcripts, so speed-to-lead matters and node count = latency.
I’d use the HTTP Request node for this and keep the native HubSpot node only where it maps cleanly to the API.
For contact upsert, the single-call PATCH endpoint is the cleaner production pattern:
PATCH https://api.hubapi.com/crm/v3/objects/contacts/{{$json.email}}?idProperty=email
Body:
{ "properties": { "email": "...", "firstname": "...", "lastname": "..." } }
A few production details I’d add:
- URL-encode the email if it comes from user input.
- Validate email before the request, otherwise HubSpot errors can look like workflow failures.
- Keep retry/backoff on for 429/5xx.
- Treat 4xx validation errors separately so bad CRM data does not retry forever.
I would not put the API call in a Code node unless you need custom signing or complex error handling. HTTP Request keeps credentials, retries, logging, and debugging much cleaner in n8n.
Welcome to the n8n community @5inspace
I would be careful about treating that PATCH as an upsert, because in HubSpot’s documentation that endpoint appears as an update for an existing contact by email, not as a create-or-update.
Explicitly test the scenario of a “new email”; if it returns an error/404, the safest path is to use the official batch upsert endpoint via HTTP Request, even with a single contact. This way you maintain a logical upsert call without reverting to the search → IF → create/update pattern.
Yeah I’d probably avoid doing the whole search, IF, create/update branch if you can.
I’d test HubSpot’s batch upsert endpoint with email or another unique property as the lookup, even if you’re only sending one contact at a time. The main thing I’d check is both cases: existing email and brand new email, because HubSpot can be picky depending on the endpoint. I’d also keep the 429/5xx retries separate from 4xx validation errors, otherwise bad CRM data can keep looping like it’s a temporary failure.
Hey @5inspace, the batch upsert endpoint you’re after is exactly POST /crm/v3/objects/contacts/batch/upsert — it handles create-or-update in a single call and works perfectly with a single contact, not just arrays. That’s what I’d run in production for this.
Dropping the search → code → branch and just hitting that endpoint in one HTTP Request node will cut your node count and shave a meaningful bit of latency. Here’s the shape:
{
“inputs”: [
{
“idProperty”: “email”,
“id”: “{{ $json.email }}”,
“properties”: {
“email”: “{{ $json.email }}”,
“firstname”: “{{ $json.firstname }}”,
“lastname”: “{{ $json.lastname }}”
}
}
]
}
A few production details that have saved me headaches:
— URL-encode the email if it comes from user input (voice transcripts can include odd characters). A quick encodeURIComponent() in a tiny Code node or even a Set node expression does it.
— Validate email format before the HTTP request. If HubSpot gets an invalid email, it’ll 422/400, and you don't want that retrying like it's a transient error.
— Set retry only on 429/5xx — let 4xx failures fall through to an error-handling path or a dead-letter queue so bad CRM data doesn't loop.
— Test with both a known email and a brand-new one to confirm your HubSpot account behaves as expected. The batch upsert endpoint has been reliable for me as a true create-or-update with email as the idProperty.
The native HubSpot node doesn't expose this, so raw HTTP Request is the right tool here — and as clawilab said, it keeps credential management and logging clean inside n8n.
You'll end up with one node instead of four, which is a big win for a speed-sensitive workflow. Give it a spin and let us know if the idProperty trick works for your voice-agent transcripts.
1 Like