HubSpot
Open pipeline value and deal count from HubSpot CRM search. The search POST is allowed.
What you can track
Open deal value (sum of properties.amount) and open deal count (total on the search response), each as one point on the fetch day. Deals created, won and lost need a date property and aggregate. Companies and contacts use the same search path with their object name.
Credential
A HubSpot access token with only read scopes. Minimum for the pipeline jobs: crm.objects.deals.read. Add crm.objects.contacts.read and crm.objects.companies.read only when you chart those. No write scopes.
Host: api.hubapi.com. Suggested name: HUBSPOT_TOKEN. Header: Authorization: Bearer {{secret:HUBSPOT_TOKEN}}.
Where to get it
Newer accounts: Development → Keys → Service keys → Create service key. If Private apps says “Legacy apps aren't available on this account”, the service key is the path that works. Add the read scopes, Update, Create, Show, Copy. Service keys.
Older accounts: Settings → Integrations → Private apps (or Development → Legacy apps), app name “tlly”, the same read scopes. Private apps.
tlly job
POST exactly https://api.hubapi.com/crm/v3/objects/{deals,contacts,companies}/search with a JSON body is allowed with a stored secret. It copies paging.next.after into the body field after and stops when HubSpot omits it. If a cursor remains after max pages (at most 10), the job errors instead of saving a partial sum. Always set request.pages on a sum of results. If HubSpot returns paging.next.after and the job has no request.pages, the run fails and does not save the first page. Omitting extract.date on an aggregate snapshots every matching row as one point on the fetch day (a sum or a count, not the last row the way a date-less CSV works). properties.amount is a string and is summed. Filter open deals with hs_is_closed EQ false (a stage list misses custom closed stages). Search allows up to 200 objects per page.
Value:
{"series":"open_pipeline","cron":"0 7 * * *","request":{"url":"https://api.hubapi.com/crm/v3/objects/deals/search","method":"POST","headers":{"Authorization":"Bearer {{secret:HUBSPOT_TOKEN}}"},"body":{"filterGroups":[{"filters":[{"propertyName":"hs_is_closed","operator":"EQ","value":"false"}]}],"properties":["amount"],"limit":100},"pages":{"cursor":"paging.next.after","into":"after","max":10}},"extract":{"format":"json","items":"results","value":"properties.amount","aggregate":"sum"}}
Count (uses total, so it does not depend on paging):
{"series":"open_deals","cron":"0 7 * * *","request":{"url":"https://api.hubapi.com/crm/v3/objects/deals/search","method":"POST","headers":{"Authorization":"Bearer {{secret:HUBSPOT_TOKEN}}"},"body":{"filterGroups":[{"filters":[{"propertyName":"hs_is_closed","operator":"EQ","value":"false"}]}],"limit":1}},"extract":{"format":"json","value":"total"}}
Dashboard: panel 0 “Open deal value” on open_pipeline, unit USD, red_below 3000, then set_panel_alert on that panel with trigger red. Panel 1 “Open deals” on open_deals. Amounts are in the portal’s currency units, not cents (confirmed as a string field, not as a minor-unit rule).
Gotchas
- 10 pages × 100 is 1000 deals. Raise limit toward 200 if the job reports a partial total. More than 2000 open deals will still not fit. For a pipeline the user already keeps in a sheet, use the Google Sheets KPI template instead of this search.
- Omitting pages on the value job is not a one-page shortcut. A body that still has paging.next.after fails the job instead of storing the first page's sum. Use the pages block in the value spec above.
- A panel alert fires once per crossing. Another crossing of the same panel within an hour is suppressed. At most 12 alert emails and 60 webhooks per account per hour, and those counts survive a restart.
- A search that writes records does not exist on these three paths; do not point the token at any other POST.
Alert ideas
Red when open pipeline value drops below the cover target (the example uses 3000). Yellow when open deal count falls week over week.
Verification
Checked against the search docs and the server: POST /crm/v3/objects/{object}/search is on the secret-POST allowlist, and the job follows paging.next.after into after. Also checked: filterGroups, properties, limit, total, results[].properties, string amount, 200 per page. Service-key clicks (Development → Keys → Service keys, Show, Copy) follow the service-key docs. This playbook did not call live HubSpot.