Two regions. Your API key says which one to call.
EU, Frankfurt
https://eu.api.complyedge.io
Key starts with ce_eu_. Use it when your contracts or customers want data kept in the EU. Preselected at sign-up.
US, Oregon
https://api.complyedge.io
Key starts with ce_. Use it when your contracts or customers want data kept in the US.
You choose the region when you create your free API key: the sign-up form asks where your checks run, EU or US. The SDK reads the key and calls the right host. With curl or your own client, call the host of your key's region; a key sent to the other region is rejected. The region is where your checks run and your audit records are stored; prompt and output text is checked there and never stored. On a paid plan, after a clean phrase pass, the injection meaning check sends that text to OpenAI in the US when the month's model budget covers it. The wider legal review sends it only when you turn it on and the month's model budget covers it (use_semantic_fallback). It does not change which laws are checked: the jurisdiction field on each check does. No general law requires either region. US law does not require US storage, and GDPR allows transfers outside the EU with safeguards such as standard contractual clauses (Chapter V). The examples below use the EU host; with a US key, use api.complyedge.io instead.
Every check names its jurisdiction; the loaded rule bundle today runs EU AI Act rules (Article 5 and related), prompt-injection rules and, for US end users, SEC Regulation FD (selective disclosure), and the same call shape carries other jurisdictions as their rule sets ship.
Authenticated endpoints expect a Bearer token in the Authorization header (API key or, where noted, a dashboard JWT session):
Authorization: Bearer ce_your_api_key
No auth: GET /health, GET /v1/public/trust/{slug}, and GET /v1/public/badge/{slug}.svg.
Don't have a key yet? Sign in to the dashboard (free, email OTP).
/health
Readiness check. Returns 200 only when OPA and TrustLint can evaluate; anything else is 503. No auth required.
Response 200
{
"status": "healthy",
"version": "0.1.0",
"commit": "a1b2c3d",
"timestamp": "2026-06-20T00:00:00Z",
"services": {
"database": { "status": "healthy", "service": "audit_logger", "database": "dynamodb" },
"trustlint": { "engine": "trustlint", "healthy": true, "rules_loaded": 63 },
"opa": true,
"enhanced_compliance_engine": true,
"rule_manager": true
}
}
Response 503
{
"status": "unhealthy",
"version": "0.1.0",
"commit": "a1b2c3d",
"timestamp": "2026-06-20T00:00:00Z",
"services": { "database": false, "trustlint": { "engine": "trustlint", "healthy": false, "rules_loaded": 0 }, "opa": false, "enhanced_compliance_engine": false, "rule_manager": false },
"error": "Internal health check error"
}
/v1/check
Core compliance check. Phrase rules run first. On a paid plan a clean pass then runs an injection check when the month's model budget covers it, which can block. The wider review runs only when you turn it on and that budget covers it.
Request body
{
"text": "Score users based on their social behavior",
"agent_id": "my-agent",
"jurisdiction": "EU",
"direction": "output",
"context": { "user_id": "ref_5d81e2c4", "user_role": "analyst", "session_id": "sess_abc" },
"decision_context": {
"deciding_system": { "version": "gpt-5.4-2026-08-01" },
"purpose_ref": "purpose:support-replies",
"data_categories": ["contact"]
},
"use_semantic_fallback": false
}
agent_id: required. Identifier for the AI agent making the call.jurisdiction: optional. Where the end user is, not where your company is based: the EU AI Act applies when an AI system's output is used in the EU, wherever the provider or deployer is established (Art. 2(1)(c)). Defaults to EU. EU runs the EU AI Act rules and the personal-data and credential rules (email address, phone number, IBAN, card number, German, French, Spanish, Dutch and Italian national IDs, API keys and private keys): text containing one is blocked; US and US-* run the Regulation FD disclosure rule; every check runs prompt-injection detection. The bundle's loaded jurisdictions are listed by GET /v1/rules/info.direction: output (default), prompt, or action for an action the AI system is about to take.context: optional. user_id, user_role and session_id are written on the audit record; without them those columns are blank in your export. user_id and session_id are stored as keyed pseudonyms (p1:…) under your account's key, never as sent: send a stable identifier, never a name or email. user_role is stored as sent and must name a role, not a person. Never send a credential (session token, JWT, API key) in these fields; ComplyEdge replaces token-shaped values with redacted: and a hash before writing. See Record who each check acts for.decision_context: optional. What you know about the decision that ComplyEdge cannot, written to the audit record as ADCR 0.1.0 fields. A check without it is still decided and recorded. When you send it, deciding_system.version, purpose_ref and data_categories are required; every other key is optional, and a key left out is left out of the record, never filled in: deciding_system.version (the version of your AI system or model), purpose_ref (your reference to the purpose of the processing), data_categories (categories of personal data the decision used, from a fixed list; [] when none), correlation_id and subject (stored as keyed pseudonyms), subject_is_employee, works_agreement_ref, legal_basis (a provision: eli or url, and label) and source_ref (your locator of the text as a URI, such as ticket:88213, never the text). References must be opaque: an email address or a credential is refused with 422. A record meets ADCR class Record only when deciding_system.version, purpose_ref and data_categories are sent.use_semantic_fallback: optional (default false). On a Developer or Enterprise plan, after the phrase rules pass, the injection meaning check runs when the month's model budget covers it: it sends the text to OpenAI and can block, and a missing answer denies. If that budget is used up, the check does not run and the phrase rules decide. Set this true to also run the wider legal review. That review can block text the rules passed. When a rule blocks, and the hit is not a format such as an IBAN or a private key, a smaller model can lift the block only if every rule that fired is cleared. On the Free plan the flag does not turn the review on. If it is set, the response says "layer2_skipped": "plan". If it is left off and the month's model budget still covers the injection check, that field is null. If the budget is used up, the field is budget even when the wider review is off.use_case, lawful_basis, dataset_operation: optional. Article 5(1)(g) carve-outs: use_case: "law_enforcement" with lawful_basis: true applies the law-enforcement exception only on an account configured as a law enforcement authority, which ComplyEdge sets and the request never can. On any other account the claim is refused, the block stands, and the record lists it with reason law_enforcement_refused. dataset_operation: "labelling" or "filtering" applies the dataset-preparation exception, as asserted by the caller.Response 200
{
"event_id": "uuid-v4",
"allowed": false,
"violations": [{
"rule_id": "rego-art5-1c-001",
"rule_description": "Regulation (EU) 2024/1689, Article 5(1)(c): the placing on the market, the putting into service or the use of AI systems for the evaluation or classification of natural persons or groups of persons over a certain period of time based on their social behaviour or known, inferred or predicted personal or personality characteristics, with the social score leading to either or both of the following: (i) detrimental or unfavourable treatment of certain natural persons or groups of persons in social contexts that are unrelated to the contexts in which the data was originally generated or collected; (ii) detrimental or unfavourable treatment of certain natural persons or groups of persons that is unjustified or disproportionate to their social behaviour or its gravity;",
"severity": "critical",
"reason": "Remove any social scoring, citizen ranking, or behaviour-based classification that leads to detrimental treatment outside the original data context.",
"confidence": 1.0,
"text_excerpt": "Score users based on their social behavior"
}],
"latency_ms": 91,
"bundle_version": "sha256:42dfbd5ac9ad",
"bundle_digest": "42dfbd5ac9adccdc4052d6fdcb88bded9281dcccf961ba31171b235505104e34",
"evaluated_rules": ["rego-art5-1g-001", "rego-art5-1f-001", "rego-art5-1e-001",
"rego-art5-1d-001", "rego-art5-1h-001", "rego-art5-1c-001",
"rego-art5-1a-001", "rego-art5-1b-001"],
"engine_path": "opa",
"opa_latency_ms": 79.2,
"audit_logged": true,
"text_hash": "0aa4e756f4df3933cc48373c6338499228d50078cacbbdd9dde925221d2b02c5",
"text_hash_alg": "hmac-sha256-1",
"timestamp": "2026-07-22T20:30:00+00:00"
}
rule_description: the article citation when a phrase rule or the injection check fired. Not on an allow. A wider-review block is labeled LLM_LAYER2_BLOCK and that text is not an article citation.confidence: 1.0 when a phrase rule decides. Below 1.0 when a model decided: the injection meaning check on a paid plan when the month's model budget covers it, or the wider legal review when you turn it on and that budget covers it.engine_path: opa when only the phrase rules ran. hybrid when a model also ran on this check: the paid injection check after a clean pass when the month's model budget covers it, including when it allows the text, or the wider review when you turn it on and that budget covers it. fallback_block when the check could not finish, including a missing injection answer. The call is denied. llm is an allowed value and is not what this check writes.layer2_skipped: null when no model check was skipped. "plan": the Free plan was asked for the wider review, and it did not run. "budget": this month's model budget is used up (25% of what the plan pays after payment fees, resets on the 1st, UTC), including when the wider review is off. The injection check does not run and the phrase rules decide. "unavailable": the model cannot be priced, so it is not called. A pass is refused, and the violations include LLM_LAYER2_UNAVAILABLE, when the account settings or the model spend counter could not be read, and when the injection check ran and did not answer.overridden_hits: null unless Layer 2 lifted a rule block. Then one entry per rule: rule_id, the model's reason and its confidence. The same entry is written on the audit record.confirm_model: the model that checked the rule hits, when one did.opa_latency_ms: phrase-rule engine time. It does not include the injection check. latency_ms is the total, including the audit write.text_hash: keyed hash (HMAC-SHA-256) of the evaluated text under your account's own key, byte-identical to the audit entry, which the record's hash chain covers (raw text is not stored). The key never leaves ComplyEdge, so the value cannot be reversed by hashing guesses; to check a text against a record, call POST /v1/audit/verify. Art. 12-ready evidence for high-risk record-keeping from Dec 2027.text_hash_alg: hmac-sha256-1, HMAC-SHA-256 under your account key over commitment: followed by the text. Records written before the keyed hash was deployed have no text_hash_alg; their text_hash is a plain SHA-256 until they expire. POST /v1/audit/verify works on records of each kind.timestamp: UTC instant of evaluation, same value written on the audit entry.text_excerpt: optional. The portion of text that triggered the violation; may be null.Rule set mapping
rule_id: a phrase-rule ID belongs to the open ComplyEdge rule set, where each rule carries its citation and approval status. An injection block is llm-art15-ipi-*, not that published ID. LLM_LAYER2_BLOCK is the wider review and is not in that rule set. A phrase-rule ID usually names the provision too: rego-art5-1c-001 applies Regulation (EU) 2024/1689, Article 5(1)(c). Two IDs are kept from earlier versions and name a different provision than the rule now applies, so rely on the citation.bundle_digest: the content digest of the exact rule set that ran (bundle_version is its short form). It is written on the audit record, so a reviewer can tell which version of the rule set decided any record, and whether the rules changed between two records.Response headers
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 94
X-RateLimit-Reset: 2026-04-13T00:00:00+00:00
Errors
401: invalid or missing API key422: invalid request body429: rate limit exceeded. Header Retry-After is the whole seconds until the daily reset (00:00 UTC). Structured body:{
"error": "rate_limit_exceeded",
"limit": 1000,
"plan": "developer",
"reset_at": "2026-06-21T00:00:00+00:00",
"upgrade_url": "https://dashboard.complyedge.io/?intent=sign-in&upgrade=volume"
}
/v1/sandbox/check
Try a request against your tenant's enforcement without recording it. Same engines, same tenant settings and same verdict as POST /v1/check; what differs is everything after the decision: no audit entry, no usage metering, no telemetry, and no daily rate-limit count. Use it to test cases before wiring an integration, or to see what a rule catches. It never produces evidence: a sandbox decision has no record to export.
Authentication
API key or dashboard session token. The dashboard's "Try a request" panel calls this endpoint.
Request body
Identical to POST /v1/check: text, agent_id, optional jurisdiction, direction, use_semantic_fallback and the Article 5(1)(g) carve-out fields.
Response 200
{
"event_id": "uuid-v4",
"allowed": false,
"violations": [{ "rule_id": "rego-art5-1c-001", "rule_description": "Regulation (EU) 2024/1689, Article 5(1)(c): the placing on the market, the putting into service or the use of AI systems for the evaluation or classification of natural persons or groups of persons over a certain period of time based on their social behaviour or known, inferred or predicted personal or personality characteristics, with the social score leading to either or both of the following: (i) detrimental or unfavourable treatment of certain natural persons or groups of persons in social contexts that are unrelated to the contexts in which the data was originally generated or collected; (ii) detrimental or unfavourable treatment of certain natural persons or groups of persons that is unjustified or disproportionate to their social behaviour or its gravity;", "severity": "critical", "confidence": 1.0 }],
"latency_ms": 9,
"bundle_version": "sha256:42dfbd5ac9ad",
"bundle_digest": "42dfbd5ac9adccdc4052d6fdcb88bded9281dcccf961ba31171b235505104e34",
"evaluated_rules": ["rego-art5-1a-001", "rego-art5-1b-001", "rego-art5-1c-001", "rego-art5-1d-001", "rego-art5-1e-001", "rego-art5-1f-001", "rego-art5-1g-001", "rego-art5-1h-001"],
"engine_path": "opa",
"opa_latency_ms": 7.4,
"audit_logged": false,
"sandbox": true,
"text_hash": "0aa4e756f4df3933cc48373c6338499228d50078cacbbdd9dde925221d2b02c5",
"text_hash_alg": "hmac-sha256-1",
"timestamp": "2026-09-20T15:30:00+00:00"
}
audit_logged: always false. The event_id is not retrievable from GET /v1/audit/events.sandbox: always true, so a response can never be mistaken for a recorded decision.X-RateLimit-* headers: sandbox calls do not count against the plan's daily limit. They have their own daily cap (see Errors).Errors
401: invalid or missing API key or session422: invalid request body429: more than 60 sandbox calls in a minute for the tenant. Body { "detail": { "error": "sandbox_rate_limit_exceeded", "limit": 60, "window": "1 minute" } }, header Retry-After: 60.429: more than 1,000 sandbox calls for the tenant in one UTC day, on every plan. Body { "detail": { "error": "sandbox_daily_limit_exceeded", "limit": 1000, "window": "1 day (UTC)" } }, header Retry-After set to the seconds until midnight UTC.503: the engine could not evaluate, so no verdict was made. Body carries "error": "compliance_evaluation_unavailable", "audit_logged": false, "sandbox": true.SDKs
Both SDKs route check here with one flag; the result carries sandbox beside audit_logged.
# Python
result = ce.check("Score users based on their social behavior", sandbox=True)
result.blocked, result.sandbox, result.audit_logged # True, True, False
// TypeScript
const trial = await ce.check("Score users based on their social behavior", { sandbox: true });
trial.allowed, trial.sandbox, trial.auditLogged // false, true, false
MCP
The MCP servers expose the same call as the tool sandbox_check (arguments text, optional jurisdiction; returns BLOCKED or ALLOWED with rule ID and citation, audit_logged: false, sandbox: true). It is the one MCP tool that needs a key; the three offline TrustLint tools never do.
npx -y @complyedge/mcp or complyedge-mcp): set COMPLYEDGE_API_KEY in the server's environment. Without it the tool is not listed and the server stays fully offline.https://mcp.complyedge.io/mcp): always listed; send your key as the Authorization: Bearer <key> header on the MCP connection (your MCP client's per-server headers). The key is never a tool argument, never logged, and the hosted server keeps no keys.{
"mcpServers": {
"complyedge": {
"url": "https://mcp.complyedge.io/mcp",
"headers": { "Authorization": "Bearer ce_your_api_key" }
}
}
}
/v1/rules/info
Metadata about the currently-loaded rule bundle, filtered to your plan's accessible tiers.
Response 200
{
"bundle_id": "sha256:42dfbd5ac9ad",
"version": "sha256:42dfbd5ac9ad",
"generated_at": "2026-06-20T00:00:00Z",
"rule_count": 63,
"jurisdictions": ["EU", "GLOBAL", "US", "US-CA", "US-NY"],
"plan": "enterprise_gpai",
"accessible_tiers": ["community", "eu_ai_act", "gpai"]
}
/v1/metrics?hours=24
Compliance metrics for your tenant over a time window. Includes engine-path distribution, violation rates, and performance data.
Query parameters
hours: time window: 24 (1d), 168 (7d), or 720 (30d). Default: 24.Response 200
{
"calls_total": 1247,
"violations_prevented": 18,
"violation_rate": 1.44,
"opa_ratio": 0.7883,
"llm_ratio": 0.0,
"fallback_ratio": 0.0096,
"hybrid_ratio": 0.1700,
"trustlint_ratio": 0.0321,
"engine_path_counts": { "opa": 983, "llm": 0, "fallback_block": 12, "hybrid": 212, "trustlint": 40 },
"avg_cost_per_call": 0.00024,
"total_cost_estimate": 0.351,
"cost_breakdown": { "opa": 0.0, "llm": 0.0, "fallback_block": 0.001, "hybrid": 0.35, "trustlint": 0.0 },
"models_used": { "gpt-6-luna": 212 },
"latency_avg_ms": 21,
"latency_p50_ms": 18,
"latency_p95_ms": 48,
"latency_p99_ms": 87,
"latency_by_engine": { "opa": 8, "llm": 0, "hybrid": 180, "fallback_block": 2, "trustlint": 6 },
"top_violations": [{ "rule_id": "rego-art5-1a-001", "count": 8 }],
"unique_violation_types": 4,
"time_window_hours": 24,
"start_time": "2026-06-19T00:00:00Z",
"end_time": "2026-06-20T00:00:00Z"
}
A check today is counted under opa, hybrid, fallback_block, or trustlint. trustlint is a sensitivity check the rules decided with no model. hybrid is the paid injection check, and the wider review when it is on. The llm key stays for older rows. On a paid plan, when only the injection check ran, the model name is that check's model. When the wider review also ran, the name is the account model. The counts and latencies in this example are the field shape, not a live reading.
Errors
400: invalid hours value (must be 24, 168, or 720)401: invalid or missing API key/v1/metrics/summary?hours=24
The public trust snapshot plus the full per-tenant stats (engine paths, violations, latency) in one call, for buyer and auditor reviews. API key required.
Response 200 (excerpt)
{
"tenant_id": "uuid-v4",
"plan": "enterprise_gpai",
"time_window_hours": 24,
"trust": {
"enforcement_active": true,
"checks_24h": 42,
"articles_enforced": ["Article 5", "Article 50"],
"last_decision_at": "2026-04-17T12:00:00Z",
"display_name": "Acme Corp"
},
"calls_total": 42,
"violations_prevented": 3,
"violation_rate": 7.14,
"block_rate": 7.14,
"engine_path_counts": { "opa": 35, "hybrid": 7, "trustlint": 0 },
"top_violations": [{ "rule_id": "rego-art5-1c-001", "count": 2 }],
"latency_avg_ms": 24,
"latency_p95_ms": 51
}
/v1/public/trust/{slug}
Public
Public-safe enforcement stats behind the AI Trust Center and the Enforcement Seal. No API key. Tenant must opt in via PATCH /v1/tenant/trust. Returns 404 if slug is unknown or not enabled.
Response 200
{
"enforcement_active": true,
"checks_24h": 42,
"checks_30d": 128,
"eu_checks_24h": null,
"eu_checks_30d": 96,
"articles_enforced": ["Article 5", "Article 50"],
"articles_screened": ["Article 4", "Article 5", "Article 50", "Article 53"],
"frameworks_screened": ["EU AI Act", "GDPR"],
"display_name": "Acme Corp",
"website_url": "https://example.com",
"trust_json_url": "https://eu.api.complyedge.io/v1/public/trust/ivd",
"badge_svg_url": "https://eu.api.complyedge.io/v1/public/badge/ivd.svg"
}
eu_checks_24h and eu_checks_30d count the checks in the last 24 hours and 30 days that ran with jurisdiction EU. They are a subset of checks_24h and checks_30d. The seal names the EU AI Act only when they are above zero; the data region of your account plays no part.
A count is exact when it is 0 or at least 25. From 1 to 24 it is null: there was activity, and the exact number stays private, the same as on the trust page and the seal.
No tenant_id, email, plaintext, or time of any single check. Cached max-age=300. Rate-limited per IP. See the Enforcement Seal embed guide.
/v1/public/badge/{slug}.svg
Public
Embeddable ComplyEdge Enforcement Seal: coral hex glyph (logo mark) + live status dot + status labels such as EU AI Act · Enforced (when your checks ran under jurisdiction EU) or Enforced. Same trust snapshot as JSON.
#10b981)#f59e0b)#ef4444)Returns 404 if slug unknown or trust not enabled. Cache-Control: public, max-age=300. Embed guide →
/v1/tenant/trust
Read trust configuration and embed URLs for the authenticated tenant.
{
"public_slug": "ivd",
"trust_public_enabled": true,
"display_name": "IVD Framework",
"website_url": "https://ivdframework.dev",
"trust_json_url": "https://api.complyedge.io/v1/public/trust/ivd",
"badge_svg_url": "https://api.complyedge.io/v1/public/badge/ivd.svg",
"preview_slug": "ivd",
"embed_badge_svg_url": "https://api.complyedge.io/v1/public/badge/ivd.svg"
}
trust_json_url, badge_svg_url: null until the page is published.suggested_website_url: https:// plus the company domain of your sign-in email, used while no website was ever saved; null for shared mailbox providers such as gmail.com, and once a website was removed.preview_slug, embed_badge_svg_url: set even while unpublished, so the embed snippet can be copied before going live.This is a live tenant, not a placeholder: both public URLs above resolve right now.
/v1/tenant/trust
Publish or update the public trust surface (AI Trust Center page and Enforcement Seal) for your tenant. Every field is optional; only the fields sent change. Slug must be unique (1-64 lowercase letters, numbers or hyphens, starting and ending with a letter or number). display_name is the tenant's company name, the same one shown in the dashboard profile: setting it here renames the tenant everywhere; a blank name is refused. Send website_url as "" to remove the website. Publishing when no website was ever saved stores the suggested one.
{
"public_slug": "acme",
"trust_public_enabled": true,
"display_name": "Acme Corp",
"website_url": "https://acme.example"
}
/v1/account/api-keys
List API keys for the authenticated tenant (JWT session). Each key carries a non-secret id for revocation plus its prefix and suffix; the value itself is shown once at creation and is not returned here for keys issued since that change.
Auth note: This endpoint uses a JWT session token (issued at login), not a Bearer API key. Pass it the same way: Authorization: Bearer <session_token>.
Response 200
{
"keys": [
{
"id": "k_9f3c2a71",
"key": "",
"key_prefix": "ce_a1b2c3d4e",
"key_suffix": "z9y8",
"value_retrievable": false,
"label": "Production",
"created_at": "2026-01-10T12:00:00Z",
"last_used": "2026-04-12T00:15:23Z",
"total_calls": 1247,
"is_active": true
}
]
}
key: always an empty string. A key's value is shown once, when it is created. Identify keys by key_prefix and key_suffix.total_calls: the tenant's calls over the last 30 days, repeated on every key.Errors
401: invalid or expired session token500: internal error retrieving keys/v1/account/api-keys
Generate an additional API key. The raw key is returned once only: save it immediately. Existing keys keep working. To rotate without downtime: create, switch your integration to the new key, then revoke the old one. The dashboard's Rotate key runs that sequence for you and asks about the previous key inside the reveal.
Auth note: Bearer API key or dashboard session JWT. Same as revoke.
Request body
{ "name": "CI/CD key" }
Response 200
{
"api_key": "ce_newkeyvalue_shown_once",
"id": "k_4d8e1b02",
"key_prefix": "ce_newkeyval",
"key_suffix": "ONCE",
"name": "CI/CD key",
"message": "Save the API key. It will not be shown again."
}
/v1/account/api-keys/{key_id}
Revoke an API key. The key must belong to the authenticated tenant. key_id is the non-secret id from the list endpoint, or the key value itself for SDK and CLI callers.
Auth note: Bearer API key or dashboard session JWT.
Response 200
{ "status": "revoked", "key_id": "k_9f3c2a71" }
When a key value was passed instead of an id, key_id comes back truncated: the full value is never echoed.
Errors
403: cannot revoke another tenant's key404: key not found/v1/account/usage?start=YYYY-MM-DD&end=YYYY-MM-DD
Daily usage metrics for your tenant within a date range.
Auth note: This endpoint uses a Bearer API key (not a JWT session token). Pass it as: Authorization: Bearer <api_key>.
Response 200
{
"tenant_id": "uuid-v4",
"start": "2026-04-01",
"end": "2026-04-12",
"total_calls": 1247,
"total_violations": 18,
"daily": [
{ "date": "2026-04-01", "total_calls": 102, "violations_detected": 2 },
{ "date": "2026-04-02", "total_calls": 98, "violations_detected": 1 }
]
}
/v1/audit/events
Retrieve compliance audit events for your tenant, newest first.
Query parameters
limit: max events (default 100, max 1000)offset: pagination offset (default 0)start_date: ISO 8601 date filter (inclusive, from 00:00:00 UTC)end_date: ISO 8601 date filter (inclusive, through 23:59:59 UTC)agent_id: filter by agent identifierallowed: filter by outcome: true or falseExample request
curl -s "https://eu.api.complyedge.io/v1/audit/events?limit=10&allowed=false" \
-H "Authorization: Bearer ce_eu_your_api_key"
Response 200
{
"events": [
{
"event_id": "uuid-v4",
"timestamp": "2026-04-12T14:23:01Z",
"allowed": false,
"jurisdiction": "EU",
"violations": [{
"rule_id": "rego-art5-1c-001",
"rule_description": "Regulation (EU) 2024/1689, Article 5(1)(c): the placing on the market, the putting into service or the use of AI systems for the evaluation or classification of natural persons or groups of persons over a certain period of time based on their social behaviour or known, inferred or predicted personal or personality characteristics, with the social score leading to either or both of the following: (i) detrimental or unfavourable treatment of certain natural persons or groups of persons in social contexts that are unrelated to the contexts in which the data was originally generated or collected; (ii) detrimental or unfavourable treatment of certain natural persons or groups of persons that is unjustified or disproportionate to their social behaviour or its gravity;",
"severity": "critical",
"reason": "Remove any social scoring, citizen ranking, or behaviour-based classification that leads to detrimental treatment outside the original data context.",
"confidence": 1.0
}],
"latency_ms": 91,
"agent_id": "bot-1"
}
],
"total": 1,
"limit": 10,
"offset": 0
}
total is the count of events in the current response page, not the full record count. Use offset to paginate.
Errors
401: invalid API key422: invalid query parameters/v1/audit/violations
Retrieve only violation events, optionally filtered by user ID or role. Useful for compliance investigations.
Query parameters
user_id: filter by one user. Send the identifier as you send it in context, or its stored p1: pseudonym; ComplyEdge pseudonymises it with your account's key before comparing.user_role: filter by role (e.g. financial_analyst)limit: max results (default 100, max 1000)Response 200
{
"violations": [ /* same shape as audit/events items */ ],
"total_count": 5,
"filters": { "user_id": "ref_5d81e2c4", "user_role": null }
}
/v1/audit/user-analytics
Calls and violations by role. There is no per-person ranking: a list of which employees break rules most is the per-person monitoring that works councils and data protection officers refuse. To look at one person on purpose, use GET /v1/audit/violations?user_id=.
Response 200
{
"user_roles": [
{ "user_role": "financial_analyst", "total_calls": 240, "violations": 12, "violation_rate": 0.05 }
],
"total_violations": 18,
"unique_violating_users": 3,
"unique_violating_roles": 2,
"role_violations": {
"financial_analyst": { "violations": 12, "unique_users": 3 }
},
"deprecations": [
{ "field": "user_violations", "status": "removed", "replacement": "user_roles for risk by role; /v1/audit/violations?user_id= for one person" },
{ "field": "role_violations", "status": "deprecated", "replacement": "user_roles" }
]
}
user_violations was removed in this release. role_violations stays for one release; read user_roles.
/v1/audit/legal-hold
Enterprise
Place or lift a legal hold over a range of your write-time chain (ADCR REC-19). Writes one legal_hold record; covered sequence numbers keep DynamoDB TTL off until the hold is lifted.
POST /v1/audit/legal-hold
{ "action": "placed", "hold_id": "hold:litigation-1", "from_seq": 1, "to_seq": 40, "reason_code": "litigation.reasonably_anticipated" }
200
{ "event_id": "uuid-v4", "event_type": "legal_hold" }
/v1/audit/contest
Enterprise
Record a person's request for human intervention, to express a view, for an explanation, or to contest a decision (ADCR RGT-03). An answer is a further contest record for the same decision_ref.
POST /v1/audit/contest
{ "agent_id": "assistant-hr", "deciding_system": { "version": "2026.10.1" },
"decision_ref": "uuid-of-decision", "request_kind": "contest",
"requested_at": "2026-10-16T12:00:00Z" }
200
{ "event_id": "uuid-v4", "event_type": "contest" }
/v1/audit/notice
Enterprise
Record that a notice, explanation or statement of reasons was issued to the person (ADCR RGT-07). The body holds references and language, never the notice text.
POST /v1/audit/notice
{ "agent_id": "assistant-hr", "deciding_system": { "version": "2026.10.1" },
"notice_kind": "explanation", "decision_ref": "uuid-of-decision",
"channel_ref": "portal:messages", "language": "en",
"issued_at": "2026-10-16T12:30:00Z", "statement_ref": "statement:expl-1" }
200
{ "event_id": "uuid-v4", "event_type": "notice" }
/v1/audit/incident
Enterprise
Record a risk, substantial modification, serious incident or suspension for a deciding system (ADCR RGT-06).
POST /v1/audit/incident
{ "agent_id": "assistant-hr", "deciding_system": { "version": "2026.10.1" },
"incident_kind": "serious_incident",
"detected_at": "2026-10-16T08:00:00Z", "reported_at": "2026-10-16T09:00:00Z",
"report_ref": "report:2026-001" }
200
{ "event_id": "uuid-v4", "event_type": "incident" }
/v1/audit/opt-out
Enterprise
Record that a lawful user opt-out suppressed recording (ADCR REC-18).
POST /v1/audit/opt-out
{ "agent_id": "assistant-hr", "deciding_system": { "version": "2026.10.1" },
"decision_ref": "uuid-of-decision", "reason_code": "lawful_opt_out" }
200
{ "event_id": "uuid-v4", "event_type": "opt_out" }
/v1/audit/classification
Enterprise
Record an Article 6(3)/(4) high-risk classification assessment (ADCR REC-17). The body holds the result and a reference to your assessment, never the assessment text.
POST /v1/audit/classification
{ "agent_id": "assistant-hr", "deciding_system": { "version": "2026.10.1" },
"classification_result": "not_high_risk",
"assessment_ref": "assessment:art6-3-2026" }
200
{ "event_id": "uuid-v4", "event_type": "classification" }
/v1/audit/fallback
Enterprise
Record that a check was skipped, degraded or replaced by a fallback as its own chain row (ADCR REC-18). A fallback object may still appear on a decision record instead.
POST /v1/audit/fallback
{ "agent_id": "assistant-hr", "deciding_system": { "version": "2026.10.1" },
"kind": "skipped", "reason_code": "engine_unavailable" }
200
{ "event_id": "uuid-v4", "event_type": "fallback" }
/v1/audit/verify
Check that a recorded decision was about exactly this text. The record's text_hash is keyed with your account's key, which never leaves ComplyEdge, so the comparison runs here. The text is used for this request only: it is not stored and not logged.
This is how you match a record to your own copy of the prompt or answer in an audit or a complaint; How audit reconciliation works walks through it end to end.
POST /v1/audit/verify
{ "event_id": "uuid-v4", "text": "Score users based on their social behavior" }
200
{ "event_id": "uuid-v4", "match": true, "text_hash_alg": "hmac-sha256-1" }
404: no record with that event_id in your accountmatch: false also when the account's key was destroyed (crypto-shred): that is what destroying it is for. The response then carries key_shredded_at (ISO 8601), so a destroyed key is never mistaken for a different text, and an export of that account names the same date in audit_key_shredded_at/v1/audit/pseudonym
The stored form of one identifier, to find one person's records in an export. Computed with your account's key; the key is never returned.
POST /v1/audit/pseudonym
{ "field": "user_id", "value": "ref_5d81e2c4" }
200
{ "field": "user_id", "pseudonym": "p1:9c2e41d07a5b3f68e1c04d92b7a6f315" }
field: user_id or session_id409: the account has no audit key yet (no record written since this release) or its key was destroyed/v1/audit/export
Audit trail export as structured JSON: every compliance event in a date range. Each event carries a statute citation when a phrase rule or the injection check fires. A wider-review block does not cite an article. It also carries an art12_purposes linkage naming which Article 12(2) record-keeping purpose(s) the entry is shaped for (Art. 12 high-risk duties bind from Dec 2027: readiness field, not a live mandate claim). Every plan can download its own records. The Enterprise evidence pack adds the write-time hash chain (chain_seq, chain_prev, chain_hash on every chained record), the timestamp anchors, the export-time chain_head, and the evidence-completeness attestation.
Plan behaviour
Every record, on every plan, is written into its account's hash chain and carries the chain_ fields it was written with (chain_seq, chain_prev, chain_hash, chain_alg, chain_status). Free and Developer exports stop there, with attestation: "none". Enterprise receives the evidence pack described below. Response also includes retention_days and retention_note.
Query parameters
start_date: required. YYYY-MM-DD, inclusive (from 00:00:00 UTC), or an ISO 8601 instant with offset, such as 2026-04-01T00:00:00-03:00 for your local midnight.end_date: required. YYYY-MM-DD, inclusive (through 23:59:59 UTC). Same day as start_date returns that full day. An ISO 8601 instant is used as given. A start after the end returns 400.format: optional. complyedge (default), the shape below, or adcr: the same records as an ADCR 0.1.0 export, on every plan; the timestamps over the chain are included on Enterprise. See ADCR format.Response 200
{
"tenant_id": "uuid-v4",
"plan": "enterprise_gpai",
"start_date": "2026-04-01",
"end_date": "2026-04-30",
"total_events": 1247,
"events": [
{
"event_id": "evt_…",
"timestamp": "2026-04-17T12:00:00Z",
"text_hash": "64-char-hex-hmac-sha256",
"text_hash_alg": "hmac-sha256-1",
"allowed": false,
"art12_purposes": [
{"ref": "Article 12(2)(a)", "purpose": "identifying situations that may result in the AI system presenting a risk…"},
{"ref": "Article 12(2)(b)", "purpose": "facilitating the post-market monitoring referred to in Article 72"},
{"ref": "Article 12(2)(c)", "purpose": "monitoring the operation of high-risk AI systems referred to in Article 26(5)"}
],
"chain_seq": 1247,
"chain_prev": "64-char-hex-sha256",
"chain_hash": "64-char-hex-sha256",
"chain_alg": "sha256-chain-1",
"chain_status": "chained",
"chain_link": "64-char-hex-sha256"
}
],
"write_time_chain": {
"algorithm": "sha256-chain-1",
"chained_rows": 1247,
"pre_chain_rows": 0,
"first_seq": 1,
"last_seq": 1247,
"chain_since": "2026-04-01T00:00:03Z"
},
"anchors": [
{
"statement": "{\"account_id\":\"uuid-v4\",\"chain_alg\":\"sha256-chain-1\",\"chain_hash\":\"64-char-hex-sha256\",\"chain_seq\":1247}",
"statement_sha256": "64-char-hex-sha256",
"chain_seq": 1247,
"tsa_name": "name of the qualified TSA",
"token_der_b64": "base64 RFC 3161 TimeStampResp",
"anchored_at": "2026-04-30T23:00:12Z"
}
],
"anchor_coverage": {"anchors_in_file": 30, "anchored_through_seq": 1247, "newest_anchor_seq": 1247, "unanchored_rows": 0},
"chain_head": "64-char-hex-sha256",
"chain_algorithm": "sha256-v1",
"evidence_completeness": {
"required_fields": ["text_hash", "agent_id", "timestamp"],
"complete": 1247,
"incomplete": 0
},
"retention_days": 184,
"retention_note": "Records expire at retention_days after they are written. A range that predates the retention window returns no events; that is expiry, not absence of activity.",
"export_note": "Input text stored only as a keyed HMAC-SHA-256 (text_hash_alg; records without it are plain SHA-256): plaintext never persisted."
}
Enterprise evidence-pack shape above. Free/Developer omit chain_head, chain_link, write_time_chain, anchors and evidence_completeness (the records keep their chain_ fields, as above), and set attestation to "none".
Sample file: /docs/sample-audit-export.json. It is 23 records from an August 2026 run, with the tenant, key and user identifiers redacted, in the format used before this release: text_hash is a plain SHA-256 with no text_hash_alg, and it carries only the export-time chain (chain_link, chain_head), with no write-time chain fields, anchors or completeness block. verify_audit_export_chain() returns true on it, and python -m audit_export verify reports its 23 rows as pre_chain with the verdict unverified, which is the correct answer for records older than the chain. It will be replaced by a sample in the current format once one exists.
ADCR format (format=adcr)
One JSON document, {"envelope": {...}, "records": [...]}, as the ADCR 0.1.0 Rulebook defines it (EXP-01). Each record is the stored row as an ADCR record, the exact bytes its chain hash covers, with its chain object (seq, prev, hash, alg), or chain_status: "pre_chain" when it was written before the chain existed. The envelope carries the period, the clock and its tolerance, the chain summary and anchor_coverage; on Enterprise it also carries a completeness attestation, and the timestamps inside the range with their tokens. Without timestamps the verifier still checks every record and link, reports no_anchor and returns unverified. How each field is filled is on the producer profile. Check the file offline with the Rulebook's reference verifier: python3 -I conformance/adcr_verify.py export.json --trust-anchors anchors.pem.
Write-time verification (sha256-chain-1)
Every record is appended to its tenant's chain as it is written: chain_seq counts from 1 with no gaps, chain_prev is the previous record's hash, and chain_hash = SHA-256(chain_prev + JCS(record)), where the record is the row as an ADCR 0.1.0 record without its chain fields and JCS is the RFC 8785 canonical form (ADCR CHN-02). The chain is written in the same transaction as the record: if it cannot be written, neither is the record. Once an hour, for every account whose chain moved, the newest chain_seq and chain_hash are sent, with no record content, to the RFC 3161 timestamp authority the service is configured with, and the signed token is stored under S3 Object Lock in COMPLIANCE mode. The hourly job has run in both regions since 22 October 2026. An export whose range has no anchor still reports no_anchor with the verdict unverified, while still checking every chain link. Each anchor names its provider in tsa_name, so you can check its status on the EU Trusted List yourself; a qualified electronic time stamp carries a presumption of accuracy of the date and time and of the integrity of the data under eIDAS Article 41(2). When anchors exist, the export carries them inside its range, and anchor_coverage states how far they reach. A failed anchor never blocks a decision or an export; records newer than the newest anchor are reported as unanchored_tail, not as verified.
chain_hash; a mismatch is an edit. Check chain_seq is dense; a gap is a removed record; a duplicate or a chain_prev that is not the previous hash is an insertion or reorder.statement names the hash the export carries at that chain_seq, then check the RFC 3161 token covers SHA-256(statement) and is signed by the provider (its root certificate, not ours).chain_status: "pre_chain" predate the chain and are reported, never claimed.Offline, with no ComplyEdge credentials: python -m audit_export verify export.json --tsa-root provider-certificate.pem, run from services/api in the open-source repository where audit_export.py lives, prints every finding on its own axis (hash_mismatch, sequence_gap, link_break, truncated_tail, anchor_mismatch, anchor_token_signature_invalid, pre_chain, unanchored_tail) and exits 0 verified, 1 failed, 2 unverified. The signature check runs openssl ts -verify (OpenSSL 1.1 or later) against the provider certificate you pass, taken from the EU Trusted List, at the time the token was issued; everything else is the standard library. Without a usable openssl the verdict is unverified, never failed. Certificate revocation is not checked.
Export-time chain (sha256-v1, still present)
Each event also includes a chain_link computed when the file is built, in chronological order. It shows the file was not altered after export; the write-time chain above is what shows the store was not altered before it:
timestamp, then event_id.chain_link = SHA-256(prev_link + canonical_json(event)) (sorted keys, no whitespace).chain_head is the final link. Recompute and compare: any altered, removed, or reordered event breaks the chain.Reimplement these three steps in your compliance toolchain, or run verify_audit_export_chain() from the open-source audit_export.py on GitHub.
/v1/assessment/pre-deployment
Assess an AI system configuration against EU AI Act requirements before deployment. Returns a risk tier, compliance score, and specific gaps to address.
Request body
{
"system_prompt": "You are a financial advisor assistant...",
"model_config": {
"provider": "openai",
"model_id": "gpt-4o",
"temperature": 0.7
},
"agent_pipeline": {
"tools": ["web_search", "code_executor"],
"memory": true,
"autonomy_level": "full",
"human_oversight": false
},
"jurisdiction": "EU"
}
Response 200
{
"compliance_score": 0.72,
"risk_tier": "high",
"violations": [
{
"rule_id": "assess-pipeline-001",
"article": "Article 14",
"description": "High-autonomy AI system without human oversight mechanism",
"required_action": "Implement human oversight per Article 14. High-risk AI systems must allow human intervention."
}
],
"required_disclosures": [
"AI system must identify itself as AI to end users"
],
"eu_ai_act_category": "essential-services",
"estimated_deadline": "2027-12-02"
}
eu_ai_act_category is the matched Annex III category slug (one of biometric-identification, critical-infrastructure, education-vocational, employment-workers, essential-services, law-enforcement, migration-asylum, justice-democracy). An unacceptable tier returns prohibited, and where no category matches it returns general-purpose.
estimated_deadline is the date the obligations for that category apply from. Annex III high-risk categories return 2 December 2027, since Regulation (EU) 2026/1744 postponed the Chapter III high-risk regime from 2 August 2026 (that superseded date is literal on purpose: it is legislative history, not a live milestone). An unacceptable tier returns 2 February 2025, the date the Article 5 prohibitions took effect. A minimal tier returns an empty string.
/v1/assessment/gpai-training
Enterprise
Article 53 GPAI model documentation check. Checks the details a general-purpose AI model provider must document under Article 53 (data sources, copyright policy, data cutoff) and returns each gap. It is an API check, not a service.
Enterprise plan required
This endpoint returns a 402 on free or developer plans. Upgrade to Enterprise to enable it on your account.
Request body
{
"model_name": "my-foundation-model",
"provider": "internal",
"version": "1.0",
"training_data_sources": ["web_crawl", "licensed_books"],
"open_source": false,
"systemic_risk": true,
"jurisdictions": ["EU"]
}
model_name: required.provider: required.Response 200
{
"art53_compliance_status": "non-compliant",
"applicable_obligations": ["53(1)(a)", "53(1)(b)", "53(1)(c)", "53(1)(d)"],
"open_source_exemption_applied": false,
"systemic_risk_strips_exemption": false,
"training_summary_checklist": [
{
"field": "model_identification",
"present": true,
"value": "my-foundation-model v1.0 by internal",
"gap": null,
"required_for": ["53(1)(a)", "53(1)(d)"]
},
{
"field": "data_sources",
"present": true,
"value": "2 source(s)",
"gap": null,
"required_for": ["53(1)(d)"]
},
{
"field": "copyright_compliance",
"present": false,
"value": null,
"gap": "Document the copyright compliance policy: how Article 4(3) opt-outs are detected and honoured.",
"required_for": ["53(1)(d)"]
}
],
"gaps_count": 3,
"estimated_documentation_effort_hours": 10,
"penalty_exposure": "Up to EUR 15,000,000 or 3% of total worldwide annual turnover, whichever is higher (EU AI Act Article 101).",
"ai_office_template_status": "Aligned to Commission Template Sections 1-3 (C(2025) 5235 final, 24 Jul 2025). Maps assessable API input fields to the mandatory Art 53(1)(d) public summary structure.",
"ai_office_template_source": "Commission C(2025) 5235 final (24 Jul 2025)",
"rule_id": "EU_AI_ACT_ART53_GPAI_TRAINING_SUMMARY_001",
"citation": "EU AI Act Article 53(1)(d), Regulation (EU) 2024/1689"
}
Errors
402: enterprise plan required422: missing model_name or provider/v1/sensitivity/detect
Proactive sensitivity detection for user input: analyzes text before it reaches the model. Detects PII, regulated data, and compliance-sensitive patterns, and returns an intervention action with guidance. The deterministic TrustLint rules always run. They do not run OPA. A model runs only on a paid plan when the account's wider legal review is on and the month's model budget covers it, and only after those rules come back clean. If that budget is used up, it does not run. Otherwise the answer is the TrustLint verdict. The TypeScript SDK calls this through ce.detectSensitivity(). It does write an audit record, attributed from context.user_id, user_role and session_id. Use POST /v1/check (SDK ce.check()) for the EU AI Act check. Phrase rules always run. On a paid plan the injection check can also block when the month's model budget covers it.
Request body
{
"input_text": "My SSN is 123-45-6789 and I need a loan",
"agent_id": "loan-assistant-v2",
"context": {
"detected_age": null,
"interaction_type": "chat",
"platform_type": "financial",
"user_jurisdiction": "EU",
"user_id": "cust_42",
"user_role": "customer",
"session_id": "conv_abc"
},
"conversation_history": [
{ "role": "user", "content": "I need help with my application" }
]
}
input_text: required. Max 10,000 characters.agent_id: optional. Defaults to null.context: optional. Platform and user context for richer analysis. user_id, user_role and session_id are written on the audit record. Token-shaped values are replaced with redacted: and a hash.conversation_history: optional. Prior turns for progressive risk scoring.Response 200
{
"event_id": "uuid-v4",
"detections": [
{
"data_type": "government_id",
"confidence": 0.98,
"regulation": "GDPR",
"excerpt": "123-45-6789",
"risk_level": "critical"
}
],
"overall_risk_score": 0.91,
"applicable_regulations": ["GDPR", "EU AI Act"],
"progressive_risk_score": 0.94,
"detection_method": "trustlint",
"intervention": {
"action": "BLOCK",
"message": "This input contains sensitive personal data that cannot be processed.",
"alternatives": [
{
"message": "Please describe your situation without including personal identifiers.",
"explanation": "Removing the government ID protects your privacy and avoids processing the identifier."
}
],
"allow_override": false,
"compliance_requirements": ["GDPR Art. 9: sensitive data prohibition"]
},
"processing_time_ms": 142
}
intervention.action: one of: ALLOW, WARN, WARN_AND_BLOCK, BLOCK, REQUIRE_PARENTAL_CONSENT, REDIRECT_SECURE_CHANNEL, PROGRESSIVE_WARNING.intervention: may be null when action is ALLOW.Errors
401: invalid or missing API key429: rate limit exceeded (shared daily counter with /v1/check)422: invalid request body