Skip to content

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.

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.

PUT /v1/pricebooks/{priceBookId}/prices accepts an array of product price changes. For specific-product updates, Quotivity:

  1. Persists the price-book entries.
  2. Returns the API response.
  3. 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.

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.

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.

Quotivity can return HTTP 429 Too Many Requests for two different reasons.

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.

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:

  1. Stop issuing HubSpot-dependent requests for that Quotivity account.
  2. Wait at least retryAfterSeconds.
  3. Retry with the same or lower concurrency.
  4. 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.

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 retryAfterSeconds and Retry-After as 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')
}

Send one request containing many product changes:

PUT /v1/pricebooks/{priceBookId}/prices
Content-Type: application/json
X-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.

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:

  1. Read the affected price-book data when practical.
  2. Compare it with the intended state.
  3. 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.

  • 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 retryAfterSeconds and Retry-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.