CookbookHandle 402 quota_exhausted

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

limitCauseWhat your app should do
sessionThe rolling 5-hour window is used up.Pause; show when it resumes (resets_at).
weeklyThe weekly window is used up.Pause until resets_at, or suggest credits.
monthlyThe monthly allowance is used up.Point an admin to upgrade or credits.
spend_limitThe organization’s (or member’s) credit spend limit was reached.Ask an admin to raise the limit.
no_creditsAllowance used and the credit wallet is empty.Ask an admin to add credits.
unavailableUsage 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
        raise

upgrade_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.
Was this page helpful?