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

SituationAction
A route needs a higher tierPOST /v1/account/tier/upgrade with target
You’re hitting limits, not featuresupgrade raises rate_limit_multiplier too
Only one route needs itcheck whether a lower-tier route does the job
A tenant needs it, you don’ttier 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.

References

Browse more account developer guides