The WriterDean API is a REST interface under /v1 and an MCP server at /api/mcp that run the same eleven operations: list templates and brands, create content and run the pipeline, read a run's trace, humanize and check text, list destinations and publish. One key with up to six scopes covers both. The API starts on the Regular plan.
This page is rendered from the catalog the routes run on. The discovery document at https://writerdean.com/v1 lists the same endpoints, and tools/list on the MCP server returns the same tools with the same schemas.
Quick start: a piece in four calls
From a key to finished markdown. The same four steps in curl, in the TypeScript client, and as MCP messages.
- 1Create a keyGo to API keys under Settings and create one with the scopes it needs. Owners and admins can do this. The key shows once and starts with wd_live_. It acts as the member who made it: what they can see in the app, the key can see.
- 2Find a brand and a templateGET /v1/brands and GET /v1/templates need no scope beyond a valid key. Every piece belongs to one brand, and the template lists the brief fields it expects and which ones it requires.
- 3Create the piecePOST /v1/content with the template slug and the brand id, plus a brief. The reply is 202 with a contentId and a runId, because the run has been accepted and is not finished yet.
- 4Poll the run, then read the pieceGET /v1/runs/{id} every two seconds until the status is done, needs_review or failed. Then GET /v1/content/{id} returns the markdown, the title and meta, the humanizer and SEO scores, and the sources. The client's waitForRun does the polling.
# 1. A brand and a template. No scope needed beyond a valid key.
curl https://writerdean.com/v1/brands -H "Authorization: Bearer wd_live_YOUR_KEY"
curl https://writerdean.com/v1/templates -H "Authorization: Bearer wd_live_YOUR_KEY"
# 2. Create the piece. Returns 202 with contentId and runId.
curl -X POST https://writerdean.com/v1/content \
-H "Authorization: Bearer wd_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"templateSlug":"blog-post","brandId":"BRAND_ID","brief":{"primaryKeyword":"content calendar"}}'
# 3. Poll every two seconds until status is done, needs_review or failed.
curl https://writerdean.com/v1/runs/RUN_ID -H "Authorization: Bearer wd_live_YOUR_KEY"
# 4. Read the piece: markdown, title, meta, scores and sources.
curl https://writerdean.com/v1/content/CONTENT_ID -H "Authorization: Bearer wd_live_YOUR_KEY"Keys and scopes
Send the key as a bearer token on every request: Authorization: Bearer wd_live_…. A key belongs to the member who created it and runs with that member's permissions under row level security, so it reads the brands and content that member reads, and removing the member from the workspace revokes the key. Keys are made and revoked in Settings under API keys, by owners and admins, and each one records when it was last used.
A key carries any set of the six scopes. Give a reporting script content:read and nothing else; give an agent that drafts and publishes the four it needs. The matrix shows which endpoints a set of scopes can call. Pick a different set and watch the list change.
7 of 11 endpoints
Templates and brands need no scope beyond a valid key.
- GET/v1/templatesallowed
- GET/v1/brandsallowed
- GET/v1/contentallowed
- POST/v1/contentallowed
- GET/v1/content/{id}allowed
- POST/v1/pipelinesallowed
- GET/v1/runs/{id}allowed
- POST/v1/humanizemissing scopeneeds humanize
- POST/v1/seo/checkmissing scopeneeds seo
- GET/v1/destinationsmissing scopeneeds publish
- POST/v1/publishmissing scopeneeds publish
| Scope | Grants |
|---|---|
content:read | List content, read a piece with its current version, and read a run with its trace. |
content:write | Create content from a template and a brief, which also starts its first run. |
pipelines:run | Start a new run on existing content. Or create a piece and run it in one call. |
humanize | Run the humanizer on any text, or score it without rewriting. |
seo | Run the on page SEO checklist against markdown. |
publish | List destinations and publish or schedule an approved piece. |
Endpoints
Every route lives under /v1 and speaks JSON. Lists come wrapped in a named array (templates, brands, content, destinations). A call that starts work returns 202 with the ids to poll. Everything else returns 200. Ids are UUIDs, dates are ISO 8601 in UTC, and a failed call returns an error object with a code and a message, listed under Errors.
Each card shows the call three ways. Pick a tab once and every sample on the page follows.
Catalog
What a workspace can write and for whom. Both need a valid key and nothing more.
/v1/templates
scopeanyreturns 200MCP list_templatesThe templates this workspace can use, with the brief fields each one expects.
curl https://writerdean.com/v1/templates \
-H "Authorization: Bearer wd_live_YOUR_KEY"Response
{
"templates": [
{
"slug": "blog-post",
"name": "Blog post",
"family": "Blog and long-form",
"contentType": "blog-post",
"description": "A searchable article on one keyword with a direct answer up front.",
"brief": [
{
"key": "primaryKeyword",
"label": "Primary keyword",
"type": "text",
"required": true
},
{
"key": "targetLength",
"label": "Target length",
"type": "number",
"required": false
}
]
}
]
}/v1/brands
scopeanyreturns 200MCP list_brandsThe brands in the workspace. Every piece belongs to one brand and reads its context.
curl https://writerdean.com/v1/brands \
-H "Authorization: Bearer wd_live_YOUR_KEY"Response
{
"brands": [
{
"id": "5f0d3d0e-2b0e-4a6f-9c1c-7d5b1f2a9e11",
"name": "Northwind Analytics",
"website": "https://northwind.example",
"language": "en",
"locale": "en-US",
"hasContext": true
}
]
}Content and runs
Create a piece and start a run on it. Read the piece back, and read the trace the run left.
/v1/content
scopecontent:readreturns 200MCP list_contentContent in the workspace, newest first, filtered by status or brand.
- query
statusbrandIdlimit
curl https://writerdean.com/v1/content?status=approved&limit=20 \
-H "Authorization: Bearer wd_live_YOUR_KEY"Response
{
"content": [
{
"id": "0b6f2c94-3a1d-4b8e-9f2a-2c9d4e7a1b33",
"title": "Why content calendars stall in month two: a practical guide",
"type": "blog-post",
"status": "approved",
"brandId": "5f0d3d0e-2b0e-4a6f-9c1c-7d5b1f2a9e11",
"primaryKeyword": "content calendar",
"language": "en",
"currentVersionId": "f3a9c1d2-4e5b-4a7c-8d9e-1b2c3d4e5f60",
"scheduledAt": null,
"publishedAt": null,
"createdAt": "2026-09-25T09:12:04.000Z",
"updatedAt": "2026-09-25T09:19:31.000Z"
}
]
}/v1/content
scopecontent:writereturns 202MCP create_contentCreate a piece from a template and a brief, and start its first run.
- body
templateSlugbrandIdbriefapprovalAfterOutline?
curl -X POST https://writerdean.com/v1/content \
-H "Authorization: Bearer wd_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"templateSlug": "blog-post",
"brandId": "5f0d3d0e-2b0e-4a6f-9c1c-7d5b1f2a9e11",
"brief": {
"primaryKeyword": "content calendar",
"audience": "Marketing leads at small software companies"
}
}'Response
{
"contentId": "0b6f2c94-3a1d-4b8e-9f2a-2c9d4e7a1b33",
"runId": "9d4e1a7b-6c2f-4e0a-8b1d-3f5a7c9e2d44",
"status": "queued"
}/v1/content/{id}
scopecontent:readreturns 200MCP get_contentOne piece: the current version with its humanizer and SEO scores, the sources it cites, and every place it went out to.
curl https://writerdean.com/v1/content/CONTENT_ID \
-H "Authorization: Bearer wd_live_YOUR_KEY"Response
{
"id": "0b6f2c94-3a1d-4b8e-9f2a-2c9d4e7a1b33",
"title": "Why content calendars stall in month two: a practical guide",
"type": "blog-post",
"status": "approved",
"brandId": "5f0d3d0e-2b0e-4a6f-9c1c-7d5b1f2a9e11",
"version": {
"number": 3,
"title": "Why content calendars stall in month two: a practical guide",
"markdown": "# Why content calendars stall in month two\n\nMost calendars are built for the first month...",
"metaDescription": "Content calendars fail in month two for four reasons. Here is how to plan for each of them.",
"slug": "why-content-calendars-stall-in-month-two",
"wordCount": 1480,
"humanizer": {
"ok": true,
"score": 1.98,
"threshold": 3,
"hardFails": 0
},
"seo": {
"ok": true,
"score": 92
},
"sources": [
{
"claim": "62 percent of teams abandon their calendar by month three",
"url": "https://example.com/report",
"status": "supported"
}
]
},
"lastRunId": "9d4e1a7b-6c2f-4e0a-8b1d-3f5a7c9e2d44",
"publications": []
}/v1/pipelines
scopepipelines:runreturns 202MCP run_pipelineRun the pipeline again on existing content, with a new brief if you send one.
- body
contentId | templateSlug + brandId + briefbrief?approvalAfterOutline?
curl -X POST https://writerdean.com/v1/pipelines \
-H "Authorization: Bearer wd_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"contentId": "0b6f2c94-3a1d-4b8e-9f2a-2c9d4e7a1b33",
"brief": {
"primaryKeyword": "content calendar",
"notes": "Add a section on quarterly planning."
}
}'Response
{
"contentId": "0b6f2c94-3a1d-4b8e-9f2a-2c9d4e7a1b33",
"runId": "9d4e1a7b-6c2f-4e0a-8b1d-3f5a7c9e2d44",
"status": "queued"
}/v1/runs/{id}
scopecontent:readreturns 200MCP get_runA run and its trace: every stage with its skills, model, tokens, cost and gates, plus the output it produced.
curl https://writerdean.com/v1/runs/RUN_ID \
-H "Authorization: Bearer wd_live_YOUR_KEY"Response
{
"id": "9d4e1a7b-6c2f-4e0a-8b1d-3f5a7c9e2d44",
"contentId": "0b6f2c94-3a1d-4b8e-9f2a-2c9d4e7a1b33",
"contentType": "blog-post",
"status": "done",
"totalCostUsd": 0.0121,
"inputTokens": 66102,
"outputTokens": 3787,
"flaggedForReview": false,
"steps": [
{
"position": 8,
"stage": "humanize",
"label": "Humanize",
"status": "done",
"skills": [
{
"name": "humanizer",
"version": "3.0.0",
"hash": "7b1c…e9f2"
}
],
"costUsd": 0.002,
"gates": [
{
"gate": "humanizer_lint",
"pass": true,
"required": true
}
]
}
]
}Text tools
The humanizer and the on page checklist on any text, with or without a piece behind it.
/v1/humanize
scopehumanizereturns 200MCP humanize_textRewrite text with the humanizer and return the lint report, or score it without rewriting.
- body
textbrandId?lintOnly?
curl -X POST https://writerdean.com/v1/humanize \
-H "Authorization: Bearer wd_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"text": "It's not just a tool, it's a mindset. Let's dive in.",
"lintOnly": true
}'Response
{
"text": "It's not just a tool, it's a mindset. Let's dive in.",
"lint": {
"ok": false,
"score": 14.5,
"threshold": 3,
"hardFails": [],
"findings": [
{
"tell": 1,
"label": "Not X but Y (it's not X, it's Y)",
"line": 1,
"severity": "high"
}
]
},
"attempts": 0,
"changed": false
}/v1/seo/check
scopeseoreturns 200MCP seo_checkThe on page checklist: title and meta lengths, keyword placement, headings, answer block, links, images, readability.
- body
markdownprimaryKeywordtitle?metaDescription?slug?secondaryKeywords?
curl -X POST https://writerdean.com/v1/seo/check \
-H "Authorization: Bearer wd_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"markdown": "# Why content calendars stall in month two\n\nMost calendars are built for the first month...",
"primaryKeyword": "content calendar",
"title": "Content calendars: a practical guide"
}'Response
{
"ok": false,
"score": 78,
"checks": [
{
"id": "title-length",
"label": "Title between 50 and 60 characters",
"pass": false,
"severity": "warn",
"detail": "36 characters"
}
]
}Publishing
Where a piece can go and the call that sends it, now or on a date.
/v1/destinations
scopepublishreturns 200MCP list_destinationsConnected publishing destinations with the ids that publish expects.
curl https://writerdean.com/v1/destinations \
-H "Authorization: Bearer wd_live_YOUR_KEY"Response
{
"destinations": [
{
"id": "c2a7e6d1-9b3f-4c8e-a1d2-6e4f8b0c7a55",
"name": "Northwind blog",
"kind": "article",
"integrationId": "e7c1d2b3-8a4f-4d6e-9b0c-5f6a7b8c9d10"
}
]
}/v1/publish
scopepublishreturns 202MCP publish_contentPublish an approved piece now, or schedule it. Publishing again to the same destination updates the earlier post.
- body
contentIdtargetIdscheduledFor?utm?
curl -X POST https://writerdean.com/v1/publish \
-H "Authorization: Bearer wd_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"contentId": "0b6f2c94-3a1d-4b8e-9f2a-2c9d4e7a1b33",
"targetId": "c2a7e6d1-9b3f-4c8e-a1d2-6e4f8b0c7a55",
"scheduledFor": "2026-10-06T09:00:00Z",
"utm": {
"utm_source": "newsletter"
}
}'Response
{
"publicationId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"status": "scheduled",
"url": null,
"scheduledFor": "2026-10-06T09:00:00.000Z"
}Runs and polling
A run is accepted in milliseconds and finishes in minutes, so the API never blocks on one. You poll GET /v1/runs/{id} and read the status. Every two seconds is a fair interval; the client's waitForRun uses it and gives up after ten minutes with the code timeout. The reply carries the whole trace as it grows: each stage with its skills and their versions, the model, the tokens, the cost, the gate results and the output, so a script can show progress the way the run page does.
| Status | Meaning | Keep polling |
|---|---|---|
queued | Accepted. A worker picks it up within seconds. | Yes |
running | Stages are executing. The steps array shows which one. | Yes |
awaiting_approval | Paused after the outline for a reviewer, when approvalAfterOutline was set. Someone approves it in the app. | No |
done | Every required gate passed. The piece has a new current version. | No |
needs_review | Finished, but a required gate did not pass within its retries. A person decides in the editor. | No |
failed | A stage failed after its retries. The error field says why. | No |
cancelled | Stopped from the run page. | No |
A replay of the captured run, as a client polls it
- POST
https://writerdean.com/v1/content202 queued - GET
https://writerdean.com/v1/runs/{id}running · Brand Context - GET
https://writerdean.com/v1/runs/{id}running · Research - GET
https://writerdean.com/v1/runs/{id}running · Strategy - GET
https://writerdean.com/v1/runs/{id}running · Outline - GET
https://writerdean.com/v1/runs/{id}running · Draft - GET
https://writerdean.com/v1/runs/{id}running · SEO layer - GET
https://writerdean.com/v1/runs/{id}running · Copy edit - GET
https://writerdean.com/v1/runs/{id}running · Humanize - GET
https://writerdean.com/v1/runs/{id}running · Quality gates - GET
https://writerdean.com/v1/runs/{id}done - GET
https://writerdean.com/v1/content/{id}version 1 · 379 words
{
"id": "9d4e1a7b-6c2f-4e0a-8b1d-3f5a7c9e2d44",
"status": "queued",
"totalCostUsd": 0,
"steps": [
{ "stage": "brand_context", "status": "pending" },
{ "stage": "research", "status": "pending" },
{ "stage": "strategy", "status": "pending" },
{ "stage": "outline", "status": "pending" },
{ "stage": "draft", "status": "pending" },
{ "stage": "seo", "status": "pending" },
{ "stage": "copyedit", "status": "pending" },
{ "stage": "humanize", "status": "pending" },
{ "stage": "qa", "status": "pending" }
],
"running": null
}The replay uses the run the live demo captured, with its real stage costs. A production run takes a few minutes, not a few seconds.
The MCP server
The MCP server gives an agent the same eleven operations as tools. It speaks the streamable HTTP transport in its stateless mode: every message is one POST to /api/mcp with the key as a bearer token, and every reply is plain JSON. There are no sessions, no server sent events, and no resources or prompts, only tools. Batched messages are answered as a batch, and notifications get a 202 with no body.
- URL
- https://writerdean.com/api/mcp
- Transport
- Streamable HTTP, stateless
- Protocol version
- 2025-06-18
- Server
- writerdean 1.0.0
Connect a client
Any client that speaks HTTP MCP and can send a header works. Three ways to point one at your workspace.
claude mcp add --transport http writerdean https://writerdean.com/api/mcp \
--header "Authorization: Bearer wd_live_YOUR_KEY"What a tool call returns
A result carries the JSON twice: as text in content for clients that only read text, and as structuredContent for the ones that read objects. A tool that fails returns isError: true with the same message the REST error would carry, inside a normal 200 envelope. A request that fails before any tool runs, because the key is missing or the plan is too low, returns a JSON-RPC error with code -32001 and the HTTP status of the cause.
The eleven tools
Each tool runs the endpoint it links to, with the same scope check. Open a tool for its input schema, exactly as tools/list returns it.
list_templatesList the content templates available to this workspace, with the brief fields each one expects.SchemainputSchema{ "type": "object", "properties": {}, "additionalProperties": false }Runs GET /v1/templates.
list_brandsList the brands in this workspace. Every piece of content belongs to one brand and reads its Brand Context.SchemainputSchema{ "type": "object", "properties": {}, "additionalProperties": false }Runs GET /v1/brands.
create_contentCreate a piece of content from a template and brief, and start the generation pipeline. Returns contentId and runId; poll get_run until status is done, then get_content for the text. The humanizer always runs last and cannot be turned off.SchemainputSchema{ "type": "object", "properties": { "templateSlug": { "type": "string", "description": "A slug from list_templates." }, "brandId": { "type": "string", "format": "uuid", "description": "A brand id from list_brands." }, "brief": { "type": "object", "description": "Brief fields for the template, for example primaryKeyword, secondaryKeywords, audience, cta, notes.", "additionalProperties": true }, "approvalAfterOutline": { "type": "boolean", "description": "Pause after the outline for human approval." } }, "required": [ "templateSlug", "brandId", "brief" ], "additionalProperties": false }Runs POST /v1/content and needs the
content:writescope.get_contentFetch a piece of content with its current version (markdown, title, meta description) plus humanizer and SEO scores, its sources, and where it has been published.SchemainputSchema{ "type": "object", "properties": { "contentId": { "type": "string", "format": "uuid", "description": "The content id." } }, "required": [ "contentId" ], "additionalProperties": false }Runs GET /v1/content/{id} and needs the
content:readscope.list_contentList content in the workspace, newest first. Filter by status (idea, brief, draft, in_review, approved, scheduled, published, needs_refresh) or brandId.SchemainputSchema{ "type": "object", "properties": { "status": { "type": "string" }, "brandId": { "type": "string", "format": "uuid", "description": "Only this brand." }, "limit": { "type": "integer", "minimum": 1, "maximum": 200 } }, "additionalProperties": false }Runs GET /v1/content and needs the
content:readscope.run_pipelineStart a new pipeline run. Pass contentId to regenerate an existing piece (optionally with a new brief), or templateSlug plus brandId and brief to create and generate in one step. Returns a runId to poll with get_run.SchemainputSchema{ "type": "object", "properties": { "contentId": { "type": "string", "format": "uuid", "description": "Existing content to regenerate." }, "templateSlug": { "type": "string" }, "brandId": { "type": "string", "format": "uuid", "description": "Brand for new content." }, "brief": { "type": "object", "additionalProperties": true }, "approvalAfterOutline": { "type": "boolean" } }, "additionalProperties": false }Runs POST /v1/pipelines and needs the
pipelines:runscope.get_runFetch a pipeline run and its trace: every stage with the skills and versions it used, the model, tokens, cost, gate results, output. Possible statuses: queued, running, awaiting_approval, done, needs_review, failed, cancelled.SchemainputSchema{ "type": "object", "properties": { "runId": { "type": "string", "format": "uuid", "description": "The run id." } }, "required": [ "runId" ], "additionalProperties": false }Runs GET /v1/runs/{id} and needs the
content:readscope.humanize_textRun the humanizer skill on text and return the rewritten text with its lint report. Pass brandId to apply that brand's voice settings (dash policy, quote style, writing samples). Set lintOnly to score without rewriting.SchemainputSchema{ "type": "object", "properties": { "text": { "type": "string", "minLength": 20 }, "brandId": { "type": "string", "format": "uuid", "description": "Optional brand for voice settings." }, "lintOnly": { "type": "boolean" } }, "required": [ "text" ], "additionalProperties": false }Runs POST /v1/humanize and needs the
humanizescope.seo_checkRun the on page SEO checklist against markdown: title and meta lengths, keyword placement, headings, answer block, links, images, readability.SchemainputSchema{ "type": "object", "properties": { "markdown": { "type": "string" }, "primaryKeyword": { "type": "string" }, "title": { "type": "string" }, "metaDescription": { "type": "string" }, "slug": { "type": "string" }, "secondaryKeywords": { "type": "array", "items": { "type": "string" } } }, "required": [ "markdown", "primaryKeyword" ], "additionalProperties": false }Runs POST /v1/seo/check and needs the
seoscope.list_destinationsList connected publishing destinations (CMS collections, blogs, social accounts) with their ids for publish_content.SchemainputSchema{ "type": "object", "properties": {}, "additionalProperties": false }Runs GET /v1/destinations and needs the
publishscope.publish_contentPublish approved content to a destination now, or schedule it with an ISO 8601 scheduledFor. Publishing the same piece to the same destination again updates the earlier post.SchemainputSchema{ "type": "object", "properties": { "contentId": { "type": "string", "format": "uuid", "description": "Approved content." }, "targetId": { "type": "string", "format": "uuid", "description": "A destination id from list_destinations." }, "scheduledFor": { "type": "string", "format": "date-time" }, "utm": { "type": "object", "additionalProperties": { "type": "string" } } }, "required": [ "contentId", "targetId" ], "additionalProperties": false }Runs POST /v1/publish and needs the
publishscope.
The order an agent follows
The server's initialize reply carries instructions, so an agent knows where to start without reading this page. On a fresh workspace the order is the one below. The instructions also state the rule the humanizer section explains.
This server is the WriterDean content factory for the workspace "Northwind Analytics". Start with list_brands and list_templates, create content with create_content, poll get_run until it is done, read it with get_content, and publish with publish_content. The humanizer is the mandatory final pass on every piece and cannot be disabled.Errors
A failed call returns { "error": { "code", "message" } } with the status below. The message is written for a person and says what to change. Match on the code, not the message.
| Code | Status | When |
|---|---|---|
unauthorized | 401 | No bearer token, or a key that does not exist or that an admin revoked. |
forbidden | 403 | The member who created the key left the workspace, or the key's owner lacks the editor role that publishing needs. |
missing_scope | 403 | The key lacks the scope this endpoint needs. The message names the scope. |
plan_required | 402 | The workspace is on the free plan. The API and the MCP server start at Regular. |
credits_exhausted | 402 | The workspace has used this month's credits and its plan does not meter overage. |
rate_limited | 429 | Too many requests from this key or from this address. The message says how many seconds to wait. |
bad_request | 400 | The body is not valid JSON, a required field is missing, or an id is not a UUID. |
humanizer_required | 400 | The request tried to turn the humanizer off. Nothing leaves the pipeline without it. |
not_found | 404 | No content, run, brand, or destination with that id exists in this workspace. |
not_ready | 409 | The piece has no finished version to publish yet. |
not_approved | 409 | The piece is not approved. Owners and admins can publish anyway. |
internal | 500 | Something failed on our side. Retry once, and write to us if it repeats. |
Rate limits and plans
The API and the MCP server are on the Regular plan and above. A key from a workspace below that is refused with plan_required before anything else is checked. Two limits apply to every request: one per address, counted before the key is read, and one per key, set by the plan. Both are token buckets that refill continuously, so a burst of the full allowance drains the bucket and it fills back at the per minute rate divided by sixty.
A refused request returns 429 with rate_limited, and the message says how many seconds to wait. Back off for that long; retrying sooner spends the tokens as they arrive.
| Limit | Requests a minute | Sustained |
|---|---|---|
| One address, any key | 120 | 2 a second |
| One key, Regular plan | 120 | 2 a second |
| One key, Pro plan | 240 | 4 a second |
| One key, Agency plan | 600 | 10 a second |
Agency limits are per workspace and can be raised on request. Write to hello@writerdean.com with the key's name and the rate you need.
The humanizer rule
Every piece the pipeline produces passes through the humanizer last, and no caller can turn that off. The check runs at the API boundary before anything is written, so a request that tries is refused whole with 400 and the code humanizer_required. The MCP server applies the same check inside a tool call.
The fields that trigger it, at the top level of the body or nested under options or pipeline:
humanizer: false or humanize: falseskipHumanizer or skip_humanizer: truedisableHumanizer or disable_humanizer: trueskipStages or skip_stages containing humanizea stages list that leaves humanize out
If the output reads too plain for a brand, the answer is in the brand document: the dash policy, the quote style and the writing samples all shape the humanizer's rewrite. The humanizer guide explains what the lint measures and how to read its findings.
Webhooks
An outbound webhook sends a signed POST to a URL you add under Integrations, Webhooks. The signing secret shows once, when the endpoint is created. Each delivery has ten seconds to get a reply, the last HTTP status is recorded on the endpoint, and a failed delivery never blocks the run or the publish that caused it. There is no retry today, so treat a delivery as at most once and reconcile with GET /v1/content/{id} when it matters.
Events that fire today
| Event | When | Data |
|---|---|---|
content.published | A publish job finished and the destination confirmed it. | contentId, title, provider, url, externalId |
publication.failed | A publish job failed after the destination refused it. | contentId, provider, error |
The subscription form lists four more names: content approved, run completed, run failed, batch completed. Those are reserved and do not fire yet. This table changes when they do.
Headers and payload
Every delivery carries content-type: application/json, the event name in x-writerdean-event, and a signature in x-writerdean-signature of the form t=<unix seconds>,v1=<hex>, where v1 is an HMAC with SHA-256 over the string t.body using your secret.
{
"event": "content.published",
"workspaceId": "3c1f9a2e-7d4b-4e8a-9b6c-1a2b3c4d5e6f",
"at": "2026-09-25T09:41:07.000Z",
"data": {
"contentId": "0b6f2c94-3a1d-4b8e-9f2a-2c9d4e7a1b33",
"title": "Why content calendars stall in month two: a practical guide",
"provider": "wordpress",
"url": "https://northwind.example/blog/why-content-calendars-stall-in-month-two",
"externalId": "1287"
}
}Verify a signature
Compute the same HMAC over the raw body and compare in constant time. Reject a timestamp older than five minutes, which is the window the sender uses. This is the function the platform itself uses, with the names changed.
import { createHmac, timingSafeEqual } from "node:crypto";
// body is the raw request body, before any JSON parsing.
export function verifyWriterDean(body: string, header: string, secret: string, toleranceSeconds = 300): boolean {
const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=") as [string, string]));
const t = Number(parts.t);
if (!t || !parts.v1) return false;
if (Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
const expected = createHmac("sha256", secret).update(`${t}.${body}`).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1);
return a.length === b.length && timingSafeEqual(a, b);
}TypeScript client
The client lives in the repository under packages/sdk and is not on npm yet. It has no dependencies and runs anywhere fetch does: Node 18 and later, Deno, Bun, edge workers and the browser. Copy src/index.ts into your project, or add the package from the workspace if you build inside the monorepo. The base URL defaults to https://writerdean.com; self hosters pass their own.
Every method maps to one endpoint and returns the unwrapped value, so listBrands() resolves to the array, not to { brands }. A failed call throws WriterDeanError with the HTTP status and the error code from the table above, so a script can branch on err.code.
| Method | Calls |
|---|---|
listTemplates() | GET /v1/templates |
listBrands() | GET /v1/brands |
listContent({ status, brandId, limit }) | GET /v1/content |
createContent({ templateSlug, brandId, brief }) | POST /v1/content |
getContent(contentId) | GET /v1/content/{id} |
runPipeline({ contentId, brief }) | POST /v1/pipelines |
getRun(runId) or waitForRun(runId) | GET /v1/runs/{id} |
humanize({ text, brandId, lintOnly }) | POST /v1/humanize |
seoCheck({ markdown, primaryKeyword }) | POST /v1/seo/check |
listDestinations() | GET /v1/destinations |
publish({ contentId, targetId, scheduledFor }) | POST /v1/publish |
import { WriterDean, WriterDeanError } from "@writerdean/sdk";
const wd = new WriterDean({ apiKey: process.env.WRITERDEAN_API_KEY!, baseUrl: "https://writerdean.com" });
try {
const report = await wd.humanize({ text: draft, brandId, lintOnly: true });
console.log(report.lint.score, report.lint.threshold);
} catch (err) {
if (err instanceof WriterDeanError && err.code === "rate_limited") {
// err.message says how many seconds to wait.
} else throw err;
}Questions about the API
Guides that go with this page
- The pipeline and the traceThe nine stages of a run and the gates between them, approvals after the outline, and how to read what a run left behind.
- Publishing and the calendarConnecting a channel, publishing or scheduling a piece, the calendar and the board, republishing, plus signed webhooks.
- Team roles and workspacesWorkspaces and the seven roles, invitations and seats, approvals, the audit log, API keys, data export.
Something missing or wrong on this page? Write to hello@writerdean.com and say which section.


