We gate our own blog. Scored 17 October 2026 with the same engine you get, glossary rules, balanced strictness.
Base URL: https://api.dokeo.co. Every endpoint is a JSON POST or GET. Bearer-token auth, HMAC-verified, rate-limited.
Authentication
Pass your tenant API key as a Bearer token. Create one while signed in at /account/api-keys, which calls POST /api/v1/account/keys.
# All requests need an Authorization header with a Bearer token
curl https://api.dokeo.co/api/v1/scan \
-H "Authorization: Bearer dk_your_tenant_key" \
-H "Content-Type: application/json" \
-d '{
"content": "Your blog post here...",
"content_type": "blog"
}'Response shape
Every scan returns the same envelope. verdict is the worst of five severities, most to least severe: BLOCKER (ships never), FAIL, WARNING, OPTIMIZATION (a suggestion, never blocks), or PASS. scores.overall is 0-100. checks_detail[] has one row per check with its own severity, reasons, and metrics.
{
"slug": "my-post",
"verdict": "WARNING",
"content_type": "blog",
"word_count": 1245,
"report": {
"scores": {
"overall": 87,
"seo": 92,
"aeo": 81,
"geo": 88
},
"meta": {
"pass_count": 6, "blocker_count": 0, "fail_count": 0,
"warning_count": 1, "optimization_count": 0
},
"checks_detail": [
{
"check": "structure",
"status": "PASS",
"severity": "PASS",
"reasons": ["1245 words, in target band"],
"metrics": { "word_count": 1245, "h2_count": 6 }
}
],
"action_items": [
{ "priority": "recommended", "action": "Add an FAQ section for AEO" }
],
"inline_issues": [],
"evaluation": {
"content_hash": "sha256:9a1f...",
"decision": { "verdict": "WARNING", "overall": 87, "findings": [["aeo", "WARNING"]] },
"decision_hash": "sha256:3f9c..."
}
}
}Endpoints
/api/v1/scanScore inline content. 1 credit per call.
| Field | Type | Notes |
|---|---|---|
| content | string | Required The post body |
| content_type | string | Default: blog One of 17 types |
| title | string | Optional Post title |
| slug | string | Optional Post slug |
| primary_keyword | string | Optional Main SEO keyword |
| strictness | string | Default: balanced strict | balanced | permissive |
/api/v1/scan/replayRe-run an earlier scan of your workspace and check the decision is identical. Free. Returns match, the inputs that changed, and both outcomes. Dokeo does not keep your text, so send it again.
| Field | Type | Notes |
|---|---|---|
| content | string | Required The same text that was scanned |
| decision_hash | string | Required report.evaluation.decision_hash from the earlier scan |
/api/v1/scan/urlFetch + score a URL. SSRF-guarded (blocks private/loopback IPs).
| Field | Type | Notes |
|---|---|---|
| url | string | Required https URL |
| content_type | string | Default: blog |
| strictness | string | Default: balanced |
/api/v1/scan/blogScore every post on a blog (discovered via RSS/sitemap).
| Field | Type | Notes |
|---|---|---|
| url | string | Required Blog index / RSS / sitemap URL |
| limit | int | Default: 10 Max posts to scan |
| content_type | string | Default: blog |
/api/v1/campaign/qaPre-send QA for an email sequence (1 to 25 emails). Runs the email checks on each message and says what it would block. Shadow mode (default) never holds a send for quality, so you can compare it with what you actually send for a few weeks. Dokeo never sends mail. Uses 1 credit per email. Returns send, action (send | hold | skip), would_block_steps and, per email, verdict, score, findings and a decision_hash you can replay. Recipients on your do-not-contact ledger are dropped from send_to in every mode. sequence_findings lists schedule problems (quiet hours, steps too close together or out of order) and steps whose voice drifts from the rest of the sequence; they hold a send in enforce mode only when they reach fail_on.
| Field | Type | Notes |
|---|---|---|
| campaign | string | Required A name for the sequence |
| emails | object[] | Required Each has step, subject and body, and optionally send_at (ISO 8601) |
| mode | string | Default: shadow shadow | enforce (enforce returns send: false when anything would block) |
| fail_on | string | Default: fail none | blocker | fail | warning | optimization |
| min_score | number | Default: 0 A score under this blocks, whatever the verdict |
| strictness | string | Default: strict strict | balanced | permissive |
| footer | string | Optional What your sender appends, such as the unsubscribe line. Scored with each email |
| recipients | string[] | Optional Up to 1000 addresses. Returns send_to, suppressed and invalid_recipients |
| timing | object | Optional recipient_timezone (IANA name), quiet_start_hour (20), quiet_end_hour (7), min_gap_hours (24). Applied to steps that have send_at |
/api/v1/campaign/suppressionsRecord a refusal on your do-not-contact ledger. Append-only: the wording is kept as you give it, a second entry for the same address keeps the first, and no model is involved. Plus-tags are ignored, so sam+news@acme.com is covered by sam@acme.com.
| Field | Type | Notes |
|---|---|---|
| target | string | Required An address, or @domain.com for a whole domain |
| reason | string | Optional Up to 200 characters |
| source_text | string | Optional What they wrote, kept as evidence |
/api/v1/campaign/suppressionsEveryone currently suppressed for your workspace, newest first, with the original wording.
/api/v1/campaign/suppressions/anonymiseFor an erasure request: replace a suppressed address with its SHA-256 digest and clear the stored wording. The person stays suppressed without Dokeo holding their address. Needs a signed-in admin session.
| Field | Type | Notes |
|---|---|---|
| target | string | Required The address or domain to anonymise |
/api/v1/campaign/suppressions/liftRemove a suppression. Needs a signed-in admin session (API keys cannot) and a reason. The history stays.
| Field | Type | Notes |
|---|---|---|
| target | string | Required The address or domain to lift |
| reason | string | Required Why it is safe to mail them again |
/api/v1/builder/researchGate 1: research keyword + N tools. LLM does web search + per-tool lookup.
| Field | Type | Notes |
|---|---|---|
| primary_keyword | string | Required |
| audience | string | Required e.g. 'B2B event teams' |
| tool_count | int | Default: 6 2-15 |
| house_product | string | Required Your brand to lead with |
| llm_provider | string | Default: auto auto | claude | gpt | mock |
| mock | bool | Default: false Use fixtures, no LLM |
/api/v1/builder/generateGate 2: write the draft. Returns draft_md + QA report + editorial score.
| Field | Type | Notes |
|---|---|---|
| research_bundle | object | Required Output of /builder/research |
| format | string | Default: listicle listicle | comparison | alternatives | buyers_guide |
| llm_provider | string | Default: auto |
| mock | bool | Default: false |
/api/v1/billing/entitlementCurrent tenant + tier + credit balance + usage this period.
/api/v1/checkout/create-sessionCreate a checkout session for a paid tier. Requires a signed-in session (Bearer JWT).
| Field | Type | Notes |
|---|---|---|
| tier | string | Required growth | pro | scale |
| cycle | string | Default: monthly monthly | annual |
/api/v1/catalog/enginesList the supported engines (SEO/AEO/GEO) and their check names.
/api/v1/admin_ext/auto_fixOne-click AI rewrite for a failed check (configured writing model). Requires a Growth+ plan; consumes from the rewrite allowance, never from scans.
| Field | Type | Notes |
|---|---|---|
| content | string | Required The post body |
| check_name | string | Required The failing check |
| reasons | string[] | Optional From check.reasons[] |
| metrics | object | Optional From check.metrics{} |
/openapi.jsonFull OpenAPI spec at https://api.dokeo.co/openapi.json. Use `dokeo openapi --out spec.json` to dump.
Errors
| Code | Meaning |
|---|---|
| 401 | Missing or invalid Bearer token |
| 402 | Out of credits. Upgrade or wait for refill. |
| 404 | Path or scan result not found. |
| 422 | Validation error (missing field, wrong content type). |
| 429 | Rate limit exceeded. Check X-RateLimit-Reset header. |
| 502 | Upstream (payment or search providers) unavailable. |
| 503 | Server missing required configuration. |