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) orwrite(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
429with aRetry-Afterheader.
Endpoints (v1)
| Method | Path | Scope | Description |
|---|---|---|---|
| GET | /api/v1/boards | read | List boards |
| GET | /api/v1/boards/{id} | read | Get one board |
| GET | /api/v1/posts?boardId=… | read | List a board's posts (cursor-paginated, limit ≤ 100) |
| POST | /api/v1/posts | write | Create a post (boardId, title, optional body, statusId) |
| GET | /api/v1/posts/{id} | read | Get one post |
| PATCH | /api/v1/posts/{id} | write | Change a post's status (statusId) |
| GET | /api/v1/posts/{id}/votes | read | List a post's votes |
| POST | /api/v1/posts/{id}/votes | write | Cast a vote (voterId) |
| GET | /api/v1/posts/{id}/comments | read | List a post's comments |
| POST | /api/v1/posts/{id}/comments | write | Add a comment (body) |
| GET | /api/v1/changelog | read | List published changelog entries |
| POST | /api/v1/changelog | write | Create 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:
| Status | code | Meaning |
|---|---|---|
| 401 | unauthorized | Missing, malformed, unknown, or revoked key |
| 403 | insufficient_scope | A read key used on a write route |
| 403 | not_entitled | The workspace's plan no longer includes API access |
| 404 | not_found | The resource doesn't exist in your workspace |
| 429 | rate_limited | Over 120 req/min — see Retry-After |
| 400 | bad_request | A required field is missing or invalid |
| 503 | service_unavailable | Verification 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
| Event | Fires when |
|---|---|
post.created | A post is created (dashboard, portal, widget, or API) |
post.status_changed | A post's status changes |
vote.cast | A vote is cast |
comment.created | A comment is added |
changelog.published | A 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-Signatureheader. 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.createdHMAC 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 minutesRetries & 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.