heedbackdocs

Public API & webhooks

The Public API lets your own tools read and write heedback data, and webhooks push signed events to your systems the moment something changes. Both are available on the Team plan.

Create and manage credentials under Settings → API and Settings → Webhooks in the dashboard.

Authentication

Every request to /api/v1/* is authenticated with an API key sent as a bearer token:

curl -H "Authorization: Bearer $HEEDBACK_API_KEY" \
  https://YOUR_WORKSPACE.heedback.app/api/v1/boards
  • Keys are shown once at creation — copy the key then; it can't be shown again. Only a short prefix is stored for display.
  • Each key has a scope: read (GET only) or write (read + write).
  • Optionally set an expiry at creation (or leave it blank to never expire).
  • Rotate a key (reissue a new secret), disable/enable it (reversible), or delete it. Any change — rotate, disable, delete, or expiry — stops the key working within ~30 seconds.
  • Requests are rate-limited to 120 requests/minute per key — over the limit returns 429 with a Retry-After header.

Endpoints (v1)

MethodPathScopeDescription
GET/api/v1/boardsreadList boards
GET/api/v1/boards/{id}readGet one board
GET/api/v1/posts?boardId=…readList a board's posts (cursor-paginated, limit ≤ 100)
POST/api/v1/postswriteCreate a post (boardId, title, optional body, statusId)
GET/api/v1/posts/{id}readGet one post
PATCH/api/v1/posts/{id}writeChange a post's status (statusId)
GET/api/v1/posts/{id}/votesreadList a post's votes
POST/api/v1/posts/{id}/voteswriteCast a vote (voterId)
GET/api/v1/posts/{id}/commentsreadList a post's comments
POST/api/v1/posts/{id}/commentswriteAdd a comment (body)
GET/api/v1/changelogreadList published changelog entries
POST/api/v1/changelogwriteCreate a draft entry (never auto-published)

Pagination: list responses return a nextCursor — pass it back as ?cursor=… for the next page.

Error format

Errors return { "error": { "code", "message" } } with these codes:

StatuscodeMeaning
401unauthorizedMissing, malformed, unknown, or revoked key
403insufficient_scopeA read key used on a write route
403not_entitledThe workspace's plan no longer includes API access
404not_foundThe resource doesn't exist in your workspace
429rate_limitedOver 120 req/min — see Retry-After
400bad_requestA required field is missing or invalid
503service_unavailableVerification is temporarily unavailable

A key for one workspace can never read or write another workspace's data — foreign ids return 404.

Webhooks

Add an endpoint under Settings → Webhooks, choose which events to receive, and copy the signing secret (shown once). heedback POSTs a JSON envelope to your URL for each subscribed event:

{
  "product": "heedback",
  "event": "post.created",
  "occurredAt": 1730000000000,
  "payload": { "postId": "…", "boardId": "…", "title": "…" }
}

Event catalog

EventFires when
post.createdA post is created (dashboard, portal, widget, or API)
post.status_changedA post's status changes
vote.castA vote is cast
comment.createdA comment is added
changelog.publishedA changelog entry goes live

Authenticating deliveries: HMAC or shared secret

When you create an endpoint, choose how deliveries authenticate themselves:

  • HMAC signature (recommended, default) — each delivery is signed and sent with an X-Webhook-Signature header. Nothing secret is transmitted, and the signature also proves the body wasn't altered in transit. Verify it as shown below.
  • Shared secret header — each delivery instead sends Authorization: Bearer <secret> with your endpoint secret. Simpler to check (compare the header to your stored secret), but the secret travels on every request and it doesn't prove body integrity. Use it only if HMAC is impractical for your receiver.

Headers

Common to every delivery:

X-Webhook-Timestamp: <unix_ms>
X-Webhook-Id: <deliveryId>   # idempotency key  delivery is at-least-once
X-Webhook-Event: post.created

HMAC mode adds X-Webhook-Signature: sha256=<hex>; shared-secret mode adds Authorization: Bearer <secret> instead.

Verifying a delivery

A webhook is an outbound request from Heedback to your URL — there's no login in front of your receiver, so anyone who learns the URL could POST fake events. The signature is how you authenticate that a delivery genuinely came from Heedback (only you and Heedback know the endpoint's secret) and wasn't tampered with in transit. Always verify it — even if your endpoint also sits behind your own auth.

Recompute the HMAC over ${timestamp}.${rawBody} with your secret and compare it to X-Webhook-Signature. Reject if the timestamp is more than 5 minutes old (replay protection), and treat X-Webhook-Id as an idempotency key.

import crypto from "node:crypto";

function verify(req, secret) {
  const ts = req.headers["x-webhook-timestamp"];
  const sig = req.headers["x-webhook-signature"]; // "sha256=<hex>"
  const expected =
    "sha256=" +
    crypto
      .createHmac("sha256", secret)
      .update(`${ts}.${req.rawBody}`)
      .digest("hex");
  const ok = crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
  const fresh = Date.now() - Number(ts) < 5 * 60_000;
  return ok && fresh;
}

Language-agnostic pseudocode:

expected = "sha256=" + HMAC_SHA256(secret, timestamp + "." + rawBody)
valid = constant_time_equals(expected, X-Webhook-Signature)
        AND (now - timestamp) < 5 minutes

Retries & delivery log

A failing endpoint is retried with exponential backoff (up to ~6 attempts). Every attempt is recorded in the delivery log on the Webhooks settings page. An endpoint that fails 10 consecutive events is auto-disabled and the workspace owner is emailed; re-enable it from settings once it's healthy again.