API Reference
OBEL GaaS
Overview
The GaaS API is a REST interface over HTTPS. All endpoints are under https://api.useobel.ai/api/v1/. Request and response bodies are JSON. Every request must set Content-Type: application/json where a body is sent.
There is no separate API version path segment yet - the /v1/ prefix is reserved for a future versioning scheme. Backwards-incompatible changes will be announced before they ship.
Authentication
Authenticate every request with an org API key as a bearer token:
Authorization: Bearer obel_sk_...
Keys are scoped. A key created for the GaaS console carries the gaas scope; a key created for the Developer-tier CLI proxy carries the proxy scope. Each endpoint below requires gaas - a valid but wrongly-scoped key returns 403, not 401. Keys are issued per partnership; see Talk to us.
Errors
Errors return a JSON body with an error string and, for validation failures, field-level detail. HTTP status communicates the error class:
| Field | Type | Description |
|---|
400 | Bad Request | Malformed JSON or a field failed validation. The response body names the offending field. |
401 | Unauthorized | Missing Authorization header, or the key is invalid/revoked. |
403 | Forbidden | The key is valid but lacks the gaas scope, or a policy rule / data-classification ceiling blocked the request outright. |
404 | Not Found | Referenced a policy rule or approval that doesn't exist for this org. |
409 | Conflict | Attempted to decide an approval that has already been decided. |
500 | Internal Error | Unexpected server-side failure. Safe to retry. |
Error shape
{ "error": "riskTier must be one of: reversible, consequential, irreversible" }
Data Types
RiskTier: caller-assigned severity used by the policy engine:
reversibleCan be undone with no lasting effect.
consequentialHas a real effect but is recoverable (e.g. sending a notification).
irreversibleCannot be undone (e.g. a financial transfer, a deletion).
PolicyEffect: what a matched policy rule does:
allowRequest proceeds. No approval, no block.
warnRequest proceeds; caller should treat the response as flagged.
blockRequest is rejected outright (HTTP 403).
require_approvalA pending approval is created (HTTP 202); the caller must wait for a decision.
PspfLevel: sovereign classification level, ascending order:
UNOFFICIALNo marking detected.
OFFICIALGeneral business information.
OFFICIAL:SENSITIVELimited disclosure.
PROTECTEDSovereign-block threshold in OBEL's own app.
SECRETBlocked.
TOP SECRETBlocked.
Scrub
Detects and redacts PII/secrets in text, and returns a sovereign classification score. Does not call a model.
POST/v1/scrub
| Field | Type | Description |
|---|
text | string | A single string to scrub. Mutually exclusive with texts. |
texts | string[] | Up to 50 strings, 50,000 characters each. Mutually exclusive with text. |
Exactly one of text or texts is required.
Request
curl https://api.useobel.ai/api/v1/scrub \
-H "Authorization: Bearer obel_sk_..." \
-H "Content-Type: application/json" \
-d '{"text": "Contact me at jane@acme.com, card 4111 1111 1111 1111"}'
Response
| Field | Type | Description |
|---|
results[].cleaned | string | Input with every detected hit redacted. |
results[].hits[].ruleName | string | Which detection rule matched, e.g. EMAIL, CREDIT_CARD. |
results[].hits[].eventType | string | Category of the hit, e.g. pii_detected, secret_detected. |
results[].hits[].matchCount | number | Occurrences of this rule in the input. |
results[].classification.level | PspfLevel|null | Highest classification marking detected, or null. |
results[].classification.blocked | boolean | True if the classification meets the sovereign-block threshold. |
policyBlocked | boolean | True if the org's scrub policy blocked this call (org setting or a matching policy rule). |
Response
{
"results": [{
"cleaned": "Contact me at [EMAIL_REDACTED], card [CARD_REDACTED]",
"hits": [
{ "ruleName": "EMAIL", "eventType": "pii_detected", "matchCount": 1, "redactedSample": "[EMAIL_REDACTED]" },
{ "ruleName": "CREDIT_CARD", "eventType": "pii_detected", "matchCount": 1, "redactedSample": "[CARD_REDACTED]" }
],
"classification": { "level": null, "levelScore": 0, "caveats": [], "blocked": false }
}],
"policyBlocked": false
}
Access Control
Checks whether the org's policy permits reading or retaining a piece of data, before your integration does either.
POST/v1/access/check
| Field | Type | Description |
|---|
resourceTyperequired | string | Caller-defined label, e.g. "customer_record". |
actionrequired | "read" | "retain" | retain is evaluated at a higher risk tier than read. |
classification | PspfLevel | null | Omit or pass null if the data is unclassified. |
Request
curl https://api.useobel.ai/api/v1/access/check \
-H "Authorization: Bearer obel_sk_..." \
-H "Content-Type: application/json" \
-d '{"resourceType": "customer_record", "action": "retain", "classification": "OFFICIAL"}'
Response
| Field | Type | Description |
|---|
decision | "allow" | "allow_with_warning" | "pending_approval" | block short-circuits to HTTP 403 instead of appearing in the body. |
approvalId | string | Present only when decision is pending_approval. |
retentionDays | number | Present only when action was retain and the request was allowed. |
Response
{ "decision": "allow", "retentionDays": 30 }
A classification above the org's configured ceiling (organization_settings.gaas_max_classification) is a hard block regardless of any policy rule. Below the ceiling, the request is evaluated by the same policy engine as every other endpoint.
Policies
Rules are matched most-specific-first: a rule naming matchEventType, matchRiskTier, and matchResourceType beats one naming only matchEventType. Ties break on priority(higher wins), then most-recently-created. No matching enabled rule falls back to the org's approval-threshold default.
POST/v1/policies
| Field | Type | Description |
|---|
namerequired | string | Label for this rule. |
effectrequired | PolicyEffect | What happens when this rule matches. |
matchEventType | string | null | e.g. "scrub", "action_evaluated", "access_check". null matches any. |
matchRiskTier | RiskTier | null | null matches any risk tier. |
matchResourceType | string | null | Your own actionType/resourceType string. null matches any. |
priority | number | Default 0. Higher wins when rules match equally specifically. |
Request
curl https://api.useobel.ai/api/v1/policies \
-H "Authorization: Bearer obel_sk_..." \
-H "Content-Type: application/json" \
-d '{
"name": "Block retaining sensitive data",
"effect": "block",
"matchEventType": "access_check",
"matchRiskTier": "irreversible"
}'
Response: 201
{
"rule": {
"id": "3f1a...",
"name": "Block retaining sensitive data",
"effect": "block",
"matchEventType": "access_check",
"matchRiskTier": "irreversible",
"matchResourceType": null,
"priority": 0,
"enabled": true,
"createdAt": "2026-09-19T00:00:00.000Z"
}
}
GET/v1/policies
Lists every rule for the org, ordered by priority (descending) then creation time (descending). No query parameters.
PATCH/v1/policies/:id
Accepts any subset of name, effect, matchEventType, matchRiskTier, matchResourceType, priority, enabled. Only provided fields change.
DELETE/v1/policies/:id
Permanently removes the rule. Returns { "ok": true }.
Actions
Gates a proposed action before your integration executes it. Submit before, not after, the action runs.
POST/v1/actions/evaluate
| Field | Type | Description |
|---|
actionTyperequired | string | Your own label, e.g. "publish_post", "wire_transfer". |
riskTierrequired | RiskTier | You classify the action; the org's policy decides the outcome. |
context | object | Arbitrary metadata. Persisted on the approval record if one is created. |
Request
curl https://api.useobel.ai/api/v1/actions/evaluate \
-H "Authorization: Bearer obel_sk_..." \
-H "Content-Type: application/json" \
-d '{"actionType": "wire_transfer", "riskTier": "irreversible", "context": {"amountUsd": 50000}}'
Response
| Field | Type | Description |
|---|
decision | "allow" | "allow_with_warning" | "pending_approval" | block returns HTTP 403 with no decision field. |
approvalId | string | Present only when decision is pending_approval. |
Response: 202 (gated)
{ "decision": "pending_approval", "approvalId": "a1b2c3d4-..." }
Do not execute the action unless the response is allow or allow_with_warning.
Approvals
GET/v1/approvals
| Field | Type | Description |
|---|
status | "pending" | "approved" | "rejected" | Default pending. |
Response
{
"approvals": [{
"id": "a1b2c3d4-...",
"actionType": "wire_transfer",
"riskTier": "irreversible",
"context": { "amountUsd": 50000 },
"status": "pending",
"createdAt": "2026-09-19T00:00:00.000Z"
}]
}
POST/v1/approvals/:id/decide
| Field | Type | Description |
|---|
decisionrequired | "approved" | "rejected" | Terminal: an approval can only be decided once. |
reason | string | Optional, up to 500 characters. Recorded on the audit chain. |
Request
curl https://api.useobel.ai/api/v1/approvals/a1b2c3d4-.../decide \
-H "Authorization: Bearer obel_sk_..." \
-H "Content-Type: application/json" \
-d '{"decision": "approved", "reason": "Verified with finance"}'
Returns 409 if the approval has already been decided. Every decision is appended to the audit chain regardless of who or what made the call.
Audit Events
Every scrub, policy decision, action evaluation, access check, and approval outcome is appended here automatically. You can also append your own connector-side events to the same chain.
POST/v1/audit/events
| Field | Type | Description |
|---|
eventTyperequired | string | Free-form category, e.g. "custom_check". |
actionrequired | string | Human-readable description of what happened. |
riskTier | RiskTier | null | Optional. |
payload | object | Arbitrary JSON. Included in the hashed content: do not put anything here you wouldn't want hashed and retained. |
Response: 201
{
"event": {
"id": "e5f6...",
"eventType": "custom_check",
"action": "Verified customer identity",
"prevHash": "a1b2c3...",
"entryHash": "d4e5f6...",
"createdAt": "2026-09-19T00:00:00.000Z"
}
}
GET/v1/audit/events
| Field | Type | Description |
|---|
limit | number | Default 50, max 200. |
Response
{
"events": [ /* newest first */ ],
"chain": { "valid": true, "eventCount": 42 }
}
entryHash = sha256(orgId + prevHash + eventType + action + payload + createdAt), chained per-org.chain.valid is recomputed from stored content on every call, not read from a cached column - altering or deleting a past row changes every hash after it, and this endpoint will report false with a brokenAt event ID.
Building a Connector
A connector is whatever code in your platform sits between a user action and a model call or agent tool execution. The pattern is the same regardless of what you're building on top of:
Gating an agent tool call
async function executeTool(tool, args, apiKey) {
// 1. Scrub anything about to be sent upstream (e.g. to a model)
const scrub = await obel.scrub({ text: JSON.stringify(args) }, apiKey);
if (scrub.policyBlocked) throw new Error("Blocked by org policy");
// 2. Classify risk: you decide the tiering for your domain
const riskTier = tool.isDestructive ? "irreversible"
: tool.hasSideEffects ? "consequential"
: "reversible";
// 3. Ask before executing
const evalResult = await obel.evaluateAction({
actionType: tool.name,
riskTier,
context: { args: scrub.results[0].cleaned },
}, apiKey);
if (evalResult.decision === "block") throw new Error("Blocked by policy");
if (evalResult.decision === "pending_approval") {
return pollApproval(evalResult.approvalId, apiKey); // GET /v1/approvals
}
// 4. allow / allow_with_warning
return tool.execute(scrub.results[0].cleaned);
}
The only per-integration decision is risk tiering - which of your platform's actions are reversible, consequential, or irreversible. Policy, approval routing, and audit are handled uniformly regardless of what you're building.
For a worked example against your specific platform, talk to us - connector reviews are part of every partnership.