Skip to main content Skip to footer

HubSpot Developer Blog

Introducing hubspot-go: A Community Go SDK for HubSpot Developers

If you've ever tried to build a production-grade HubSpot integration in Go, you've probably hit the same wall we did: there previously wasn't a first-party Go SDK. The community options that exist tend to cover a slice of the API, are unmaintained, or feel awkward to use from idiomatic Go code.

HubSpot officially maintains SDKs for Node.js, Python, PHP, Ruby, and .NET. That covers a lot of the ecosystem — but not the growing population of backend teams running Go. At Scopious Digital, we build HubSpot integrations for our own products and for clients, and most of that backend work lives in Go services. Calling the HubSpot API through a hand-rolled HTTP layer over and over got old fast, and we kept re-implementing the same retry, rate-limit, pagination, and typed-response boilerplate in every new project.

So we did what a lot of teams eventually do: we sat down, ported the official Node.js SDK to idiomatic Go and released it as a community edition under MIT. It's called hubspot-go, and it's available now on GitHub:

github.com/scopiousdigital/hubspot-go
*Disclaimer: This is not a HubSpot-built, maintained, or supported SDK and is shared without implied warranty.

The big point of discussion internally was - do we port the SDK as close to 1-1 or do we shift and take a more go-centric approach. Although we debated this a lot and are still unsure we made the right decision, we decided to port it as close to 1-1. The main benefit of this is familiarity and ease of maintenance. When HubSpot makes a change - we make a change.



Who this post is for

This post is about why Go is a great fit for HubSpot workloads, the design decisions that shaped the port, and how to go from go get to a working contact create → search → update flow in about five minutes. If you're a HubSpot developer who has been Go-curious, or a Go developer who has been fighting the HubSpot REST API by hand, this is for you.

This guide assumes you're comfortable with:

  • Writing and running a basic Go program (Go 1.21+)
  • HubSpot concepts like apps, authentication, and CRM objects
  • Reading API reference docs

You don't need prior experience with the Node.js SDK — but if you have it, you'll find the mental model almost identical. Object shapes, method names, and semantics were kept close to the Node SDK on purpose, so existing HubSpot developers and AI coding assistants trained on HubSpot content can move between languages without relearning the API.



Why Go is a particularly good fit for HubSpot integrations

Most HubSpot integrations are essentially data-movement problems: sync contacts, listen for webhooks, batch update deals, reconcile properties with a data warehouse. These workloads are exactly where Go shines, for three reasons.

Concurrency is a native concern. Go routines and channels make it natural to fan out batch requests, parallelize webhook processing, or run multiple syncs on a schedule without reaching for external runtimes or orchestrators. When you're dealing with HubSpot's per-second rate limits and 100-item batch ceilings, being able to compose concurrency primitives cleanly is a real productivity win.

Type safety catches mistakes at compile time. HubSpot CRM properties are a bag of loosely-typed strings under the hood, but the shapes of requests and responses are well-defined. A typed SDK means you find out at compile time that you passed a FilterGroup where a Filter was expected, not in the production logs.

Deployment is boring in the best way. A single statically-linked binary with no runtime to install or patch makes ops much simpler than shipping a Node service. For internal sync jobs, webhook listeners, and CLI tools, this is a meaningful operational upgrade.

None of this is news to Go developers — but it was the specific combination of these three things that finally pushed us to stop working around the missing SDK and build it properly.

What's in the box

The library is a direct port of the official HubSpot Node.js SDK, restructured to feel native to Go. At a high level, you get:

  • A top-level hubspot.Client with functional options for configuration
  • Domain-organized subpackages: crm, cms, marketing, automation, settings, OAuth, webhooks, files, and communication preferences
  • Full CRUD + batch + search coverage for 18+ CRM object types, with identical method signatures across all of them
  • Built-in automatic retries for 5xx and 429 responses
  • Client-side rate limiting with configurable requests-per-second and burst
  • Auto-pagination helpers for list endpoints
  • Typed error helpers like hubspot.IsNotFound and hubspot.IsRateLimited

Two of these deserve a closer look before we get to the tutorial, because they're where the SDK earns its keep.

Full CRM coverage, uniformly

HubSpot's CRM API uses the same shape of requests across object types — contacts, companies, deals, tickets, products, line items, quotes, calls, emails, meetings, notes, tasks, leads, and more. The SDK follows that same symmetry. Every CRM object exposes the same set of methods: Create, GetByID, Update, Archive, List, GetAll, Search, BatchCreate, BatchRead, BatchUpdate, BatchArchive, and BatchUpsert. Learn the shape once, use it everywhere. Swapping client.CRM.Contacts for client.CRM.Deals in your code changes the object type and nothing else.

This matters a lot when you're building non-trivial integrations that touch multiple object types, because it compresses the API surface you actually need to remember to a single mental model.

Retries and rate limiting, built in

HubSpot rate limits are real, and 5xx blips happen. The Node SDK leaves resilience outside of rate-limiting to you; we decided the Go SDK should bake it in behind a single option each.

WithRetries(n) wraps 5xx responses and 429 Too Many Requests with exponential backoff, respecting Retry-After headers when HubSpot sends them. WithRateLimiter(rps, burst) applies a token-bucket limiter on the client side so you can stay under your tenant's quota without hand-rolling a limiter per service. The result is that a batch sync over a large list of contacts stops being a source of incidents.



Installation

go get github.com/scopiousdigital/hubspot-go

That's it. There's no code generation step and no configuration file to manage.


Production-ready client configuration

Here's the configuration we actually use in our own services. Adjust the retry and rate-limit numbers to match your HubSpot tier and SLOs.

client := hubspot.NewClient( hubspot.WithAccessToken("token"), hubspot.WithRetries(3), // retry 5xx and 429 hubspot.WithRateLimiter(10, 5), // 10 req/s, burst 5 hubspot.WithHTTPClient(&http.Client{Timeout: 60 * time.Second}), )

That's the whole resilience story: three lines of options.

End-to-end example: create → search → update a contact

Let's walk through the most common CRM pattern in a single program: create a contact, look them up by email then update a property. This is the shape of thousands of real integrations.

package main import ( "context" "errors" "fmt" "log" "os" "github.com/scopiousdigital/hubspot-go" "github.com/scopiousdigital/hubspot-go/crm" ) func main() { ctx := context.Background() client := hubspot.NewClient( hubspot.WithAccessToken(os.Getenv("HUBSPOT_TOKEN")), hubspot.WithRetries(3), hubspot.WithRateLimiter(10, 5), )

1. Create a contact

created, err := client.CRM.Contacts.Create(ctx, &crm.SimplePublicObjectInputForCreate{ Properties: crm.Properties{ "email": "jane@example.com", "firstname": "Jane", "lastname": "Doe", }, }) if err != nil { log.Fatalf("create failed: %v", err) } fmt.Printf("created contact id=%s\n", created.ID)


2. Search for the contact by email

results, err := client.CRM.Contacts.Search(ctx, &crm.PublicObjectSearchRequest{ FilterGroups: []crm.FilterGroup{ { Filters: []crm.Filter{ { PropertyName: "email", Operator: crm.FilterOperatorEQ, Value: "jane@example.com", }, }, }, }, Properties: []string{"email", "firstname", "lastname"}, Limit: 10, }) if err != nil { log.Fatalf("search failed: %v", err) } if len(results.Results) == 0 { log.Fatal("no contact found for email") } found := results.Results[0] fmt.Printf("found contact id=%s firstname=%s\n", found.ID, found.Properties["firstname"])


3. Update the contact

updated, err := client.CRM.Contacts.Update(ctx, found.ID, &crm.SimplePublicObjectInput{ Properties: crm.Properties{"firstname": "Jane (updated)"}, }, nil) if err != nil { // Typed error helpers if hubspot.IsNotFound(err) { log.Fatal("contact disappeared between search and update") } var apiErr *hubspot.APIError if errors.As(err, &apiErr) { log.Fatalf("API error %d (%s): %s", apiErr.HTTPStatusCode, apiErr.Category, apiErr.Message) } log.Fatalf("update failed: %v", err) } fmt.Printf("updated contact id=%s firstname=%s\n", updated.ID, updated.Properties["firstname"]) }

That's the full lifecycle: one client, three idiomatic calls, typed responses, and real error handling. Notice how the filter syntax for search mirrors the HubSpot REST payload exactly — we kept the shape intentionally close to the JSON you'd send by hand so the mental model transfers in either direction.

Scaling up: batch operations and pagination

For anything beyond a handful of records, you'll want the batch APIs. They take up to 100 inputs per call and are covered by the same retry and rate-limit machinery.


Batch upsert by email — great for data warehouse sync jobs

_, err := client.CRM.Contacts.BatchUpsert(ctx, &crm.BatchUpsertInput{ Inputs: []crm.BatchObjectUpsertInput{ {ID: "jane@example.com", IDProperty: "email", Properties: crm.Properties{"firstname": "Jane"}}, {ID: "bob@example.com", IDProperty: "email", Properties: crm.Properties{"firstname": "Bob"}}, }, })

For pagination, you have a choice. List gives you a single page plus a cursor if you want manual control; GetAll handles the cursor loop for you and returns every matching record. Use GetAll for one-shot jobs, and List when you need to stream or stop early.

all, err := client.CRM.Contacts.GetAll(ctx, &crm.GetAllOptions{ Properties: []string{"email", "firstname"}, })


Best practices and common pitfalls

A few things we've learned the hard way while shipping HubSpot integrations:

Always pass a context with a timeout. Every method takes ctx as its first argument. Use context.WithTimeout at the call site so a slow HubSpot response can't pin a goroutine indefinitely.

Use batch endpoints wherever you can. A batch of 100 counts as one rate-limit-consuming request, not 100. If you find yourself writing a for loop around Create, stop and reach for BatchCreate instead.

Prefer BatchUpsert with an IDProperty. For most sync flows, what you actually want is "make the record in HubSpot match my source of truth, keyed by email (or a custom unique property)." Upsert expresses that directly and avoids the create-then-update race.

Respect rate limits even with retries on. Retries handle occasional 429s gracefully, but they don't replace a sensible WithRateLimiter. If you hammer the API flat out and rely on retries, you'll spend all your throughput on backoff.

Use the typed error helpers. hubspot.IsNotFound(err) and hubspot.IsRateLimited(err) are easier to read and more reliable than matching on HTTP status codes yourself and they keep your call sites clean.

Where this goes from here

hubspot-go is a community edition, MIT licensed and maintained in the open. It already covers the surface area we depend on in production — the full CRM, CMS, marketing, automation, settings, OAuth, webhooks, and files APIs — and we're working through the long tail of remaining endpoints as we and other users hit them.

If you're a HubSpot developer who has been waiting for a real Go SDK, we'd love for you to try it, open issues and send PRs:

Our goal is a Go SDK that feels as well-supported as the first-party options in other languages — and the fastest way there is real usage from real HubSpot developers.

If you build something with it, tell us, open an issue, contribute via PR and help keep the project moving.

References and further reading


Thanks to the HubSpot developer ecosystem and to the maintainers of the official Node.js SDK, whose design and type definitions made this port possible.


Anze Koprivec

Anze runs Scopious Digital, where a small team of engineers builds the HubSpot integrations that other agencies can't. He writes mostly about what breaks and why.