> ## Documentation Index
> Fetch the complete documentation index at: https://developers.quotivity.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Public API Rate Limits and Best Practices

> Use these guidelines to build reliable integrations with the Quotivity Public API, especially when creating, updating, or deleting price-book entries.

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

### Specific-product updates

`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.

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

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

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

### Quotivity request throttling

The API key exceeded its request rate or burst allowance. This response may use a generic body:

```json theme={null}
{
  "message": "Too Many Requests"
}
```

If a `Retry-After` header is present, wait for that duration. Otherwise, use exponential backoff with jitter.

### HubSpot account cooldown

A request that requires synchronous HubSpot work can return:

```json theme={null}
{
  "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.

## 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 `retryAfterSeconds` and `Retry-After` as minimum delays

Example TypeScript retry helper:

```typescript theme={null}
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

Send one request containing many product changes:

```http theme={null}
PUT /v1/pricebooks/{priceBookId}/prices
Content-Type: application/json
X-API-Key: your-api-key
```

```json theme={null}
[
  {
    "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

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.

## 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 `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.
