Developers

REST API reference.

A small, predictable JSON API over HTTPS. Every response wraps results in a data field; every error in an error field. Base URL: https://YOUR-APP/api/v1

Authentication

The REST API and webhooks are on the Team plan (and the 14-day trial). Create a key in Settings → Developers. Keys start with mur_live_, are scoped to one workspace, and are shown only once. Send the key as a Bearer token on every request.

requestbash
curl https://YOUR-APP/api/v1/posts?board=feature-requests&sort=top \
  -H "Authorization: Bearer mur_live_…"

The same key works with the MCP server. CORS is enabled, but never ship a key to the browser — call the API from your server.

Errors & limits

Errors return a non-2xx status and a body of the form { "error": { "status": 404, "message": "Post not found" } }. Validation failures return 400 with details about the offending field; a missing or invalid key returns 401.

FieldTypeDescription
400Bad requestValidation failed — check the message for the field.
401UnauthorizedMissing, malformed or revoked API key.
402Plan requiredThis needs the Team plan, or a plan limit was reached. The body includes an upgrade_url.
404Not foundThe object doesn’t exist in this workspace.
500Server errorSomething went wrong on our side. Safe to retry idempotent requests.

Posts

GET/api/v1/posts

List posts, newest or top first. Merged posts are excluded.

FieldTypeDescription
boardstringBoard slug or id.
statusstringopen, under_review, planned, in_progress, complete, closed.
qstringText search over titles and descriptions.
sortstringtrending (default), top, new, mrr, score, updated.
limitnumber1–100, default 25.
200 OKjson
{
  "data": [
    {
      "id": "p_81",
      "title": "Bulk-edit tickets from the inbox",
      "status": "planned",
      "status_label": "Planned",
      "board": { "id": "b_1", "slug": "feature-requests", "name": "Feature requests" },
      "category": { "id": "c_4", "name": "Inbox" },
      "tags": ["power-users"],
      "eta": "2026-11",
      "votes": 248,
      "comments": 31,
      "mrr": 118400,
      "score": 92,
      "url": "https://YOUR-APP/p/lumen/posts/p_81"
    }
  ],
  "count": 1
}
POST/api/v1/posts

Create a post. Pass an author email to attribute it to a customer (created if new).

FieldTypeDescription
boardstringRequired. Board slug or id (or board_id).
titlestringRequired. 3–140 characters.
bodystringMarkdown, up to 10,000 characters.
author_emailstringCustomer to attribute the post to.
author_namestringDisplay name for a new customer.
companystringCompany name to attach.
create a postbash
curl -X POST https://YOUR-APP/api/v1/posts \
  -H "Authorization: Bearer mur_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "board": "feature-requests",
    "title": "Scheduled CSV exports",
    "body": "Weekly export of the reports dashboard to S3.",
    "author_email": "grace@northwind.io",
    "company": "Northwind"
  }'
GET/api/v1/posts/{id}

Retrieve one post with its voters (name, email, company, segment, priority), comments and AI summary.

PATCH/api/v1/posts/{id}

Update a post. Changing status notifies voters; message is included in the update email.

FieldTypeDescription
statusstringNew status id.
messagestringOptional note sent with the status change.
title / bodystringEdit content.
ownerstring | nullTeam member email or id.
etastring | nullMonth in YYYY-MM format.
categorystring | nullCategory id or name.
update statusbash
curl -X PATCH https://YOUR-APP/api/v1/posts/p_81 \
  -H "Authorization: Bearer mur_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "status": "in_progress", "eta": "2026-11", "message": "Kicking off this cycle." }'

Votes

POST/api/v1/posts/{id}/votes

Vote on behalf of a customer. Idempotent per customer: repeat calls update the priority and return 200 instead of 201.

FieldTypeDescription
emailstringRequired. The customer’s email.
namestringName for a new customer.
companystringCompany to attach.
prioritystringnice, important (default) or must.
vote on behalfbash
curl -X POST https://YOUR-APP/api/v1/posts/p_81/votes \
  -H "Authorization: Bearer mur_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "email": "grace@northwind.io", "company": "Northwind", "priority": "must" }'

Comments

POST/api/v1/posts/{id}/comments

Add a comment. Without an email it posts as the workspace. Set internal for a team-only note.

FieldTypeDescription
bodystringRequired. Markdown.
emailstringComment as this customer.
namestringName for a new customer.
internalbooleanTeam-only. Default false.
parent_idstringReply to an existing comment.

Boards

GET/api/v1/boards

List boards with slug, name, privacy and post count. Use the slug wherever a board parameter is accepted.

Changelog

GET/api/v1/changelog

Published changelog entries, newest first, with labels and linked posts.

FieldTypeDescription
statusstringpublished (default) or all.
labelstringFilter by label, e.g. New or Fixed.
limitnumber1–100, default 20.

Autopilot capture

POST/api/v1/autopilot/capture

Send raw text — a transcript, an email, a review — and Autopilot extracts requests and suggests a matching post for each. Results appear in the Autopilot inbox for review.

FieldTypeDescription
textstringRequired. 10–50,000 characters.
sourcestringintercom, zendesk, gong, slack, hubspot, g2, email, api… (body or query).
customer_emailstringAttribute captured feedback to this customer.
customer_namestringName for a new customer.
company_namestringCompany to attach.
capturebash
curl -X POST "https://YOUR-APP/api/v1/autopilot/capture?source=intercom" \
  -H "Authorization: Bearer mur_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "We would love to bulk edit tickets from the inbox — 40 agents are waiting on this.",
    "customer_email": "grace@northwind.io",
    "company_name": "Northwind"
  }'

Webhooks

Add endpoints in Settings → Developers → Webhooks and choose the events to receive. Murmur sends a JSON POST with an X-Murmur-Event header and an X-Murmur-Signature header: the hex HMAC-SHA256 of the raw body, keyed with your endpoint’s secret.

post.createdpost.status_changedpost.mergedvote.createdcomment.createdchangelog.publishedautopilot.captured
payloadjson
{
  "event": "post.status_changed",
  "workspace": "lumen",
  "created_at": "2026-10-07T09:14:22.000Z",
  "data": {
    "post": { "id": "p_81", "title": "Bulk-edit tickets from the inbox", "status": "in_progress" },
    "from": "planned",
    "to": "in_progress"
  }
}

Always verify the signature against the raw request body before parsing it, and use a constant-time comparison.

verify.tsts
import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(rawBody: string, signature: string, secret: string) {
  const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
  return signature.length === expected.length &&
    timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}

// In your handler: verify(await req.text(), req.headers.get("x-murmur-signature")!, SECRET)