Docs

Docs

API Contract

The cPod backend exposes a REST API documented as an OpenAPI 3.1 specification. The spec is the authoritative source of truth for every endpoint, request/response shape, and error code in the platform.

SDK vs direct API. Use the typed SDKs (@zysec-ai/cpod-sdk, cpod-sdk, etc.) rather than calling the REST API directly. The spec exists to define the contract between SDK and backend — not as a primary integration point for application developers.


Emulator vs Production Backend#

The cPod emulator is a fully-functional stub backend built to serve the same REST contract as the production cpod-backend. During development you point the SDK at the emulator; in production you point it at https://api.cyberpod.app. The contract is identical — the only difference is where the data lives and how tokens are issued.

EmulatorProduction
Base URLhttp://localhost:4000https://api.cyberpod.app
Auth token endpointhttp://localhost:4000/api/v1/auth/tokenhttps://api.cyberpod.app/api/v1/auth/token
Token issuanceAny client_id / client_secret acceptedReal OAuth 2.0 credentials required
Data persistenceIn-memory (reset on restart)Durable PostgreSQL + S3
Interactive docshttp://localhost:4000/docs

Switching base URL in the SDK:

typescript
import { CpodClient } from '@zysec-ai/cpod-sdk'
 
// Development — emulator
const dev = new CpodClient({
  apiKey: 'any-key',
  baseUrl: 'http://localhost:4000',
})
 
// Production
const prod = new CpodClient({
  apiKey: process.env.CPOD_API_KEY!,
  // baseUrl defaults to https://api.cyberpod.app
})

Set CPOD_BASE_URL=http://localhost:4000 in your .env file. All four SDKs pick this up automatically via from_env() / CpodClient.from_env() / fromEnv().


Authentication#

All endpoints except /api/v1/auth/token and /health require a Bearer token.

Obtaining a token#

bash
curl -s http://localhost:4000/api/v1/auth/token \
  -H "Content-Type: application/json" \
  -d '{"client_id":"dev","client_secret":"dev","grant_type":"client_credentials"}'
# → {"access_token":"eyJ...","token_type":"Bearer","expires_in":3600}

Using the token#

http
GET /api/v1/people/persons HTTP/1.1
Host: api.cyberpod.app
Authorization: Bearer eyJ...
Accept: application/json

The SDKs handle token acquisition and refresh automatically when you provide apiKey (SDK shorthand for the OAuth client secret).


Domain → API Prefix Mapping#

Every EDM domain has a dedicated base path. All entity CRUD operations follow the pattern GET|POST /v1/{prefix}, GET|PATCH|DELETE /v1/{prefix}/{id}.

DomainAPI PrefixEntities
People/api/v1/people/personsPerson
People (self)GET /api/v1/people/persons/mePerson linked to JWT sub — no userId parameter
People (admin sync)POST /api/v1/people/persons/sync-user-linksIdempotent User ↔ Person link backfill (tenant_admin, supports dry_run)
Groups/v1/groupsGroup
Technology/v1/technologyTechnologyAsset, AccessEntitlement
Licenses/v1/licensesSoftwareLicense, LicenseAssignment
Assets/v1/assetsPhysicalAsset, CloudResource
Risk/v1/riskVulnerability, ComplianceControl, RiskItem
Relationships/v1/relationshipsRelationship
DataSources/v1/datasourcesDataSource
Contracts/v1/contractsVendor, Contract, ContractObligation
Employee/v1/employeePersonSkill, CalendarEvent, LeaveRequest, MeetingNote
Work/v1/workTimeEntry, Comment, Capacity
Performance/v1/performancePerformanceReview, PersonGoal, LearningRecord
Investments/v1/investmentsTechPortfolioItem, CostCenter
Customer/v1/customerAccount, Contact, Deal, Activity
Projects/v1/projectsProject, Task, Sprint, Feature
OKR/v1/okrObjective, KeyResult
GRC/v1/grcGrcFramework, GrcControl, GrcEvidence, GrcIncident, GrcRisk
SOC/v1/socSocAlert, SocInvestigation, SocPlaybook
Telemetry/v1/telemetryAuditEvent, AppEvent, Trace
Finance/v1/financeInvoice, PurchaseOrder, Expense, Budget, BudgetLine
HR/v1/hrJobPosting, Applicant, OnboardingTask, OffboardingTask
Helpdesk/v1/helpdeskTicket, SlaPolicy
Notifications/v1/notificationsNotification, Announcement
Approvals/v1/approvalsApprovalRequest, ApprovalStep
Org/v1/orgLocation, Department
Catalog/v1/catalogProduct, ProductCategory
Procurement/v1/procurementSupplier
Knowledge/v1/knowledgeDocument, Chunk, KnowledgeEntity
Learning/v1/learningCohort, Assessment
RFP/v1/rfpRfpRecord, RfpQuestion, RfpResponse
Integration/v1/integrationApplication, Connector, ApiKey, Webhook
Storage/v1/storageStorageObject, StorageRecord

Standard Request/Response Envelope#

Single-entity response (ApiResponse<T>)#

Every endpoint that returns a single entity wraps it in this shape:

json
{
  "data": { /* entity object */ },
  "meta": {
    "requestId": "req-01J...",
    "timestamp": "2026-05-22T10:00:00.000Z"
  }
}

List response (PaginatedResponse<T>)#

List endpoints (GET /v1/{prefix}) return:

json
{
  "data": [ /* entity objects */ ],
  "total": 1024,
  "page": 1,
  "pageSize": 50,
  "hasMore": true,
  "meta": {
    "requestId": "req-01J...",
    "timestamp": "2026-05-22T10:00:00.000Z"
  }
}
FieldTypeDescription
dataT[]Array of entities for this page
totalintegerTotal matching record count across all pages
pageintegerCurrent page number (1-based)
pageSizeintegerNumber of items per page
hasMorebooleantrue when page * pageSize < total
meta.requestIdstringTraceable request ID for support
meta.timestampstringUTC ISO 8601 timestamp of the response

Mutation request body#

Create and update requests send a JSON body with Content-Type: application/json. Only the fields you include are applied (partial update / PATCH semantics for updates).

json
{
  "firstName": "Alice",
  "lastName": "Smith",
  "email": "alice@acme.com",
  "type": "employee"
}

Error Codes#

All errors follow RFC 9457 Problem Details format:

json
{
  "title": "Validation Error",
  "status": 422,
  "detail": "email must be a valid email address",
  "instance": "/api/v1/people/persons",
  "requestId": "req-01J..."
}
StatusTitleTypical Cause
400Bad RequestMalformed JSON, missing required fields
401UnauthorizedMissing, expired, or invalid Bearer token
403ForbiddenValid token but insufficient scope/permissions
404Not FoundEntity ID does not exist in this tenant
422Unprocessable EntityPayload passes schema validation but fails business rules (e.g. duplicate email)
429Too Many RequestsRate limit exceeded — back off and retry after Retry-After header
500Internal Server ErrorUnexpected backend failure — contact support with requestId

Specification File#

The OpenAPI 3.1 spec lives at docs/api-contract/openapi.yaml. All schemas use JSON Schema Draft 2020-12 with additionalProperties: false.

View interactively#

bash
npx @redocly/cli preview-docs docs/api-contract/openapi.yaml
# Opens http://localhost:8080

Contract Validation (CI)#

Every PR touching docs/api-contract/, emulator/src/routes/, or sdks/ triggers the API Contract Validation workflow (.github/workflows/api-contract.yml), which runs:

1. Spectral lint#

RuleSeverityDescription
operation-operationIderrorEvery operation must have a unique operationId
operation-descriptionwarnEvery operation should include a description
operation-tagswarnEvery operation should belong to a tag
operation-success-responseerrorEvery operation must define at least one 2xx response
oas3-schemaerrorSchema objects must be valid OAS 3.1
cpod-id-patternwarnEntity id properties must declare a regex pattern
cpod-no-inline-schemaswarnInline schemas with >3 properties should be extracted to components/schemas
cpod-pagination-shapewarnList-endpoint responses should include items and total

2. Path completeness check#

scripts/check-api-contract.mjs asserts ≥ 40 paths, ≥ 10 component schemas, OpenAPI 3.1.x, info.contact present, and every path starting with /v1/.

3. SDK alignment check#

scripts/validate-api-contract.ts compares SDK service names against OpenAPI tags and fails if any service has no corresponding tag (or vice-versa).


Keeping the Contract in Sync#

When you add a new endpoint, follow this order:

  1. Emulator route — add the handler in emulator/src/routes/
  2. OpenAPI spec — add the path + schema to docs/api-contract/openapi.yaml
  3. SDK methods — implement the client method in all four SDKs
  4. Tests — add at minimum a smoke test per SDK
  5. Docs — update the relevant domain page under docs/pages/docs/domains/

Never hand-edit the spec for an endpoint that does not yet exist in the emulator. The spec and the emulator must stay in lock-step — they are tested together in CI.


Entity ID Prefixes#

All cPod entity IDs use a short kebab prefix so they are self-describing in logs:

EntityPrefixExample
Personper-per-01J...
Groupgrp-grp-01J...
TechnologyAssettast-tast-01J...
AccessEntitlementent-ent-01J...
SoftwareLicenselic-lic-01J...
LicenseAssignmentlasgn-lasgn-01J...
PhysicalAssetpast-past-01J...
CloudResourcecres-cres-01J...
Vulnerabilityvuln-vuln-01J...
ComplianceControlctrl-ctrl-01J...
RiskItemrisk-risk-01J...
Relationshiprel-rel-01J...
DataSourceds-ds-01J...
Vendorvnd-vnd-01J...
Contractctr-ctr-01J...
ContractObligationobl-obl-01J...

The cpod-id-pattern Spectral rule warns when an id property is missing a pattern constraint, keeping the spec honest about ID shapes.


Groups & Permissions API#

All endpoints require Authorization: Bearer <token>. Access rules:

RoleGroups visibleCan modify
global_adminAll tenantsAny group
tenant_adminOwn tenantAny group in own tenant
userOnly groups they're a member ofOnly groups they're a member of

Create group#

bash
POST /api/v1/groups
Authorization: Bearer <token>
Content-Type: application/json
 
{
  "display_name": "CRM Writers",
  "app_id": "app-crm-sync-58d93289",   // optional: scope to an app
  "description": "Can write people records"   // optional
}

Response (201):

json
{
  "id": "grp_019f4824-...",
  "display_name": "CRM Writers",
  "app_id": "app-crm-sync-58d93289",
  "tenant_id": "tnt_global",
  "members": [],
  "member_details": []
}

List groups#

bash
GET /api/v1/groups
Authorization: Bearer <token>

Returns groups based on access rules above. Each group includes members and app_id.

Get group#

bash
GET /api/v1/groups/{group_id}
Authorization: Bearer <token>

Update group#

bash
PATCH /api/v1/groups/{group_id}
Authorization: Bearer <token>
Content-Type: application/json
 
{
  "display_name": "Updated Name",
  "description": "Updated description"
}

Delete group#

bash
DELETE /api/v1/groups/{group_id}
Authorization: Bearer <token>

Add member#

bash
POST /api/v1/groups/{group_id}/members
Authorization: Bearer <token>
Content-Type: application/json
 
{
  "user_id": "usr_711484e0-..."
}

Remove member#

bash
DELETE /api/v1/groups/{group_id}/members/{user_id}
Authorization: Bearer <token>

Set group permissions#

Grants the group { domain: [operations] } permissions. Operations: read, write (where write = create + update + delete).

bash
PUT /api/v1/groups/{group_id}/permissions
Authorization: Bearer <token>
Content-Type: application/json
 
{
  "permissions": {
    "people": ["read", "write"],
    "contracts": ["read"],
    "helpdesk": ["read", "write"]
  }
}

Response:

json
{
  "group_id": "grp_019f4824-...",
  "permissions": {
    "people": ["read", "write"],
    "contracts": ["read"],
    "helpdesk": ["read", "write"]
  }
}

Get group permissions#

bash
GET /api/v1/groups/{group_id}/permissions
Authorization: Bearer <token>

Delete all group permissions#

bash
DELETE /api/v1/groups/{group_id}/permissions
Authorization: Bearer <token>

Get user effective permissions#

Returns the merged permissions from all groups the user belongs to, optionally filtered by app_id. This is what the middleware calls internally when X-App-Id is present.

bash
GET /api/v1/users/{user_id}/permissions
Authorization: Bearer <token>

With app_id filter (same endpoint, control-plane internal):

code
GET /api/v1/users/{user_id}/effective-permissions?app_id=app-crm-sync

Response:

json
{
  "permissions": {
    "people": ["read", "write"],
    "contracts": ["read"]
  }
}

When app_id is provided, only groups where app_id IS NULL OR app_id = provided contribute permissions. Plain groups (no app_id) apply everywhere; app-scoped groups only contribute when their app_id matches.

Permission enforcement at runtime#

When an app sends a request with X-App-Id:

ScenarioResult
User has domain:write for the appGranted
User lacks the required permission403 EdmForbidden
No X-App-Id header (normal user call)⏭️ Bypassed (owner scope applies)
App not registered⏭️ Bypassed (X-App-Id stripped)