Skip to main content

API reference

Public API

The public developer API is mounted at https://api.featurejet.com/api/v1. Every endpoint is scoped to the API key organization; unknown and cross-org boards or posts return 404. Use it to connect customer feedback to the tools where your team already decides and ships.

Authentication

Create a key in the dashboard, then send it as a bearer token or X-API-Key. Raw keys are shown once and stored only as hashed keys with display prefixes. Keep real keys out of committed files, public terminals, and shared shell history.

Authorization: Bearer fj_live_REPLACE_ME
X-API-Key: fj_live_REPLACE_ME

Endpoints

GET

/api/v1/boards

Params: none

Returns: ApiBoardListResponse

GET

/api/v1/boards/{slug}

Params: slug

Returns: BoardAdmin

POST

/api/v1/boards

Params: slug, name, description

Returns: BoardAdmin, 201

POST

/api/v1/boards/{slug}/publish

Params: slug

Returns: BoardAdmin — off by default (403 publish_disabled); publishing is a dashboard step

POST

/api/v1/boards/{slug}/unpublish

Params: slug

Returns: BoardAdmin — back to draft, nothing deleted

GET

/api/v1/boards/{slug}/posts

Params: status, q, tag, sort=top|new, limit=1..500, offset>=0

Returns: PostListResponse

POST

/api/v1/boards/{slug}/posts

Params: title, body, status, tags, pinned

Returns: PostPublic, 201

GET

/api/v1/boards/{slug}/posts/{id}

Params: slug, id

Returns: PostPublic

PATCH

/api/v1/boards/{slug}/posts/{id}

Params: title, body, status, tags, pinned, queue_rank, implementation_evidence, notify_voters

Returns: PostPublic

DELETE

/api/v1/boards/{slug}/posts/{id}

Params: slug, id

Returns: 204

GET

/api/v1/boards/{slug}/queue

Params: limit=1..500 (default 10)

Returns: PostListResponse — top unclaimed posts, owner-ranked first

POST

/api/v1/boards/{slug}/posts/{id}/claim

Params: ttl_seconds=1..86400 (default 3600), agent_label

Returns: claim {post_id, agent_label, claimed_at, expires_at}; 409 if claimed by another key

DELETE

/api/v1/boards/{slug}/posts/{id}/claim

Params: slug, id

Returns: 204; 409 if another key holds the claim

POST

/api/v1/boards/{slug}/posts/{id}/evidence

Params: kind=branch|commit|pr|transcript|url, ref, label

Returns: PostPublic, 201 — evidence renders on the public post page

POST

/api/v1/boards/{slug}/posts/{id}/propose-status

Params: status=open|planned|in_progress|shipped|declined, note

Returns: PostPublic — records a PENDING proposal only; nothing changes and no voter is emailed until a human confirms

GET

/api/v1/boards/{slug}/posts/{id}/votes

Params: slug, id

Returns: ApiVoteListResponse

GET

/api/v1/boards/{slug}/posts/{id}/comments

Params: slug, id

Returns: CommentListResponse with reaction_counts and own_reactions

POST

/api/v1/boards/{slug}/posts/{id}/comments

Params: body, parent_comment_id

Returns: CommentPublic, 201 — posted as the board team

GET

/api/v1/boards/{slug}/roadmap

Params: slug

Returns: PostListResponse for open, planned, in_progress, shipped

GET

/api/v1/boards/{slug}/changelog

Params: limit=1..500, offset>=0

Returns: PostListResponse for shipped posts

GET

/api/v1/boards/{slug}/ship-stats

Params: period=month|week

Returns: ShipStatsResponse

GET

/api/v1/boards/{slug}/analytics

Params: period=30d|90d

Returns: BoardAnalyticsResponse

GET

/api/v1/webhooks

Params: none

Returns: WebhookListResponse — read-only; endpoints are created in the dashboard

Boards and posts

List boards
curl https://api.featurejet.com/api/v1/boards \
  -H "Authorization: Bearer fj_live_REPLACE_ME"
Board list response
{
  "boards": [{
    "id": 1,
    "slug": "acme",
    "name": "Acme Feedback",
    "description": "Feature requests from customers",
    "welcome_message": "Tell us what we should ship next",
    "published": true,
    "created_at": "2026-07-09T14:00:00Z",
    "logo_url": null
  }]
}
Board response
{
  "id": 1,
  "slug": "acme",
  "name": "Acme Feedback",
  "description": "Feature requests from customers",
  "welcome_message": "Tell us what we should ship next",
  "published": true,
  "created_at": "2026-07-09T14:00:00Z",
  "logo_url": null
}
Post list response
{
  "posts": [{
    "id": 42,
    "title": "Dark mode",
    "body": "Please add a system theme",
    "status": "planned",
    "vote_count": 12,
    "voter_has_voted": false,
    "following": false,
    "is_admin": true,
    "hidden": false,
    "pinned": false,
    "roadmap_estimate": null,
    "shipped_at": null,
    "created_at": "2026-07-09T14:30:00Z",
    "attachments": [],
    "tags": []
  }],
  "total": 1
}
Create a post
curl https://api.featurejet.com/api/v1/boards/acme/posts \
  -H "Authorization: Bearer fj_live_REPLACE_ME" \
  -H "Content-Type: application/json" \
  -d '{"title":"Dark mode","body":"Please add a system theme","status":"planned","tags":["UI"],"pinned":false}'
Update a post
curl -X PATCH https://api.featurejet.com/api/v1/boards/acme/posts/42 \
  -H "Authorization: Bearer fj_live_REPLACE_ME" \
  -H "Content-Type: application/json" \
  -d '{"status":"shipped","notify_voters":true}'
Post response
{
  "id": 42,
  "title": "Dark mode",
  "body": "Please add a system theme",
  "status": "planned",
  "vote_count": 12,
  "voter_has_voted": false,
  "following": false,
  "is_admin": true,
  "hidden": false,
  "pinned": false,
  "roadmap_estimate": null,
  "shipped_at": null,
  "created_at": "2026-07-09T14:30:00Z",
  "queue_rank": 1,
  "implementation_evidence": [],
  "claim": null,
  "attachments": [],
  "tags": [{"id": 7, "name": "UI", "slug": "ui", "color": null}]
}

Endpoint examples

Get board
curl https://api.featurejet.com/api/v1/boards/acme \
  -H "Authorization: Bearer fj_live_REPLACE_ME"
List posts with filters
curl "https://api.featurejet.com/api/v1/boards/acme/posts?status=planned&tag=ui&sort=new&limit=25&offset=0" \
  -H "Authorization: Bearer fj_live_REPLACE_ME"
Read one post
curl https://api.featurejet.com/api/v1/boards/acme/posts/42 \
  -H "Authorization: Bearer fj_live_REPLACE_ME"
Votes
curl https://api.featurejet.com/api/v1/boards/acme/posts/42/votes \
  -H "Authorization: Bearer fj_live_REPLACE_ME"

{"post_id":42,"vote_count":12}
Comments
curl https://api.featurejet.com/api/v1/boards/acme/posts/42/comments \
  -H "Authorization: Bearer fj_live_REPLACE_ME"

{
  "comments": [{
    "id": 9,
    "body": "We need this too",
    "is_admin": false,
    "hidden": false,
    "vote_count": 3,
    "voter_has_voted": false,
    "reaction_counts": {"👍": 2},
    "own_reactions": [],
    "voter_blocked": false,
    "parent_comment_id": null,
    "created_at": "2026-07-09T14:45:00Z",
    "attachments": [],
    "replies": []
  }]
}
Create a comment (posted as your team)
curl https://api.featurejet.com/api/v1/boards/acme/posts/42/comments \
  -H "Authorization: Bearer fj_live_REPLACE_ME" \
  -H "Content-Type: application/json" \
  -d '{"body":"Shipping this week — thanks for flagging it.","parent_comment_id":9}'

{
  "id": 12,
  "body": "Shipping this week — thanks for flagging it.",
  "is_admin": true,
  "hidden": false,
  "vote_count": 0,
  "voter_has_voted": false,
  "reaction_counts": {},
  "own_reactions": [],
  "voter_blocked": false,
  "parent_comment_id": 9,
  "created_at": "2026-07-18T09:00:00Z",
  "attachments": [],
  "replies": []
}
Delete post
curl -X DELETE https://api.featurejet.com/api/v1/boards/acme/posts/42 \
  -H "Authorization: Bearer fj_live_REPLACE_ME"

# 204 No Content

Agent workflow: queue, claims, evidence, and proposals

The queue returns the top unclaimed posts in the board owner's curated order — ranked posts first (queue_rank, lower is sooner), then unranked planned posts by votes. Claim a post before working it so two agents never build the same thing: claims are atomic, carry a TTL (default one hour, max one day), extend on re-claim by the same key, and lapse on their own if the agent dies. Evidence entries are shown to voters on the public post page, so the work is verifiable, not just a status flip. Claims are never shown on public board pages.

When you want a human gate on the last step, propose-status is the agent-honest alternative to a status PATCH: it records a pending proposal, leaves the public board untouched, and emails nobody until the board owner confirms it from the dashboard. A propose-scoped key can propose but can never flip a status directly.

Pull the next unclaimed item
curl "https://api.featurejet.com/api/v1/boards/acme/queue?limit=1" \
  -H "Authorization: Bearer fj_live_REPLACE_ME"

{
  "posts": [{
    "id": 42,
    "title": "Dark mode",
    "status": "planned",
    "queue_rank": 1,
    "vote_count": 12,
    "claim": null
  }],
  "total": 6
}
Claim a post (atomic, with TTL)
curl https://api.featurejet.com/api/v1/boards/acme/posts/42/claim \
  -H "Authorization: Bearer fj_live_REPLACE_ME" \
  -H "Content-Type: application/json" \
  -d '{"ttl_seconds":7200,"agent_label":"overnight-agent"}'

{
  "post_id": 42,
  "agent_label": "overnight-agent",
  "claimed_at": "2026-07-18T09:00:00Z",
  "expires_at": "2026-07-18T11:00:00Z"
}

# 409 conflict if another API key holds a live claim;
# re-claiming with the SAME key extends the lease.
Release a claim
curl -X DELETE https://api.featurejet.com/api/v1/boards/acme/posts/42/claim \
  -H "Authorization: Bearer fj_live_REPLACE_ME"

# 204 No Content — claims also lapse on their own when the TTL expires
Attach implementation evidence
curl https://api.featurejet.com/api/v1/boards/acme/posts/42/evidence \
  -H "Authorization: Bearer fj_live_REPLACE_ME" \
  -H "Content-Type: application/json" \
  -d '{"kind":"pr","ref":"https://github.com/acme/app/pull/12","label":"PR #12"}'

# 201 — returns the post; evidence shows in the public post page's
# "Implementation" section:
{
  "implementation_evidence": [{
    "kind": "pr",
    "ref": "https://github.com/acme/app/pull/12",
    "label": "PR #12",
    "added_at": "2026-07-18T09:12:00Z"
  }]
}
Propose a status change for a human to confirm
curl https://api.featurejet.com/api/v1/boards/acme/posts/42/propose-status \
  -H "Authorization: Bearer fj_live_REPLACE_ME" \
  -H "Content-Type: application/json" \
  -d '{"status":"shipped","note":"Shipped in PR #12 — please confirm."}'

# 200 — NOTHING changed on the public board and no voter was emailed.
# The pending proposal is visible to the org only, until a human confirms
# it from the dashboard:
{
  "id": 42,
  "status": "planned",
  "proposal": {
    "status": "shipped",
    "note": "Shipped in PR #12 — please confirm.",
    "proposed_at": "2026-07-18T09:20:00Z",
    "proposed_by_api_key_id": 3
  }
}

# 422 if the post already has the proposed status.
# A propose-scoped key can call this but can NEVER flip a status directly.

Roadmap, changelog, stats, and ship velocity

Status values are open, planned, in_progress, shipped, and declined. List endpoints default to 100 rows and cap at 500 with limit and offset. Roadmap returns visible open, planned, in_progress, and shipped posts. Changelog returns shipped posts, while analytics shows whether requests are becoming launched features.

Roadmap
curl https://api.featurejet.com/api/v1/boards/acme/roadmap \
  -H "Authorization: Bearer fj_live_REPLACE_ME"
Changelog
curl "https://api.featurejet.com/api/v1/boards/acme/changelog?limit=25&offset=0" \
  -H "Authorization: Bearer fj_live_REPLACE_ME"
Roadmap response
{
  "posts": [{
    "id": 42,
    "title": "Dark mode",
    "body": "Please add a system theme",
    "status": "planned",
    "vote_count": 12,
    "voter_has_voted": false,
    "following": false,
    "is_admin": true,
    "hidden": false,
    "pinned": false,
    "roadmap_estimate": null,
    "shipped_at": null,
    "created_at": "2026-07-09T14:30:00Z",
    "attachments": [],
    "tags": []
  }],
  "total": 1
}
Changelog response
{
  "posts": [{
    "id": 43,
    "title": "Export requests",
    "body": "CSV export is now live",
    "status": "shipped",
    "vote_count": 18,
    "voter_has_voted": false,
    "following": false,
    "is_admin": true,
    "hidden": false,
    "pinned": false,
    "roadmap_estimate": null,
    "shipped_at": "2026-07-09T15:00:00Z",
    "created_at": "2026-07-01T12:00:00Z",
    "attachments": [],
    "tags": []
  }],
  "total": 1
}
Ship stats
curl "https://api.featurejet.com/api/v1/boards/acme/ship-stats?period=month" \
  -H "Authorization: Bearer fj_live_REPLACE_ME"

{
  "period": "month",
  "period_label": "this month",
  "shipped_count": 4,
  "voter_count": 91,
  "posts": []
}
Analytics
curl "https://api.featurejet.com/api/v1/boards/acme/analytics?period=30d" \
  -H "Authorization: Bearer fj_live_REPLACE_ME"
Analytics response
{
  "period": "30d",
  "totals": {"posts": 48, "votes": 312, "comments": 86, "voters": 124},
  "status_breakdown": {"open": 20, "planned": 9, "in_progress": 4, "shipped": 12, "declined": 3},
  "posts_over_time": [{"date": "2026-07-09", "post_count": 3}],
  "votes_over_time": [{"date": "2026-07-09", "vote_count": 18}],
  "top_requests": [{"id": 42, "title": "Dark mode", "status": "planned", "vote_count": 12, "comment_count": 4, "created_at": "2026-07-09T14:30:00Z", "shipped_at": null}],
  "ship_velocity": {"shipped_total": 6, "shipped_per_week": 1.4, "shipped_per_month": 6},
  "engagement": {"comments_per_post": 1.79, "posts_with_votes_percent": 72.9}
}

Errors and rate limits

Errors are returned as an envelope. Rate limiting is per API key, defaults to 120 requests per minute, and 429 responses include Retry-After.

{
  "error": {
    "code": "validation_error",
    "message": "request validation failed"
  }
}

Prefer a typed client? Explore the official Python and Node.js SDKs, or connect through MCP.

API Reference — FeatureJet Docs