This is HubSpot's currently recommended way for single-account API access — simpler than Private Apps and
with full platform support going forward. You create one Service Key in HubSpot, copy its token, and paste it
into the dashboard's admin panel. From then on, any Python widget can call api.hubapi.com with
that token.
Bearer <token> header, so any
widget written for one works unchanged with the other. If you already have a Private App, you can keep it
and skip this guide — but new connections should use Service Keys.
Fraznet Dashboard.Grant only what you need — you can come back later and grant more. The most common scopes for reporting:
| Scope | What it lets the dashboard read |
|---|---|
| crm.objects.contacts.read | Contacts |
| crm.objects.companies.read | Companies |
| crm.objects.deals.read | Deals (most reporting widgets use this) |
| crm.objects.line_items.read | Deal line items / products |
| crm.schemas.deals.read | Deal property metadata (lets you reference custom properties by name) |
| crm.schemas.contacts.read | Contact property metadata |
| crm.schemas.companies.read | Company property metadata |
| Scope | What it lets the dashboard read |
|---|---|
| crm.objects.custom.read + crm.schemas.custom.read | Custom objects you've defined in HubSpot |
| tickets | Support tickets |
| e-commerce | Products / orders if you use the ecommerce bridge |
| automation | Workflows (read-only) |
| files | Files API (uncommon for dashboards) |
*.write — Fraznet doesn't need it.
If a widget ever appears to need a write scope, that's a sign the widget is doing something it shouldn't.
pat-na1-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX or similar.Open the HubSpot query library card → + Add query and paste this as the snippet:
import os, json, httpx
token = os.environ['HUBSPOT_TOKEN']
r = httpx.post(
'https://api.hubapi.com/crm/v3/objects/deals/search',
headers={'Authorization': f'Bearer {token}', 'Content-Type': 'application/json'},
json={'filterGroups': [], 'limit': 1},
)
data = r.json()
print(data['total'])
Name it Total deals, save it, then create a Stat widget. Switch the language to Python,
pick Total deals from the HubSpot library dropdown that now appears, and save. The widget should show
your total deal count within a few seconds.
Each snippet returns JSON in the shape the widget expects.
import os, json, httpx
token = os.environ['HUBSPOT_TOKEN']
total = 0
after = None
while True:
body = {
'filterGroups': [{'filters': [
{'propertyName': 'dealstage', 'operator': 'NEQ', 'value': 'closedwon'},
{'propertyName': 'dealstage', 'operator': 'NEQ', 'value': 'closedlost'},
]}],
'properties': ['amount'], 'limit': 100,
}
if after: body['after'] = after
r = httpx.post('https://api.hubapi.com/crm/v3/objects/deals/search',
headers={'Authorization': f'Bearer {token}','Content-Type':'application/json'}, json=body)
j = r.json()
for row in j.get('results', []):
try: total += float(row['properties'].get('amount') or 0)
except: pass
page = j.get('paging', {}).get('next')
if not page: break
after = page['after']
print(total)
import os, json, httpx
from collections import Counter
token = os.environ['HUBSPOT_TOKEN']
counts = Counter()
after = None
while True:
body = {'filterGroups': [], 'properties': ['dealstage'], 'limit': 100}
if after: body['after'] = after
r = httpx.post('https://api.hubapi.com/crm/v3/objects/deals/search',
headers={'Authorization': f'Bearer {token}','Content-Type':'application/json'}, json=body)
j = r.json()
for row in j.get('results', []):
counts[row['properties'].get('dealstage', 'unknown')] += 1
page = j.get('paging', {}).get('next')
if not page: break
after = page['after']
print(json.dumps([{'label': k, 'value': v} for k, v in counts.most_common()]))
import os, json, httpx
token = os.environ['HUBSPOT_TOKEN']
r = httpx.post('https://api.hubapi.com/crm/v3/objects/deals/search',
headers={'Authorization': f'Bearer {token}','Content-Type':'application/json'},
json={'sorts': [{'propertyName': 'createdate', 'direction': 'DESCENDING'}],
'properties': ['dealname', 'amount', 'dealstage', 'closedate'],
'limit': 25})
out = []
for row in r.json().get('results', []):
p = row['properties']
out.append({
'Deal': p.get('dealname',''),
'Amount': p.get('amount',''),
'Stage': p.get('dealstage',''),
'Close date': p.get('closedate','')[:10],
})
print(json.dumps(out))
print() the result? Did httpx.post(...) error? Wrap the call in a try/except and print the exception once to see.Questions: jr.frisby@frazil.com