Install
openclaw skills install compliancescanRun a GDPR/DSGVO website compliance scan via the compliancescan.eu API; report the 0-100 score and findings, check credits, and list past scans.
openclaw skills install compliancescanRun a GDPR/DSGVO compliance scan of a website through the compliancescan.eu REST API
(https://compliancescan.eu/api/v1): start a full scan, report the 0-100 score and key
findings, check credits, and browse past scans. Report only what the API returns — never
invent results.
The public API runs full scans only (type: "full"; quick scans are web-UI only) and
requires a Business or Enterprise plan key (csk_live_…). POST /scans is
synchronous: it blocks until the scan finishes and returns the result — no polling.
This skill is model-invocable: it must reason over inputs, pick the right endpoint, parse
JSON, and format the result. Do NOT convert it to command-dispatch: tool.
COMPLIANCESCAN_API_KEY MUST already be in the environment (csk_live_…). It is set once in
openclaw.json (skills.entries.compliancescan.apiKey) and injected by OpenClaw for the
run. If it is empty, STOP and tell the user to configure it (see README). NEVER ask for the
key in chat, and never print it or the Authorization header.
Scopes: the reads (B–F) need scan:read; only starting a scan (A) needs scan:write.
Every call except GET /health sends:
-H "Authorization: Bearer $COMPLIANCESCAN_API_KEY"
(-H "X-API-Key: $COMPLIANCESCAN_API_KEY" is also accepted.)
The machine code is the stable identifier and lives in either .code or .error
(middleware errors put it in error; handler errors put it in code). Read .code // .error with jq. Messages may be German — rely on the code. See Failure handling.
Confirm COMPLIANCESCAN_API_KEY is set; if empty, STOP (see Credential).
Validate url: an http(s) URL or a bare domain only (the server prepends https://
if the scheme is missing). Reject anything with shell metacharacters.
Build the request body with jq -n so the URL is a safe JSON value — never interpolate
untrusted input into the shell command. Then POST. The scan may take ~30 s to several
minutes, so use a long read timeout. type MUST be "full":
BODY=$(jq -nc --arg u "$URL" '{url:$u, type:"full"}') # add maxPages only if requested:
# BODY=$(jq -nc --arg u "$URL" --argjson n "$MAXPAGES" '{url:$u, type:"full", maxPages:$n}')
curl -sS --max-time 600 -w '\n%{http_code}' \
-X POST https://compliancescan.eu/api/v1/scans \
-H "Authorization: Bearer $COMPLIANCESCAN_API_KEY" \
-H "Content-Type: application/json" \
-d "$BODY"
The last output line is the HTTP status; everything before it is the JSON body. If the
status is not 200, go to Failure handling.
On 200 the body is { "status": "completed", "scan": { … } }. Report .scan per
Output format. The POST does NOT return a scan id.
KNOWN API QUIRK (today): in the POST response,
trackers/tracker_listcome back0/ empty,issues[]is empty, andsecurity_headersare allfalse— even when the site has trackers/issues — due to a serialization bug. Do NOT report "0 trackers / no issues / no security headers" from the POST; treat those three as unknown. The POST IS reliable for:gdpr_score,pages_scanned,has_privacy_policy/has_imprint/has_cookie_banner,cookie_banner,third_parties,cookies,ssl,mail_security,pre_consent,imprint_check,dse_check, fonts, chatbots. For the authoritative tracker list, fetchGET /scans/:id(section F) for the newest matching row — but note:idin turn omits ssl / mail_security / pre_consent / issues / chatbots / fonts. No single call returns everything right now; state which fields are unavailable rather than inventing zeros.
curl -sS https://compliancescan.eu/api/v1/account \
-H "Authorization: Bearer $COMPLIANCESCAN_API_KEY" | jq .
Report plan, credits.remaining, scans.running / scans.pending, and api_usage.
credits.remaining is the source of truth for credits (response headers are informational).
curl -sS "https://compliancescan.eu/api/v1/scans?limit=10&offset=0" \
-H "Authorization: Bearer $COMPLIANCESCAN_API_KEY" | jq .
Returns { scans: [ { id, url, type, score, gdpr_score, trackers, third_parties, scanned_at } ], pagination: { total, limit, offset, has_more } }. Use a row's id with F.
curl -sS "https://compliancescan.eu/api/v1/scans/latest?limit=50" \
-H "Authorization: Bearer $COMPLIANCESCAN_API_KEY" | jq .
Returns { scans: [ { id, domain, url, score, gdpr_score, trackers, … } ], count } — one
newest row per domain. Use for "what's the latest score for each domain?" / "is X improving?".
curl -sS https://compliancescan.eu/api/v1/scans/status \
-H "Authorization: Bearer $COMPLIANCESCAN_API_KEY" | jq .
Returns { pending: [ { id, url, type, status, queued_at, started_at } ], count }
(status is pending or running). Use this after a POST timed out, before re-scanning.
curl -sS https://compliancescan.eu/api/v1/scans/<ID> \
-H "Authorization: Bearer $COMPLIANCESCAN_API_KEY" | jq .
Returns { id, url, type, score, scanned_at, results: { gdpr {score, privacy_policy, cookie_banner, legal_notice, imprint_source}, trackers {count, list}, third_parties {count, list}, cookies {count, list}, pages_scanned } }. This is the reliable source for the
tracker list; it does NOT include ssl / mail_security / pre_consent / issues / chatbots.
Return concise markdown, including a line only when the field is actually present:
<scan.gdpr_score> / 100 for <url> (<scan.pages_scanned> pages)has_privacy_policy), imprint ✓/✗ (has_imprint),
cookie banner ✓/✗ (has_cookie_banner, with cookie_banner.detected_cmp if present)<scan.third_parties>, cookies <scan.cookies>. Trackers:
report from section F; from the POST alone, state the tracker list is not yet available
rather than reporting 0.pre_consent (compliant + violations), external fonts
(has_external_fonts / font_providers), AI Act (ai_act_disclosure_required /
chatbots), mail/DNS (mail_security), TLS (ssl).issues[] is available (section F / a fixed POST), bullet
each {severity, code, message}, critical first. If unavailable, say so — do NOT claim
"no issues" from the POST response.Authorization header.--max-time timeout is NOT a
clean error — do not retry it (the scan may have run and charged); use section E instead.null, or known to be
unreliable (the section-A quirk), say so rather than inventing it.Read .code // .error, then:
MISSING_URL / INVALID_URL / INVALID_SCAN_TYPE / INVALID_ID — fix the input
(ensure type is "full", a valid URL, a numeric id) and retry once.UNAUTHORIZED / INVALID_API_KEY — key missing, invalid, or revoked. Tell the
user to check COMPLIANCESCAN_API_KEY. Stop.CREDITS_REQUIRED — out of credits and monthly allowance. Tell the user to buy
credits or upgrade the plan. Stop.PLAN_REQUIRED (needs Business/Enterprise) / EMAIL_NOT_VERIFIED (verify the
account email) / INSUFFICIENT_SCOPE (key lacks scan:write) / PRIVATE_IP (target is a
private/internal address — use a public URL). Stop.NOT_FOUND — the scan id does not exist for this account. Stop.RATE_LIMIT_EXCEEDED / DAILY_LIMIT_EXCEEDED / QUEUE_FULL — respect the
Retry-After header (seconds); tell the user to retry later. Stop.INTERNAL_ERROR / scan failure — the page may be unreachable or the scan errored
(a charged credit is auto-refunded on failure). Show the code/message and stop.--max-time hit) — the scan may still be running. Check section E
(/scans/status), then section D (/scans/latest), instead of re-POSTing. Do not retry blindly.