Skip to main content
Posts

Create a new post

Creates a new post (feedback submission) in the specified board.

POST
/v2/posts

Required Fields

  • title - Post title (minimum 2 characters). Required unless intakeMode is feedback; see "Intake mode" below.

Optional Fields

  • boardId - Board ID to create the post in. Omit to use the organization's default board.
  • intakeMode - request (default) or feedback; see "Intake mode".
  • source - { channel, externalId, url?, conversationId?, label? }; see "Provenance and idempotency".
  • attachTo - An open request this post belongs with; see "attachTo, or link-insight?".
  • internal - Store on the hidden internal board (never portal-visible); not with boardId.
  • content - Post content in HTML format
  • tags - Array of tag names to attach
  • statusId - Status ID to set (defaults to board's default status)
  • commentsEnabled - Whether comments are allowed (default: true)
  • inReview - Whether post is pending moderation (default: false; always true for source.channel: 'call')
  • customFields - Custom field values as key-value pairs
  • eta - Estimated completion date (Unix timestamp or ISO date)
  • assigneeId - Admin ID to assign this post to
  • visibility - Post-level restriction: 'public', 'authorOnly' (author and admins) or 'companyOnly' (the author's company). Board and organization access controls still apply.

Author Attribution

For posts created on behalf of users, use the author object:

  • id - Featurebase user ID
  • userId - External SSO user ID
  • email - User's email address
  • name - Display name
  • profilePicture - Profile picture URL

Resolution priority: id > userId > email > authenticated user

Intake mode

intakeMode is the caller's declared intent and the ONE field that selects how the post is processed; author and source only describe where the text came from.

  • request (default): a finished request, stored exactly as supplied — no AI extraction, no rewrite. Featurebase may still link existing customer evidence TO it in the background.
  • feedback: raw customer feedback (a Slack message, a call note, a survey answer). Processed like a portal post: organized into requests when the workspace's "Organize submissions" lane is on, otherwise its claims are extracted and matched against existing requests. The workspace's Autopilot dial, plan, AI budget, moderation and spam settings apply, also when the post is filed under the API key's own user. statusId and eta are rejected (400). Feedback creates are rate-limited per workspace more strictly than requests (429 with Retry-After).

In feedback mode title is optional: omit it and the first line of content becomes the title, cut to 120 characters; content is then required (400 on content when both are empty). In request mode title stays required, minimum 2 characters.

At most 5 requests organized from one submission notify the team (admin notifications, mentions, Slack, Discord, tracker pushes); the rest are created silently, still returned in processing.results and still fire post.created.

Length limits for feedback

Text is read whole or rejected (400 on content; the message states the limit and the length sent). The count is plain text: title plus content without HTML. The limit follows source.channel: call 120,000 (a two-hour transcript), email and api 60,000, slack and discord 30,000; no channel means api. Workspaces with the AI switched off have no limit. To send more, split the text and give each part its own source.externalId.

Long text is read in parts, then each ask is matched and filed: seconds for a message, tens of minutes for a two-hour transcript. A repeated ask is reported once. One submission yields at most 30 requests: the 29 strongest, plus one held request listing the rest. Without the organize lane, at most 50 asks are captured. At most 20 submissions per workspace per hour may exceed one reading pass (about 16,000 characters with the organize lane, otherwise 6,000, or 24,000 for a call); past that, 429 with Retry-After.

The response reports intakeMode and processing: status is queued (an AI run was enqueued; its result lands on the post later), skipped with a reason (request_mode, autopilot_off, pipeline_paused, support_board, spam_held, staff_authored, …), or existing on an idempotent replay. Fetch GET /v2/posts/{id} to read where the feedback ended up.

Provenance and idempotency

source records where a request came from and makes the create idempotent: sending the same (channel, externalId) twice returns the first post unchanged, with deduped: true. Ids are stored as api:<externalId>, so they never collide with Featurebase's own. The channels feedback, widget and support are reserved for the portal, the widget and the inbox (400). With source and no author, the post is attributed to a guest named after source.label (or the channel): a relayed request belongs to the customer who said it, not to the API key.

attachTo, or link-insight?

attachTo records that the NEW post belongs with an existing request. The post stays a post: it keeps kind: 'issue', stays in GET /v2/posts, keeps its own votes, and it is not listed by GET /v2/posts/{targetId}/insights. The link is silent: no notification, no ack. Its author does not become a supporter of the target when the post is created. A held post's author (inReview) becomes a supporter (voter and subscriber) of the target when a member approves it, if they can open the target; source.channel: 'call' posts are always held.

POST /v2/posts/{id}/link-insight files the post as a quote under the request instead: it becomes an insight, leaves the posts resource (GET /v2/posts/{id} answers 404), appears in GET /v2/posts/{targetId}/insights, and its author counts as a supporter.

Two customers asking for the same thing who each keep their own request → attachTo. A sentence that is evidence for a request you already track → create the post, then link-insight. Both take an open request in this workspace as target.

attachTo cannot be combined with intakeMode: 'feedback' (400 invalid_parameter on attachTo): one says where the text belongs, the other asks Featurebase to decide. The link is made AFTER the post is written, so an unusable target is a 422 that says the post was created — not rolled back. The target is refused when it is missing in this workspace, not a request, merged, a processed submission, awaiting moderation, held as spam, or the new post itself.

Backdating (Imports)

  • createdAt - Override creation date for importing historical data

Response

  • 201 - created. The post object, plus intakeMode and processing.
  • 200 - a post already existed for this (source.channel, source.externalId): that post, unchanged, with deduped: true and processing.status: 'existing'; a replay never links anything a second time.
  • 422 - attachTo named a request that cannot carry evidence. The post was still created; the error message says so.
Authorizationstringheaderrequired

API key as Bearer token. Use: Authorization: Bearer sk_...

Featurebase-Versionenum<string>header

API version for this request. Defaults to your organization's configured API version if not specified.

Available options: 2026-08-19.orbit, 2026-01-01.nova, 2025-12-12.clover
Example: 2026-08-19.orbit

Body

application/json
assigneeIdstring

Admin ID to assign this post to

Example: 507f1f77bcf86cd799439013
attachTostring

Id of an existing open request this post is evidence for. The post is created, then linked. A held post's author (inReview) becomes a supporter (voter and subscriber) of the target when a member approves it, if they can open the target; source.channel: 'call' posts are always held.

Example: 507f1f77bcf86cd799439014
authorobject

Author to attribute the post to. If not provided, uses the authenticated user — unless source is given, in which case a guest author is synthesised from source.label (or the channel name), because a relayed request belongs to the customer who said it. Supports multiple identification methods: id (Featurebase ID), userId (external SSO ID), or email.

boardIdstring

Board ID to create the post in. Omit to use the default board of the organization.

Example: 507f1f77bcf86cd799439011
commentsEnabledboolean | nulldefault:true

Whether comments are enabled on this post

Example: true
contentstringdefault:

Post content (HTML). Required when intakeMode is 'feedback' and no title is given. In 'feedback' mode its plain text is limited by source.channel (see intakeMode); longer content is rejected with a 400.

Example: <p>It would be great to have dark mode.</p>
createdAtstring | null

Creation date (for backdating imports)

Example: 2025-01-15T10:30:00.000Z
customFieldsobject

Custom field values keyed by field ID (ObjectId). Send each value in the form its field type takes: text: a string; number: a number or a numeric string ("5"); checkbox: true/false or "true"/"false"; date: an ISO 8601 string; select: an option label or id; multi-select: an option label or id, or an array of them. null clears a field. A value the field type cannot take is rejected with a 400.

etastring | null

Estimated completion date

Example: 2025-12-31T23:59:59.000Z
inReviewboolean | nulldefault:false

Whether post is pending moderation. Ignored for source.channel: 'call': a call is always created pending moderation (inReview: true).

Example: false
intakeModeenum<string>

What the text is. 'request' (default): a finished request — stored exactly as supplied, no AI claim extraction, regardless of author. 'feedback': raw customer feedback — processed like a portal post (organized into a request, or its claims extracted and matched against existing requests) under the workspace's Autopilot dial, plan, AI budget and moderation settings. In 'feedback' mode title is optional (the first line of content becomes it), statusId and eta are rejected (400) because raw feedback has no decision yet, and text is read in full up to the limit of its source.channel — 120,000 characters of plain text for 'call' (a two-hour transcript), 60,000 for 'email' and 'api', 30,000 for 'slack' and 'discord'; without a source.channel the 'api' limit applies — while anything longer is rejected (400) rather than trimmed. Long text takes tens of minutes rather than seconds to process, and one submission yields at most 30 requests.

Available options: request, feedback
Example: feedback
integrationsobjectdefault:{}

Push the created post to third-party integrations configured on your organization. Each integration must be explicitly set to true to trigger; omitted integrations will not be pushed to.

internalboolean

Create this request on the internal board. Never visible on the portal.

Example: true
notifyAdminsbooleandefault:false

Whether to send email notifications to admins when this post is created. When true, admins will receive the same email notifications as when a post is created from the dashboard. Defaults to false (no emails sent).

Example: true
portalHiddenboolean

When true, hides the issue from portal/public surfaces. Omitted or false is visible. Ignored for source.channel: 'call': a call is always created hidden (portalHidden: true).

Example: true
sourceobject

Provenance of this request, and the idempotency key for the create. With source and no author, the post is attributed to a guest named after label (or the channel) so it belongs to the customer who said it, not to the API key.

statusIdstring

Status ID to set

Example: 507f1f77bcf86cd799439012
tagsstring | string[]

Tag names to attach

titlestring

Post title. Required unless intakeMode is 'feedback', where raw customer text rarely has one: omit it (or send it blank) and the first line of content becomes the title, cut to 120 characters.

Required string length: 2 - 512
Example: Add dark mode support
upvotesinteger | null

Initial upvotes count. Defaults to 1 (post author is automatically added as voter). Use 0 to create a post without any votes.

Required range: 0 <= x
Example: 5
visibilityenum<string>

Post visibility. 'public' = visible to all users, 'authorOnly' = only visible to the author and admins, 'companyOnly' = only visible to users in the same company as the author

Available options: public, authorOnly, companyOnly
Example: public

Response

application/json

Created

accessobjectrequired
anchorobject | nullrequired

When kind is 'insight', where exactly the insight points back into its origin: an insight source record with character ranges into its fullText, or the native conversation/message/comment/post ids.

assigneeIdstring | nullrequired

ID of the admin assigned to this post, null if unassigned

Example: 507f1f77bcf86cd799439013
authorobject | nullrequired
boardIdstringrequired

Board (category) ID this post belongs to

Example: 507f1f77bcf86cd799439011
commentCountnumberrequired

Total number of comments

Example: 5
contentstringrequired

Post content in HTML format

Example: <p>It would be great to have a dark mode option for the dashboard.</p>
createdAtstringrequired

ISO 8601 timestamp when created

Example: 2023-12-12T00:00:00.000Z
customFieldsobjectrequired

Custom field values keyed by field ID

dedupedenum<boolean>

Present and true only on POST /v2/posts, when the request carried a source.externalId that already had a post. The existing post is returned unchanged with HTTP 200; a newly created post returns HTTP 201 without this field.

Available options: true
Example: true
etastring | nullrequired

Estimated completion time as ISO 8601 timestamp, null if not set

Example: 2025-01-01T00:00:00.000Z
featuresobjectrequired
groupKeystring | nullrequired

When kind is 'insight', the triage grouping key (source record id, conversation id, origin post id, or the insight's own id for singletons). Legacy insights may be null and group as singletons.

idstringrequired

Unique identifier

Example: 507f1f77bcf86cd799439011
inReviewbooleanrequired

Whether the post is pending moderation review

Example: false
insightSourceobject | nullrequired

Provenance of an insight: which channel it came from and how it was captured.

intakeModeenum<string>

Present only on POST /v2/posts: the intakeMode the post was processed under ('request' when the request named none). On an idempotent replay (deduped: true) this is the mode the post was ORIGINALLY created with.

Available options: request, feedback
Example: request
integrationsobjectrequired

Third-party integration links associated with this post

isPinnedbooleanrequired

Whether the post is pinned to the top

Example: false
kindenum<string>required

Discriminates an actionable work item ('issue') from a customer submission whose claims were extracted into insights ('record' — not a work item). Defaults to 'issue' for all pre-existing posts. Default list responses return issues only; pass kind='record' to opt in. Raw signal ('insight') is never returned by the posts resource — insights are served by /v2/insights.

Available options: issue, insight, record
Example: issue
linkedInsightCountnumberrequired

Number of insights linked to this issue as supporting evidence. Only meaningful when kind is 'issue'.

Example: 0
linkedIssueIdstring | nullrequired

When kind is 'insight', the ID of the issue this insight supports. Null when the insight is unlinked or when kind is 'issue'.

objectenum<string>required

Object type identifier

Available options: post
Example: post
opportunityAmountnumber | nullrequired

Total opportunity amount from linked HubSpot deals and Salesforce opportunities

Example: 30000
portalHiddenbooleanrequired

True when the issue is hidden from portal/public surfaces. Missing stored values are returned as false.

Example: false
postUrlstringrequired

Full URL to view the post

Example: https://feedback.example.com/p/add-dark-mode-support
processingobject

On POST /v2/posts — queued: a processing run (claim extraction or the Organize rewrite) was enqueued and its result lands asynchronously on the post. skipped: nothing was enqueued; reason says which gate decided ('request_mode' for every intakeMode: 'request' create). existing: the create was an idempotent replay and the post was not processed again. On GET /v2/posts/{id} this field is present only for posts created with intakeMode: 'feedback' and reports how far that processing has got ('queued', 'processing', 'complete', 'needs_review', or 'skipped' with the same reason the create returned), with results listing what was made of the submission once the run has finished.

slugstringrequired

URL-friendly slug

Example: add-dark-mode-support
statusobjectrequired
tagsobject[]required

Tags attached to this post

titlestringrequired

Post title

Example: Add dark mode support
updatedAtstringrequired

ISO 8601 timestamp when last modified

Example: 2023-12-13T00:00:00.000Z
upvotesnumberrequired

Total number of upvotes

Example: 42