Skip to main content
The HubSpot CRM over the API an integration actually calls: contacts, companies, deals, notes and tasks with string-typed properties, associations written in both directions, batch writes that can come back part-failed, and a search index that is behind the writes until the sandbox is advanced. Slug: hubspot

The Attio twin already covers “a CRM”

So this one is not another object-and-record model. It is the three behaviours a HubSpot test portal cannot give you.

1. The search index lags the write

A contact created through POST /crm/v3/objects/contacts is not returned by POST /crm/v3/objects/contacts/search until HubSpot has indexed it. Agents that create a record and then search for it work on a developer’s machine and fail in production — and you cannot ask HubSpot to be slow on demand, so there is no way to test the branch against a real portal. Here the lag is state. The default is one advance, so create-then-search fails out of the box; POST /__twin__/v1/advance closes the gap, and set_search_lag makes it longer or turns it off for a test that does not want the behaviour. The record is readable by id the whole time: it exists, only the index is behind.

2. Everything in properties is a string

Numbers, booleans, enumerations, datetimes — all of them. "17", not 17; "true", not true. A caller that assumes typed JSON breaks against HubSpot, and a twin that returned typed JSON would break the pinned SDK, which annotates the map dict[str, str]. Values are coerced to the string form their property’s type has and stored that way, so the round trip is stable in both directions.

3. Duplicate contacts and batch partial failures

A contact create carrying an email another contact holds is a 409 whose message names the existing record’s id — integrations parse it out to recover, which is why it is load-bearing. And a batch can answer 207 multi-status with some rows written and some refused, which the pinned SDK deserializes into a different model from the success: a caller that only handled the success shape reads an AttributeError rather than a failure, and because 207 is a 2xx, nothing raises. Neither is reachable on demand in a shared portal.

Two versions in the paths is not a mistake

HubSpot versions its CRM by path and by nothing else — no dated version, no version header. Objects, properties, owners and pipelines are /crm/v3; associations are /crm/v4. The manifest publishes v3+v4 so this is on the record rather than looking like a typo.

What it models

Advancing the sandbox steps five things, in order: the search index catches up by one, any rate-limit window expires, deals move one stage along their own pipeline stamping the stage’s entered date, contacts move one step along the lifecycle stage sequence stamping its date, and tasks past their due date become overdue. All five are finite, so advancing repeatedly settles rather than reporting forever.

Idempotency

HubSpot has no idempotency keys. It has two other mechanisms and both are modelled: batch/upsert with an idProperty, where a replay updates rather than duplicating and new says which happened; and the duplicate-contact 409 naming the existing id, which is how a caller recovers without one. A replay of any other create makes a second record, and so does this twin.

Scenarios

search-lagging, crm-with-pipeline, paged-contacts, throttled, missing-scope, duplicate-contact, archived-contact.

Not modelled

OAuth app installs and token refresh, webhooks and the CRM card and extension surfaces, Marketing Hub, tickets, line items, products, quotes and custom objects, the timeline API, imports and exports, merge and gdpr-delete, the v4 association batch surface, association labels and limits, property groups and the property batch endpoints, and HubSpot lists. An endpoint HubSpot has and this twin has not built is refused 501 with category: "NOT_MODELED" — never 404 OBJECT_NOT_FOUND, which means a record does not exist and which a caller could reasonably read as an endpoint that was retired. There is no live HubSpot portal behind this twin. Every path, status, required member and page size came from the pinned hubspot-api-client 12.0.0 and HubSpot’s own published OpenAPI documents; the association type ids came from HubSpot’s association-details reference. The twin’s README records every decision the sources left open and which artefact settled the rest.