Skip to content

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.

  1. 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.dev

    ORDAINER_KEY (the name the console shows) works with this package too. If ORDAINER_API_KEY is 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.

  2. The package is called ordainer and is on PyPI:

    Terminal window
    pip install ordainer

    For the async client (AsyncClient), install the extra instead:

    Terminal window
    pip install 'ordainer[async]'
  3. 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 call
    if decision.allowed:
    print(call, decision.decision, decision.remaining, run_research())
    else:
    blocked = deny_response(decision) # what you send back to your caller
    print(call, blocked["status"], blocked["body"]["error"]["code"], decision.reason)
    print(blocked["body"]["error"]["message"])
    Terminal window
    python3 decide.py

    Calls 1 to 5 run your tool (allow, with remaining counting down). On call 6 the tool does not run: decision.allowed is False, decision.reason is quota_exceeded, and deny_response(decision) gives you a 429 plan_limit_reached answer (status, headers with Retry-After, and a JSON body) to send to your caller. If your clients treat 429 as “retry soon”, pass deny_status=402 to Client. Only 429 and 402 are accepted; anything else raises ConfigurationError. 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 a BlockedError (.status, .headers, .body, .decision) otherwise. client.guard(fn, tool=...) runs fn(decision) only when allowed.

  4. 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 pilot row, switch to shadow. Then click Reset sandbox and run the script again. Every call runs your tool (decision.allowed is True, and from call 6 decision.would_deny is True), and stderr shows:

    [ordainer] shadow: would deny tool=research.run reason=quota_exceeded remaining=0/5 request_id=req_gHgqUYUyo96jVrvu (call allowed)

    Leaving mode unset means “do what the plan in the console says”. To pin a mode in code instead, pass mode="shadow" or mode="enforce" to Client. 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, pass on_decision=lambda d: .... More: Shadow vs enforce.

import asyncio
from 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.

  • 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 a quota_exceeded deny for the same key and tool, that call is denied instead of running (Sticky deny). Pass fail_mode="closed" to block instead. An error answer (any 4xx, such as unknown_key, or a 429 rate_limited) is not an outage: it’s blocked in every fail_mode, except 403 subscription_inactive, which follows billing_lapse (default "open": allowed), and 429 safety_limit_reached, which is allowed with decision.safety_limited set. billing_lapse is 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.