Errors & Rate Limiting
Error codes, rate limiting rules, and best practices for handling API errors.
Error format
Every error response follows the same structure:
{
"error": {
"code": "not_found",
"message": "Product not found"
}
}| Field | Type | Description |
|---|---|---|
code | string | Machine-readable error code |
message | string | Human-readable description |
Error codes
| Code | HTTP Status | Description |
|---|---|---|
unauthorized | 401 | Missing, invalid, or revoked API key |
forbidden | 403 | Endpoint requires a secret key but a publishable key was used |
bad_request | 400 | Invalid request body or parameters |
not_found | 404 | Resource does not exist |
rate_limited | 429 | Too many requests (see below) |
internal_error | 500 | Server error (contact support if persistent) |
HTTP status codes
| Status | Meaning |
|---|---|
| 200 | Success |
| 400 | Bad request (check the error message for details) |
| 401 | Unauthorised (check your API key) |
| 403 | Forbidden (use a secret key for this endpoint) |
| 404 | Not found |
| 429 | Rate limited (wait and retry) |
| 500 | Server error |
Rate limiting
Each API key has its own rate limit bucket. Limits are per key, not per IP address.
| Key type | Limit |
|---|---|
Publishable (q7_pk_) | 100 requests per minute |
Secret (q7_sk_) | 30 requests per minute |
Response headers
Every response includes a rate limit header:
X-RateLimit-Remaining: 87This tells you how many requests you have left in the current window.
When you hit the limit
The API returns a 429 status with a Retry-After header:
{
"error": {
"code": "rate_limited",
"message": "Too many requests"
}
}Retry-After: 12Wait the number of seconds in Retry-After before sending another request.
Avoiding rate limits
Cache responses. Product data does not change every second. Cache API responses for 60 seconds or longer on your server or at the edge.
Use static generation. If you build with Astro, Next.js, or Nuxt, fetch data at build time instead of on every page view. One API call serves thousands of visitors.
Batch requests. Fetch a full page of products (up to 100 per page) instead of loading them one at a time.
Use ISR. With Next.js or similar frameworks, use Incremental Static Regeneration to rebuild pages in the background. Each rebuild makes one API call regardless of traffic.
Pagination
List endpoints return paginated results with a meta object:
{
"data": [ ... ],
"meta": {
"page": 1,
"per_page": 25,
"total": 84
}
}| Parameter | Default | Max | Description |
|---|---|---|---|
page | 1 | No limit | Page number (1-indexed) |
per_page | 25 | 100 | Items per page |
To fetch all items, increment page until page * per_page >= total.
async function fetchAllProducts(apiKey: string) {
const products = []
let page = 1
let total = Infinity
while ((page - 1) * 100 < total) {
const res = await fetch(
`https://api.quantm7.com/v1/products?page=${page}&per_page=100`,
{ headers: { 'Authorization': `Bearer ${apiKey}` } }
)
const json = await res.json()
products.push(...json.data)
total = json.meta.total
page++
}
return products
}Best practices
Handle errors gracefully
Always check the response status before reading the body.
const res = await fetch('https://api.quantm7.com/v1/products/classic-tee', {
headers: { 'Authorization': `Bearer ${apiKey}` }
})
if (!res.ok) {
const { error } = await res.json()
if (res.status === 404) {
// Product does not exist, show a 404 page
return notFound()
}
if (res.status === 429) {
// Rate limited, wait and retry
const retryAfter = parseInt(res.headers.get('Retry-After') || '5')
await new Promise(r => setTimeout(r, retryAfter * 1000))
// Retry the request...
}
throw new Error(`API error: ${error.message}`)
}
const { data: product } = await res.json()Use the right key for the job
| Task | Key to use |
|---|---|
| Fetch products on a page | Publishable |
| Load collections at build time | Publishable |
| Render blog posts | Publishable |
| Create a cart from a server function | Secret |
| Process checkout from your backend | Secret |
Keep secret keys on the server
Never import or reference your secret key in client-side code. Use API routes or server functions to proxy cart and checkout operations.
// GOOD: Secret key stays on the server
// src/pages/api/create-cart.ts (Astro server endpoint)
export async function POST() {
const res = await fetch('https://api.quantm7.com/v1/cart', {
method: 'POST',
headers: {
'Authorization': `Bearer ${import.meta.env.Q7_SECRET_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ currency: 'GBP' })
})
return new Response(JSON.stringify(await res.json()))
}// BAD: Secret key exposed to the browser
const res = await fetch('https://api.quantm7.com/v1/cart', {
method: 'POST',
headers: {
// This key is visible in the browser's network tab
'Authorization': 'Bearer q7_sk_NEVER_DO_THIS'
}
})