← Help center · Developers
REST API and webhooks
Create and publish articles from your own tools, and get signed events when things happen.
Authentication
Create a key in Settings → Integrations & API (owners and admins). Send it on every request:
curl https://ewriting.net/api/v1/me \ -H "Authorization: Bearer ewk_…"
A key acts as the person who created it, with their current role. Read-only keys can only use GET. Limit: 120 requests per minute per key. Errors look like {"error":{"code":"…","message":"…"}}.
Endpoints
GET /api/v1/me— the workspace and role of the key.GET /api/v1/websites— connected websites.GET /api/v1/articles?status=draft,in_review&websiteId=&q=&limit=50&offset=0POST /api/v1/articles—{"title","contentHtml","websiteId","excerpt","targetKeyword","seo":{"title","description"},"idempotencyKey"}. The sameidempotencyKeynever creates a second article.GET /api/v1/articles/{id}— full article with its publications.PATCH /api/v1/articles/{id}— partial update; send the currentversion. A stale version gets 409.POST /api/v1/articles/generate—{"topic","keyword","websiteId","lengthWords"}; starts an AI draft (202).POST /api/v1/articles/{id}/publish—{"action":"create_draft|publish|schedule|update|seo_update","scheduledAt","idempotencyKey"}; returns an operation (202). Website publishing rules and approvals apply exactly as in the dashboard.GET /api/v1/keywords?status=planned— the saved keyword list with its metrics.GET /api/v1/operations/{id}—succeededonly after the website was read back and checked.
Webhooks
Events: article.created, approval.needed, operation.succeeded, operation.failed (and ping from Send test). Each is a POST with a JSON body {"id","event","createdAt","data":{…}}. Failed deliveries are retried with backoff; a webhook is turned off after 20 failures in a row.
Verify the X-EWriting-Signature: t=…,v1=… header: compute HMAC-SHA256 of `${t}.${rawBody}` with your signing secret, compare with v1, and reject old timestamps.
import crypto from "node:crypto";
function verify(rawBody, header, secret) {
const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return typeof v1 === "string" && v1.length === expected.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}