Rate Limits
Rate limiting applies to Personal Access Token requests only. Cookie sessions are exempt.
- Burst: 10 requests per second
- Sustained: 600 requests per minute
Rate limit buckets are per-token — different tokens have independent limits.
Response Headers
Rate limit information is included in response headers:
X-RateLimit-Limit: 10— burst capacityX-RateLimit-Remaining: 5— tokens remaining in bucketRetry-After: 30— seconds to wait (only on 429 responses)
Handling 429 Responses
When rate limited, the API returns a 429 status. Use exponential backoff:
# Check the Retry-After header and wait that many seconds
curl -i https://yourshop.zeroshop.io/admin/api/products \
-H "Authorization: Bearer zspat_..."
# If 429: Retry-After: 30 → wait 30 seconds
import requests
import time
def api_get(url, headers, max_retries=3):
for attempt in range(max_retries):
resp = requests.get(url, headers=headers)
if resp.status_code != 429:
return resp
wait = int(resp.headers.get("Retry-After", 2 ** attempt))
time.sleep(wait)
return resp
async function apiGet(url, headers, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const resp = await fetch(url, { headers });
if (resp.status !== 429) return resp;
const wait = parseInt(resp.headers.get("Retry-After") || 2 ** attempt);
await new Promise(r => setTimeout(r, wait * 1000));
}
}
def api_get(url, headers, max_retries: 3)
max_retries.times do |attempt|
uri = URI(url)
req = Net::HTTP::Get.new(uri)
headers.each { |k, v| req[k] = v }
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
return resp unless resp.code == "429"
wait = (resp["Retry-After"] || 2**attempt).to_i
sleep(wait)
end
end
Idempotency
For write operations (POST, PUT, PATCH) using PAT authentication, you can send an
Idempotency-Key header to safely retry requests:
curl -X POST https://yourshop.zeroshop.io/admin/api/products \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: import-row-42-2026-04-13" \
-d '{"name": "Widget", "price": 999}'
resp = requests.post(
"https://yourshop.zeroshop.io/admin/api/products",
headers={
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json",
"Idempotency-Key": "import-row-42-2026-04-13"
},
json={"name": "Widget", "price": 999}
)
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/products", {
method: "POST",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json",
"Idempotency-Key": "import-row-42-2026-04-13"
},
body: JSON.stringify({ name: "Widget", price: 999 })
});
uri = URI("https://yourshop.zeroshop.io/admin/api/products")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer zspat_..."
req["Content-Type"] = "application/json"
req["Idempotency-Key"] = "import-row-42-2026-04-13"
req.body = { name: "Widget", price: 999 }.to_json
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
Key details:
- Same key + same body → returns cached 2xx response
- Same key + different body → returns 409 Conflict
- Only successful (2xx) responses are cached for 24 hours
- GET requests ignore the header
- Scoped per token