eWriting.net

← 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=0
  • POST /api/v1/articles — {"title","contentHtml","websiteId","excerpt","targetKeyword","seo":{"title","description"},"idempotencyKey"}. The same idempotencyKey never creates a second article.
  • GET /api/v1/articles/{id} — full article with its publications.
  • PATCH /api/v1/articles/{id} — partial update; send the current version. 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} — succeeded only 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));
}