Python quickstart
You’ll ask Ordainer before one paid tool runs in a Python script and watch the sixth call get blocked, then try shadow mode (record what would be denied, block nothing). Switching between shadow and enforce is a console click, with no redeploy.
You need Python 3.9+. The package uses only Python’s standard library. The optional async client needs httpx, which the [async] extra installs. There is no Django, Flask or FastAPI middleware in 0.1.0: call client.decide() from your own handler.
-
Sign up and copy your test key
Section titled “Sign up and copy your test key”Follow Your first deny to sign up and produce your first deny with the pilot test key. Then put the key in your shell:
Terminal window export ORDAINER_API_KEY=ord_test_...export ORDAINER_BASE_URL=https://api.ordainer.devORDAINER_KEY(the name the console shows) works with this package too. IfORDAINER_API_KEYis also set, that one is used.If you lose a key, create a new test key in Console → Keys. The console never shows a full key again, only its first characters.
-
Install the package
Section titled “Install the package”The package is called
ordainerand is on PyPI:Terminal window pip install ordainerFor the async client (
AsyncClient), install the extra instead:Terminal window pip install 'ordainer[async]' -
Watch your tool get blocked
Section titled “Watch your tool get blocked”Click Reset sandbox in the console, then save and run this script:
decide.py from ordainer import Client, deny_response# Reads ORDAINER_API_KEY (or ORDAINER_KEY) and ORDAINER_BASE_URL. Timeout 0.3 s, fails open on timeouts and outages.client = Client()def run_research():return "expensive research done"for call in range(1, 7):decision = client.decide(tool="research.run") # a new idempotency key per callif decision.allowed:print(call, decision.decision, decision.remaining, run_research())else:blocked = deny_response(decision) # what you send back to your callerprint(call, blocked["status"], blocked["body"]["error"]["code"], decision.reason)print(blocked["body"]["error"]["message"])Terminal window python3 decide.pyCalls 1 to 5 run your tool (
allow, withremainingcounting down). On call 6 the tool does not run:decision.allowedisFalse,decision.reasonisquota_exceeded, anddeny_response(decision)gives you a429 plan_limit_reachedanswer (status,headerswithRetry-After, and a JSONbody) to send to your caller. If your clients treat 429 as “retry soon”, passdeny_status=402toClient. Only429and402are accepted; anything else raisesConfigurationError. All the blocked responses: Errors → SDK blocked responses.Prefer exceptions?
client.decide_or_raise(tool=...)returns the decision when the call may run and raises aBlockedError(.status,.headers,.body,.decision) otherwise.client.guard(fn, tool=...)runsfn(decision)only when allowed. -
Shadow mode for your real plan
Section titled “Shadow mode for your real plan”On a real product, start in shadow: we make the real decision and record it, but your tool runs even when we would say no. To see it on the sandbox: Console → Plans → on the
pilotrow, switch to shadow. Then click Reset sandbox and run the script again. Every call runs your tool (decision.allowedisTrue, and from call 6decision.would_denyisTrue), and stderr shows:[ordainer] shadow: would deny tool=research.run reason=quota_exceeded remaining=0/5 request_id=req_gHgqUYUyo96jVrvu (call allowed)Leaving
modeunset means “do what the plan in the console says”. To pin a mode in code instead, passmode="shadow"ormode="enforce"toClient. A pinned mode overrides the console. When the would-denies look right, click switch to enforce on that plan. To send decisions to your own metrics, passon_decision=lambda d: .... More: Shadow vs enforce.
import asynciofrom ordainer import AsyncClient
async def main(): async with AsyncClient() as client: decision = await client.decide(tool="research.run") print(decision.allowed, decision.decision)
asyncio.run(main())Same options, same rules, same messages as Client.
Next steps
Section titled “Next steps”- One agent key per customer integration or agent: pass
api_key=per call:client.decide(tool="research.run", api_key=customer_key). Counts are per key. - Retries: each call gets a fresh random idempotency key unless you pass
idempotency_key=built from your own data (your job ID or queue message ID), one key per real operation. A key replays the first answer for 24 hours.retries=n(0 to 5, default 1) retries a timeout, network error or 503 with the same key. More: Idempotency and retries. - Outages: if we’re slow (0.3 s per attempt; at most about 0.65 s with the default retry), unreachable, answer with a 5xx, or send an answer that isn’t a decision, the call is allowed and a warning is logged. The exception is a sticky deny: within 60 seconds (
sticky_deny) of aquota_exceededdeny for the same key and tool, that call is denied instead of running (Sticky deny). Passfail_mode="closed"to block instead. An error answer (any 4xx, such asunknown_key, or a429 rate_limited) is not an outage: it’s blocked in everyfail_mode, except403 subscription_inactive, which followsbilling_lapse(default"open": allowed), and429 safety_limit_reached, which is allowed withdecision.safety_limitedset.billing_lapseis the billing-lapse setting for when your own Ordainer subscription has lapsed:"open"runs calls unchecked,"closed"blocks them. See Fail open vs fail closed. - Options:
api_key,base_url,timeout(seconds, 0.3),fail_mode("open"),retries(1),retry_budget(seconds, 0.65),mode,billing_lapse("open"),deny_status(429),sticky_deny(seconds, 60),logger,on_decision. Full list: the package README. - Going live: Live keys are not available yet. Live keys open when paid plans launch. Your test keys cover everything in your beta.
- Every field and error: Decide API reference and Errors.