Connect your systems to the policy router
Your application sends its AI requests to https://governai.space/api/v1/route instead of calling a model provider directly. Nothing is installed on your side: you need an account, a key, a policy, and one HTTPS call from your server. Every decision comes back with a reference to a tamper-evident audit record you can export later.
1. Getting set up
We create your organisation
There is no self-serve sign-up. We create your organisation, set its regulator and monthly budget, and send your first administrator a sign-in.
Your administrator
Signs in and turns on two-factor at Security. Writes the policy at Policies, issues a key at Keys, and invites reviewers at Team.
Your engineer
Puts the key in your server’s secret store, replaces the provider call with the request below, and handles the five decisions and the failure codes.
A key is shown once, when it is issued. It belongs to one organisation, carries its own per-minute rate limit, and holds one or more scopes: route — call the policy router; read — see recent decisions for your own organisation; evidence — export the complete audit record for a period, with hashes. Give each system its own key with only the scopes it needs.
2. The first call
curl -s https://governai.space/api/v1/route \
-H "authorization: Bearer $GH_KEY" \
-H 'content-type: application/json' \
-d '{
"policy": "your-policy-slug",
"model": "hf-eleutherai-pythia-160m",
"use": "summarisation",
"prompt": "Summarise this complaint for the case file: ..."
}'const res = await fetch('https://governai.space/api/v1/route', {
method: 'POST',
headers: {
authorization: `Bearer ${process.env.GH_KEY}`,
'content-type': 'application/json',
},
body: JSON.stringify({
policy: 'your-policy-slug',
model: 'hf-eleutherai-pythia-160m',
use: 'summarisation',
prompt,
}),
signal: AbortSignal.timeout(30_000),
});
const result = await res.json();import os, requests
res = requests.post(
"https://governai.space/api/v1/route",
headers={"authorization": f"Bearer {os.environ['GH_KEY']}"},
json={
"policy": "your-policy-slug",
"model": "hf-eleutherai-pythia-160m",
"use": "summarisation",
"prompt": prompt,
},
timeout=30,
)
result = res.json()To see a real response before you have an account, use the public demo key and the example on the router page. It is shared by everyone and capped at 20 requests a minute, so it is for looking at the shape of a response, not for testing load.
3. Handling the decision
200 response always carries decision, reason_codes and an audit block. Branch on decision, never on whether completion is present.| decision | What happened | What your code should do |
|---|---|---|
| allow | The prompt passed policy unchanged and was sent to the model you asked for. | Use completion. |
| allow_with_redaction | Something matching a redaction rule was stripped before the model saw it. redactions.inbound lists what kind and how many — never the values. | Use completion. Expect it to refer to [REDACTED:…] placeholders where the stripped text was. |
| reroute | The model you asked for is not eligible under the policy, so the policy’s fallback model answered instead. | Use completion. served.model names the model that actually answered; record it, not the one you requested. |
| require_human_review | A review trigger matched. The model answered, but the output is withheld and queued for a person. | completion is null. Keep review.ref and show “held for review”. Nothing is pushed back to your system later: a reviewer reads the output in the console and releases or rejects it there, within 72 hours, after which it is discarded. Design the workflow so the reviewer acts on a released output. |
| block | The use is prohibited, the model is unknown, or it is ineligible with no fallback. No model was called. | Do not retry the same request. reason_codes says why; show the user a refusal. |
{
"decision": "allow_with_redaction",
"reason_codes": ["..."],
"policy": { "slug": "your-policy-slug", "min_gri": 20 },
"requested": { "model": "hf-eleutherai-pythia-160m", "gri": 24 },
"served": { "model": "hf-eleutherai-pythia-160m", "gri": 24 },
"redactions": { "inbound": [{ "key": "pan", "count": 1 }], "outbound": [] },
"completion": "...",
"review": null,
"usage": { "prompt_tokens": 41, "completion_tokens": 120, "cost_usd": 0.0004, "mode": "live" },
"spend": { "month_to_date_usd": 12.31, "budget_usd": 250 },
"request_id": "...",
"audit": { "seq": 1842, "entry_hash": "...", "prev_hash": "...", "prompt_sha256": "..." }
}Store audit.seq, audit.entry_hash and request_id next to your own record of the request. They are how you, or an examiner, find the matching entry in the evidence export later.
4. When the call does not succeed
| Status | reason_codes | Meaning |
|---|---|---|
| 400 | request/invalid-json · request/missing-field | Your request is malformed. Fix it; retrying will not help. |
| 401 | auth/invalid-or-missing-key | Missing, unknown, revoked or expired key — the message is the same for all four on purpose. Check which key is deployed. |
| 403 | auth/scope-missing-route · auth/cross-tenant-policy | The key lacks the route scope, or the policy belongs to another organisation. |
| 404 | registry/policy-not-found | No policy with that slug. Policy slugs are listed in your console. |
| 413 | request/prompt-too-large | The prompt is over 100,000 characters. |
| 429 | quota/rate-limit-exceeded | Over the key’s per-minute limit. Wait retry-after seconds. x-ratelimit-limit and x-ratelimit-remaining come back on every response. |
| 402 | billing/monthly-budget-exhausted | Your organisation’s monthly budget is spent. An admin raises it in the console. |
| — | timeout · 5xx · connection refused | The service did not answer. Nothing about this request was recorded here. See “If we are unreachable” below. |
5. Keys stay on your server
- Never put a key in a web page, a mobile app, or a code repository. The API sends no CORS headers on purpose, so a browser cannot call it directly. Call it from your backend.
- Keep the key in your secret manager and read it from the environment at runtime.
- Rotate a key from Keys. The old key keeps working for 24 hours so you can deploy the new one without downtime. Revoke a key the moment it may have leaked; that takes effect immediately.
- If your network restricts outbound traffic, allow HTTPS to
governai.space.
6. Collecting the evidence
evidence scope pulls the complete audit record for a period. Give that job its own key; do not add the scope to the key your application uses.curl -s "https://governai.space/api/v1/evidence?from=2026-08-01&to=2026-08-31&format=csv" \ -H "authorization: Bearer $GH_EVIDENCE_KEY" -o evidence-2026-08.csv # Is the chain intact? No key needed for the integrity result. curl -s https://governai.space/api/v1/audit/verify
Dates are YYYY-MM-DD; without them you get the last 30 days. format is json (the default) or csv. Every record carries the exact string its hash covers, so anyone can recompute SHA-256 of it and compare it with entry_hash without using any of our code. The export also records the chain head at the moment it was taken.
7. What passes through, and what we keep
Kept
The decision, reason codes, the kinds and counts of anything redacted, the models requested and served, token counts, cost, timing, and a salted hash of the prompt. Held outputs are stored encrypted for up to 72 hours while they wait for review.
Not kept
The prompt and the model’s reply. They pass through to be checked and answered, and are not written to the log. When the router runs live, the redacted prompt is sent to the model provider to be answered.
For reviewers: the console on your phone
iPhone and iPad
Open governai.space/console in Safari, tap Share, then Add to Home Screen. The installed app keeps its own sign-in, separate from Safari, so you sign in once more inside it. Sign in with your password and two-factor code; Google sign-in can finish in Safari instead of the app.
Android
Open governai.space/console in Chrome, open the menu, then Install app (on some phones, Add to Home screen).