Use OAuth scopes
Use OAuth scopes to restrict Struct's answer based on the user's permissions. This example allows Struct to recommend publishing a document only when the user has the documents:publish scope and the document is approved.
Your backend supplies the user's permissions. Struct enforces the policy you write using those permissions.
Never send an OAuth access token, refresh token or ID token to Blatent—in headers, input, state or any other request field. Send the permission names your rules need, not credentials or the full token payload.
Authenticate calls to Struct with your Blatent API key. OAuth tokens should be shared only with their intended identity provider or resource API, never unrelated third parties. See OAuth bearer-token guidance.
1. Supply the user's scopes
Start with a user your application has already authenticated. Your backend reads their scopes from your authentication middleware and includes the relevant names in the request's state.
In this example, the list lives at state.oauth.scopes. These are ordinary state fields that your policy will read. The state file below contains sample permissions; replace them with the current user's verified scopes in your backend code.
Struct does not authenticate the user or fetch their permissions. That remains your application's responsibility.
2. Define the permitted answers
This example recommends publish or review for a draft:
- JSON Schema defines the fields and ensures that the reason matches the action.
- The OPA policy, written in Rego, allows
publishonly whenstate.document.approvedis true andstate.oauth.scopescontainsdocuments:publish. - Descriptions guide the model's choice among the permitted answers.
reviewremains an allowed alternative.
Save these files together. The state file contains sample values; in your application, take approval from your trusted data and scopes from your authentication middleware.
{
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": [
"publish",
"review"
],
"description": "Recommend publish only if the draft is ready, state.document.approved is true, and state.oauth.scopes contains documents:publish. Otherwise select review."
},
"reason": {
"type": "string",
"enum": [
"ready",
"needs_review"
],
"description": "Use ready for a publication recommendation; otherwise needs_review."
}
},
"required": [
"action",
"reason"
],
"additionalProperties": false,
"if": {
"properties": {
"action": {
"const": "publish"
}
},
"required": [
"action"
]
},
"then": {
"properties": {
"reason": {
"const": "ready"
}
}
},
"else": {
"properties": {
"reason": {
"const": "needs_review"
}
}
}
}
{
"document": {
"approved": true
},
"oauth": {
"scopes": [
"documents:publish"
]
}
}
package permissions
default allow := false
allow if { input.output.action == "review" }
allow if {
input.output.action == "publish"
input.state.document.approved == true
"documents:publish" in input.state.oauth.scopes
}
In Rego, input.state means the request's state; input.output means the proposed answer. Schema conditions relate output fields to each other, while OPA relates the output to your supplied state and permissions.
3. Send the request
The clients below load the saved schema, state and policy. They authenticate with the Blatent API key:
Before you run this example
Call https://api.blatent.ai from your server. Set your API key in the same terminal where you run the example:
export BLATENT_API_KEY='YOUR_SERVICE_KEY'
Python requires Python 3.10+ and HTTPX:
python -m pip install "httpx>=0.28,<1"
TypeScript requires Node.js 24+ and uses its built-in fetch. The cURL examples require curl and jq.
Save the displayed files and the selected client in one directory. Run the client there with python example.py, node example.ts or bash example.sh. Downloads use the example's name in place of example.
- Python
- TypeScript
- cURL
import json
import os
from pathlib import Path
import httpx
request = {
"input": "Review this draft: \"Maintenance is complete. All services are available.\" Recommend publish only when it is ready and the supplied state and permissions allow it.",
"policy": {
"engine": "opa",
"entrypoint": "data.permissions.allow",
"modules": {
"permission-context.rego": Path("permission-context.rego").read_text(encoding="utf-8"),
},
},
"policy_max_retries": 8,
"schema": json.loads(Path("permission-context.schema.json").read_text(encoding="utf-8")),
"state": json.loads(Path("permission-context.state.json").read_text(encoding="utf-8")),
}
with httpx.Client(
base_url="https://api.blatent.ai",
headers={"Authorization": f"Bearer {os.environ['BLATENT_API_KEY']}"},
timeout=195,
) as client:
response = client.post("/v1/resolve", json=request)
response.raise_for_status()
print(json.dumps(response.json()["output"]))
import { readFileSync } from "node:fs";
const request = {
"input": "Review this draft: \"Maintenance is complete. All services are available.\" Recommend publish only when it is ready and the supplied state and permissions allow it.",
"policy": {
"engine": "opa",
"entrypoint": "data.permissions.allow",
"modules": {
"permission-context.rego": readFileSync("permission-context.rego", "utf8"),
},
},
"policy_max_retries": 8,
"schema": JSON.parse(readFileSync("permission-context.schema.json", "utf8")),
"state": JSON.parse(readFileSync("permission-context.state.json", "utf8")),
};
const response = await fetch(
"https://api.blatent.ai" + "/v1/resolve",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.BLATENT_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(request),
signal: AbortSignal.timeout(195_000),
},
);
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(JSON.stringify((await response.json()).output));
set -eu
body=$(jq -n --argjson body '{"input":"Review this draft: \"Maintenance is complete. All services are available.\" Recommend publish only when it is ready and the supplied state and permissions allow it.","policy":{"engine":"opa","entrypoint":"data.permissions.allow","modules":{}},"policy_max_retries":8}' \
--slurpfile f0 'permission-context.schema.json' \
--slurpfile f1 'permission-context.state.json' \
--rawfile f2 'permission-context.rego' \
'$body | setpath(["schema"]; $f0[0]) | setpath(["state"]; $f1[0]) | setpath(["policy","modules","permission-context.rego"]; $f2)')
curl --fail-with-body --silent --show-error \
"https://api.blatent.ai/v1/resolve" \
-H "Authorization: Bearer $BLATENT_API_KEY" \
-H 'Content-Type: application/json' \
--data-binary "$body"
For your backend integration, replace the sample scopes before sending the request. If your authentication middleware provides a space-separated scope claim, add this line before client.post(...) in the Python example:
request["state"]["oauth"]["scopes"] = verified_claims.get("scope", "").split()
Here, verified_claims is the middleware's verified authentication result. If it already provides a list of scopes, assign that list directly.
Illustrative output: {"action":"publish","reason":"ready"}.
| Application context | Permitted answers |
|---|---|
Approved document and documents:publish scope | publish or review |
| Missing scope, missing approval, or unapproved document | review |
The policy limits what may be returned; it does not force the model to choose publish. If no compliant answer can be found within the retry budget, the API returns 422 POLICY_SEARCH_EXHAUSTED without output. See OPA search and failure behavior.
Build the state and mandatory policy in your backend. A user must not be able to grant themselves a scope or remove the policy. This request produces a recommendation; your application remains responsible for authorizing and performing any action.
Related: application state · API authentication.