Map a regulatory change you already interpreted onto your control inventory, evidence, owners, deadlines, and gaps.
- Product and pricing
- Developer documentation
- Create a free account
- OpenAPI contract
- Postman collection
The public demo runs the real production engine, stores nothing, meters nothing, and requires no API key. The data below is synthetic.
cat > request.json <<'JSON'
{
"controls": [
{
"controlId": "CTL-014",
"name": "Consumer disclosure template review",
"owner": "Payments Compliance",
"status": "implemented",
"tags": [
"payments",
"disclosure",
"us"
],
"lastTestedAt": "2026-07-01T00:00:00Z",
"testFrequencyDays": 180,
"evidence": [
{
"kind": "policy_document",
"artifactId": "POL-221",
"collectedAt": "2026-07-02T00:00:00Z"
},
{
"kind": "notification_record",
"artifactId": "NR-9930",
"collectedAt": "2026-07-14T00:00:00Z"
}
]
},
{
"controlId": "CTL-027",
"name": "Fee change customer notification job",
"owner": "Payments Engineering",
"status": "planned",
"tags": [
"payments",
"notification",
"us"
],
"testFrequencyDays": 90
},
{
"controlId": "CTL-101",
"name": "Account record retention schedule",
"owner": "Records Management",
"status": "implemented",
"tags": [
"recordkeeping",
"us"
],
"lastTestedAt": "2024-02-01T00:00:00Z",
"evidence": [
{
"kind": "retention_schedule",
"artifactId": "RS-04",
"collectedAt": "2024-02-01T00:00:00Z"
}
]
}
],
"change": {
"changeId": "REG-2026-0117",
"citation": "12 CFR 1026.19(e)",
"title": "Revised timing and content of cost-of-credit disclosures",
"jurisdiction": "US",
"regulator": "CFPB",
"changeType": "amendment",
"publishedAt": "2026-03-02T00:00:00Z",
"effectiveAt": "2026-09-01T00:00:00Z",
"transitionEndsAt": "2026-12-01T00:00:00Z",
"currency": "USD",
"obligations": [
{
"obligationId": "OB-1",
"type": "disclosure",
"text": "Provide the revised cost-of-credit disclosure within three business days of application.",
"appliesTo": [
"payments",
"disclosure"
],
"enforcement": "penalty",
"maxPenaltyMinor": 500000000
},
{
"obligationId": "OB-2",
"type": "notification",
"text": "Notify existing customers of the fee schedule change at least 45 days before it applies.",
"appliesTo": [
"payments",
"notification"
],
"enforcement": "penalty",
"maxPenaltyMinor": 250000000
},
{
"obligationId": "OB-3",
"type": "recordkeeping",
"text": "Retain each issued disclosure for five years and produce it on request.",
"appliesTo": [
"recordkeeping",
"disclosure"
],
"enforcement": "supervisory"
}
]
}
}
JSON
curl -sS -X POST https://controlgraph-api.com/v1/demo/map \
-H 'content-type: application/json' \
--data-binary @request.jsonSelected fields from the deterministic 200 response (evaluated at
2026-09-06T20:30:00.000Z for this example):
{
"change": {
"changeId": "REG-2026-0117",
"summary": {
"obligations": 3,
"covered": 1,
"weak": 1,
"gaps": 1,
"controlsMatched": 2,
"controlsUnmatched": 1,
"teams": 2,
"highestPriority": "critical",
"earliestDeadline": "2026-09-01T00:00:00.000Z",
"passedDeadlines": 3,
"unmetEvidenceKinds": 4
},
"obligations": [
{
"obligationId": "OB-2",
"coverage": "weak",
"coverageReason": "1 control(s) match on (payments, notification), but none of them is currently operating and 2 required evidence kind(s) are not held. See findings.",
"priority": {
"score": 78,
"band": "critical",
"components": {
"enforcement": 40,
"coverage": 18,
"urgency": 10,
"evidence": 10
},
"derivation": [
"enforcement = penalty -> 40",
"coverage = weak -> 18",
"urgency = 86 day(s) to 2026-12-01 (31–90 days) -> 10",
"evidence = 2 of 2 required kind(s) unmet -> 10",
"total = 40 + 18 + 10 + 10 = 78 -> critical"
]
},
"owners": [
"Payments Engineering"
]
},
{
"obligationId": "OB-3",
"coverage": "gap",
"coverageReason": "Nothing in the inventory carries all of (recordkeeping, disclosure), so this obligation is unmapped. Deny by default: an unmatched obligation is a gap, never an assumption of coverage.",
"priority": {
"score": 75,
"band": "critical",
"components": {
"enforcement": 25,
"coverage": 30,
"urgency": 10,
"evidence": 10
},
"derivation": [
"enforcement = supervisory -> 25",
"coverage = gap -> 30",
"urgency = 86 day(s) to 2026-12-01 (31–90 days) -> 10",
"evidence = 2 of 2 required kind(s) unmet -> 10",
"total = 25 + 30 + 10 + 10 = 75 -> critical"
]
},
"owners": []
},
{
"obligationId": "OB-1",
"coverage": "covered",
"coverageReason": "Control CTL-014 (\"Consumer disclosure template review\") matches on (payments, disclosure), is implemented and was tested 67 day(s) ago against a 180-day frequency, and every required evidence kind is held.",
"priority": {
"score": 50,
"band": "high",
"components": {
"enforcement": 40,
"coverage": 0,
"urgency": 10,
"evidence": 0
},
"derivation": [
"enforcement = penalty -> 40",
"coverage = covered -> 0",
"urgency = 86 day(s) to 2026-12-01 (31–90 days) -> 10",
"evidence = 0 of 2 required kind(s) unmet -> 0",
"total = 40 + 0 + 10 + 0 = 50 -> high"
]
},
"owners": [
"Payments Compliance"
]
}
],
"warnings": []
},
"dueSoonWindowDays": 30,
"notice": "This is arithmetic and graph mapping over inputs you supply. It does not read, interpret or apply law, and it is not legal advice.",
"requestId": "req_example"
}The first useful result is the deny-by-default coverage summary: one obligation is covered, one is weak, and one has no matching control. The highest priority is critical and every graph edge states why it exists.
You supply the legal interpretation, dates, obligations, tags, controls, and evidence. ControlGraph performs deterministic graph mapping; it does not read law and is not legal advice.
curl -sS -X POST https://controlgraph-api.com/v1/keys \
-H 'content-type: application/json' \
-d '{"email":"you@example.com","name":"github-quickstart","source":{"source":"github","medium":"developer","campaign":"controlgraph-github","content":"readme"}}'
curl -sS -X POST https://controlgraph-api.com/v1/keys/claim \
-H 'content-type: application/json' \
-d '{"token":"PASTE_ONE_TIME_TOKEN_FROM_EMAIL"}'
export API_KEY='PASTE_API_KEY_FROM_CLAIM_RESPONSE'
curl -sS -X POST https://controlgraph-api.com/v1/changes \
-H "Authorization: Bearer $API_KEY" \
-H 'content-type: application/json' \
--data-binary @request.jsonThe key-request response is 202 and sends a one-time claim token by email. The
claim response is the only place the raw API key is returned; store it securely
and never commit it. The authenticated endpoint accepts the same request shape
as the demo, with the documented production batch limits and metering.
Assign the unmatched recordkeeping obligation, collect the missing evidence kinds, and rerun with the updated control inventory.
The stable code catalogue for this product is GET /v1/obligation-types. Branch on
machine-readable codes, not human-readable detail text.
401: the authenticated endpoint did not receive a valid active key. SetAPI_KEYto the value returned once by/v1/keys/claim; do not send a claim token as a bearer credential.400 invalid_request: readerror.details.pathwhen present and correct the named field. This service does not emit422; a client-side schema tool may show422before a request reaches the API.429 quota_exceededor429 rate_limited: inspecterror.code, honorRetry-Afterwhen present, and retry with bounded exponential backoff. A quota exhaustion requires a later quota window or plan change, not a tight retry loop.
Every API error has {"error":{"code","message","requestId"}}. Share the
request ID with support, never the API key, claim token, or customer payload.
- Python:
./sdk/python/regulation_control.py - TypeScript:
./sdk/typescript/index.ts
The live OpenAPI document is authoritative for operations and schemas. This overlay is a customer-runnable example aligned to that contract; it does not replace the OpenAPI document or claim that an unresolved external contract is authoritative.
The key request above uses controlgraph-github as the stable GitHub campaign. The
Postman collection uses postman / collection / controlgraph-postman / public-collection. These are attribution inputs, not claims of customers or
revenue.