🟢HubSpot Developer Practical Textbook — 2026 Edition Developer Edition
Chapter 9  ·  App Architecture & OAuth 2.0

External collaboration design——
App & OAuth

Public App structure, OAuth 2.0 authentication flow, token management, Scopes design, and Marketplace application. Codify design patterns for safely building HubSpot apps for multiple portals.

OAuth 2.0 Authorization Code
Multi-portal compatible
Time required: Approximately 90 minutes
9-1 Private App vs Public App

Select an authentication method depending on the purpose.

🔑 Private App (Access Token)

Usage:Custom development exclusively for your own portal
certification:Bearer token (never expires)
Features:Simple/Supports only 1 portal
Use case:Internal tools/data synchronization/automation scripts

🌐 Public App(OAuth 2.0)

Usage:SaaS products that support multiple customer portals
certification:OAuth 2.0 (access token + refresh token)
Features:Marketplace listing/Multi-tenant support
Use case:ISV products/HubSpot integration SaaS

Judgment criteria: If you only want to use it in your own HubSpot portal Private App. If you want to use it on multiple customer portals or post it on Marketplace. Public App + OAuth 2.0 Select.
9-2 OAuth 2.0 authentication flow

Get access to the customer portal with the Authorization Code flow.

  1. 1
    Authorization request (redirects user to HubSpot) When you press the "Connect with HubSpot" button in the app, you will be redirected to the HubSpot authorization screen. Redirect to a URL that includes the scope and Client ID.
  2. 2
    User approves permission Click "Allow" on the HubSpot screen. Authorize access to the specified scope.
  3. 3
    Receive authorization code (callback URL) HubSpot accesses the specified redirect_uri. URL query parameters code is included.
  4. 4
    exchange code for token Use code + Client Secret on the server side to obtain access token + refresh token.
  5. 5
    Store your tokens securely Encrypted storage in the database using the portal ID as a key. The access token (valid for 6 hours) can be updated at any time using a refresh token.
Node.js / Express — OAuth 2.0 flow implementation
import express from 'express'; import { Client } from '@hubspot/api-client'; const app = express(); const CLIENT_ID = process.env.HUBSPOT_CLIENT_ID; const CLIENT_SECRET = process.env.HUBSPOT_CLIENT_SECRET; const REDIRECT_URI = process.env.HUBSPOT_REDIRECT_URI; const SCOPES = ['crm.objects.contacts.read', 'crm.objects.contacts.write', 'crm.objects.deals.read']; // Step 1: Redirect to HubSpot's authorization page app.get('/auth/hubspot', (req, res) => { const authUrl = new URL('https://app.hubspot.com/oauth/authorize'); authUrl.searchParams.set('client_id', CLIENT_ID); authUrl.searchParams.set('redirect_uri', REDIRECT_URI); authUrl.searchParams.set('scope', SCOPES.join(' ')); authUrl.searchParams.set('state', generateCsrfToken(req)); // CSRF countermeasures res.redirect(authUrl.toString()); }); // Step 3 & 4: Callback processing app.get('/auth/hubspot/callback', async (req, res) => { const { code, state } = req.query; // CSRF validation if (!verifyCsrfToken(req, state)) { return res.status(403).send('Invalid state'); } // code → token exchange const tokenResponse = await fetch('https://api.hubapi.com/oauth/v1/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'authorization_code', client_id: CLIENT_ID, client_secret: CLIENT_SECRET, redirect_uri: REDIRECT_URI, code, }), }); const tokens = await tokenResponse.json(); // { access_token, refresh_token, expires_in: 21600, hub_id, hub_domain } // Step 5: Save token to DB await saveTokens(tokens.hub_id, { accessToken: tokens.access_token, refreshToken: tokens.refresh_token, expiresAt: Date.now() + tokens.expires_in * 1000, hubDomain: tokens.hub_domain, }); res.redirect('/dashboard'); });
Node.js — Automatic refresh of access tokens
async function getValidAccessToken(portalId) { const stored = await db.getTokens(portalId); // Updated 5 minutes before expiry date const needsRefresh = stored.expiresAt - Date.now() < 5 * 60 * 1000; if (!needsRefresh) return stored.accessToken; // Get new access token with refresh token const res = await fetch('https://api.hubapi.com/oauth/v1/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'refresh_token', client_id: CLIENT_ID, client_secret: CLIENT_SECRET, refresh_token: stored.refreshToken, }), }); const newTokens = await res.json(); // Save updated token to DB await db.updateTokens(portalId, { accessToken: newTokens.access_token, expiresAt: Date.now() + newTokens.expires_in * 1000, }); return newTokens.access_token; } // Usage example: Generate API client by specifying portal ID async function getClientForPortal(portalId) { const accessToken = await getValidAccessToken(portalId); return new Client({ accessToken }); }
9-3 Scopes design best practices

Gain user trust by requesting the minimum necessary scope.

scopeauthorityPurpose
crm.objects.contacts.readcontact readingView contact information
crm.objects.contacts.writecontact writingCreating/updating contacts
crm.objects.deals.readbusiness negotiation readingViewing deal information
crm.objects.deals.writebusiness negotiation writingCreate/update a deal
crm.schemas.custom.readCustom Object Schema ReadCheck schema structure
contentCMS content read/writeBlog/page operations
formsform reading and writingform management
transactional-emailSend transactional emailsystem mail
oauthOAuth basics (required)Required for all OAuth Apps
Principle of least privilege: Request only the minimum scope for the functionality your app needs. Excessive scope will be flagged during Marketplace review and will reduce user approval rates. If you add features later, you will need to require users to re-authenticate.
9-4 Preparation for Marketplace application

Organize requirements and preparations for listing on the HubSpot App Marketplace.

📋 Marketplace Application Checklist

Technical requirements:OAuth 2.0 implementation / HTTPS required / Webhook signature verification / Proper error handling
Security:Minimum user data collection, encrypted storage, GDPR/data deletion support
UX requirements:Installation flow (within 30 seconds), clear error messages, uninstall function
content:App description, screenshots, demo videos, support documents
test:Operation confirmation in developer sandbox/testing on multiple portals

⚠ Implementation of uninstallation process: When an app is uninstalled, HubSpot app.uninstall Send a webhook. Once you receive this event, please delete the token and personal data for that portal. This is a mandatory requirement for Marketplace review.
Node.js — Handling uninstall webhooks
app.post('/webhook/hubspot', verifySignature, async (req, res) => { res.status(200).json({ received: true }); for (const event of req.body) { if (event.subscriptionType === 'app.uninstall') { const { portalId } = event; // remove token await db.deleteTokens(portalId); // Delete user data for that portal (GDPR compliant) await db.deletePortalData(portalId); console.log(`Portal ${portalId} uninstalled and data cleaned up`); } } });
9-5 Summary of this chapter

✅ Chapter 9 Checklist

  • Be able to determine whether to use Private Apps or Public Apps
  • OAuth 2.0 Authorization Code flow can be explained in 5 steps
  • Can implement CSRF countermeasures (state parameter)
  • Can implement automatic refresh logic for access tokens
  • You can design token management that supports multi-portal.
  • Allows you to choose scope based on the principle of least privilege
  • Data cleanup can be implemented with uninstall webhooks
  • Understand the key requirements for Marketplace submissions
About the next chapter (Chapter 10):Learn about HubSpot MCP Server and AI development collaboration. We will explain MCP Server setup, connection with Claude Code / Cursor, and AI-driven development flow at a practical level.