Your first deny
One paste, six calls, and the sixth answers deny. That deny means your tool would not have run.
In a few minutes · Free test key · curl, PowerShell or Python: no install · Node 20+: npm install ordainer
-
Get a test key
Section titled “Get a test key”Sign up with your work email and open the link we send you. No credit card needed. The link works once, for 60 minutes; check spam if it hasn’t arrived. Stuck? Email support@ordainer.ai and we’ll help. On the page it opens, tick the box to agree to the Terms of Service and Data Processing Addendum and click Continue.
The Your sandbox is ready page shows two test keys, both for the customer “Example Co (sandbox)”. They work with any tool name; the examples use the toy tool
research.run:- pilot: 5 allows a month, across all tools. Use this one; you’ll hit its limit right away.
- prod: no plan allowance limit. Only your organization’s monthly test limit applies. Its answers show
limitandremainingasnull.
Keys are shown once, so copy the pilot key now. You don’t need your own product yet. Test keys start with
Get startedord_test_(live keys:ord_live_). An oldsk_test_…/sk_live_…key from before the rename gets401 old_key_format: create a new key in Console → Keys. -
Run one paste
Section titled “Run one paste”On Node 20+, use the Node SDK tab: it installs our SDK and calls
decide()six times. The bash tab is the welcome page’s call, with the key and the base URL in environment variables instead of filled in: run it 6 times, and the sixth answer is deny. The PowerShell tab runs the same call six times for you. Every tab, and every example below, reads the key fromORDAINER_KEYand the base URL fromORDAINER_BASE_URL, the same names the console uses.Terminal window export ORDAINER_KEY=ord_test_... # your pilot keyexport ORDAINER_BASE_URL=https://api.ordainer.devnpm install ordainernode -e 'const { Ordainer } = require("ordainer");const ordainer = new Ordainer(); // reads ORDAINER_KEY and ORDAINER_BASE_URL(async () => {for (let i = 1; i <= 6; i++) {const d = await ordainer.decide({ tool: "research.run" });console.log(i, d.decision, d.reason, d.remaining + "/" + d.limit, d.requestId);}})();'Terminal window export ORDAINER_KEY=ord_test_... # your pilot keyexport ORDAINER_BASE_URL=https://api.ordainer.devcurl -s -X POST $ORDAINER_BASE_URL/v1/decide \-H "Authorization: Bearer $ORDAINER_KEY" \-H "Content-Type: application/json" \-d '{"tool":"research.run"}'; echoTerminal window $env:ORDAINER_KEY = 'ord_test_...' # your pilot key$env:ORDAINER_BASE_URL = 'https://api.ordainer.dev'1..6 | ForEach-Object {Invoke-RestMethod -Method Post -Uri "$env:ORDAINER_BASE_URL/v1/decide" `-Headers @{ Authorization = "Bearer $env:ORDAINER_KEY" } `-ContentType 'application/json' -Body '{"tool":"research.run"}' |ConvertTo-Json -Compress}Works in PowerShell 7 and Windows PowerShell 5.1. A deny is a normal answer, so
Invoke-RestMethoddoesn’t throw on it.Calls 1 to 5 answer allow, with
remaininggoing 4, 3, 2, 1, 0. Call 6 answers:Response to call 6 {"decision": "deny","reason": "quota_exceeded","remaining": 0,"limit": 5,"resets_at": "2026-11-01T00:00:00.000Z","mode": "enforce","request_id": "req_CuJXkng3xBlh4g3h"}The Node SDK tab (like the welcome page’s Node 20+ version) prints one short line per call instead:
output 1 allow ok 4/5 req_nLdLl5aMrmmrMYcp2 allow ok 3/5 req_GIPD1vUyw3h4MfUB3 allow ok 2/5 req_U3vV27ZOCb6EIqd94 allow ok 1/5 req_pTiutAI2ThuRY-pU5 allow ok 0/5 req_58JB9SJoM050SEH66 deny quota_exceeded 0/5 req_mXeb0hk-kaepjzajRun it again
Section titled “Run it again”Click Reset sandbox (on the welcome page or the console Overview). It clears the sandbox customer’s test-key counters, audit rows and idempotency keys, so the pilot key allows 5 again. Customers you’ve added yourself keep their test rows, and live data is never touched.
-
Now from your code
Section titled “Now from your code”The same call in your language. On Node, use our SDK (
npm install ordainer, Node 20+). The other tabs are plain HTTP examples: no client library to install. We also have a Ruby gem (gem install ordainer) and a Python package (pip install ordainer). The Python tab below is plain HTTP with no install; every other language calls the HTTP API like this. Each one sends a newIdempotency-Keyper call, runs the tool onallow, stops on a deny, and printsreason,remainingandX-Request-Id.Run it in the shell from step 2’s Node SDK tab (
ordainerinstalled,ORDAINER_KEYandORDAINER_BASE_URLset). Came from the curl tab? Runnpm install ordainerfirst.decide.js // Node 20+. Run it 6 times: node decide.jsconst { Ordainer } = require('ordainer');// Reads ORDAINER_KEY and ORDAINER_BASE_URL. Timeout 300 ms per try (1 retry by default, 650 ms at most), fails open on timeouts and outages.const ordainer = new Ordainer();async function main() {// decide() never throws, and a deny is a normal result. Each call sends a new idempotency key.const d = await ordainer.decide({ tool: 'research.run' });const left = d.decision === null ? 'no decision' : d.limit === null ? 'no cap' : `${d.remaining}/${d.limit} left`;if (d.allowed) {const why = d.wouldDeny ? `would deny (shadow): ${d.reason}, run the tool anyway`: d.decision === 'allow' ? 'allow: run the tool': 'no answer (failed open): run the tool';console.log(`${why} (${left}, request_id ${d.requestId})`);return 0;}if (d.error) {console.error(`error: ${d.error.code}: ${d.error.message}`); // for example missing_key or unknown_keyreturn 1;}console.log(`deny: ${d.reason}, don't run the tool (${left}, resets ${d.resetsAt}, request_id ${d.requestId})`);return 2;}main().then((code) => { process.exitCode = code; });d.allowedalready accounts for shadow mode and fail-open, so it’s the one field to branch on. Putting it in front of an Express route instead? The Node / Express quickstart usesordainer.middleware().Terminal window curl -s -X POST $ORDAINER_BASE_URL/v1/decide \-H "Authorization: Bearer $ORDAINER_KEY" \-H "Content-Type: application/json" \-H "Idempotency-Key: research-run-$(date +%s)-$RANDOM" \-d '{"tool":"research.run"}'decide.py import jsonimport osimport sysimport urllib.errorimport urllib.requestimport uuid# Python 3.8+, standard library only. Plain HTTP, no SDK.# Run in the shell from step 2 (ORDAINER_KEY and ORDAINER_BASE_URL exported): python3 decide.pyBASE_URL = os.environ.get("ORDAINER_BASE_URL") or sys.exit("Export ORDAINER_BASE_URL first (step 2).")def read_json(res, status):# A proxy or load balancer can answer with HTML (for example a 502 page): don't crash on it.try:return json.load(res)except ValueError:return {"error": {"code": "bad_response", "message": f"HTTP {status} with a body that isn't JSON"}}def decide(tool: str, idempotency_key: str):req = urllib.request.Request(f"{BASE_URL}/v1/decide",data=json.dumps({"tool": tool}).encode(),method="POST",headers={"Authorization": f"Bearer {os.environ['ORDAINER_KEY']}","Content-Type": "application/json","Idempotency-Key": idempotency_key,},)try:# 2 s here; pick a timeout that fits your request path (the Node SDK uses 300 ms per attempt).with urllib.request.urlopen(req, timeout=2) as res:return res.status, res.headers, read_json(res, res.status)except urllib.error.HTTPError as err: # any non-2xx answer is an error, usually with a JSON bodyreturn err.code, err.headers, read_json(err, err.code)except (urllib.error.URLError, TimeoutError, OSError) as err: # no answer: timeout or network errorreturn None, {}, {"error": {"code": "no_answer", "message": str(err)}}def main() -> int:# A new key for each real call, so running this file 6 times reaches the deny.# Reuse a key only to retry the same call: the retry is answered once and counted once.status, headers, body = decide("research.run", f"research-run-{uuid.uuid4()}")if status != 200 or "error" in body: # "error" in a 200 means the body wasn't JSONe = body["error"] # branch on e["code"], never on the messageprint(f"error {status}: {e['code']}: {e['message']}", file=sys.stderr)return 1 # your choice: fail open (run the tool) or fail closed (don't)left = "no cap" if body["limit"] is None else f"{body['remaining']}/{body['limit']} left"request_id = headers.get("X-Request-Id")if body["decision"] == "allow":print(f"allow: run the tool ({left}, request_id {request_id})")return 0if body["mode"] == "shadow":print(f"would deny (shadow): {body['reason']}, run the tool anyway ({left}, request_id {request_id})")return 0print(f"deny: {body['reason']}, don't run the tool ({left}, resets {body['resets_at']}, request_id {request_id})")return 2if __name__ == "__main__":sys.exit(main())decide.ps1 # PowerShell 7 or Windows PowerShell 5.1. Plain HTTP, no SDK.# Run after step 2 ($env:ORDAINER_KEY and $env:ORDAINER_BASE_URL set): ./decide.ps1$params = @{Method = 'Post'Uri = "$env:ORDAINER_BASE_URL/v1/decide"# A new key for each real call; reuse it only to retry the same call.Headers = @{ Authorization = "Bearer $env:ORDAINER_KEY"; 'Idempotency-Key' = "research-run-$([guid]::NewGuid())" }ContentType = 'application/json'Body = '{"tool":"research.run"}'TimeoutSec = 5}try {$body = Invoke-RestMethod @params} catch {# Invoke-RestMethod throws on any 4xx or 5xx (for example 401 or 429).# The JSON body is in ErrorDetails.Message; the request id is in it too.$err = $_$e = $nullif ($err.ErrorDetails.Message) {try { $e = ($err.ErrorDetails.Message | ConvertFrom-Json -ErrorAction Stop).error } catch { $e = $null }}if ($e.code) {Write-Error "error: $($e.code): $($e.message) (request_id $($e.request_id))"} else {Write-Error "no answer: $($err.Exception.Message)" # timeout or network error}exit 1 # your choice: fail open (run the tool) or fail closed (don't)}if (-not $body.decision) { Write-Error "no answer: unexpected response"; exit 1 } # a 200 that isn't Ordainer JSON (proxy or portal page)$left = if ($null -eq $body.limit) { 'no cap' } else { "$($body.remaining)/$($body.limit) left" }if ($body.decision -eq 'allow') { "allow: run the tool ($left, request_id $($body.request_id))"; exit 0 }if ($body.mode -eq 'shadow') { "would deny (shadow): $($body.reason), run the tool anyway ($left)"; exit 0 }"deny: $($body.reason), don't run the tool ($left, resets $($body.resets_at), request_id $($body.request_id))"Every example applies one rule, the same in any language:
The one rule: run the tool ifdecisionisallowormodeisshadow.For Java, see the Java example (JDK HttpClient, no SDK). More languages (Node fetch, TypeScript, Go, C#, PHP, Rust, Kotlin, Swift) are coming to these docs.
Using Node and Express? Our SDK is on npm:
npm install ordainer. The Node / Express quickstart walks you through it. Using Ruby or Rails? Our gem is on RubyGems:gem install ordainer. The Ruby / Rails quickstart walks you through it. Using Python? Our package is on PyPI:pip install ordainer. The Python quickstart walks you through it. Or keep using plain HTTP as above.
Next: run in shadow mode for a week
Section titled “Next: run in shadow mode for a week”Before you block real calls, run your plan in shadow mode for about a week. Your code runs every call, and the ones your plan would have blocked are logged as would-denies, so you see what Ordainer would deny on real traffic without blocking anyone. To turn it on, open Console → Plans and click Shadow on the plan’s row (new plans you add start in shadow), and leave mode out of your requests so the plan’s setting applies. When the would-denies on the Overview look right, click Enforce, with no deploy. Roll out with shadow mode walks through each step.