Skip to content
Dokeo

Docs

Campaign QA

Check an email sequence before it sends.
What is a content quality gate?
dokeo.co/blog/what-is-a-content-quality-gate
WARNING
76overall
77SEO
86AEO
66GEO
Fix before it ships12 of 19 checks pass
internal_linkingNo contextual links found (internal or external)
Fix
slopquestion-then-answer pattern found 2 time(s)
Fix

We gate our own blog. Scored 17 October 2026 with the same engine you get, glossary rules, balanced strictness.

Pilot feature

Available to workspaces through the API, in shadow mode first. There is no connector for a sending tool: you call the API from your workflow and it answers. Dokeo never sends mail. It decides, and you send.

Check a sequence

Send up to 25 emails per call. Each is checked against the email rules and scored with your footer, so the unsubscribe check sees it. Strict is the default: one failing check is a FAIL. It uses one credit per email.

bash
curl https://api.dokeo.co/api/v1/campaign/qa \
  -H "Authorization: Bearer $DOKEO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "campaign": "Q4 outbound",
    "footer": "If this is not relevant, reply unsubscribe.",
    "recipients": ["[email protected]", "[email protected]"],
    "timing": { "recipient_timezone": "Europe/Amsterdam" },
    "emails": [
      { "step": "1", "subject": "Quick question", "body": "Hi Sam, ...", "send_at": "2026-11-03T10:00:00" },
      { "step": "2", "subject": "Re: Quick question", "body": "Hi Sam, following up ...", "send_at": "2026-11-03T11:00:00" }
    ]
  }'

The answer:

json
{
  "schema": "dokeo.campaign_qa/v1",
  "mode": "shadow",
  "send": true,
  "action": "send",
  "would_block": true,
  "would_block_steps": ["2"],
  "send_to": ["[email protected]"],
  "suppressed": [
    { "recipient": "[email protected]", "matched": "[email protected]", "scope": "address", "reason": "asked to stop" }
  ],
  "sequence_findings": [
    { "step": "2", "check": "timing.step_spacing", "severity": "WARNING",
      "reasons": ["only 1.0 hours after step 1, under the 24 hour minimum"] }
  ],
  "emails": [
    { "step": "1", "verdict": "WARNING", "score": 80, "blocked": false, "decision_hash": "sha256:3f9c...", "findings": [] }
  ]
}
  • mode is shadow by default. Quality never holds a send, so you can compare what it would have blocked with what you really sent for a few weeks. Set "mode": "enforce" to get send: false when anything would block.
  • action is send, hold (enforce mode and something would block) or skip (nobody is left to send to).
  • fail_on and min_score work as in the CLI.
  • Every email gets a decision_hash. Replay it later with POST /api/v1/scan/replay using the text Subject: ..., a blank line, the body, a blank line and the footer.

A do-not-contact list that never forgets a no

Anyone on the list is dropped from send_to in every mode, shadow included, because a refusal is not a quality opinion. An address or a whole @domain can be on it, and [email protected] is covered by [email protected]. No model is involved. A second entry for the same person keeps the first one, so the earliest refusal and its wording are what you see.

bash
# Add one refusal. The wording is kept as you give it.
curl https://api.dokeo.co/api/v1/campaign/suppressions \
  -H "Authorization: Bearer $DOKEO_API_KEY" -H "Content-Type: application/json" \
  -d '{ "target": "[email protected]", "reason": "reply", "source_text": "please stop emailing me" }'

# Add a whole domain with @domain.com

# Load an existing list in one call: a plain list, or a CSV with a header row.
# Safe to run twice: anyone already on the list keeps their first wording.
curl https://api.dokeo.co/api/v1/campaign/suppressions/import \
  -H "Authorization: Bearer $DOKEO_API_KEY" -H "Content-Type: application/json" \
  -d '{ "csv": "email,reason,text\[email protected],reply,please remove me\[email protected],domain,\n" }'

# See who is on it
curl https://api.dokeo.co/api/v1/campaign/suppressions -H "Authorization: Bearer $DOKEO_API_KEY"

Only an admin can lift an entry (POST /campaign/suppressions/lift, with a reason), and the history stays. For a deletion request, an admin can anonymise an entry (POST /campaign/suppressions/anonymise): the address becomes a hash and the wording is cleared, and the person stays suppressed.

Timing and voice

Give each email a send_at and set timing to flag sends in quiet hours (default 20:00 to 07:00 in the recipient's zone), steps under 24 hours apart, and a step scheduled at or before the one before it. Dokeo also flags an email that reads like a different writer than the rest of the sequence: sentence length, contractions, exclamation marks, greeting and subject case. These come back as sequence_findings. They hold a send in enforce mode only when they reach your fail_on. The defaults are conservative starting points, not a claim that they win more replies.

What the gate did this month

Counts only: no address, subject or text is stored in the summary.

bash
curl "https://api.dokeo.co/api/v1/campaign/summary?days=28" -H "Authorization: Bearer $DOKEO_API_KEY"

# {
#   "window_days": 28, "campaigns": 3, "would_block_rate_percent": 12.4,
#   "totals": { "runs": 41, "emails": 188, "would_block": 23, "suppressed_recipients": 9,
#               "timing_flags": 6, "voice_flags": 4, "holds": 0, "skips": 2 },
#   "by_day": [ { "day": "2026-11-03", "runs": 5, "emails": 24, "would_block": 3, "suppressed": 1 } ],
#   "top_reasons": [ { "check": "email.no_unsubscribe", "count": 11 } ],
#   "ledger_active": 112
# }

From n8n or Zapier

Any tool that can make an HTTP request works. In n8n:

NodeHTTP Request
Method and URLPOST https://api.dokeo.co/api/v1/campaign/qa
AuthenticationGeneric credential, Header Auth. Name: Authorization. Value: Bearer dk_your_key
Send bodyOn, content type JSON, using the JSON from the call above, with your own fields filled from the previous node
Next nodeIF. Condition: {{ $json.action }} equals send
True branchYour sender, using {{ $json.send_to }} as the recipient list
False branchLog it, or alert yourself. action is hold or skip, and the reason is in the response

In Zapier, use "Webhooks by Zapier", a custom request, with the same URL, header and JSON body.

Limits

  • 25 emails and 1,000 recipients per call. 5,000 rows per list import.
  • One credit per email checked. The ledger, the import and the summary use none.
  • Campaign QA calls are not stored as scans. Each email is logged for replay by its hash; recipients are never logged.
  • The full request and response shapes are in the REST API reference.