Handle 402 quota_exhausted
A 402 with error.code quota_exhausted (error.type insufficient_quota) means the request could not be paid for: the plan’s allowance for a window is used up and usage credits cannot cover it. Retrying immediately fails again. The body tells you which limit was hit and when it clears, so your application can say something useful.
{
"error": {
"type": "insufficient_quota",
"code": "quota_exhausted",
"message": "Your monthly plan token allowance is used up. ...",
"param": null,
"request_id": "req_6f1c2a9e0b7d4c3a8e5f1b2d",
"limit": "monthly",
"resets_at": "2026-11-01T00:00:00.000Z",
"upgrade_url": "https://syntic.ai/pricing",
"add_credit_url": "https://syntic.ai/agents/billing#api-topup"
}
}What each limit means
limit | Cause | What your app should do |
|---|---|---|
session | The rolling 5-hour window is used up. | Pause; show when it resumes (resets_at). |
weekly | The weekly window is used up. | Pause until resets_at, or suggest credits. |
monthly | The monthly allowance is used up. | Point an admin to upgrade or credits. |
spend_limit | The organization’s (or member’s) credit spend limit was reached. | Ask an admin to raise the limit. |
no_credits | Allowance used and the credit wallet is empty. | Ask an admin to add credits. |
unavailable | Usage could not be checked just now. | This one is temporary: retry after a few seconds. |
import os
from datetime import datetime, timezone
import requests
class QuotaPaused(Exception):
def __init__(self, limit, resets_at, message):
super().__init__(message)
self.limit, self.resets_at = limit, resets_at
def call(body: dict) -> dict:
r = requests.post(
"https://app.syntic.ai/v1/chat/completions",
headers={"Authorization": f"Bearer {os.environ['SYNTIC_API_KEY']}"},
json={**body, "stream": False},
timeout=(10, 300),
)
if r.status_code == 402:
e = r.json()["error"]
if e.get("limit") == "unavailable":
raise TimeoutError("usage check unavailable; retry shortly")
resets = datetime.fromisoformat(e["resets_at"].replace("Z", "+00:00")) if e.get("resets_at") else None
raise QuotaPaused(e.get("limit"), resets, user_message(e.get("limit"), resets))
r.raise_for_status()
return r.json()
def user_message(limit: str, resets: datetime | None) -> str:
when = f" It will be available again in about {wait_text(resets)}." if resets else ""
return {
"session": "The assistant has reached its usage limit for now." + when,
"weekly": "The assistant has reached its weekly usage limit." + when,
"monthly": "The assistant has reached this month's usage limit. An administrator can add credits.",
"spend_limit": "The assistant has reached its spending limit. An administrator can raise it.",
"no_credits": "The assistant is out of credits. An administrator can add more.",
}.get(limit, "The assistant is temporarily unavailable.")
def wait_text(resets: datetime) -> str:
minutes = max(1, int((resets - datetime.now(timezone.utc)).total_seconds() // 60))
return f"{minutes} minutes" if minutes < 120 else f"{minutes // 60} hours"Pause the whole app, not just one request
When one request gets a 402 with session, weekly or monthly, every request will until resets_at. Store the pause time in a shared place (memory, Redis, a database row) and check it before calling the API. This saves pointless requests and lets your UI show a clear banner instead of failing per message.
paused_until = None # datetime, shared across workers in a real app
def guarded_call(body):
global paused_until
if paused_until and datetime.now(timezone.utc) < paused_until:
raise QuotaPaused("paused", paused_until, "The assistant is paused until the usage window resets.")
try:
return call(body)
except QuotaPaused as p:
if p.resets_at:
paused_until = p.resets_at
raiseupgrade_url and add_credit_url are meant for the account’s administrators. Show them in an admin view, not to anonymous end users of your product.
Prevent it
- Turn on usage credits and auto-reload so overflow beyond the plan is paid from the wallet; see Billing & credits.
- Watch usage in the Console and set spend limits deliberately; see Usage & cost.
- Track cost per request in your own logs; see Count cost from usage.