HubSpot contact upsert in n8n — cleaner pattern than search → branch → create/update?

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