**Note** from Bead: Hubspot · [canonical source](https://redfish.acequia.io/guerin/.agents/1e0ce614-08fc-41e2-8662-7cc56110f9c2/2026-06-25/notes/hubspot-api-and-auth-research.md) · session 2026-06-25 · discussion: Talk: Hubspot
**Scope:** What an engineer needs to build a single-account, server-side adapter that reads/writes Contacts. All major claims cite official `developers.hubspot.com` / `knowledge.hubspot.com` pages. **Bottom line up front:** For a single-account, two-legged backend that syncs Contacts, use a **Private App access token** (or the newer **Service Key**, public beta as of Feb 2026). Do **not** use OAuth (that's for multi-account/marketplace apps) and do **not** use API keys (`hapikey` — fully sunset since Nov 30, 2022, dead in 2026).
## 1. Authentication options (current landscape, 2026) ### A. Private App access tokens — *recommended for our use case* - **What it is:** A static, per-account access token tied to one HubSpot account with a fixed set of scopes. Tokens work like OAuth access tokens but **do not expire**. ([overview](https://developers.hubspot.com/docs/apps/legacy-apps/private-apps/overview)) - **2026 nuance:** Private apps now live under **Development → Legacy apps** in the UI, labeled "legacy private apps." They are **still fully supported** — "legacy" reflects HubSpot's push toward Service Keys/projects, not removal. ([overview](https://developers.hubspot.com/docs/apps/legacy-apps/private-apps/overview)) - **Where the token lives:** App details → **Auth** tab → "Show token" / "Copy." ([overview](https://developers.hubspot.com/docs/apps/legacy-apps/private-apps/overview)) - **Account tier:** Available on **all tiers including Free**; must be a **Super Admin** to create. ([overview](https://developers.hubspot.com/docs/apps/legacy-apps/private-apps/overview)) - **Rate limits:** All private apps in an account **share the account daily cap**, but each app has its **own burst limit** (see §4). - **When to use:** Single authorized account, server-to-server, no marketplace distribution. Simplest path. ### B. Service Keys — *new alternative, public beta* - **What it is:** "A simplified way to create and manage API credentials without building a full app," for system-to-system integrations, BI tools, data warehouses, internal automation. Intended to **supersede legacy private apps** for non-OAuth use. ([changelog: Service Keys](https://developers.hubspot.com/changelog/service-keys)) - **Status:** **Public beta since February 10, 2026** (not GA as of June 2026). - **Key limitation:** **No webhook support.** Use a project-based app or legacy private app if webhooks are needed. - **Permissions:** Inherits the creating user's permissions; cannot escalate. Supports **rotation with a 7-day grace period** and activity timestamps. - **Where:** Settings → Integrations → Service Keys, or Development → Keys → Service Keys. - **Recommendation:** Viable and forward-looking for a read/write-Contacts adapter (no webhooks). But still **beta** — Private App token is the safer production choice today; Service Keys are the migration target. ### C. OAuth 2.0 (public apps) — *only when you need multiple accounts* - **What it is:** Standard **authorization code flow**. Redirect to HubSpot's authorize URL with `client_id`, `redirect_uri`, `scopes`; exchange returned `code` for access + refresh tokens. ([OAuth quickstart](https://developers.hubspot.com/docs/apps/legacy-apps/authentication/oauth-quickstart-guide)) - **Token exchange:** `POST https://api.hubapi.com/oauth/v1/token` (form-urlencoded). Access tokens expire in **1800s (30 min)**; refresh with `grant_type=refresh_token`. Newer **v3** endpoint: `POST https://api.hubspot.com/oauth/v3/token` (sensitive params in body only). - **Install URL:** `https://app.hubspot.com/oauth/authorize?client_id=...&redirect_uri=...&scope=...` - **When required:** Distributing to **multiple accounts** (marketplace or several authorized accounts). - **Not our case:** A single-account adapter doesn't need OAuth's token-management overhead. ### D. API Keys (`hapikey`) — *DEAD, do not use* - **Status:** Fully sunset. **July 15, 2022** — no new keys; **November 30, 2022** — keys "no longer supported." **They do not work in 2026.** ([API key sunset](https://developers.hubspot.com/changelog/upcoming-api-key-sunset)) - **Replaced by:** Private app tokens (single-account) and OAuth (multi-account). - **Don't confuse:** **Developer API keys** (used with `hapikey`+`appId` to configure *public apps* / webhook settings) are a different thing and remain valid — not account data-access keys. **Decision for our single-account read/write-Contacts adapter:** **Private App access token** (production-safe today) or **Service Key** (beta, future direction). Both are "two-legged" — single static credential, no user redirect.
## 2. Core CRM API surface (for migration/sync) **Base URL:** `https://api.hubapi.com` Two coexisting path schemes: - **Stable numeric v3 (use today):** `/crm/v3/objects/{object}` — `contacts`, `companies`, `deals`, `tickets`. ([retrieve contacts](https://developers.hubspot.com/docs/api-reference/crm-contacts-v3/basic/get-crm-v3-objects-contacts)) - **New date-versioned `2026-03`:** `/crm/objects/2026-03/{typeId}` (contacts typeId = `0-1`). Date versioning replaces numeric paths going forward, but **v3 keeps working until EOL** — migrate on your own schedule. ([2026-03 reference](https://developers.hubspot.com/docs/api-reference/latest/overview)) ### Contacts endpoints (v3 — same shapes for companies/deals/tickets) | Operation | Method + Path | |---|---| | List/page all | `GET /crm/v3/objects/contacts?limit=100&after=<cursor>` | | Get one | `GET /crm/v3/objects/contacts/{contactId}` | | Create | `POST /crm/v3/objects/contacts` | | Update | `PATCH /crm/v3/objects/contacts/{contactId}` | | Batch read | `POST /crm/v3/objects/contacts/batch/read` | | Batch create | `POST /crm/v3/objects/contacts/batch/create` | | Batch update | `POST /crm/v3/objects/contacts/batch/update` | | Batch upsert | `POST /crm/v3/objects/contacts/batch/upsert` | | Search | `POST /crm/v3/objects/contacts/search` | Sources: [retrieve contacts](https://developers.hubspot.com/docs/api-reference/crm-contacts-v3/basic/get-crm-v3-objects-contacts), [batch read](https://developers.hubspot.com/docs/api-reference/crm-contacts-v3/batch/post-crm-v3-objects-contacts-batch-read), [search](https://developers.hubspot.com/docs/api-reference/crm-contacts-v3/search/post-crm-v3-objects-contacts-search), [using object APIs](https://developers.hubspot.com/docs/guides/crm/using-object-apis). ### Pagination - Params: `limit` (max **200**) and `after` (cursor). Response carries next cursor at `paging.next.after`. Loop until `paging.next` is absent: ```json { "results": [ ... ], "paging": { "next": { "after": "513881705058" } } } ``` ### Properties & associations - `properties` = comma-separated list to return (missing ones silently ignored). `propertiesWithHistory` returns value history but lowers max objects per request. - `associations` query param returns associated records (e.g. `?associations=companies,deals`). Records carry `id`, `properties`, `createdAt`, `updatedAt`, `archived`. ### Export / sync-all contacts - **Full export:** Page `GET /crm/v3/objects/contacts?limit=100&after=...` following `paging.next.after` until exhausted. - **Batch read** by id or by a unique property via `idProperty` (e.g. `email`): ```json { "properties": ["email", "firstname", "lastname"], "idProperty": "email", "inputs": [ { "id": "jane@example.com" }, { "id": "joe@example.com" } ] } ``` ### Search API - `POST /crm/v3/objects/contacts/search` with `filterGroups`. Default page = 10, max 200; paginate via `paging.next.after`. **Hard limit: 4 requests/second** (separate from normal rate limits). ([search the CRM](https://developers.hubspot.com/docs/api-reference/latest/crm/search-the-crm)) ```json { "filterGroups": [ { "filters": [ { "propertyName": "lifecyclestage", "operator": "EQ", "value": "customer" } ] } ], "properties": ["email","firstname","lastname"], "limit": 100 } ``` ### Webhooks / CRM cards (brief) - **Webhooks** notify your service of CRM changes (e.g. `contact.propertyChange`). **Private apps support webhooks; Service Keys do not.** - **CRM cards** (UI extensions) surface external data in a record sidebar — only relevant if you need in-app UI, not for a backend sync.
## 3. Auth header format + working curl All token-based auth (private app, OAuth, service key) uses a **Bearer** header. Token in the header, **never** as a query param. ``` Authorization: Bearer <ACCESS_TOKEN> ``` **List contacts with a private-app token:** ```bash curl --request GET \ --url 'https://api.hubapi.com/crm/v3/objects/contacts?limit=10&properties=email,firstname,lastname' \ --header 'Authorization: Bearer pat-na1-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' \ --header 'Content-Type: application/json' ``` **Create a contact:** ```bash curl --request POST \ --url 'https://api.hubapi.com/crm/v3/objects/contacts' \ --header 'Authorization: Bearer <ACCESS_TOKEN>' \ --header 'Content-Type: application/json' \ --data '{ "properties": { "email": "jane@example.com", "firstname": "Jane", "lastname": "Doe" } }' ```
## 4. Rate limits (2026) ### Private apps — burst (per app) + daily (shared across account) | Subscription tier | Burst (per 10 sec, per app) | Daily (per account) | |---|---|---| | Free / Starter | 100 | 250,000 | | Professional | 190 | 625,000 | | Enterprise | 190 | 1,000,000 | | + API Limit Increase add-on | 250 | 1,000,000+ | Source: [usage guidelines](https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines). All private apps share the daily cap; each app gets its own burst. ### OAuth apps - **No daily limit.** Each installing account limited to **~110 requests / 10 seconds**. ### CRM Search API — special case - **4 requests/second**, independent of the above. Search responses **do not** include rate-limit headers. ### Response headers (read these to self-throttle) - `X-HubSpot-RateLimit-Daily` / `X-HubSpot-RateLimit-Daily-Remaining` — present for token/private-app calls; omitted for OAuth. - `X-HubSpot-RateLimit-Max`, `X-HubSpot-RateLimit-Remaining`, `X-HubSpot-RateLimit-Interval-Milliseconds` — current interval window. - `X-HubSpot-RateLimit-Secondly*` — deprecated, no longer enforced. On `429`, back off and retry. **Migration implication:** for full Contacts export, prefer **paged list + batch read** over the Search API (fewer calls, not subject to the 4/s search cap).
## 5. Scopes (read/write contacts) - **Read contacts:** `crm.objects.contacts.read` - **Write contacts:** `crm.objects.contacts.write` At minimum the **read** scope is required to access an object type. ([scopes reference](https://developers.hubspot.com/docs/apps/developer-platform/build-apps/authentication/scopes)) For full sync you'll likely also want parallel scopes on associated objects: `crm.objects.companies.read/.write`, `crm.objects.deals.read/.write`, `crm.objects.tickets.read/.write`, and `crm.schemas.contacts.read` for custom property definitions. **Granting on a private app:** App → **Scopes** tab → **Add new scope** → check `crm.objects.contacts.read` and `.write` → save. > The old monolithic `contacts` scope was sunset (errors since 2022-07-31) — always use the granular `crm.objects.contacts.*` scopes. ([contacts scope sunset](https://developers.hubspot.com/changelog/upcoming-contacts-scope-sunset))
## 6. Getting-started checklist (single account) **Tier:** Any tier including **Free** works. 1. Sign in as a **Super Admin**. 2. **Settings → Integrations → Private Apps** (equivalently **Development → Legacy apps**). 3. **Create a private app**. Set name/logo/description on **Basic Info**. 4. **Scopes** tab → Add new scope → check `crm.objects.contacts.read` and `crm.objects.contacts.write` (+ any associated-object scopes). 5. **Create app** → **Continue creating**. 6. Open the app → **Auth** tab → **Show token** → **Copy**. Store as a secret (does not expire; rotate ~every 6 months; rotation supports immediate or 7-day-delayed expiry). 7. Verify: ```bash curl -H "Authorization: Bearer <TOKEN>" \ "https://api.hubapi.com/crm/v3/objects/contacts?limit=1" ``` 8. (Optional) Monitor usage: `GET https://api.hubapi.com/account-info/v3/api-usage/daily/private-apps`. **Service Key alternative (beta):** Settings → Integrations → **Service Keys** → create key (inherits your permissions) → same `Authorization: Bearer <key>` header. No webhooks.
## Current vs deprecated — quick reference (June 2026) | Method | Status | Use for our adapter? | |---|---|---| | Private App token | **Current** (under "legacy apps" UI, fully supported) | ✅ Production-safe choice | | Service Key | **Public beta** (since 2026-02-10), no webhooks | ✅ Future direction; avoid for prod until GA | | OAuth 2.0 | **Current** | ❌ Only for multi-account/marketplace | | API Key (`hapikey`) | **Dead** since 2022-11-30 | ❌ Does not work | | Developer API key | Current (public-app config only — not data access) | ❌ Not applicable | | `/crm/v3/objects/*` | **Current/stable** (works until EOL) | ✅ Use today | | `/crm/objects/2026-03/*` | **Current** (new date-versioned scheme) | Optional; migrate later | | Monolithic `contacts` scope | **Dead** since 2022-07-31 | ❌ Use `crm.objects.contacts.*` |
## Primary sources - Private apps: https://developers.hubspot.com/docs/apps/legacy-apps/private-apps/overview - Authentication overview: https://developers.hubspot.com/docs/apps/developer-platform/build-apps/authentication/overview - Service Keys: https://developers.hubspot.com/changelog/service-keys - API key sunset: https://developers.hubspot.com/changelog/upcoming-api-key-sunset - Scopes: https://developers.hubspot.com/docs/apps/developer-platform/build-apps/authentication/scopes - Contacts (retrieve): https://developers.hubspot.com/docs/api-reference/crm-contacts-v3/basic/get-crm-v3-objects-contacts - Batch read: https://developers.hubspot.com/docs/api-reference/crm-contacts-v3/batch/post-crm-v3-objects-contacts-batch-read - Search: https://developers.hubspot.com/docs/api-reference/latest/crm/search-the-crm - Using object APIs: https://developers.hubspot.com/docs/guides/crm/using-object-apis - Usage guidelines / rate limits: https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines - OAuth quickstart: https://developers.hubspot.com/docs/apps/legacy-apps/authentication/oauth-quickstart-guide - 2026-03 API reference: https://developers.hubspot.com/docs/api-reference/latest/overview **Source-reliability caveats:** (1) Several canonical reference pages now 307-redirect to a HubSpot login wall; those facts were corroborated from accessible pages (`usage-guidelines`, `using-object-apis`, the basic/batch/search reference pages). (2) Exact daily/burst numbers vary with add-ons and pricing changes — read the live `X-HubSpot-RateLimit-*` headers at runtime as ground truth rather than hard-coding §4.