Errors
Every error carries a stable code to branch on, since one HTTP status can mean several things
An error answer is JSON with a code and a message:
{ "code": "insufficient_scope", "message": "The token lacks the required scope." }
code is stable and safe to branch on. message is for people and can change. Some routes add fields: credits_required carries the balance, and plan_invalid, email_invalid and content_invalid list every broken rule in details. A few older routes also send runsRemaining; ignore it.
Over MCP, a failed tool call returns isError: true with the text code: message, using the same codes. A credits_required tool error also carries the fields below as structuredContent.
Branch on the code
402 means more than one thing. credits_required means you need credits. no_runs_remaining, plan_limit and agent_limit are plan limits that credits do not lift. paid_required means the workspace needs paid access. Always read code:
status=$(curl -s -o answer.json -w '%{http_code}' https://app.makesmores.com/api/v1/viral-score \
-H "Authorization: Bearer $SMORE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"network": "x", "post": "Shipping our first public API today"}')
if [ "$status" != 200 ]; then
case "$(jq -r .code answer.json)" in
credits_required) echo "Top up: $(jq -r .topup_url answer.json)" ;;
rate_limited) echo "Slow down and retry later" ;;
*) jq . answer.json ;;
esac
fi
import os, time, requests
def call(path, body):
r = requests.post(
f"https://app.makesmores.com/api/v1/{path}",
headers={"Authorization": f"Bearer {os.environ['SMORE_API_KEY']}"},
json=body,
)
if r.ok:
return r.json()
err = r.json()
if err.get("code") == "credits_required":
raise RuntimeError(f"Out of credits ({err['reason']}): top up at {err['topup_url']}")
if err.get("code") == "rate_limited":
time.sleep(int(r.headers.get("Retry-After", "5")))
return call(path, body)
raise RuntimeError(f"{err.get('code')}: {err.get('message')}")
402 credits_required
The call needs more credits than the workspace has, or than the key's monthly cap allows. Nothing ran and nothing was charged.
{
"code": "credits_required",
"reason": "balance",
"message": "Not enough credits for this call.",
"balance": 1200,
"needed": 3318,
"topup_url": "https://app.makesmores.com/app/console/billing"
}
| Field | Meaning |
|---|---|
reason | balance when the workspace is short, key_cap when this key reached its monthly cap |
balance | Credits available to this call: purchased and granted credits only for a scan, or what the key has left this month for key_cap |
needed | What the call deducts before it runs: its most possible cost. For a draft, every model's share added up; for a scan, max_credits |
topup_url | Where an owner buys credits |
For balance, an owner tops up on Billing. For key_cap, an owner or admin raises the key's cap on API keys, or you use another key. Scans need purchased credits, so a workspace with only free credits gets balance for a scan. See Credits.
Auth and permission errors
| Status | Code | Meaning |
|---|---|---|
| 401 | invalid_token | The key or token is missing, wrong, revoked or expired. Authenticate again or create a new key |
| 403 | insufficient_scope | The key or connection lacks the scope this route needs. Create a key with the scope ticked, or reconnect and approve it |
| 403 | admin_required | Only owners and admins can do this, such as changing trackers or the watched set |
| 403 | owner_required | Only the workspace owner can do this |
| 403 | pilot_disabled | The product is not open for this workspace yet |
| 402, 403 | paid_required | The action needs paid access. Purchased runs pass it, and so do purchased or granted credits for approved outreach and X topic watches |
Limits
| Status | Code | Meaning |
|---|---|---|
| 429 | rate_limited | Too many calls in the window. Wait for the seconds in Retry-After |
| 429 | drafts_queue_full | Too many drafts are open. Poll the open ones and submit again when one finishes |
| 402 | plan_limit, agent_limit, no_runs_remaining | Your plan's limit for agents or runs |
Shared limits stay 429 or 403, never 402. Rate limits lists them by product.
Request errors
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | The body or query is not valid: a missing or unknown field, a duplicate key, a wrong type or a null |
| 400 | draft_type_coming_soon, model_coming_soon | That draft type or model is listed as coming soon and does not run yet. Nothing was stored or charged |
| 404 | not_found, draft_not_found, tracker_not_found, scan_not_found | Nothing with that id in this workspace |
| 404 | console_off | The console, GET /api/v1/models and GET /api/v1/credits are switched off for now |
| 404 | scans_off | Scans are not open for this workspace |
| 409 | idempotency_conflict | The idempotency_key was used for a different request. Send a new key for new work |
| 409 | reddit_signals_off, drafts_off | The product is switched off for now |
| 410 | model_closed | The model was retired. Nothing was charged |
| 422 | plan_invalid, email_invalid, content_invalid | The request breaks the product's rules. details lists each {field, rule} |
| 503 | credits_unavailable | S'more could not check credits, so the call did not run. Try again shortly |
| 503 | reddit_signals_unavailable, scoring_unavailable, scoring_busy, scans_unavailable | Try again shortly |
Each product page lists the codes particular to it.
Retries
- Retry
429afterRetry-After, and503with backoff. - Do not retry
4xxcodes other than429without changing the request. - Drafts and scans take an
idempotency_key. Resending the same body with the same key returns the first job and charges nothing more, so retrying a submit after a timeout is safe.