EDEFENSIVE INTELLIGENCE
Defensive
Intelligence

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:

FieldTypeDescription
400Bad RequestMalformed JSON or a field failed validation. The response body names the offending field.
401UnauthorizedMissing Authorization header, or the key is invalid/revoked.
403ForbiddenThe key is valid but lacks the gaas scope, or a policy rule / data-classification ceiling blocked the request outright.
404Not FoundReferenced a policy rule or approval that doesn't exist for this org.
409ConflictAttempted to decide an approval that has already been decided.
500Internal ErrorUnexpected 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
FieldTypeDescription
textstringA single string to scrub. Mutually exclusive with texts.
textsstring[]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

FieldTypeDescription
results[].cleanedstringInput with every detected hit redacted.
results[].hits[].ruleNamestringWhich detection rule matched, e.g. EMAIL, CREDIT_CARD.
results[].hits[].eventTypestringCategory of the hit, e.g. pii_detected, secret_detected.
results[].hits[].matchCountnumberOccurrences of this rule in the input.
results[].classification.levelPspfLevel|nullHighest classification marking detected, or null.
results[].classification.blockedbooleanTrue if the classification meets the sovereign-block threshold.
policyBlockedbooleanTrue 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
FieldTypeDescription
resourceTyperequiredstringCaller-defined label, e.g. "customer_record".
actionrequired"read" | "retain"retain is evaluated at a higher risk tier than read.
classificationPspfLevel | nullOmit 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

FieldTypeDescription
decision"allow" | "allow_with_warning" | "pending_approval"block short-circuits to HTTP 403 instead of appearing in the body.
approvalIdstringPresent only when decision is pending_approval.
retentionDaysnumberPresent 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
FieldTypeDescription
namerequiredstringLabel for this rule.
effectrequiredPolicyEffectWhat happens when this rule matches.
matchEventTypestring | nulle.g. "scrub", "action_evaluated", "access_check". null matches any.
matchRiskTierRiskTier | nullnull matches any risk tier.
matchResourceTypestring | nullYour own actionType/resourceType string. null matches any.
prioritynumberDefault 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
FieldTypeDescription
actionTyperequiredstringYour own label, e.g. "publish_post", "wire_transfer".
riskTierrequiredRiskTierYou classify the action; the org's policy decides the outcome.
contextobjectArbitrary 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

FieldTypeDescription
decision"allow" | "allow_with_warning" | "pending_approval"block returns HTTP 403 with no decision field.
approvalIdstringPresent 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
FieldTypeDescription
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
FieldTypeDescription
decisionrequired"approved" | "rejected"Terminal: an approval can only be decided once.
reasonstringOptional, 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
FieldTypeDescription
eventTyperequiredstringFree-form category, e.g. "custom_check".
actionrequiredstringHuman-readable description of what happened.
riskTierRiskTier | nullOptional.
payloadobjectArbitrary 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
FieldTypeDescription
limitnumberDefault 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.