Skip to main content

REST API Reference

FieldValue
Document IDASCEND-SDK-012
Version2026.05
Last UpdatedApril 2026
AuthorAscend Engineering Team
PublisherOW-KAI Technologies Inc.
ClassificationEnterprise Client Documentation
ComplianceSOC 2 CC6.1/CC6.2, PCI-DSS 7.1/8.3, HIPAA 164.312, NIST 800-53 AC-2/SI-4

Reading Time: 15 minutes | Skill Level: Intermediate

Overview

The ASCEND REST API allows direct HTTP integration from any programming language or platform. All endpoints use JSON request/response bodies and require authentication.

API Key Security

Store API keys in environment variables, never in source code or version control. Include your key in the Authorization: Bearer header on every request.

Base URL

https://pilot.owkai.app

Authentication

All requests require an API key via one of these methods:

curl -H "Authorization: Bearer owkai_your_key_here" \
https://pilot.owkai.app/api/v1/actions/submit

X-API-Key Header

curl -H "X-API-Key: owkai_your_key_here" \
https://pilot.owkai.app/api/v1/actions/submit

Both Headers (Enterprise)

For banking-level security, include both:

curl -H "Authorization: Bearer owkai_your_key_here" \
-H "X-API-Key: owkai_your_key_here" \
https://pilot.owkai.app/api/v1/actions/submit

Authentication Requirements Per Endpoint

EndpointMethodAuth
/api/v1/actions/submitPOSTAPI key or Bearer
/api/sdk/kill-switch/statusGETAPI key or Bearer
/api/audit/logsGETAPI key or Bearer
/api/governance/workflows/{id}/approvePOSTCognito JWT only
/api/governance/workflows/{id}/denyPOSTCognito JWT only
warning

Approval and denial endpoints require a Cognito JWT (user session token). API keys are rejected with HTTP 401. Agents cannot approve their own actions — this separation is enforced at the API layer.

Common Headers

HeaderRequiredDescription
AuthorizationYesBearer <api_key>
Content-TypeYesapplication/json
X-API-KeyOptionalAlternate authentication
X-Correlation-IDOptionalRequest tracing ID
X-Request-TimestampOptionalISO 8601 timestamp

Action Endpoints

Submit Action

Submit an agent action for governance evaluation.

Endpoint: POST /api/v1/actions/submit

Request Body:

{
"agent_id": "my-agent-001",
"agent_name": "My AI Agent",
"action_type": "database_read",
"description": "Read customer data for report",
"tool_name": "postgresql",
"resource_id": "customers_table",
"action_details": {
"table": "customers",
"operation": "SELECT",
"columns": ["id", "name", "email"]
},
"context": {
"session_id": "sess_abc123",
"environment": "production"
},
"risk_indicators": {
"data_classification": "pii"
}
}

Required Fields:

FieldTypeDescription
agent_idstringUnique agent identifier
agent_namestringHuman-readable agent name
action_typestringAction category
descriptionstringWhat the action does
tool_namestringTool/service being used

Optional Fields:

FieldTypeDescription
resource_idstringTarget resource identifier
action_detailsobjectAction-specific parameters
contextobjectExecution context
risk_indicatorsobjectPre-computed risk signals

Optional Governance Fields

mcp_server_name (string, optional)

The registered MCP server name for this tool call. Triggers Layer 13 MCP governance enforcement (G-P0-01). The server must be registered and active in Agent Registry → MCP Servers.

Case-sensitive

mcp_server_name is matched exactly as registered. "my-server" and "My-Server" are treated as different registrations. Use the exact string from your MCP server registration.

Error responses when server is not registered:

{
"detail": {
"error": "MCP server governance violation",
"detail": "MCP server is not registered. Register at Agent Registry → MCP Servers.",
"mcp_server_name": "your-server-name",
"correlation_id": "action_20260504_..."
}
}

Register an MCP server: POST /api/registry/mcp-servers


model_id (string, optional)

The model identifier from your organization's Model Registry. Triggers model compliance enforcement (G-P0-02) per SR-11-7 and EU AI Act Art. 9.

The model_id is the string identifier you assigned when registering the model — not the database ID. Example: "gpt-4-turbo-2024-04-09".

Auto-resolution

If you omit model_id, ASCEND checks whether the submitting agent has a linked model (configured in Agent Registry). If a linked model exists, it is used automatically. If not, the action proceeds without model governance.

Error responses when model is not registered:

{
"detail": {
"error": "Model governance violation",
"detail": "Model is not registered in the organization model registry.",
"model_id": "your-model-id",
"correlation_id": "action_20260504_..."
}
}

Register a model: Model Registry in the ASCEND dashboard. To list registered models via API, a Cognito JWT session token is required (use the dashboard to discover model identifiers).

Response (200 OK):

{
"id": 12345,
"action_id": "act_abc123xyz",
"status": "approved",
"risk_score": 3.5,
"risk_level": "low",
"summary": "Action approved - low risk database read",
"created_at": "2025-12-16T10:30:00Z",
"cvss_vector": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N",
"nist_controls": ["AC-3", "AU-12"],
"mitre_techniques": []
}

Response Fields:

FieldTypeDescription
idintegerNumeric action ID
action_idstringString action ID
statusstringapproved, denied, pending
risk_scorefloatRisk score (0-100)
risk_levelstringlow, medium, high, critical
summarystringDecision explanation
denial_reasonstringReason if denied
pending_approversarrayApprovers if pending

Example:

curl -X POST https://pilot.owkai.app/api/v1/actions/submit \
-H "Authorization: Bearer owkai_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"agent_id": "my-agent",
"agent_name": "My Agent",
"action_type": "database_read",
"description": "Query customers",
"tool_name": "postgresql"
}'

Get Action Status

Check the status of a submitted action.

Endpoint: GET /api/v1/actions/{action_id}/status

Response (200 OK):

{
"id": 12345,
"status": "approved",
"risk_score": 3.5,
"risk_level": "low",
"updated_at": "2025-12-16T10:30:00Z"
}

Example:

curl https://pilot.owkai.app/api/v1/actions/12345/status \
-H "Authorization: Bearer owkai_your_key_here"

Get Action Details

Get full action details including audit trail.

Endpoint: GET /api/v1/actions/{action_id}

Response (200 OK):

{
"id": 12345,
"agent_id": "my-agent-001",
"agent_name": "My AI Agent",
"action_type": "database_read",
"description": "Query customers",
"status": "approved",
"risk_score": 3.5,
"risk_level": "low",
"created_at": "2025-12-16T10:30:00Z",
"audit_trail": [
{
"timestamp": "2025-12-16T10:30:00Z",
"event": "submitted",
"actor": "agent"
},
{
"timestamp": "2025-12-16T10:30:01Z",
"event": "approved",
"actor": "auto"
}
]
}

List Actions

List recent actions with optional filtering.

Endpoint: GET /api/v1/actions

Query Parameters:

ParameterTypeDefaultDescription
limitint50Max results (1-100)
offsetint0Pagination offset
statusstring-Filter by status
agent_idstring-Filter by agent

Response (200 OK):

{
"actions": [
{
"id": 12345,
"agent_id": "my-agent",
"action_type": "database_read",
"status": "approved",
"risk_level": "low",
"created_at": "2025-12-16T10:30:00Z"
}
],
"total": 150,
"limit": 50,
"offset": 0,
"has_more": true
}

Example:

curl "https://pilot.owkai.app/api/v1/actions?limit=10&status=pending" \
-H "Authorization: Bearer owkai_your_key_here"

Agent Endpoints

Register Agent

Register a new agent with ASCEND.

Endpoint: POST /api/registry/agents

Request Body:

{
"agent_id": "my-agent-001",
"display_name": "My AI Agent",
"agent_type": "supervised",
"environment": "production",
"capabilities": ["data_access", "file_operations"],
"allowed_resources": ["production_db"],
"metadata": {
"version": "1.0.0",
"team": "data-engineering"
}
}

Response (201 Created):

{
"agent_id": "my-agent-001",
"status": "active",
"trust_level": "standard",
"created_at": "2025-12-16T10:30:00Z"
}

Get Agent Status

Endpoint: GET /api/registry/agents/{agent_id}

Response (200 OK):

{
"agent_id": "my-agent-001",
"display_name": "My AI Agent",
"status": "active",
"trust_level": "standard",
"last_activity": "2025-12-16T10:30:00Z",
"action_count": 150,
"denial_count": 5
}

Approval Endpoints

Check Approval Status

Endpoint: GET /api/sdk/approval/{approval_id}

Response (200 OK):

{
"approval_id": "apr_abc123",
"status": "approved",
"approved_by": "admin@company.com",
"decided_at": "2025-12-16T10:35:00Z",
"comments": "Approved for production deployment"
}

Approve Action (Admin)

Endpoint: POST /api/actions/{action_id}/approve

Request Body:

{
"comments": "Approved after security review"
}

Response (200 OK):

{
"status": "approved",
"approved_by": "admin@company.com",
"approved_at": "2025-12-16T10:35:00Z"
}

Health & Info

Health Check

Endpoint: GET /health

Response (200 OK):

{
"status": "healthy",
"timestamp": "2025-12-16T10:30:00Z"
}

Deployment Info

Endpoint: GET /api/deployment-info

Response (200 OK):

{
"version": "2.5.0",
"environment": "production",
"region": "us-east-2",
"features": ["smart_rules", "mcp_governance", "byok"]
}

Kill-Switch Status

Check whether your organization's kill-switch is currently active. The ASCEND SDK polls this endpoint automatically. REST API callers must implement polling manually to receive kill-switch signals.

Endpoint: GET /api/sdk/kill-switch/status

Auth: API key or Bearer token

Response when inactive:

{"blocked": false, "reason": null}

Response when active:

{"blocked": true, "reason": "Kill-switch activated"}
Fail-closed requirement

If this endpoint is unreachable, treat the response as blocked: true. Never default to blocked: false on a connection error. See Fail-Closed Reference Implementations below.

Error Responses

Error Format

All errors return JSON with this structure:

{
"detail": "Error message here",
"error_code": "ERROR_CODE",
"status_code": 400
}

HTTP Status Codes

CodeMeaningCommon Causes
200SuccessRequest completed
201CreatedResource created
400Bad RequestInvalid JSON, missing fields
401UnauthorizedInvalid API key
403ForbiddenInsufficient permissions
404Not FoundResource doesn't exist
409ConflictDuplicate resource
422UnprocessableValidation failed
429Too Many RequestsRate limit exceeded
500Server ErrorInternal error

Error Codes

CodeDescription
INVALID_API_KEYAPI key is invalid or expired
MISSING_REQUIRED_FIELDRequired field not provided
INVALID_ACTION_TYPEUnrecognized action type
AGENT_NOT_FOUNDAgent ID not registered
RATE_LIMIT_EXCEEDEDToo many requests
POLICY_VIOLATIONAction violates policy

Response Body Schema

All governance decisions are returned in the response body. Extract decision context from these fields:

FieldTypeDescription
idintegerInternal action record ID
action_idstringUnique action identifier
statusstringapproved, pending_approval, denied
risk_scorefloatComposite risk score (0–100)
risk_levelstringlow, medium, high, critical
requires_approvalbooleanWhether human review is pending
correlation_idstringUse for support tickets and audit correlation
compliance_mappingobjectNIST, MITRE ATT&CK framework mappings
messagestringHuman-readable decision summary
Gateway paths return headers instead

If you need decision context as HTTP response headers (X-Ascend-Action-Id, X-Ascend-Risk-Score, etc.) rather than body fields, use the Envoy/Istio, Lambda Authorizer, or Kong gateway integration paths. Those paths inject governance metadata as headers automatically — no body parsing required.

Rate Limits

TierRequests/MinuteRequests/Hour
Free601,000
Pro60010,000
Enterprise6,000Unlimited

Rate limit headers:

X-RateLimit-Limit: 60
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1702725600
Retry-After: 30

Webhooks

Configure Webhook

Endpoint: POST /api/sdk/webhooks/configure

Request Body:

{
"url": "https://your-app.com/webhooks/ascend",
"events": ["action.approved", "action.denied", "policy.violation"],
"secret": "whsec_your_secret_here"
}

Webhook Payload:

{
"event": "action.approved",
"timestamp": "2025-12-16T10:30:00Z",
"data": {
"action_id": "act_abc123",
"agent_id": "my-agent-001",
"risk_score": 3.5
},
"signature": "v1=abc123..."
}

Complete Example

import requests
import os

BASE_URL = "https://pilot.owkai.app"
API_KEY = os.environ["ASCEND_API_KEY"]

def evaluate_action(action_type, description, tool_name, **kwargs):
"""Evaluate an action for governance."""
headers = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json"
}

payload = {
"agent_id": "my-agent-001",
"agent_name": "My AI Agent",
"action_type": action_type,
"description": description,
"tool_name": tool_name,
**kwargs
}

response = requests.post(
f"{BASE_URL}/api/v1/actions/submit",
headers=headers,
json=payload,
timeout=30
)

if response.status_code == 200:
result = response.json()
return result
elif response.status_code == 401:
raise Exception("Invalid API key")
elif response.status_code == 422:
raise Exception(f"Validation error: {response.json()}")
else:
raise Exception(f"API error: {response.status_code}")

# Usage
result = evaluate_action(
action_type="database_read",
description="Query customer data",
tool_name="postgresql",
action_details={"table": "customers"}
)

if result["status"] == "approved":
print(f"Approved! Action ID: {result['id']}")
else:
print(f"Status: {result['status']}")

Fail-Closed Reference Implementations

Banking-grade callers must wrap every governed action with these four behaviors:

  1. Kill-switch pre-check — call GET /api/sdk/kill-switch/status before every action; treat any non-200 or connection error as blocked: true.
  2. Fail-closed on ASCEND unreachable — never proceed when the platform is unreachable.
  3. Exponential backoff with jitter — retry HTTP 429 and 503 with 2^attempt seconds + up to 25% jitter; cap at 5 attempts.
  4. HTTP 402 treated as kill-switch active — backend returns 402 when an org's kill-switch or spend limit is engaged; treat identically to a blocked: true status response.

The Python quick-start in Complete Example above is for prototype use. The implementations below are the banking-grade reference for production REST callers.

Python

import os
import random
import time
import requests

BASE_URL = "https://pilot.owkai.app"
API_KEY = os.environ["ASCEND_API_KEY"]
TIMEOUT = 30
MAX_RETRIES = 5


class AscendBlocked(Exception):
"""Raised when ASCEND blocks the action — fail-closed."""


def _sleep_with_jitter(attempt: int) -> None:
base = 2 ** attempt # 1s, 2s, 4s, 8s, 16s
delay = base + random.uniform(0, 0.25 * base)
time.sleep(delay)


def _is_kill_switch_active() -> bool:
"""Fail-closed: any error treats kill-switch as ACTIVE."""
try:
r = requests.get(
f"{BASE_URL}/api/sdk/kill-switch/status",
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=5,
)
if r.status_code != 200:
return True
return bool(r.json().get("blocked"))
except Exception:
return True


def evaluate_action_governed(payload: dict) -> dict:
if _is_kill_switch_active():
raise AscendBlocked("kill-switch active or status endpoint unreachable")

headers = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
}

for attempt in range(MAX_RETRIES):
try:
r = requests.post(
f"{BASE_URL}/api/v1/actions/submit",
headers=headers,
json=payload,
timeout=TIMEOUT,
)
except requests.RequestException:
raise AscendBlocked("ASCEND unreachable")

if r.status_code == 402:
raise AscendBlocked("kill-switch / spend limit active")
if r.status_code in (429, 503):
if attempt == MAX_RETRIES - 1:
raise AscendBlocked(f"max retries exceeded ({r.status_code})")
_sleep_with_jitter(attempt)
continue
if r.status_code == 200:
return r.json()
raise AscendBlocked(f"unexpected status {r.status_code}")

raise AscendBlocked("max retries exceeded")

Node.js

const BASE_URL = 'https://pilot.owkai.app';
const API_KEY = process.env.ASCEND_API_KEY;
const TIMEOUT_MS = 30_000;
const MAX_RETRIES = 5;

class AscendBlocked extends Error {
constructor(reason) {
super(`ASCEND blocked: ${reason}`);
this.name = 'AscendBlocked';
}
}

function sleepWithJitter(attempt) {
const base = 1000 * 2 ** attempt; // 1s, 2s, 4s, 8s, 16s
const delay = base + Math.random() * 0.25 * base;
return new Promise((resolve) => setTimeout(resolve, delay));
}

async function isKillSwitchActive() {
// Fail-closed: any error treats kill-switch as ACTIVE.
try {
const r = await fetch(`${BASE_URL}/api/sdk/kill-switch/status`, {
headers: { Authorization: `Bearer ${API_KEY}` },
signal: AbortSignal.timeout(5_000),
});
if (!r.ok) return true;
const body = await r.json();
return Boolean(body.blocked);
} catch {
return true;
}
}

async function submitActionGoverned(payload) {
if (await isKillSwitchActive()) {
throw new AscendBlocked('kill-switch active or status endpoint unreachable');
}

for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
let r;
try {
r = await fetch(`${BASE_URL}/api/v1/actions/submit`, {
method: 'POST',
headers: {
Authorization: `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
signal: AbortSignal.timeout(TIMEOUT_MS),
});
} catch {
throw new AscendBlocked('ASCEND unreachable');
}

if (r.status === 402) throw new AscendBlocked('kill-switch / spend limit active');
if (r.status === 429 || r.status === 503) {
if (attempt === MAX_RETRIES - 1) {
throw new AscendBlocked(`max retries exceeded (${r.status})`);
}
await sleepWithJitter(attempt);
continue;
}
if (r.status === 200) return await r.json();
throw new AscendBlocked(`unexpected status ${r.status}`);
}
throw new AscendBlocked('max retries exceeded');
}

Go

package ascend

import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"math/rand"
"net/http"
"os"
"time"
)

const (
BaseURL = "https://pilot.owkai.app"
Timeout = 30 * time.Second
MaxRetries = 5
)

var ErrAscendBlocked = errors.New("ascend blocked")

func sleepWithJitter(ctx context.Context, attempt int) error {
base := time.Duration(1<<attempt) * time.Second // 1s, 2s, 4s, 8s, 16s
jitter := time.Duration(rand.Float64() * float64(base) * 0.25)
select {
case <-time.After(base + jitter):
return nil
case <-ctx.Done():
return ctx.Err()
}
}

func isKillSwitchActive(ctx context.Context, client *http.Client) bool {
// Fail-closed: any error treats kill-switch as ACTIVE.
req, _ := http.NewRequestWithContext(ctx, "GET",
BaseURL+"/api/sdk/kill-switch/status", nil)
req.Header.Set("Authorization", "Bearer "+os.Getenv("ASCEND_API_KEY"))
r, err := client.Do(req)
if err != nil {
return true
}
defer r.Body.Close()
if r.StatusCode != http.StatusOK {
return true
}
var body struct {
Blocked bool `json:"blocked"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
return true
}
return body.Blocked
}

func SubmitActionGoverned(ctx context.Context, payload map[string]any) (map[string]any, error) {
client := &http.Client{Timeout: Timeout}

if isKillSwitchActive(ctx, client) {
return nil, fmt.Errorf("%w: kill-switch active or status endpoint unreachable", ErrAscendBlocked)
}

body, _ := json.Marshal(payload)
for attempt := 0; attempt < MaxRetries; attempt++ {
req, err := http.NewRequestWithContext(ctx, "POST",
BaseURL+"/api/v1/actions/submit", bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrAscendBlocked, err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("ASCEND_API_KEY"))
req.Header.Set("Content-Type", "application/json")

r, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("%w: ASCEND unreachable", ErrAscendBlocked)
}
defer r.Body.Close()

switch {
case r.StatusCode == http.StatusPaymentRequired: // 402
return nil, fmt.Errorf("%w: kill-switch / spend limit active", ErrAscendBlocked)
case r.StatusCode == http.StatusTooManyRequests || r.StatusCode == http.StatusServiceUnavailable:
if attempt == MaxRetries-1 {
return nil, fmt.Errorf("%w: max retries exceeded (%d)", ErrAscendBlocked, r.StatusCode)
}
if err := sleepWithJitter(ctx, attempt); err != nil {
return nil, err
}
continue
case r.StatusCode == http.StatusOK:
var result map[string]any
if err := json.NewDecoder(r.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("%w: invalid response", ErrAscendBlocked)
}
return result, nil
default:
return nil, fmt.Errorf("%w: unexpected status %d", ErrAscendBlocked, r.StatusCode)
}
}
return nil, fmt.Errorf("%w: max retries exceeded", ErrAscendBlocked)
}

Java

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;

public class AscendClient {
private static final String BASE_URL = "https://pilot.owkai.app";
private static final Duration TIMEOUT = Duration.ofSeconds(30);
private static final int MAX_RETRIES = 5;

private final String apiKey = System.getenv("ASCEND_API_KEY");
private final HttpClient http = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build();

public static class AscendBlocked extends RuntimeException {
public AscendBlocked(String reason) { super("ASCEND blocked: " + reason); }
}

private void sleepWithJitter(int attempt) throws InterruptedException {
long base = (1L << attempt) * 1000L; // 1s, 2s, 4s, 8s, 16s
long jitter = (long) (ThreadLocalRandom.current().nextDouble() * base * 0.25);
Thread.sleep(base + jitter);
}

private boolean isKillSwitchActive() {
// Fail-closed: any error treats kill-switch as ACTIVE.
try {
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/api/sdk/kill-switch/status"))
.header("Authorization", "Bearer " + apiKey)
.timeout(Duration.ofSeconds(5))
.GET().build();
HttpResponse<String> r = http.send(req, HttpResponse.BodyHandlers.ofString());
if (r.statusCode() != 200) return true;
return r.body().contains("\"blocked\":true");
} catch (Exception e) {
return true;
}
}

public Map<String, Object> submitActionGoverned(String jsonPayload) throws InterruptedException {
if (isKillSwitchActive()) {
throw new AscendBlocked("kill-switch active or status endpoint unreachable");
}

for (int attempt = 0; attempt < MAX_RETRIES; attempt++) {
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/api/v1/actions/submit"))
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "application/json")
.timeout(TIMEOUT)
.POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
.build();

HttpResponse<String> r;
try {
r = http.send(req, HttpResponse.BodyHandlers.ofString());
} catch (Exception e) {
throw new AscendBlocked("ASCEND unreachable");
}

int code = r.statusCode();
if (code == 402) throw new AscendBlocked("kill-switch / spend limit active");
if (code == 429 || code == 503) {
if (attempt == MAX_RETRIES - 1) throw new AscendBlocked("max retries exceeded (" + code + ")");
sleepWithJitter(attempt);
continue;
}
if (code == 200) {
// Parse JSON via your preferred library (Jackson, Gson, etc.)
return Map.of("body", r.body());
}
throw new AscendBlocked("unexpected status " + code);
}
throw new AscendBlocked("max retries exceeded");
}
}

Next Steps


Document Version: 2026.05 | Last Updated: May 2026