HubSpot API Missing Scopes Error: Why the 403 Persists and How to Fix It
Few things annoy someone integrating with HubSpot more than the 403 error. You read the message, it mentions permission, you go to the app settings, check the scopes, and they are all there, enabled, just right. You go back to the code, run it again, and the 403 is still there, staring at you, as if HubSpot were messing with you.
The good news is that this behavior, as annoying as it is, has an almost always predictable cause, and the fix usually takes less than five minutes once you understand what is happening. The 403 on the HubSpot API means, in nearly every case, one thing: the token you are using does not have the scope that endpoint requires. What confuses people, especially with OAuth apps, is that having the scope enabled in the config and having it active in the token can be two different moments. In this guide, I will show you why the 403 appears even with everything seemingly right, and how to solve it definitively.
What a scope is, anyway
The HubSpot token receives scopes, which are nothing more than permissions. Each scope unlocks a specific set of endpoints. To read contacts, the token needs the crm.objects.contacts.read scope. To write contacts, it needs crm.objects.contacts.write. To touch automation, forms, or any other area, there are dedicated scopes. The same scopes apply whether you use a private app token, an account service key, or an OAuth app. The logic is granular on purpose: each token should have exactly the permissions of the task it performs, and nothing beyond that. It is the principle of least privilege, which protects the portal if the token leaks.
Why the 403 appears even with the scope enabled
Here is the heart of the confusion, and it depends on the credential type. With a static credential, a private app token or a service key, the token reflects the app's current scopes as soon as you save them, so a persistent 403 usually points to something other than the token itself. With OAuth, the access token carries the scopes consented at install, so a scope added later only takes effect after a reinstall. There are four causes that account for the vast majority of cases, and it is worth knowing them all.
- The scope was not actually added and saved. It sounds obvious, but the most common static-credential cause is a scope that was ticked but not saved, or saved on a different app than the one your token belongs to. Once you add the scope and click Save, the existing token reflects it, so start by confirming the scope is really enabled on the right app.
- The scope is wrong for that specific endpoint. Some schema endpoints, for instance, need their own crm.schemas scopes, separate from the object read scope. Always check the exact scope that endpoint's documentation asks for.
- Confusion between read and write. A token that only has the read scope gets a 403 the moment it tries to create or update a record, even if reading works perfectly.
- OAuth apps need a reinstall. In OAuth apps, the access token carries the scopes consented at install. Adding a scope in the config is not enough: you must reinstall and re-consent for it to take effect. This is the OAuth version of the classic 'token created before the scope.'
How to fix it, step by step
Solving the 403 is straightforward when you attack the right cause. The roadmap below covers the path that resolves the overwhelming majority of cases.
- Open the app in the portal settings and calmly check the list of enabled scopes.
- Add the exact scope the endpoint requires, paying attention to the difference between read and write and between records and schema.
- Save the app to apply the scope. With a static credential, the existing token starts working with the new scope the moment you click Save, with no regeneration. Regenerating is only a security rotation, not a way to gain a scope.
- Update the token in your credential vault and in every integration that uses it, if you did rotate it for security.
- If it is an OAuth app, reinstall the app in the portal so the new scope is effectively consented.
|
A tip from someone who has been burned: if you added the scope and the 403 persists on a static credential, do not regenerate the token hoping to fix it. Confirm the scope was really saved on the right app, that it is the exact scope the endpoint needs, and that you are not confusing read with write. On OAuth, the fix is to reinstall and re-consent. Regenerating a static token is for security, not for gaining a scope. |
How HubSpot authenticates today
Before troubleshooting, it helps to know which credential you are actually holding, because the fix for a 403 depends on it. HubSpot's authentication has more than one path now, and they behave differently when you change scopes. The old API Keys (hapikey) were sunset in 2022 and are no longer an option.
- Private app (static token): the simplest option, still fully supported. You add scopes in the app settings and click Save, and the existing token reflects them. Best when you only need to call a few APIs.
- Account service keys (beta): a scoped static key for direct REST calls, managed under Development, Keys. You can add scopes after creating the key and use them immediately. They cannot authenticate webhooks or UI extensions.
- OAuth apps: for integrations installed by more than one account. The access token carries the scopes consented at install, so a new scope needs a reinstall and re-consent.
- New developer platform apps (built with the HubSpot CLI): configuration as code, authenticated by a static token or OAuth. This is where webhooks, app cards, custom workflow actions, and serverless live as app features.
- Developer API key: a developer-account credential used to manage public apps and their webhook subscriptions, not to make data calls. Do not confuse it with the scoped token that triggers a 403.
One practical consequence: if you need webhooks, you need an app, not a service key. Legacy private apps configure webhook subscriptions in the app UI, while new platform apps configure them as code, where webhooks v3 works with a static token and v4 requires OAuth. And a security note that applies to every static token: since October 2024, HubSpot automatically deactivates tokens found publicly exposed, for example in a GitHub repository, so keep them in a vault. Account service keys are still in beta, so treat their exact behavior as subject to change.
403 and 401 are not the same thing
It is worth separating two errors many people confuse, because the handling is different. The 401 is an authentication problem: the token is wrong, missing, or invalid, and HubSpot does not even know who you are. The 403, on the other hand, is an authorization problem: HubSpot knows perfectly well who you are, the token is valid, but it does not have permission for that specific operation. In short, 401 is I do not know who you are, 403 is I know who you are, but you cannot do this. Knowing which of the two you are seeing already points to half the solution.
Common mistakes that cause needless 403s
Beyond the OAuth case of a scope added after install, some habits produce 403s that could be avoided. One is reusing the same token for everything, from read to write, from one area to another, which makes it hard to reason about what it can actually do. Another is not documenting which scopes each integration uses, which turns every 403 into an investigation from scratch instead of a quick check against a known list.
There is also the mistake of testing only the happy path during development. If you only exercise reading because it is what is ready first, the write scope is never validated, and the 403 only appears when the write part goes to production, at the worst time. The practice that avoids all of this is simple: one token per integration, with minimal, documented scopes, and each endpoint tested in isolation before you trust it. Scope discipline is, at heart, security discipline.
A quick checklist before running at volume
The worst time to discover a missing scope is in the middle of a run over thousands of records, when the routine has already processed half and stalls. The best time is before you start, in seconds. So before any large load, make a test call to each endpoint the routine will use. If any returns 403, you find the missing scope right away, calmly, and fix it before the problem costs you time and half-processed data. This small pre-test ritual saves hours of rework.
In practice: the integration that stalled on writing
An operation built an integration that read contacts from HubSpot without the slightest problem throughout the entire development. Everything looked ready. When it came time to activate the part that updated contacts, the 403 arrived, and the team froze, thinking they had a deep bug in the write logic. They did not. The token had been created with only the read scope, because during development only reading was needed.
The fix was to add the write scope to the app and save it, and the existing token started working. Five minutes of work for a problem that had eaten the whole morning of debugging in the wrong place. Since then, the team adopted the ritual of testing each endpoint, read and write, before considering an integration done.
Checklist against the 403
- Does the app have enabled the exact scope the endpoint requires?
- Did you distinguish read from write when choosing the scope?
- Did you save the scope, and reinstall the app if it is OAuth, after adding it?
- Is the scope enabled on the same app your token actually belongs to?
- Did you test each endpoint with an isolated call before running at volume?
Frequently asked questions
I added the scope and still get 403. Why?
On a static credential (private app or service key), a saved scope applies to the existing token immediately, so if the 403 persists, confirm the scope was really saved on the right app, that it is the correct one for the endpoint, and that you are not confusing read with write. Regenerating the token does not add a scope. On OAuth, the scope only takes effect after you reinstall and re-consent.
How do I know which scope an endpoint requires?
Each endpoint's documentation lists the required scope. Mind the difference between the record scope and the schema scope, and between read and write scopes.
Are 403 and 401 the same?
No. 401 is invalid authentication: wrong or missing token. 403 is valid authentication without permission: the token is right, but the scope for that operation is missing.
Do I need to reinstall the app for the scope to apply?
With a static credential (private app or service key), no: add the scope, click Save, and the existing token reflects it. With OAuth, yes: besides adding the scope in the config, you must reinstall and re-consent for it to take effect.
Can a read token write if I need it to?
No. A token with only the read scope gets a 403 when trying to create or update. Add the write scope and save the app (reinstall, if OAuth).
How do I avoid discovering the 403 only mid-load?
Make a test call to each endpoint the routine will use before running at volume. That way any missing scope shows up in seconds, not in the middle of a run over thousands of records.
Tired of hunting a missing scope in the middle of an integration? At Insight Sales we set up apps and tokens with least privilege and test each endpoint before running. Talk to our team and find out how we can help.
Ready to take your operation to the next level?
Talk to a specialist and see how we can help.