An AI outreach tool that enriches leads, drafts emails, and logs activity back to HubSpot usually hits its first 429 not because it is calling too fast, but because of how HubSpot’s limits are actually structured, which differs from what most rate-limit retry logic assumes. Here is what HubSpot’s own documentation says, and the five places that assumption breaks.
The daily limit is shared across every private app in the account, not per app
According to HubSpot’s own usage-details documentation, the burst limit, measured per 10 seconds, applies individually to each private app: 100 requests for Free and Starter accounts, 190 for Professional and Enterprise, or 250 with a purchased API Limit Increase. The daily limit does not follow the same per-app logic. It is a single shared pool across every private app installed in that account: 250,000 requests a day for Free and Starter, 625,000 for Professional, and 1,000,000 for Enterprise, resetting at midnight in the account’s own time zone. An outreach tool built and tested as the only private app in a sandbox account will never notice this. The same gap shows up with other APIs an AI sales stack depends on: a model API’s own quota and caching behavior can look fine in a single-app test and still behave differently once an account runs several integrations at once. The same tool deployed into a customer’s account that also runs three other private integrations shares its daily budget with all three, and can run out of quota from traffic its own logs show nothing about.
The Search API gives you no headers to back off with
HubSpot’s rate-limit headers, X-HubSpot-RateLimit-Max, X-HubSpot-RateLimit-Remaining, and the daily equivalents, are how a well-behaved client is supposed to see a limit coming before it gets a 429. HubSpot’s own documentation states plainly that CRM Search API responses do not include any of these headers. A lead-enrichment tool that queries contacts through the Search endpoint and reads those headers to decide when to slow down will read nothing, every time, and has no early warning at all for that specific endpoint. The fix is treating Search API calls as rate-limited with no visibility, which in practice means throttling them proactively on a fixed schedule rather than reactively from headers that will never appear.
Refreshing the access token on a 401, instead of tracking expires_in
This one is not about the rate limit at all, but it shows up in the same retry logic and causes the same kind of outage. HubSpot’s own usage guidelines say directly that a 401 Unauthorized response is not a signal to request a new access token, and that tokens should instead be refreshed based on the expires_in value returned when the token was issued. Code that treats every 401 as “token expired, refresh and retry” can end up refreshing tokens far more than necessary, or masking an actual authorization problem behind a refresh loop that keeps technically working.
Polling for updates instead of using webhooks
Every poll against the CRM to check for new or changed records spends quota that a webhook would not. HubSpot’s own developer usage guidelines call this out directly as a best practice: webhook calls made through workflow actions do not count toward the rate limit at all, while the equivalent polling loop does, request after request, all day. The same polling-versus-push tradeoff shows up in the Gmail API, where a history-based sync call spends quota on every poll regardless of whether anything actually changed. The catch worth knowing before counting on this is that workflow webhook actions require a HubSpot Marketing Enterprise subscription, so this fix is not available on every plan a customer might be on, and an outreach tool built for HubSpot broadly needs a polling fallback for accounts below that tier rather than assuming webhooks are always the answer.
Treating every 429 the same way, instead of reading policyName
{“status”: “error”, “message”: “You have reached your daily limit.”, “errorType”: “RATE_LIMIT”, “policyName”: “DAILY”}
A HubSpot 429 response body, abbreviated
The policyName field in that error body says which limit actually got hit: DAILY for the daily pool, or the name of whichever secondly policy applied otherwise. Backing off for ten seconds and retrying does nothing useful against a DAILY response, since the daily pool will still be empty ten seconds later. Code that backs off the same fixed interval regardless of policyName will spin uselessly against a daily limit until the account’s midnight reset, burning the retry budget on calls that were never going to succeed before then. Reading the specific field an API actually returns, rather than guessing from the status code alone, is the same lesson three different model providers’ tool-calling APIs teach about checking what a response body actually signals before writing retry logic against it.
The tier table, for sizing an integration before it ships
| Plan / app type | Burst limit (per 10 seconds) | Daily limit |
|---|---|---|
| Free / Starter (private app) | 100 per 10 seconds, per app | 250,000 per day, shared across all private apps |
| Professional (private app) | 190 per 10 seconds, per app | 625,000 per day, shared across all private apps |
| Enterprise (private app) | 190 per 10 seconds, per app | 1,000,000 per day, shared across all private apps |
| Any tier with API Limit Increase (private app) | 250 per 10 seconds, per app | +1,000,000 per day, up to two purchases |
| Public Marketplace OAuth app | 110 per 10 seconds, per installing account (excludes Search API) | Not raised by the API Limit Increase |
The 5% error-rate ceiling that applies if this ever needs certifying
HubSpot’s own best-practice guidance sets a specific bar for any app that wants Marketplace certification: keep error responses under 5% of total daily requests. An outreach tool that occasionally gets ahead of its own burst limit during a sync spike is not automatically disqualified by one bad afternoon, but a tool that routinely rides the edge of its rate limit, relying on retries to paper over a high raw error rate, is accumulating exactly the number certification review is built to catch. The same scrutiny applies to the outreach content itself: AI-drafted sales copy that reads as generic is a separate failure mode from a reliability problem, but both get caught by the same kind of review before anything ships.
Reading the headers that do exist, before assuming you need a fixed delay
resp = requests.get(url, headers=auth_headers)
if resp.status_code == 429:
body = resp.json()
if body.get('policyName') == 'DAILY':
# the daily pool is empty; nothing short of tomorrow fixes this
raise DailyQuotaExhausted(body)
# a secondly/burst policy was hit instead; a short backoff is appropriate
time.sleep(10)
return retry(url)
remaining = resp.headers.get('X-HubSpot-RateLimit-Remaining')
if remaining is None:
# Search API and a few other endpoints never send this header
time.sleep(SEARCH_API_FIXED_DELAY)
elif int(remaining) < 5:
time.sleep(2) # getting close to the window's cap; ease off proactively
This treats the Search API’s missing headers as the signal they actually are, rather than a null value to work around: when none of the expected headers come back, fall back to a conservative fixed delay instead of assuming full quota, and when they do come back, use the real remaining count rather than a guess. Checking policyName on any 429 that does happen, rather than retrying blind, is what keeps a daily-limit hit from turning into a retry loop that burns the rest of the day for no reason.
Where this fits with the rest of an AI sales stack
None of this is specific to outreach tools; any AI feature reading from or writing to HubSpot from outside a user’s own browser session runs into the same shared daily pool, the same headerless Search endpoint, and the same 401-versus-expires_in distinction. Building the retry logic once, against what HubSpot’s documentation actually specifies rather than against the generic rate-limit pattern most APIs use, is cheaper than discovering each gap separately in a different customer’s account.
Public Marketplace apps play by a different burst rule than private ones
Everything above about the 100/190/250 per-10-second burst limit applies to private apps, the kind built for a single customer’s own account. An outreach tool distributed publicly through the HubSpot Marketplace, installed by many different customers, gets a different number entirely: 110 requests per 10 seconds per installing account, and HubSpot’s documentation notes this explicitly excludes the CRM Search API and is not raised by purchasing an API Limit Increase, since that increase only applies to private apps. A team that built and tuned its retry logic against a private-app sandbox, then ships the same integration as a public Marketplace listing, is tuning against the wrong number from day one. Checking which of the two models an integration actually runs under, before sizing any retry or throttling logic against either table, avoids finding this out from a customer’s support ticket instead.
Caching the data that does not change every call
HubSpot’s own best-practice guidance also recommends caching data that gets reused across page loads or batch jobs, specifically naming object properties, owners, and form settings as examples. An outreach tool that re-fetches a workspace’s property definitions on every single contact it enriches is spending quota on data that is, in practice, static between deploys. Pulling that kind of reference data once per sync run, or on a much longer refresh interval, frees up burst and daily quota for the calls that actually have to happen per contact: the enrichment lookup, the email send, the activity log write. None of those three can be cached away, which is exactly why the ones that can should not be competing with them for the same budget. A renewal or win-back email generated from that cached data still needs the same scrutiny applied to any AI-drafted outreach message: fewer wasted API calls do not matter if the email they produce reads as templated rather than personalized. The same logic extends past HubSpot specifically: any CRM or marketing platform an AI sales tool integrates with publishes its own version of this split between what changes per record and what stays fixed across an entire sync run, and treating every field as equally perishable is what turns a reasonable integration into one that quietly outgrows its own quota as a customer’s contact list grows.



