Cortex

Technical documentation

CORTEX Seal · API v1

API documentation

Everything needed to create an agent, publish its manifest, issue its credential, verify it from outside and revoke it with effect. Issuer: did:web:digitalcortex.tech, operated by Cosmolabs OÜ. This page is published before sign-up on purpose: if the documentation does not convince you, the account will not either.

Base URL

digitalcortex.tech

Format

W3C VC 2.0 · vc+jwt · ES256

Authentication

Bearer cs_live_ / cs_test_

OpenAPI

3.1 · /openapi/cortex-seal-v1.yaml

01

10-minute quickstart

Five calls: create the agent, publish its manifest, issue, verify and revoke. All in sandbox, with no card and no consequences.

When you create your workspace at /seal/signup you receive a sandbox key, shown once. The prefix decides the environment: a cs_test_ key can only create and issue in SANDBOX.

0 · export the key
export CORTEX_API_KEY="cs_test_REEMPLAZA_POR_LA_TUYA"
1 · create the agent
curl -X POST 'https://digitalcortex.tech/functions/v1Agents' \
  -H "Authorization: Bearer $CORTEX_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "slug": "soporte",
    "name": "Soporte",
    "role": "First-line customer support",
    "autonomy_max": "A2",
    "public": true
  }'

Returns 201 with the id, the agent’s DID and the verification links. The slug is unique per workspace, not globally: two customers may each have an agent called soporte. It enters the DID, so it cannot be changed afterwards.

2 · publish the manifest
curl -X POST 'https://digitalcortex.tech/functions/v1AgentManifest?id=<AGENT_ID>' \
  -H "Authorization: Bearer $CORTEX_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "version": "1.0.0",
    "changelog": "First version",
    "manifest": {
      "schema": "https://digitalcortex.tech/schemas/agent-manifest/1.0",
      "name": "Soporte",
      "role": "First-line customer support",
      "operator": { "legalName": "ACME Robotics S.L.", "registryCode": "B12345678", "jurisdiction": "ES" },
      "autonomyMax": "A2",
      "skills": ["ticket.read", "ticket.reply"],
      "prohibited": ["payment.execute", "data.delete"],
      "tools": ["helpdesk:read", "helpdesk:write"],
      "limits": { "maxRiskWithoutApproval": "LOW", "maxTransactionEur": null }
    }
  }'
3 · issue the credential
curl -X POST 'https://digitalcortex.tech/functions/v1Credentials' \
  -H "Authorization: Bearer $CORTEX_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H 'Content-Type: application/json' \
  -d '{
    "agent_id": "<AGENT_ID>",
    "scope_skills": ["ticket.read", "ticket.reply"],
    "scope_prohibited": ["payment.execute", "data.delete"],
    "max_risk_without_approval": "LOW",
    "ttl_days": 90
  }'

The Idempotency-Key header is mandatory when issuing. Without it, a client retry would burn a second revocation-list index. Replaying the same key with the same body answers 200 with the same result; with a different body, 409.

4 · verify from outside, with no key
curl -X POST 'https://digitalcortex.tech/functions/v1Verify' \
  -H 'Content-Type: application/json' \
  -d '{"tenant": "acme", "agent": "soporte", "skill": "ticket.reply"}'
5 · revoke
curl -X POST 'https://digitalcortex.tech/functions/v1CredentialRevoke?id=CRD-2026-0002' \
  -H "Authorization: Bearer $CORTEX_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"reason": "End of the quickstart", "skip_cascade": true}'

reason is mandatory: a revocation with no reason is not auditable. With skip_cascade: true the credential is revoked without touching a single linked resource.

The same thing with an SDK turns the five calls into five lines. Both are written and tested, with the source published under Apache-2.0, but they are not on npm or PyPI yet: for now you install them from the repository. Section 12 shows them in full.

02

Concepts

Seven pieces and how they fit. If you only read one section before writing code, read this one.

PieceWhat it is
ManifestThe JSON document where you declare what the agent may do, what it is expressly forbidden to do, with which tools and at which autonomy level. It is what the credential binds by hash. Open schema at /spec.
A0–A5 levelsA graded autonomy scale. autonomyMax declares the ceiling, not the usual level, and A5 is never a valid value there.
CredentialA W3C Verifiable Credential 2.0 in vc+jwt form, ES256-signed by did:web:digitalcortex.tech, valid for 90 days, carrying the manifestHash inside.
Status listA W3C Bitstring Status List v1.0: signed, public and cached for 60 s. It is where revocation is published and what any verifier reads.
SealThe deterministic SVG image derived from the credential. In sandbox it carries a diagonal SANDBOX — NOT VALID band.
VerifierThe public page /verify/<tenant>/<agent> and the v1Verify endpoint. Eight live checks, with no account and no contract.
Kill switchThe resources linked to the credential and the stop plan executed on revocation, with a mandatory dry run and an auditable record of every attempt.
LevelWhat it means
A0Observe. Reads and records; produces no output directed at third parties.
A1Analyse. Produces analysis and recommendations for people.
A2Prepare. Drafts and stages, without sending or executing.
A3Execute internally. Acts on the operator’s own systems, with no outward effect.
A4Execute with human approval. The highest attainable.
A5Forbidden. A class of vetoed actions: autonomous money, deleting audit records, escalating privileges.
The scale is published as an open specification under CC BY 4.0, so it can be used without us.

03

Authentication

Two paths and only two: an API key for your machines and the user session for the dashboard. They never mix.

Authorization: Bearer cs_live_7f3a2b9c4d1e8a6f0b2c5d
  • Format: cs_live_<22 characters> in production or cs_test_<22> in sandbox.
  • The secret is shown once. Only its SHA-256 and its first 12 characters are stored, and those are what you will see in the logs.
  • Validation reads the record on every request: a revocation takes effect immediately, with no cache to wait for.
  • A malformed authorisation header answers 401 and never falls back to the session path.

Scopes

ScopeAllows
agents:readRead and list agents
agents:writeCreate, update and publish manifests
credentials:readRead issued credentials
credentials:issueIssue
credentials:revokeRevoke
verifyVerify (the public endpoint does not require it)
keys:manageCreate, list and revoke API keys
webhooks:manageManage outgoing webhooks
A CI key should be able to issue but not revoke. A public website’s key, verify only.

User session, for the dashboard only

The /seal/app dashboard holds no key: it calls /functions/sealApi?fn=<resource>, which checks the browser session. Permissions come from the role in the workspace: OWNER and ADMIN have all of them; DEVELOPER all but keys:manage and webhooks:manage; VIEWER read and verify only.

Managing keys

# Crear: el secreto se devuelve UNA vez
curl -X POST 'https://digitalcortex.tech/functions/v1ApiKeys' \
  -H "Authorization: Bearer $CORTEX_API_KEY" -H 'Content-Type: application/json' \
  -d '{"name": "Production CI", "environment": "PRODUCTION", "scopes": ["agents:read", "credentials:issue"]}'

# Listar: nunca devuelve el secreto ni el hash
curl 'https://digitalcortex.tech/functions/v1ApiKeys' -H "Authorization: Bearer $CORTEX_API_KEY"

# Revocar: efecto inmediato
curl -X DELETE 'https://digitalcortex.tech/functions/v1ApiKeys?id=6aadb…' -H "Authorization: Bearer $CORTEX_API_KEY"

Two ways to authorise this, and only two: a key holding keys:manage, or a user session that owns or administers the workspace.

04

Errors

Every error response shares one body shape and carries a request identifier you can paste into a ticket.

{
  "error": "plan_limit_reached",
  "message": "The STARTER plan allows 5 agents in production and there are already 5. The SANDBOX environment has no limit.",
  "request_id": "req_6f3a2b9c4d1e8a6f0b2c5d1e",
  "docs_url": "/docs/seal"
}
HTTPerrorWhen
400invalid_request · scope_empty · scope_conflict · manifest_schema_invalidValidation
401invalid_api_key · api_key_revoked · api_key_expiredAuthentication
402plan_limit_reached · subscription_past_dueBilling
403insufficient_scope · wrong_environmentPermissions
404not_foundAlso when the resource belongs to another workspace
409slug_taken · no_manifest · manifest_mismatch · already_revoked · dry_run_requiredState
422signing_unavailableThe signing service did not answer. Nothing was issued.
429rate_limitedLimit, with a Retry-After header
5xxinternal_errorNever the exception message

Every authenticated response carries Cache-Control: no-store. All of them accept OPTIONS. The errors worth retrying are 429, 5xx and 422 signing_unavailable; the SDKs expose that as isRetryable so you do not have to maintain the list yourself.

05

Agents

The root resource. An agent is an identity with a stable slug inside your workspace.

Verb and pathWhat it doesScope
POST /functions/v1AgentsCreates an agent. 201.agents:write
GET /functions/v1Agents?id=Reads one. 200.agents:read
GET /functions/v1Agents?limit=&environment=Paginated list with next_cursor. Maximum 100, default 25.agents:read
PATCH /functions/v1Agents?id=Updates name, role, mission, brand_code, public, autonomy_max and archived.agents:write
201
{
  "id": "6aadb7188529f7ee594cd071",
  "slug": "soporte",
  "name": "Soporte",
  "autonomy_max": "A2",
  "environment": "SANDBOX",
  "public": true,
  "did": "did:web:digitalcortex.tech:t:acme:sandbox:soporte",
  "credential_status": "none",
  "links": {
    "verify": "/verify/acme/soporte",
    "did_document": "/functions/agentDid?t=acme&slug=soporte"
  }
}

Changing the slug answers 409 slug_taken: the slug enters the DID and the public URL, and an identifier that changes is not an identifier.

06

Manifest

What the credential binds cryptographically. If the agent’s scope changes, the hash changes and the credential stops matching.

Schema published as an open specification: /spec · /schemas/agent-manifest-1.0.json. Required fields: name, role, operator, autonomyMax, skills[] and prohibited[]. prohibited is required even when empty: declaring what the agent may not do is half the value of the document.

Verb and pathWhat it doesScope
POST /functions/v1AgentManifest?id=Publishes a manifest version. 201.agents:write
GET /functions/v1AgentManifest?id=Reads the published version and its hash.agents:read
201
{
  "version": "1.0.0",
  "manifest_hash": "ecab5c35f2c6157d…",
  "published_at": "2026-09-18T22:30:00.000Z",
  "credential_invalidated": false
}

credential_invalidated: true warns that the new hash breaks the live credential and it has to be re-issued. Better said here than discovered in the public verifier.

400 scope_conflict
{
  "error": "scope_conflict",
  "message": "The manifest does not meet the minimum schema.",
  "conflicting": ["ticket.reply"],
  "details": [{ "field": "skills", "message": "'ticket.reply' is in both skills and prohibited." }]
}

The hash is computed over recursive canonical JSON — keys sorted by code point, no insignificant whitespace — not over a plain JSON.stringify. Both SDKs export that function (canonicalJson and manifest_hash) so you can compute it yourself before sending and never have to take our word for what got hashed.

07

Credentials

Issue, read and revoke. 90-day validity by default.

Verb and pathWhat it doesScope
POST /functions/v1CredentialsIssues. Requires Idempotency-Key. 201.credentials:issue
GET /functions/v1Credentials?id=Reads an issued credential.credentials:read
POST /functions/v1CredentialRevoke?id=Revokes. Requires reason. 200.credentials:revoke
201
{
  "id": "CRD-2026-0002",
  "credential_id": "urn:uuid:8d690c9b-36d6-4a20-a04a-7ef0424937d3",
  "jwt": "eyJhbGciOiJFUzI1NiIsInR5cCI6InZjK2p3dCIsImtpZCI6…",
  "did": "did:web:digitalcortex.tech:t:acme:sandbox:soporte",
  "environment": "SANDBOX",
  "issued_at": "2026-09-18T22:30:00.000Z",
  "expires_at": "2026-12-17T22:30:00.000Z",
  "status_list": { "id": "cs-acme-1", "index": 16202, "url": "/functions/statusList?list=cs-acme-1" },
  "verify_url": "/verify/acme/soporte",
  "seal_url": "/functions/agentSeal?t=acme&slug=soporte",
  "credential_url": "/functions/agentCredential?t=acme&slug=soporte",
  "warnings": []
}

Errors specific to issuance: 409 no_manifest (publish the manifest first), 409 manifest_mismatch, 400 scope_empty, 400 scope_conflict, 402 plan_limit_reached and 422 signing_unavailable. In real responses the link fields carry absolute URLs; they are shortened here so they fit.

200 on revocation
{
  "ok": true,
  "id": "CRD-2026-0002",
  "agent_slug": "soporte",
  "revoked_at": "2026-09-18T22:45:00.000Z",
  "status_list_version": 2,
  "revoked_count": 1,
  "visible_within_seconds": 60
}

08

Verification

Public and unauthenticated. This is the point of the product: anyone can check without an account with you or with us.

# Por espacio de trabajo y agente
curl -X POST 'https://digitalcortex.tech/functions/v1Verify' -H 'Content-Type: application/json' \
  -d '{"tenant": "acme", "agent": "soporte"}'

# Por credencial concreta
curl -X POST 'https://digitalcortex.tech/functions/v1Verify' -H 'Content-Type: application/json' \
  -d '{"jwt": "eyJhbGciOiJFUzI1NiIsInR5cCI6InZjK2p3dCI…"}'

# Comprobando ademas que una accion concreta esta en el alcance
curl -X POST 'https://digitalcortex.tech/functions/v1Verify' -H 'Content-Type: application/json' \
  -d '{"tenant": "acme", "agent": "soporte", "skill": "ticket.reply"}'
200
{
  "valid": true,
  "requires_approval": false,
  "checks": {
    "format":          { "ok": true, "detail": "JWS with typ=vc+jwt and kid present" },
    "keyDeclared":     { "ok": true, "detail": "The key is declared in the issuer's assertionMethod" },
    "signature":       { "ok": true, "detail": "Valid ES256 signature" },
    "issuerMatch":     { "ok": true, "detail": "Issued by Cosmolabs OU (did:web:digitalcortex.tech)" },
    "validity":        { "ok": true, "detail": "In force, expires in 90 days" },
    "revocation":      { "ok": true, "detail": "Not revoked" },
    "manifestBinding": { "ok": true, "detail": "Manifest 1.0.0 (ecab5c35f2…157d) unchanged" },
    "scope":           { "ok": true, "detail": "'ticket.reply' within scope" },
    "didDocument":     { "ok": true, "detail": "The published DID document declares this key" }
  },
  "subject":   { "did": "did:web:…", "name": "Soporte", "autonomyLevel": "A2" },
  "principal": { "legalName": "ACME Robotics S.L.", "registryCode": "B12345678", "jurisdiction": "ES" }
}

Unauthenticated, the limit is 60 requests per minute per IP. Today this endpoint verifies only credentials issued by did:web:digitalcortex.tech. A credential that fails verification answers 200 with valid: false: not being valid is an answer, not an error.

The eight steps, one by one, with what each does and does not prove, are in the open specification. You can run them yourself without going through our servers: the credential is a standard JWS and any JOSE library will verify it.

09

Public resources

Unauthenticated, cacheable and meant for a third party to check on their own.

PathWhat it servesCache
/verify/<tenant>/<agent>Human-readable page with the checks and the sealSPA
/.well-known/did.jsonThe issuer’s DID document, static and immutableLong
/functions/agentDid?t=&slug=The subject’s DID document, served live300 s
/functions/agentRegistry?t=Public registry of the workspace120 s
/functions/statusList?list=Signed revocation list, W3C Bitstring Status List v1.060 s + 300 s
/functions/agentCredential?t=&slug=The live token, application/vc+jwt300 s
/functions/agentSeal?t=&slug=The seal, as SVG60 s

A sandbox credential carries the DID …:t:<tenant>:sandbox:<slug>, the field environment with value SANDBOX inside the signed credential itself, and its seal comes out with a diagonal SANDBOX — NOT VALID band.

Calls without ?t= still work and resolve against the legacy workspace: those paths live inside credentials that are already signed and cannot be broken. If a slug existed in more than one workspace and ?t= is omitted, the answer is 409 asking for the parameter: another customer’s agent is never returned by accident.

10

Kill switch

A linked resource ties an agent’s credential to something the agent actually uses. On revocation your stop plan runs, and every attempt leaves signed evidence.

Stop methods

MethodWhat it doesWho executes itReversible?
STOP_ISSUANCEWe stop issuing tokens for that resourceUsYes
WEBHOOKHMAC-signed POST to your endpointYou, at your endPer your runbook
PROVIDER_APIDirect call to GitHub or SlackThe providerNo
MANUAL_RUNBOOKA task for a personA personDepends

Create, list and delete

curl -X POST 'https://digitalcortex.tech/functions/v1Bindings' \
  -H "Authorization: Bearer $CORTEX_API_KEY" -H 'Content-Type: application/json' \
  -d '{
    "agent_id": "<AGENT_ID>",
    "type": "API_KEY",
    "provider": "openai",
    "label": "The agent production key",
    "revocation_method": "WEBHOOK",
    "url": "https://yourservice.example/killswitch",
    "external_id": "key_abc123"
  }'

curl 'https://digitalcortex.tech/functions/v1Bindings?agent_id=<AGENT_ID>' -H "Authorization: Bearer $CORTEX_API_KEY"
curl -X DELETE 'https://digitalcortex.tech/functions/v1Bindings?id=…' -H "Authorization: Bearer $CORTEX_API_KEY"

external_id never travels in the clear. What reaches the public credential is ref = sha256(workspace_salt ‖ provider ‖ external_id): you can prove the correspondence when it suits you, and nobody else can infer the structure of your accounts.

Adding or removing a resource changes bindingsHash, which lives inside the manifest, so it changes manifestHash and invalidates the live credential. That is correct behaviour: if the agent’s hands change, its identity document changes. The verifier’s manifestBinding check spots it on its own.

Dry run

curl -X POST 'https://digitalcortex.tech/functions/v1BindingTest?id=…' -H "Authorization: Bearer $CORTEX_API_KEY"

It revokes nothing: the webhook receives X-Cortex-Dry-Run: 1, Slack receives auth.revoke with test=true, and GitHub is not called at all, because its endpoint has no test mode and revoking there is irreversible. An irreversible resource that has never passed a dry run is not executed: 409 dry_run_required. The real risk of this feature is not that it fails: it is believing you are covered while your endpoint has been down for three months.

Revoke with a cascade

curl -X POST 'https://digitalcortex.tech/functions/v1CredentialRevoke?id=CRD-2026-0002' \
  -H "Authorization: Bearer $CORTEX_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" -H 'Content-Type: application/json' \
  -d '{
    "reason": "The agent is withdrawn from service",
    "dry_run": false,
    "grace_seconds": 300,
    "approved_by": "another.person@yourcompany.example"
  }'
ParameterWhat it does
reasonMandatory. A revocation with no reason is not auditable.
dry_runFull rehearsal: nothing is revoked, neither the credential nor the resources.
grace_seconds0–900. The credential is revoked immediately; the cascade waits and can be held.
approved_byMandatory when there are irreversible resources, and different from the requester.
skip_cascadeRevokes the identity without touching a single resource.
200
{
  "ok": true, "id": "CRD-2026-0002",
  "revoked_at": "2026-09-19T09:12:00.000Z",
  "visible_within_seconds": 60,
  "run_id": "6ab2…", "cascade_status": "PENDING",
  "summary": { "total": 3, "ok": 0, "failed": 0, "skipped": 0 },
  "evidence_hash": null,
  "execute_after": "2026-09-19T09:17:00.000Z",
  "best_effort": true
}

Hold, release and the evidence

curl -X POST 'https://digitalcortex.tech/functions/v1RevocationHold?id=<run_id>' \
  -H "Authorization: Bearer $CORTEX_API_KEY" -H 'Content-Type: application/json' \
  -d '{"action": "hold"}'     # o "release" para ejecutar ya

curl 'https://digitalcortex.tech/functions/v1RevocationRuns?id=<run_id>'   -H "Authorization: Bearer $CORTEX_API_KEY"
curl 'https://digitalcortex.tech/functions/v1RevocationRuns?agent_id=…'    -H "Authorization: Bearer $CORTEX_API_KEY"

hold does not undo the revocation of the credential: that is immediate and public from the first second. What is held is the execution against your resources.

200
{
  "run_id": "6ab2…", "mode": "EXECUTE", "status": "COMPLETED",
  "results": [
    { "binding_id": "6ab1…", "provider": "github", "method": "PROVIDER_API",
      "status": "OK", "http_code": 202, "latency_ms": 310, "error_code": "" }
  ],
  "summary": { "total": 3, "ok": 3, "failed": 0, "skipped": 0 },
  "evidence_hash": "sha256:4e90…",
  "best_effort": true
}

No response body from any provider is stored: only the HTTP code, the latency and a normalised error class. A third-party error can carry sensitive material inside, and keeping it would create a problem where there was none.

What is promised and what is not

PromiseValueWhy it is defensible
We stop issuing tokens after revocation≤ 5 sIt is one record read in our own database
The public status list reflects the revocation≤ 60 s60 s cache with 300 s stale-while-revalidate, already deployed
Delivery attempt of the signed order≤ 10 sWith retries at 1 m, 5 m, 30 m, 2 h and 12 h, and a recorded acknowledgement
Auditable record of every attempt, with code and latency100%It stays in your workspace’s audit chain
All four numbers are ours and are measured on our own systems. They are declared inside the signed credential itself, with bestEffort true.

11

Webhooks and HMAC signature

The stop order your endpoint receives, and how to check that we signed it.

The request
POST /killswitch HTTP/1.1
Content-Type: application/json
X-Cortex-Signature: t=1789775186,v1=9cb63972…
X-Cortex-Event: agent.killswitch
X-Cortex-Dry-Run: 1          # only on dry runs

{"agent_did":"did:web:…","bindings":[{"id":"…","provider":"openai","ref":"sha256:…","type":"API_KEY"}],
 "credential_id":"CRD-2026-0002","dry_run":false,"event":"agent.killswitch",
 "issued_at":"2026-09-19T09:12:00.000Z","reason":"…","run_id":"…"}

The signature is v1 = HMAC-SHA256(secret, "<t>.<raw body>"), hexadecimal. Reject any t older than five minutes: the timestamp is inside the signature precisely so a signed body cannot be replayed. Answer 202 as soon as you have queued the work. If you do not answer 2xx we retry at 1 m, 5 m, 30 m, 2 h and 12 h, and after the fifth failure the attempt is recorded as DEAD_LETTER.

Verify the signature — Node 18 or newer
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifyCortexSignature(rawBody, header, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(
    String(header || '').split(',').map((kv) => kv.split('=').map((s) => s.trim()))
  );
  const t = Number(parts.t);
  const given = parts.v1;
  if (!t || !given) return false;
  if (Math.abs(Math.floor(Date.now() / 1000) - t) > toleranceSeconds) return false;

  const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
  const a = Buffer.from(expected, 'utf8');
  const b = Buffer.from(given, 'utf8');
  return a.length === b.length && timingSafeEqual(a, b);
}

// Express: use express.raw({ type: 'application/json' }) to get the body AS IT ARRIVED.
app.post('/killswitch', express.raw({ type: 'application/json' }), (req, res) => {
  const raw = req.body.toString('utf8');
  if (!verifyCortexSignature(raw, req.get('X-Cortex-Signature'), process.env.CORTEX_WEBHOOK_SECRET)) {
    return res.sendStatus(401);
  }
  const order = JSON.parse(raw);
  if (req.get('X-Cortex-Dry-Run') === '1') return res.sendStatus(202);
  queue.push(order);           // queue and answer: do not cut inside the handler
  res.sendStatus(202);
});
Verify the signature — Python 3.10 or newer
import hashlib, hmac, json, time

def verify_cortex_signature(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    try:
        parts = dict(p.strip().split("=", 1) for p in (header or "").split(","))
        t = int(parts["t"])
        given = parts["v1"]
    except (ValueError, KeyError):
        return False
    if abs(int(time.time()) - t) > tolerance:
        return False
    signed = f"{t}.".encode() + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, given)


# FastAPI
@app.post("/killswitch")
async def killswitch(request: Request):
    raw = await request.body()
    if not verify_cortex_signature(raw, request.headers.get("x-cortex-signature", ""), SECRET):
        raise HTTPException(status_code=401)
    if request.headers.get("x-cortex-dry-run") == "1":
        return Response(status_code=202)
    await queue.put(json.loads(raw))
    return Response(status_code=202)

Both SDKs ship the check already written, with constant-time comparison: verifyWebhookSignature in TypeScript and verify_webhook_signature in Python.

12

SDKs and OpenAPI

Two minimal libraries that wrap the query-parameter addressing and type the errors, plus an OpenAPI 3.1 description faithful to what actually exists.

PackageDependenciesStatus
@cortexseal/sdkNone. Uses the platform fetch.Source published under Apache-2.0. Not on npm yet.
cortex_sealhttpx.Source published under Apache-2.0. Not on PyPI yet.
TypeScript
import { CortexSeal, verify } from '@cortexseal/sdk';

const seal = new CortexSeal({ apiKey: process.env.CORTEX_API_KEY! });

const agent = await seal.agents.create({ slug: 'soporte', name: 'Soporte', role: 'First line', autonomyMax: 'A2' });
await seal.manifests.publish(agent.id, { version: '1.0.0', manifest });
const cred = await seal.credentials.issue({
  agentId: agent.id,
  scopeSkills: ['ticket.read'],
  scopeProhibited: ['payment.execute'],
});

const result = await verify({ tenant: 'acme', agent: 'soporte', skill: 'ticket.reply' });
if (!result.valid || result.requires_approval) throw new Error('out of scope, or needs approval');

await seal.credentials.revoke(cred.id, { reason: 'Withdrawn from service', skipCascade: true });
Python
import os
from cortex_seal import CortexSeal, verify

seal = CortexSeal(os.environ["CORTEX_API_KEY"])

agent = seal.agents.create(slug="soporte", name="Soporte", role="First line", autonomy_max="A2")
seal.manifests.publish(agent["id"], version="1.0.0", manifest=manifest)
cred = seal.credentials.issue(
    agent_id=agent["id"],
    scope_skills=["ticket.read"],
    scope_prohibited=["payment.execute"],
)

result = verify(tenant="acme", agent="soporte", skill="ticket.reply")
if not result["valid"] or result.get("requires_approval"):
    raise RuntimeError("out of scope, or needs approval")

seal.credentials.revoke(cred["id"], reason="Withdrawn from service", skip_cascade=True)
  • Both generate the Idempotency-Key header on their own when issuing and revoking.
  • Both raise typed errors carrying status, code, request_id and isRetryable, so you can paste the identifier into a ticket.
  • Both refuse to send a revocation with no reason before touching the network, and the Python one also catches a scope conflict or an incomplete manifest locally.
  • Neither verifies signatures locally: verify() calls the public endpoint. If you want to verify entirely on your own machine, the credential is a standard JWS and any JOSE library will do it — that is the point of using a standard.

OpenAPI 3.1 reference

The full description is at /openapi/cortex-seal-v1.yaml: every endpoint, every schema, every error code and the stop-order webhook. It describes the API as it is, with its query parameters instead of path segments and with a note on why: a specification that describes a nicer API than the real one is worse than no specification.

# Generar un cliente a partir de la descripcion OpenAPI
npx @openapitools/openapi-generator-cli generate \
  -i 'https://digitalcortex.tech/openapi/cortex-seal-v1.yaml' -g typescript-fetch -o ./cortex-client

# O simplemente mirarla
curl -s 'https://digitalcortex.tech/openapi/cortex-seal-v1.yaml' | head -40

13

Plan limits

The unit of price is an active agent in production, per month. There are no usage meters and no end-of-month reconciliation.

curl 'https://digitalcortex.tech/functions/v1Usage' -H "Authorization: Bearer $CORTEX_API_KEY"

It returns the plan limits, the period’s usage, the subscription state and whether issuance is blocked. Never complete payment identifiers.

PlanAgents in productionRequests/minRetentionSeats
FREE · Sandbox03090 days1
STARTER · Builder56090 days3
AGENCY · Agency30120395 days10
ENTERPRISENegotiated600730 daysUnlimited
Sandbox is unlimited on every plan. Public verification, unauthenticated, allows 60 requests per minute per IP. Prices are at [/seal/pricing](/seal/pricing).

14

Changelog

Semantic versioning. A change that breaks an existing client is a major version and is announced 30 days in advance.

DateVersionChanges
2026-09-19v1.0.0First public release of API v1: agents, manifest, credentials, verification, keys, linked resources, kill switch with cascade and evidence, signed webhooks, usage and plans. The OpenAPI 3.1 description, the manifest 1.0 schema and both SDKs are published too, all under open licences.
2026-09-18v1.0.0-rcMulti-workspace: public resources accept ?t=<tenant>, and paths without that parameter still resolve against the legacy workspace, because they live inside credentials that are already signed. /verify/<tenant>/<agent> becomes the canonical route and /verify/<agent> keeps answering 200.
2026-09-17v0.9manifestHash is now computed over the declared manifest rather than the agent’s whole record. The old calculation included fields the system itself wrote after issuance, so republishing changed the hash for reasons that had nothing to do with scope. Credentials issued under the old calculation still verify as before.

Further reading

Responsible for issuance: Cosmolabs OÜ · registry code 17106582 · Ahtri tn 12, 15551 Tallinn, Estonia · legal@digitalcortex.tech · Seal abuse reports: abuse@digitalcortex.tech · CORTEX Seal is a non-qualified trust service: it is not a qualified trust service nor an electronic seal within the meaning of Regulation (EU) No 910/2014.