FRAZNET DASHBOARD

Connecting HubSpot via a Service Key

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.

Both Service Keys and the older Private Apps use the same 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.

Before you start

1. Open the Service Keys page

  1. Click the settings gear in the top right of HubSpot.
  2. In the left sidebar, scroll down to Integrations → Service Keys.
  3. Click Create a Service Key.

2. Name the key

3. Pick scopes

Grant only what you need — you can come back later and grant more. The most common scopes for reporting:

CRM (recommended starter set)

ScopeWhat it lets the dashboard read
crm.objects.contacts.readContacts
crm.objects.companies.readCompanies
crm.objects.deals.readDeals (most reporting widgets use this)
crm.objects.line_items.readDeal line items / products
crm.schemas.deals.readDeal property metadata (lets you reference custom properties by name)
crm.schemas.contacts.readContact property metadata
crm.schemas.companies.readCompany property metadata

Optional — only if you use these features

ScopeWhat it lets the dashboard read
crm.objects.custom.read + crm.schemas.custom.readCustom objects you've defined in HubSpot
ticketsSupport tickets
e-commerceProducts / orders if you use the ecommerce bridge
automationWorkflows (read-only)
filesFiles API (uncommon for dashboards)
Grant read scopes only for dashboards. Never grant *.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.

4. Create the key and copy the token

  1. Click Create in the top right.
  2. HubSpot will show the key value once. Copy it now — you can't see it again. It looks like pat-na1-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX or similar.
Treat this token like a password. Anyone with it can read everything in scope. If it leaks, come back to the Service Keys page and click Rotate to invalidate the old one.

5. Paste the token into Fraznet

  1. In Fraznet, click the user chip (top right) → Admin → HubSpot.
  2. Under HubSpot API connection, paste the token into Paste new token.
  3. Click Save token. The current-token field will switch to a masked preview ("pat-…XXXX").

6. Sanity-check with a query

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.

Snippet recipes

Each snippet returns JSON in the shape the widget expects.

Stat — "Open pipeline value" (sum of deal amounts not closed)

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)

Pie / Donut — Deals by stage

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()]))

Result table — Recent deals

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))

Troubleshooting

Questions: jr.frisby@frazil.com