Public API rate limits and best practices
Guidelines for building reliable Quotivity integrations, especially when creating, updating, or deleting price-book entries.
Use these guidelines to build reliable integrations with the Quotivity Public API, especially when creating, updating, or deleting price-book entries.
Request limits
Section titled “Request limits”Each API key has a target request rate of 10 requests per second with a burst capacity of 20 requests. Throttling is applied on a best-effort basis, so clients must control their own request rate rather than relying on the API to pace requests.
For price-book writes, we recommend:
- Keep sustained traffic at or below 10 requests per second per API key.
- Keep concurrent price-book writes in the low single digits. Start with 2 concurrent requests and increase to no more than 4 only after measuring your integration.
- Combine multiple product changes into one request instead of sending one request per product.
- Process one price book sequentially when practical. This makes retries and reconciliation easier.
- Do not retry a request that already returned a successful response.
Price-book write behavior
Section titled “Price-book write behavior”Specific-product updates
Section titled “Specific-product updates”PUT /v1/pricebooks/{priceBookId}/prices accepts an array of product price changes. For
specific-product updates, Quotivity:
- Persists the price-book entries.
- Returns the API response.
- Synchronizes the products’ price-book membership to HubSpot asynchronously.
A 200 OK response means the price data was persisted successfully. HubSpot’s hapily_price_books
property may take a short time to reflect the change.
Prefer sending product changes in batches. Batches of up to 500 entries provide predictable response behavior while substantially reducing request volume.
If a request resolves to more than 500 entries, Quotivity returns 204 No Content and processes the
entries and HubSpot membership asynchronously.
Deleting entries
Section titled “Deleting entries”DELETE /v1/pricebooks/{priceBookId}/prices accepts an array of product IDs. A 204 No Content
response means the price-book entries were deleted successfully. HubSpot membership cleanup happens
asynchronously and is eventually consistent.
All-product updates
Section titled “All-product updates”An all-product price update can require synchronous HubSpot CRM Search work. These requests may return a HubSpot rate limit response. Clients must follow the retry guidance below.
HTTP 429 responses
Section titled “HTTP 429 responses”Quotivity can return HTTP 429 Too Many Requests for two different reasons.
Quotivity request throttling
Section titled “Quotivity request throttling”The API key exceeded its request rate or burst allowance. This response may use a generic body:
{ "message": "Too Many Requests"}If a Retry-After header is present, wait for that duration. Otherwise, use exponential backoff with
jitter.
HubSpot account cooldown
Section titled “HubSpot account cooldown”A request that requires synchronous HubSpot work can return:
{ "message": "HubSpot API rate limit reached. Please wait a few seconds and try again.", "code": "HUBSPOT_RATE_LIMIT", "retryAfterSeconds": 10}When code is HUBSPOT_RATE_LIMIT:
- Stop issuing HubSpot-dependent requests for that Quotivity account.
- Wait at least
retryAfterSeconds. - Retry with the same or lower concurrency.
- Continue exponential backoff if another 429 is returned.
The cooldown is account-scoped. Sending more concurrent requests during the cooldown does not make recovery faster.
Recommended retry policy
Section titled “Recommended retry policy”Retry only transient failures:
- HTTP 429
- HTTP 500, 502, 503, or 504
- Network timeouts or connection failures where no response was received
Do not automatically retry validation, authentication, authorization, or not-found responses such as HTTP 400, 401, 403, or 404.
Recommended defaults:
- Maximum attempts: 5
- Initial delay when no server delay is provided: 1 second
- Backoff: exponential
- Jitter: full jitter
- Maximum delay: 60 seconds
- Honor
retryAfterSecondsandRetry-Afteras minimum delays
Example TypeScript retry helper:
type RateLimitBody = { code?: string retryAfterSeconds?: number}
const sleep = (milliseconds: number) => new Promise((resolve) => setTimeout(resolve, milliseconds))
async function requestWithRetry(url: string, init: RequestInit, maxAttempts = 5): Promise<Response> { for (let attempt = 0; attempt < maxAttempts; attempt++) { try { const response = await fetch(url, init)
if (response.ok) { return response }
const shouldRetry = response.status === 429 || [500, 502, 503, 504].includes(response.status) if (!shouldRetry || attempt === maxAttempts - 1) { return response }
const body = (await response.clone().json().catch(() => ({}))) as RateLimitBody const retryAfterHeader = Number(response.headers.get('retry-after')) const serverDelaySeconds = body.retryAfterSeconds ?? (Number.isFinite(retryAfterHeader) ? retryAfterHeader : undefined) const exponentialDelaySeconds = Math.min(60, 2 ** attempt) const minimumDelaySeconds = serverDelaySeconds ?? exponentialDelaySeconds const jitterMilliseconds = Math.random() * minimumDelaySeconds * 1000
await sleep(minimumDelaySeconds * 1000 + jitterMilliseconds) } catch (error) { if (attempt === maxAttempts - 1) { throw error }
const delayMilliseconds = Math.random() * Math.min(60_000, 1000 * 2 ** attempt) await sleep(delayMilliseconds) } }
throw new Error('Retry attempts exhausted')}Batching example
Section titled “Batching example”Send one request containing many product changes:
PUT /v1/pricebooks/{priceBookId}/pricesContent-Type: application/jsonX-API-Key: your-api-key[ { "productId": "1001", "adjustments": { "USD": { "type": "override", "amount": 125 } } }, { "productId": "1002", "adjustments": { "USD": { "type": "percent", "amount": -10 } } }]Avoid sending the same changes as many concurrent one-product requests. Small fan-out requests increase throttling risk, create unnecessary asynchronous work, and make recovery harder.
Handling ambiguous outcomes
Section titled “Handling ambiguous outcomes”If a request times out or the connection closes before a response is received, the write may still have succeeded. Before retrying a large request:
- Read the affected price-book data when practical.
- Compare it with the intended state.
- Retry only missing or incorrect entries.
The price endpoints set the requested state, but repeated requests still create redundant asynchronous membership work. Quotivity does not currently support an idempotency-key header for these endpoints.
Integration checklist
Section titled “Integration checklist”- Batch product changes instead of sending one request per product.
- Limit sustained traffic to 10 requests per second per API key.
- Use 2–4 concurrent workers at most for price-book writes.
- Honor both
retryAfterSecondsandRetry-After. - Add exponential backoff with jitter and a retry limit.
- Treat HTTP 200 and 204 as successful outcomes.
- Expect temporary eventual consistency in HubSpot membership.
- Reconcile ambiguous outcomes before retrying.
- Log the HTTP status, endpoint, attempt number, and delay, but never log API keys or sensitive payload data.
Ask the help center
Answers come from these guides, with links to the articles used.
Ask anything about configuring quoting in Quotivity Studio.