Checking the tier and limits a route needs before you call it
Every capability declares its minimum tier and live rate in discovery, and your account declares its own. Two reads turn a runtime 402 into a startup check.
Finding out at runtime that a route needs a higher tier is avoidable. Infrai declares minimum_tier on every capability in GET /v1/discovery/{capability}, and GET /v1/account/tier declares what your account has — so the compatibility check is two reads you can run at boot instead of a 402 in production.
The same pair of reads also gives you the live price and your rate-limit multiplier, which are the other two things people discover the hard way.
What your account is
curl -sS "https://api.infrai.cc/v1/account/tier" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
{
"ok": true,
"data": {
"tier": "standard",
"account_id": "acct_...275b",
"since": "2026-07-01T11:53:08Z",
"rate_limit_multiplier": 1.0,
"features": ["pay_as_you_go", "auto_recharge"],
"pro_subscription": null
}
}
rate_limit_multiplier is the field worth noting: limits scale with tier rather than being fixed per plan, so a higher tier raises your ceiling instead of just unlocking features. features is the capability list at the plan level — pay_as_you_go and auto_recharge here.
What a route requires
curl -sS "https://api.infrai.cc/v1/discovery/sms.template.create" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
The response carries minimum_tier alongside method, path, regions, vendors_ready and a live billing block. sms.template.create declares pro, which is why it answers 402 PRO_REQUIRED on a standard account — declared up front, in machine-readable form, rather than discovered by a failing call.
Most routes declare standard. The ones that don’t are worth knowing about before you build a feature on them.
A preflight you run at boot
import os
import requests
API = "https://api.infrai.cc"
KEY = os.environ["INFRAI_API_KEY"]
TIER_ORDER = ["anonymous", "standard", "pro", "team", "enterprise"]
SESSION = requests.Session()
SESSION.headers.update({"Authorization": f"Bearer {KEY}"})
def account_tier() -> str:
resp = SESSION.get(f"{API}/v1/account/tier", timeout=20)
resp.raise_for_status()
return resp.json()["data"]["tier"]
def capability(cap_id: str) -> dict:
resp = SESSION.get(f"{API}/v1/discovery/{cap_id}", timeout=20)
resp.raise_for_status()
return resp.json()
def preflight(required: list[str]) -> dict:
"""Refuse to start rather than fail on the first customer request. A tier
mismatch found at boot is a config bug; found at runtime it's an incident."""
mine = account_tier()
my_rank = TIER_ORDER.index(mine) if mine in TIER_ORDER else 0
blocked, priced = [], {}
for cap_id in required:
cap = capability(cap_id)
needed = cap.get("minimum_tier", "standard")
rank = TIER_ORDER.index(needed) if needed in TIER_ORDER else 0
if rank > my_rank:
blocked.append({"capability": cap_id, "needs": needed, "have": mine})
billing = cap.get("billing") or {}
if billing.get("is_billable"):
priced[cap_id] = f"${billing.get('price_usd')} {billing.get('unit')}"
return {"tier": mine, "blocked": blocked, "prices": priced}
if __name__ == "__main__":
report = preflight(["email.send", "sms.send", "sms.template.create", "pdf.generate"])
if report["blocked"]:
for row in report["blocked"]:
print(f"BLOCKED {row['capability']}: needs {row['needs']}, account is {row['have']}")
for cap_id, price in report["prices"].items():
print(f"{cap_id:<24} {price}")
That function does double duty: it fails fast on a tier mismatch and it prints the live price of every billable route your service depends on. Both are things you want in a startup log.
What to do about a mismatch
| Situation | Action |
|---|---|
| A route needs a higher tier | POST /v1/account/tier/upgrade with target |
| You’re hitting limits, not features | upgrade raises rate_limit_multiplier too |
| Only one route needs it | check whether a lower-tier route does the job |
| A tenant needs it, you don’t | tier is account-level; it isn’t per key |
curl -sS -X POST "https://api.infrai.cc/v1/account/tier/upgrade" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
-H "Content-Type: application/json" \
-d '{"target": "pro", "return_url": "https://app.example.com/billing/done"}'
target is an enum of pro, team or enterprise. The response tells you whether a checkout step is needed (requires_checkout, checkout_url) or a sales conversation (requires_sales_contact), plus immediate_effect and the price_usd for the period — so your own admin UI can render the next step instead of sending someone to find it.
Check what you already have first:
curl -sS "https://api.infrai.cc/v1/account/subscription/get" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
The limitation to plan around
Tier is a property of the account, not of a key. You can’t put one tenant on pro and another on standard within the same account, and scoped keys don’t change that — scopes bound which capabilities a key can reach, not which tier applies. If your product resells per-plan access, that mapping stays in your own billing logic, and the platform’s tier is a floor you buy once.
Nor can you raise a rate limit independently of a tier. The multiplier moves with the plan.
What is genuinely easier here than in a multi-vendor stack: one tier answer covers every capability group. There’s no matrix of which plan you’re on at six providers, and the adjacent work is already included — POST /v1/account/webhooks/register will push tier.upgraded to your own service so your feature flags flip without polling, and the notification email goes through POST /v1/email/send on the same key. One account, one bill, one thing to check at boot.
Both account reads and the discovery read report billing_class: free in discovery, so preflighting costs nothing per call. The prices your preflight prints are the live ones for your account (verified 2026-09-21), and they move downward as vendor contracts improve — which is the reason to print them at boot rather than paste them into a config file.