Block AI requests that break EU law, and log every check with the rule, the law and the time, never the prompt or the AI's answer.
https://eu.api.complyedge.io and its key starts with ce_eu_. US (Oregon) calls https://api.complyedge.io and its key starts with ce_. The SDK reads the key and picks the host. Setting COMPLYEDGE_API_URL does not change the account's region. 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 decide 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). Choose the region your own contracts or customers ask for. The examples below use the EU host.Install the SDK for your language. The examples on this page need Python complyedge 0.2.20 or newer, or Node.js @complyedge/sdk 0.2.6 or newer: older versions do not send user_id or session_id, so those fields stay blank on your audit records.
Python
pip install "complyedge>=0.2.20"
Node.js
npm install @complyedge/sdk@^0.2.6
Wrap any AI function with the @compliance_check decorator:
from complyedge import compliance_check
@compliance_check(jurisdiction='EU')
def process_ai_output(text: str) -> str:
return text
# Try a prohibited input
# raises ComplianceError; firing rule is on exception.violations[0].rule_id
process_ai_output('Score users based on their social behavior')
The decorator reads COMPLYEDGE_API_KEY from the environment automatically. It calls the ComplyEdge API before returning, raising a ComplianceError if the text triggers a prohibited-practice rule. The firing rule ID is on exception.violations (the message lists evaluated rule IDs, not only the one that fired).
The decorator fails open by default. Without COMPLYEDGE_API_KEY, or when the API cannot be reached, the function runs unchecked and nothing is recorded; only a warning is logged. Set COMPLYEDGE_FAIL_MODE=closed to raise ComplianceUnavailableError instead when the API cannot be reached.
The TypeScript SDK calls the same /v1/check endpoint as Python. Phrase rules run on every check. 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. The decision is written to the audit record (joined to your account's hash chain as it is written, exported with the chain on Enterprise; Art. 12-ready record-keeping for high-risk duties from Dec 2027, deferred there by Reg (EU) 2026/1744: not a live Art. 12 mandate today).
import { ComplyEdgeClient } from '@complyedge/sdk';
const ce = new ComplyEdgeClient({
apiKey: 'ce_your_api_key', // must be passed explicitly, not read from env
jurisdiction: 'EU',
});
const result = await ce.check(
'Score users based on their social behavior',
{ direction: 'prompt' },
);
// keep the eventId next to your own copy of the text
appLog.info({ prompt: 'Score users based on their social behavior', complyedgeEventId: result.eventId });
if (!result.allowed) {
console.error('Blocked:', result.violations[0].ruleId);
console.error('Audit event:', result.eventId, '| logged:', result.auditLogged);
}
Want to try cases before anything is recorded? Pass sandbox=True (Python) or { sandbox: true } (TypeScript): the same rules and verdict through POST /v1/sandbox/check, with no audit entry and no usage, and nothing counted against your plan's limits. The sandbox has its own cap: 60 checks a minute and 1,000 a day per account. The dashboard's "Try a request" panel and the MCP tool sandbox_check use the same endpoint.
The TypeScript SDK returns a ComplianceResult with camelCase field names over the same payload as the Python response. See Section 5 for both shapes. Use direction: 'prompt' for user/agent input and 'output' for model output (SDK accepts only those two values). Legacy sensitivity detection stays available as ce.detectSensitivity(), which does not run OPA.
Both SDKs call /v1/check and carry the same fields, including bundle_digest (bundleDigest in TypeScript). Python returns them snake_case; TypeScript returns them camelCase.
Python, /v1/check response
{
"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"
}
allowed: whether the check allowed the text. A phrase-rule block and a paid injection block both set this false.rule_description: the article citation when a phrase rule or the injection check fired. A wider-review block is labeled LLM_LAYER2_BLOCK and that text is not an article citation.rule_id and bundle_digest: a phrase-rule ID belongs to the open ComplyEdge rule set, which ties it to the article and paragraph it applies. An injection block is llm-art15-ipi-*, not that published ID, and it still carries the Article 15 citation. LLM_LAYER2_BLOCK is the wider review and is not in that rule set. The digest names the exact rule set that decided.violations: list of triggered rules with severity, reason, and confidence (1.0 when a phrase rule decides. Below 1.0 when a model decided: the injection check on a paid plan when the month's model budget covers it, or the wider 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: 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.evaluated_rules: every rule in the packages walked up to and including the one that fired, not only the rules that matched. Packages are queried in parallel; the response still walks them in declared order (first-violation-wins). article5 is first, so an Article 5 block reports the eight Article 5 rules.event_id: unique ID for the audit log entry. Store it next to your own copy of the text (app_log.info("chat turn", prompt=prompt, complyedge_event_id=result.event_id)), because ComplyEdge keeps the decision, not the text. How audit reconciliation works shows how the two are matched in an audit.text_hash, keyed hash (HMAC-SHA-256, text_hash_alg) 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). It cannot be reversed by hashing guesses; check a text against it with POST /v1/audit/verify.timestamp, UTC instant of evaluation, same value written on the audit entryTypeScript, /v1/check response
{
eventId: "uuid-v4",
allowed: false,
status: "violation", // convenience mirror of `allowed`
violations: [
{
ruleId: "rego-art5-1c-001",
ruleDescription: "Regulation (EU) 2024/1689, Article 5(1)(c): ...",
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,
textExcerpt: "Score users based on their social behavior"
}
],
latencyMs: 91,
bundleVersion: "sha256:42dfbd5ac9ad",
bundleDigest: "42dfbd5ac9adccdc4052d6fdcb88bded9281dcccf961ba31171b235505104e34",
evaluatedRules: ["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"],
enginePath: "opa",
opaLatencyMs: 79.2,
auditLogged: true,
textHash: "0aa4e756f4df3933cc48373c6338499228d50078cacbbdd9dde925221d2b02c5",
textHashAlg: "hmac-sha256-1",
timestamp: "2026-07-22T20:30:00+00:00",
jurisdiction: "EU",
processingTimeMs: 142 // client-measured round trip
}
allowed: same field as Python; status is a convenience mirrorviolations[].ruleId: camelCase in TypeScript (vs rule_id in Python)textHash / timestamp: same evidence fields as Python (text_hash, text_hash_alg, timestamp)processingTimeMs: measured client-side, so it includes network time; latencyMs is server-reportedEvery audit record has three attribution fields: user_id (the person the agent acts for), user_role (their role) and session_id (the conversation, run or job). They are filled only when you send them. Leave them out and those columns stay blank in your audit export.
Python
from complyedge import ComplyEdge, compliance_check
# Per call
ce = ComplyEdge(api_key=api_key)
# user_id: an opaque reference from your own system, never a name, email or staff number
ce.check(text, user_id='ref_5d81e2c4', user_role='recruiter', session_id='conv_abc')
# Or once, for an agent that always acts for the same person
ce = ComplyEdge(api_key=api_key, user_id='ref_5d81e2c4', user_role='recruiter', session_id=run_id)
# Decorator: a dict, or a function read on every call
@compliance_check(jurisdiction='EU', attribution=lambda: {'user_id': current_user.id, 'session_id': request_id})
def answer(prompt: str) -> str:
...
TypeScript
// Per call
await ce.check(text, { userId: 'ref_5d81e2c4', userRole: 'recruiter', sessionId: 'conv_abc' });
// Or as client defaults; a per-call value wins
const ce = new ComplyEdgeClient({ apiKey, userId: 'ref_5d81e2c4', userRole: 'recruiter', sessionId: runId });
Over HTTP, send them in context: { "user_id": "...", "user_role": "...", "session_id": "..." }. The OpenAI middleware records the request's user field as userId. Use a stable identifier from your own system, never a name or email address. ComplyEdge stores user_id and session_id as keyed pseudonyms (p1:…) under your account's key, not as sent; to find one person's records, get the stored form from POST /v1/audit/pseudonym. user_role is stored as sent, so it must name a role, not a person.
Never send a credential as an attribution field. A login token, session cookie, JWT or API key does not belong in session_id or user_id: it would sit in every audit record and every export. If your session identifier doubles as a login token, send a hash of it instead, for example the first 16 hex characters of its SHA-256. As a safety net, ComplyEdge replaces any value shaped like a token or key with redacted: followed by a hash, before the record is written.
Python SDK
COMPLYEDGE_API_KEY=ce_your_key_here
COMPLYEDGE_ENABLED=false # set to "false" to disable; omit it and the SDK is enabled
COMPLYEDGE_FAIL_MODE=closed # block when the API cannot be reached; default is open
COMPLYEDGE_API_KEY: read automatically by the decorator. No need to pass it in code.COMPLYEDGE_ENABLED: opt-out flag. The SDK is enabled by default; set to false to disable (e.g. in local dev).COMPLYEDGE_FAIL_MODE: open (default) runs the function unchecked and unrecorded when the API cannot be reached; closed raises ComplianceUnavailableError instead.TypeScript SDK
COMPLYEDGE_API_URL=https://eu.api.complyedge.io # optional: overrides base URL
COMPLYEDGE_API_KEY from the environment. Pass apiKey explicitly in the constructor.COMPLYEDGE_ENABLED is not read by the TypeScript SDK.Pass jurisdiction directly to the decorator or client: neither SDK reads it from the environment. Set it to where the end user is, not where your company is based: the EU AI Act applies when the AI's output is used in the EU (Art. 2(1)(c)).