---
name: hubspot-oauth-app
description: Creates or signs in to a HubSpot developer account and registers a HubSpot app to obtain OAuth2 client ID and client secret — with the right redirect URLs, required vs optional scopes, install limits, and a safe credential handoff. Use when asked to get HubSpot OAuth credentials, set up a HubSpot developer/app account, create a HubSpot public app, rotate a HubSpot client secret, or fix a HubSpot install error like "invalid scope" or "redirect URI mismatch". For any other vendor's developer portal, use that vendor's skill instead.
---

# HubSpot OAuth2 App Registration

Get a working HubSpot OAuth2 client — developer account, app, redirect URLs, scopes, client ID and secret — for a
platform that connects HubSpot on behalf of many customers.

This is mostly careful reading and careful form-filling. Two steps are expensive to get wrong: **scope placement**
(HubSpot fails the install if a scope your authorize URL sends is not declared on the app) and the **25-install cap**
on unlisted marketplace-distribution apps (it silently stops customer #26 from connecting, long after anyone is
watching). Slow down at both.

## Inputs to collect before you start

Ask for these in one batch rather than one at a time. Never invent them.

| Input | Notes |
| --- | --- |
| **App name** | What appears on HubSpot's consent screen |
| **Account / registration email** | The HubSpot account that will own the app; for an existing app, the owner account on record |
| **Redirect URLs** | Every callback host your platform serves (§4) |
| **Scope set** | The scopes your connector actually uses (§5) |
| **Logo, privacy policy URL, terms URL, support email** | Needed for a Marketplace listing (§7) |
| **New app or existing app?** | See §1 — the default answer is "existing" |

## Quick Start

1. Confirm a **new app** is actually needed — existing customer connections are bound to the current `client_id` (§1).
2. Sign in to (or create) the HubSpot account that owns the app; hand control back for email/2FA verification (§2).
3. Create a **developer test account** for install testing (§2).
4. Create the app with the HubSpot CLI (projects platform); pick `oauth` and the right distribution (§3).
5. Register **every** redirect URL your platform uses (§4).
6. Put only the always-sent scopes in `requiredScopes`; everything else in `optionalScopes` (§5).
7. `hs project upload`, then read client ID / client secret from the app's **Auth** tab (§6).
8. Check the install cap and decide about a Marketplace listing (§7).
9. Verify with a real authorize → callback round trip (§8), then hand the credentials over — never commit them (§9).

## Platform state (verified 2026-09-18 — re-verify before trusting)

HubSpot changed all three of the things this task depends on during 2026. Check the changelog before following any
older write-up, including cached knowledge or a stale internal doc:

- **Legacy public apps can no longer be created** — sunset 2026-05-26 (new accounts) / 2026-06-23 (existing accounts).
  Existing legacy public apps keep working untouched. New apps are built on the **projects platform** (current version
  `2026.09`) via the HubSpot CLI — there is no "create app" button anymore.
- **Separate app developer accounts are legacy** — on 2026-03-09 legacy developer accounts were migrated into standard
  HubSpot accounts. App work now lives under **Development** in a standard account's nav.
- **Unlisted marketplace-distribution apps are capped at 25 installs** (enforced since 2025-09-22) until a Marketplace
  listing is approved. Private distribution is capped lower: 10 accounts, or 100 for Solution Partners.

If the portal does not look like this, stop and report what you actually see rather than clicking on.

## 1. Decide: reuse the existing app, or register a new one

**A new app means new credentials, and every existing customer connection is bound to the old `client_id`.** Creating
one because the portal looks unfamiliar breaks every live connection.

Reuse the existing app (sign in, edit in place) for: adding a scope for a new object type, adding a redirect URL for a
new region, rotating a leaked secret, or diagnosing an install failure. Its client ID, owner account and app ID live in
your platform's secret config / console — look them up there, not by guessing from the portal listing.

Register a **new** app only when the user explicitly wants one: a second app for a new region or brand, a replacement
for a compromised app, or a projects-platform migration they have asked for. Say which path you are taking before you act.

## 2. Account: sign in or sign up

- **Sign in** at `https://app.hubspot.com/` as the account that owns the app, then open **Development** in the left
  nav. Legacy apps live under **Development → Legacy apps**.
- **Sign up** (only when there is no account, or the user wants a separate one) for a free HubSpot account at
  `https://app.hubspot.com/signup-hubspot/`. Use the registration email the user gave you. Never invent identity
  details — company size, phone number, job title, expected volume. Ask, or leave optional fields blank.
- **Super Admin** permission is required to install apps and to read the developer API key.
- **Developer test account** — free, ~90-day enterprise trial, up to 10 per account, cannot sync with other accounts:
  **Development → Testing → Test Accounts → Create developer test account**. Use one for install testing so nothing
  touches a real customer portal.

Hand control back to the user, saying exactly what is needed, for anything only they can do: CAPTCHA, email
verification links, SMS/authenticator codes, accepting developer terms. Do not retry a blocked step in a loop.

Before accepting developer terms, skim them for anything a founder would want to know about — exclusivity, revenue
share, per-call fees, data-use commitments, restrictions on reselling access. Boilerplate needs no commentary; anything
unusual gets flagged before you tick the box.

**If this session has no browser automation** (the usual case for a CLI or cloud run), do not pretend to click. Hand
the user an exact, ordered click path with the literal values to paste (§4 redirect URLs, §5 scope lists), then
continue once they report back with the client ID and app ID. The CLI steps in §3 you can run yourself once `hs` is
authenticated.

## 3. Create the app (projects platform, CLI)

```bash
npm install -g @hubspot/cli@latest     # v7.6.0+ required for app projects
hs init                                # or: hs account auth   (CLI 7.4+, global config)
hs project create                      # choose the "App" template
```

At the prompts:

- **Distribution** → `marketplace` for a multi-tenant connector any HubSpot account can install. `private` only for a
  one-off or bring-your-own-app customer install (10-account cap).
- **Authentication** → `oauth`. `static` has no client secret and cannot serve multiple accounts.
- **Features** → skip cards, workflow actions and settings unless the user asked for them. A data connector needs API
  access and, optionally, webhooks.

Then edit `app-hsmeta.json` — this file, not a UI form, is the app config:

```jsonc
{
    "uid": "my_app",                    // globally unique within the project
    "type": "app",
    "name": "My App",                   // shown on the consent screen
    "description": "…",
    "logo": "./logo.png",
    "auth": {
        "type": "oauth",
        "redirectUrls": [ /* §4 */ ],
        "requiredScopes": [ /* §5 */ ],
        "optionalScopes": [ /* §5 */ ],
        "conditionallyRequiredScopes": []
    }
}
```

Fill `supportEmail`, `documentationUrl` and `supportUrl` with values the user supplies — they are required for a
Marketplace listing later, and a guess there becomes a public claim. Then:

```bash
hs project upload      # creates a build and registers the app
hs project open        # opens the project; click the app → Auth tab
```

## 4. Redirect URLs

Register **every** callback host your platform serves, so a customer connecting in any region works without a second
app. Get the list from the platform owner or the console rather than assuming; for Unified.to the callbacks are one
path per data center:

```
https://api.unified.to/oauth/code          # us (default)
https://api-eu.unified.to/oauth/code       # eu
https://api-au.unified.to/oauth/code       # au
https://api-dev.unified.to/oauth/code      # dev
```

Notes:

- HubSpot allows multiple redirect URLs per app and they stay editable — but a wrong one fails at connect time, not at
  save time. Re-read each one character by character against the source list.
- Customers on a **custom API domain** call back to their own host. Those customers bring their own HubSpot app and
  credentials — do **not** add customer domains to the shared app.
- `http://localhost` is accepted by HubSpot for local testing only; never leave one on a production app.

## 5. Scopes — required vs optional (this is the part that breaks)

HubSpot splits an app's scopes three ways, and the split must match what your authorize URL actually sends:

| App setting | HubSpot behavior |
| --- | --- |
| `requiredScopes` | Must be granted **and** must appear in the `scope` query parameter of every authorize URL |
| `optionalScopes` | Sent in `optional_scope`; HubSpot drops any the customer's **account tier** cannot grant |
| `conditionallyRequiredScopes` | Required only when included in the install URL |

The rule that catches people: HubSpot forgives an optional scope the customer's tier lacks, but it does **not**
forgive a scope the app never declared — an undeclared scope in `optional_scope` fails the authorize request. So
declare every scope the connector can ever ask for, and keep `requiredScopes` down to the ones sent on *every* install.

A multi-object connector typically sends a small fixed `scope` (enough to identify the account) and varies the rest per
workspace. **Unified.to's HubSpot connector does exactly that**: `scope=oauth crm.objects.owners.read` on every
authorize URL, with every per-object scope in `optional_scope`. So its app config is:

- **`requiredScopes`: exactly `oauth` and `crm.objects.owners.read`.** Anything else marked required is never sent in
  `scope`, and HubSpot then refuses the install for every customer.
- **`optionalScopes`: the full union of the per-object scopes.** As of 2026-09-18 that is: `content`,
  `crm.extensions_calling_transcripts.read`, `crm.lists.read`, `crm.lists.write`, `crm.objects.companies.read`,
  `crm.objects.companies.write`, `crm.objects.contacts.read`, `crm.objects.contacts.write`, `crm.objects.deals.read`,
  `crm.objects.deals.write`, `crm.pipelines.orders.read`, `crm.pipelines.orders.write`, `crm.schemas.custom.read`,
  `files`, `media_bridge.read`, `sales-email-read`, `scheduler.meetings.meeting-link.read`,
  `settings.users.teams.read`, `tickets`.

That list drifts as object coverage grows — confirm the current set with the connector's owner before you submit, and
add any scope the connector has started sending. Request only what the connector actually uses: over-broad scope sets
are the most common reason a Marketplace listing is rejected, and reviewers read the justification. List what you chose
and why in your summary so the choice can be challenged.

## 6. Capture the credentials

From `hs project open` → the app → **Auth** tab (legacy app: **Development → Legacy apps → <app> → Auth**), capture:

- **Client ID** and **client secret**
- **App ID**
- Authorize URL `https://app.hubspot.com/oauth/authorize` (EU variant `https://app-eu1.hubspot.com/oauth/authorize` —
  HubSpot-side routing, not a separate app) and token URL `https://api.hubapi.com/oauth/v3/token`. Older integrations
  pin `…/oauth/v1/token`; confirm which one the connector uses rather than switching it as a side effect of this task.
- The exact scope strings as HubSpot spells them, not as the docs prose describes them
- Whether these are test or production credentials

**Developer API key** — required for HubSpot's webhook-subscription endpoints, passed as `?hapikey=`. Find it under
**Settings → Integrations → API key** (Super Admin only) in the account that owns the app. Only fetch it if webhooks
are in scope for this request.

Treat the secret as live credential material: report it once so the user can paste it into their secret store, say
plainly that it is now in the transcript and can be rotated, then move on without belaboring it.

**Rotating an existing app's secret breaks every token refresh until the new value is deployed everywhere** — never
rotate without explicit human go-ahead and a cutover plan.

## 7. Install limits and Marketplace listing

- Unlisted marketplace-distribution app → **25 installs**. For a multi-tenant connector that is a hard ceiling on
  customer connections, so raise it with the user before the app ships.
- Lifting it means submitting a **Marketplace listing**: reviewed by HubSpot's Ecosystem Quality team, initial response
  in about 10 business days, whole process up to 60 days.
- Unlisted or unverified apps also show customers an extra confirmation step at install.

If asked to submit the listing, describe the platform accurately — it connects HubSpot's API on behalf of mutual
customers, who authorize access themselves through OAuth — and tie each requested scope to a concrete function.

**Stop and ask** before answering anything you would have to invent: security or compliance questionnaires,
certifications, retention periods, subprocessors, breach procedures, customer counts, volume projections, demo videos,
legal attestations, or a named responsible individual. A plausible-sounding guess about compliance is worse than an
unanswered question, and it is expensive to walk back once submitted.

After submitting, record the expected turnaround, the reference number, and where the decision will arrive.

## 8. Verify end-to-end

A registration that half-worked is worse than one that stopped early, because the failure surfaces later as a customer
who cannot connect. Prove it before closing out:

1. Install the app on the **developer test account** (app → Distribution tab → test install; tick the unverified-app
   confirmation).
2. Run a real connect through your platform's own flow — console "add connection", or the authorize URL your connector
   builds — against credentials matching the ones you just captured, and complete the callback.
3. Confirm the connection stored an access **and** a refresh token, and that one read call succeeds.

Common failures and their real cause:

| Symptom | Cause |
| --- | --- |
| `invalid scope` at authorize | A scope sent in `optional_scope` is not declared on the app |
| Redirect mismatch | The callback host for that region is missing from `redirectUrls` |
| Install blocked for one customer | Install cap reached, or the installing user is not a Super Admin |
| Refresh fails after a change | Secret rotated without deploying the new value everywhere |

## 9. Hand off — never commit the secret

- **Do not** write the client secret or developer API key into source control, a test, a fixture, a committed `.env`,
  a ticket, a PR body, or a chat channel. Values go to the user, for the secret store or console.
- If a code change is genuinely needed (an app ID, a new callback host), keep it secret-free and say plainly what the
  human must set out of band.
- Close with: what was created and in which environment; client ID; where the secret was delivered; authorize and token
  URLs; the exact required and optional scope strings; install-cap and listing status with dates; and anything left for
  the user to do, listed plainly.

## Stop and ask

Hand back to a human rather than guessing when: the account is gated behind verification you cannot complete; the
existing production app would have to change in a way that could break live connections (secret rotation, removing a
redirect URL, moving a scope from optional to required); a listing form asks for compliance, legal or volume claims;
or HubSpot's portal does not match the **Platform state** section above.

When a portal behaves in a way the docs did not describe, say what you found rather than picking the option that lets
you keep going.

## References

- Developer platform overview — https://developers.hubspot.com/docs/apps/developer-platform/overview
- Create an app — https://developers.hubspot.com/docs/apps/developer-platform/build-apps/create-an-app
- App configuration (`app-hsmeta.json`) — https://developers.hubspot.com/docs/apps/developer-platform/build-apps/app-configuration
- OAuth quickstart — https://developers.hubspot.com/docs/apps/developer-platform/build-apps/authentication/oauth/oauth-quickstart-guide
- Account types (standard / test / sandbox) — https://developers.hubspot.com/docs/getting-started/account-types
- Legacy public app creation sunset — https://developers.hubspot.com/changelog/legacy-public-app-creation-sunset
- Marketplace install limits — https://developers.hubspot.com/changelog/new-marketplace-distribution-app-install-limits
- Marketplace listing requirements — https://developers.hubspot.com/docs/apps/developer-platform/list-apps/listing-your-app/app-marketplace-listing-requirements
- Webhooks API guide (developer API key) — https://developers.hubspot.com/docs/api-reference/latest/webhooks/guide
