LivePublicator API
Draft and publish press releases for your clients. JSON over HTTPS. The full spec is at /api/v1/openapi.json (OpenAPI 3.1).
Keys and the plan
Get a key in your account ($99 a month, 10 release credits a month, see the plan). Send it on every call:
Authorization: Bearer lp_live_...- Checks and drafts are free. Publishing uses 1 credit. Each renewal adds 10 credits and unused credits roll over. At 50 or more credits the renewal is skipped (no charge). A release we don't publish gives its credit back.
- Up to 10 releases a day and 120 calls a minute per key.
- The first 3 releases of each new key are checked by a person before they go live.
- Every POST accepts an
Idempotency-Keyheader. Send the same key with the same body again and you get the first answer back, never a second release.
Draft and publish
1. Send the finished release. We check it and keep the text as you sent it (we do not write or change it):
curl https://livepublicator.com/api/v1/releases \
-H "Authorization: Bearer $LP_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: acme-launch-1" \
-d '{
"company_name": "Acme Robotics",
"company_domain": "acmerobotics.com",
"client_authorized": true,
"headline": "Acme Robotics Opens a Second Factory in Austin",
"summary": "Acme Robotics opens a second factory in Austin, Texas, adding 120 jobs, with production starting in March.",
"body": "Acme Robotics today opened ... (250 to 1,000 words, text, Markdown or HTML)",
"key_facts": ["120 new jobs", "Production starts in March 2027"],
"quote": "Austin gives us the people and space to grow.",
"quote_name": "Dana Lee",
"quote_title": "CEO",
"contact_name": "Sam Park",
"contact_title": "Head of Communications",
"contact_email": "press@acmerobotics.com",
"image_url": "https://acmerobotics.com/press/austin.jpg"
}'If something needs fixing, you get 422 with every problem at once, each with the field, what is wrong and how to fix it. Fix them all and send again. Add "validate_only": true to check without creating anything.
{ "error": { "code": "validation", "message": "2 problems to fix. Fix all of them and send the release again.",
"problems": [
{ "field": "quote", "problem": "Missing.", "fix": "Send one quote from a real person at the company (quote, quote_name, quote_title). More quotes can go in the body." },
{ "field": "contact_email", "problem": "This email doesn't look right.", "fix": "Send the press contact's full email, for example press@acme.com." }
] } }With no problems, the answer has the release id, status: "draft" and a free private preview_url. No image: the page shows a cover card with the logo and headline (never an AI image). An image under 600 px wide, or one we cannot read, never blocks the release: the answer has image_note saying why, and the page uses the cover card.
2. Publish it (1 credit):
curl -X POST https://livepublicator.com/api/v1/releases/RELEASE_ID/publish \
-H "Authorization: Bearer $LP_KEY" \
-H "Idempotency-Key: acme-launch-1-publish"The status becomes live (with live_url), scheduled (if you sent publish_at) or review (a person checks it first).
The same in JavaScript:
const api = (path, init = {}) =>
fetch("https://livepublicator.com/api/v1" + path, {
...init,
headers: { Authorization: "Bearer " + process.env.LP_KEY, "Content-Type": "application/json", ...init.headers },
}).then(async (r) => ({ status: r.status, body: await r.json() }));
const draft = await api("/releases", {
method: "POST",
headers: { "Idempotency-Key": "acme-launch-1" },
body: JSON.stringify({ company_name: "Acme Robotics", company_domain: "acmerobotics.com", client_authorized: true, headline, summary, body, key_facts, quote, quote_name, quote_title, contact_name, contact_title, contact_email }),
});
if (draft.status === 422) throw new Error(draft.body.error.problems.map((p) => p.field + ": " + p.fix).join("
"));
const r = draft.body;
const published = await api("/releases/" + r.id + "/publish", { method: "POST", headers: { "Idempotency-Key": "acme-launch-1-publish" } });
console.log(published.body.status, published.body.live_url);Endpoints
POST /releasesCreate a draft from your finished release: company_name, company_domain, headline, summary (one sentence), body (250 to 1,000 words), key_facts (1 to 8), quote with quote_name and quote_title, contact_name, contact_title, contact_email; optional faq, contact_phone, logo_url, publish_at (ISO 8601, up to 60 days ahead), and a cover image as image_url (https) or image_base64 (landscape, 1200 px wide or more, JPEG, PNG or WebP, up to 5 MB). validate_only: true checks without creating. Free.POST /releases/{id}/publishPublish a draft. 1 credit.GET /releases/{id}Status, preview_url, live_url and notes from the content check.GET /releases?limit=20Your releases, newest first (up to 100).GET /creditsCredits left, the next renewal, plan status, releases published today.Errors
Errors are JSON with a code, a message and, for 422, the field names:
{ "error": { "code": "validation", "message": "Some fields need fixing.", "fields": { "company_domain": "Required: the company's public website, e.g. acme.com." } } }- 401 unauthorized: no key, or an unknown key.
- 402 no_credits: no credits left.
- 403 revoked (the key was revoked) or plan_paused (a payment failed, or the plan ended).
- 409 duplicate: already published, or an Idempotency-Key reused with a different body.
- 422 validation:
problemslists every problem with its field and fix (alsofields, by name). - 429 daily_cap: 10 releases a day per key (or 120 calls a minute).
Rate limits
Each key can make 120 calls a minute. Every answer says where you are, so you can slow down before you hit the limit:
X-RateLimit-Limit: calls allowed a minute (120).X-RateLimit-Remaining: calls left this minute. A call without a valid key is not counted.X-RateLimit-Reset: when this minute ends, in Unix seconds.RateLimitandRateLimit-Policy: the same, in the IETF draft form.- On a 429,
Retry-Aftersays how many seconds to wait.
Webhooks
Add a webhook URL (https) to a key in your account. We send release.published and release.held (waiting for a person, or not published), and try again for about an hour if your server does not answer 2xx.
{ "id": "delivery id", "event": "release.published", "created_at": "2026-10-05T09:00:00Z", "data": { "id": "...", "status": "live", "live_url": "..." } }Check the X-LivePublicator-Signature header (t=unix seconds,v1=hex): an HMAC-SHA256 of t + "." + raw body with the key's webhook secret.
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(rawBody, header, secret) {
// header: "t=1759600000,v1=<64 hex characters>"
const { t, v1 } = Object.fromEntries((header ?? "").split(",").map((part) => part.split("=")));
if (!t || !v1 || v1.length !== 64 || Math.abs(Date.now() / 1000 - Number(t)) > 600) return false;
const want = createHmac("sha256", secret).update(t + "." + rawBody).digest("hex");
return timingSafeEqual(Buffer.from(want), Buffer.from(v1));
}MCP server for AI tools
The same releases as tools for AI assistants: create_release, update_release, add_image, publish_release, get_release_status, list_releases, fix_live_release, get_account, and preview_press_release (no sign-in). Server URL: https://livepublicator.com/api/mcp (Streamable HTTP). In Claude, ChatGPT and other assistants, add it as a connector and sign in (OAuth): see AI assistants. In your own code it also takes your API key in the Authorization header. The same credits and limits apply, and publishing always uses a credit, so keep the key private.
Claude Code:
claude mcp add --transport http livepublicator https://livepublicator.com/api/mcp --header "Authorization: Bearer lp_live_..."Claude Desktop (Settings, Developer, Edit config), through the mcp-remote bridge:
{
"mcpServers": {
"livepublicator": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://livepublicator.com/api/mcp", "--header", "Authorization: Bearer lp_live_..."]
}
}
}ChatGPT and the OpenAI API: in the Responses API or the Agents SDK, add an MCP tool with server_url set to the URL above and headers set to { "Authorization": "Bearer lp_live_..." }. Connectors inside the ChatGPT app ask for a sign-in (OAuth), which we don't offer yet: use the API route for now.
By using the API you accept the API terms. Questions: use the contact page.