Skip to content

Moderated content feed ​

On Pro plan and higher, a form can expose a public, moderated feed of its approved submissions, with anonymous reactions and a report control. Visitors submit through your normal form, you approve what should be public, and your site shows it.

The short version, for static sites:

  • No API key. The feed, its files, reactions and reports are public endpoints on https://f.bootform.com. Nothing secret goes in your page.
  • CORS is open on every one of them (feed, files, react, report, status). Any origin can call them with fetch(), so a site on GitHub Pages, Netlify or plain HTML works with no server of your own.
  • Photos and files are included. A file uploaded under a public field comes with a public link you can use directly as an <img src>.
  • Only what you approve is shown, and only the fields you mark public.

Good fits: testimonials, guestbooks, community boards, a map of reported places, lost and found, giveaways and swaps. For listings that go stale, give the form an item status such as "taken" or "found": visitors mark an item, and the feed shows it.

Setting it up ​

From your form's Workflow tab, under Moderated content feed:

  • Enable submission moderation turns on the Approve/Reject/Undecided control inside each submission (see Managing submissions) and the Pending review folder. The feed shows nothing until this is on.
  • Public fields lists the fields that are safe to show publicly, file fields included. Every other field stays private even though it's stored: the feed omits it entirely rather than blanking it.
  • Report threshold hides a submission from the feed (pending your re-review) once it collects this many reports. Off by default; leave blank to keep it off. To bring a hidden submission back, approve it again: that starts its report count over. Visitors who already reported it can't report it a second time.
  • Item status (optional) lets visitors mark an item, for example as "taken". See Item status.

Every submission needs your approval before it appears. New submissions start as pending, and only the ones you approve are published.

Setting it up through the API ​

The same settings are part of PATCH https://api.bootform.com/forms/{form_id} (with an API key):

json
{
  "content_feed": {
    "moderation_enabled": true,
    "public_fields": ["description", "category", "lat", "lng", "photo"],
    "report_threshold": 3,
    "item_status": { "label": "taken", "threshold": 1 }
  }
}

public_fields replaces the whole list each time you send it. Send report_threshold: 0 to turn auto-hide off. An AI agent connected to the MCP server can do the same with bootform_configure_form (content_feed_enabled, public_fields, report_threshold, item_status_label). To approve a submission, PATCH .../forms/{form_id}/submissions/{submission_id} with {"moderation_status": "approved"} (or rejected, or pending to undo). The MCP tool bootform_moderate_submission does the same. Approving before moderation_enabled is on returns 422 MODERATION_NOT_ENABLED.

What the feed returns ​

GET https://f.bootform.com/forms/{form_id}/feed?page=1&per_page=50

form_id is the form's ID or its vanity slug. Items are approved submissions, newest first:

json
{
  "ok": true,
  "items": [
    {
      "id": "0b9c6a7e-4c2d-4f1a-9a55-2f6d1e0c8b11",
      "fields": { "description": "Two chairs, good condition", "category": "furniture", "lat": "37.98", "lng": "23.72" },
      "created_at": "2026-10-09T08:30:00Z",
      "reactions": { "up": 4, "down": 0 },
      "status": { "label": "taken", "set": false, "set_at": null, "count": 0 },
      "files": [
        {
          "field": "photo",
          "name": "chairs.jpg",
          "mime": "image/jpeg",
          "size": 184320,
          "url": "https://f.bootform.com/forms/11111111-1111-4111-8111-111111111111/feed/0b9c6a7e-4c2d-4f1a-9a55-2f6d1e0c8b11/files/0"
        }
      ]
    }
  ],
  "total": 1,
  "page": 1,
  "per_page": 50
}

Field values are always strings, exactly as submitted. status is null unless the form has an item status. Full schema: openapi.json / openapi.yaml.

Item status ​

An item status is one word or short phrase visitors can mark a feed item with: "taken" on a giveaway, "found" on a lost item, "sold", "full" on an event. It keeps a listing accurate without you moderating every change, and without a backend of your own.

Turn it on in the same Workflow tab section: type the label, choose how many visitors must mark an item before it counts (1 by default), and optionally hide marked items after some hours. Through the API it is content_feed.item_status (above): sending it replaces the whole status, and an empty label removes it. Through MCP, bootform_configure_form takes item_status_label, item_status_threshold and item_status_hide_after_hours.

Visitors mark an item with no key:

POST https://f.bootform.com/forms/{form_id}/feed/{submission_id}/status   (no body)

The answer is the item's status after the mark, for example {"ok": true, "status": {"label": "taken", "set": true, "set_at": "2026-10-09T12:00:00Z", "count": 1}}. Each IP address can mark an item once (429 on a repeat), and a form with no item status answers 422 STATUS_NOT_ENABLED.

The feed shows it on every item: set turns true once enough visitors marked it, or once you set it yourself. A set item stays in the feed with its status, unless you chose to hide marked items after a delay. To list only what's still available, add ?status=open; ?status=set lists only the marked ones.

You can correct it from the submission's page in the dashboard (Mark as taken or Clear), with PATCH .../submissions/{submission_id} and {"feed_status": "set"} or {"feed_status": "clear"}, or with MCP bootform_moderate_submission (feed_status). Your "set" counts whatever the visitor count; "clear" removes the status and every visitor mark, so the item starts over.

For a map of giveaways, for example, load ?status=open for the pins and give each popup a "Mark as taken" button that calls the status endpoint.

Photos and files ​

A file appears in files when the form field it was uploaded under is in Public fields, so a <input type="file" name="photo"> becomes public by adding photo to the list. Files under any other field stay private. File uploads need the Starter plan or higher (the feed itself needs Pro), and the size limits are on the pricing page.

Each file's url is a stable BootForm link. Use it as it is, for example as an <img src>:

  • Every request checks that the submission is still in the feed. While it is, the link redirects to the file, served from storage for 15 minutes at a time.
  • When you reject or archive the submission, mark it spam, remove the field from Public fields, or it reaches the report threshold, the link starts answering 404 within about a minute.
  • Use mime to decide how to show a file: render image/* inline, link to anything else.

Files uploaded before 9 October 2026 didn't record which field they came from, so they stay private.

Photos carry their location. A photo straight from a phone usually has GPS coordinates in its EXIF metadata, and BootForm stores and serves files exactly as uploaded. If the place a photo was taken shouldn't be public, strip the metadata in the browser before submitting, for example by redrawing the image onto a canvas:

js
async function withoutMetadata(file, maxSize = 1600) {
  const bitmap = await createImageBitmap(file)
  const scale = Math.min(1, maxSize / Math.max(bitmap.width, bitmap.height))
  const canvas = document.createElement('canvas')
  canvas.width = Math.round(bitmap.width * scale)
  canvas.height = Math.round(bitmap.height * scale)
  canvas.getContext('2d').drawImage(bitmap, 0, 0, canvas.width, canvas.height)
  const blob = await new Promise((resolve) => canvas.toBlob(resolve, 'image/jpeg', 0.85))
  return new File([blob], 'photo.jpg', { type: 'image/jpeg' })
}

// before sending: formData.set('photo', await withoutMetadata(formData.get('photo')))

This also shrinks large photos, so uploads stay well under your plan's per-file limit.

Showing the feed on your site ​

Option 1: the hosted widget ​

Copy the snippet from the same Workflow tab section and paste it anywhere on your page:

html
<script src="https://bootform.com/widget/v1/bootform-widget.js" data-form-id="11111111-1111-4111-8111-111111111111" async></script>

It renders itself in place of the tag: a list of approved submissions (public fields only, images shown inline, other files as links), thumbs up/down reaction buttons with live counts, and a report control. It's framework-free, has no dependencies, and uses plain (not shadow-DOM-isolated) CSS. Embed it more than once on the same page for multiple feeds.

AttributeDefaultDescription
data-form-id(required)Your form's ID or vanity slug
data-api-basehttps://f.bootform.comOverride the API host. Only needed for local or staging testing
data-per-page20Items loaded per page/"Load more" click

Customizing the widget's look ​

The widget takes its text color from your page and uses translucent borders and transparent buttons, so it fits a light or a dark site without any setup. It renders directly into the page (no shadow DOM), so your own CSS overrides it. It just needs to load after the widget's script and use a class with equal or higher specificity. Every element carries one of these classes:

ClassElement
.bf-feedThe root container
.bf-feed__statusLoading/empty-state text
.bf-feed__errorError banner
.bf-feed__listThe <ul> of submissions
.bf-feed__itemOne submission's card
.bf-feed__fieldsA submission's field list (<dl>)
.bf-feed__filesThe row of a submission's files
.bf-feed__fileOne file's link
.bf-feed__imageAn image shown inline
.bf-feed__metaThe row under a submission's fields (timestamp + actions)
.bf-feed__actionsReaction/report button group
.bf-feed__reactionA single reaction button
.bf-feed__reaction--activeA reaction button the current visitor has already picked
.bf-feed__reportThe report button
.bf-feed__markThe "Mark as ..." button (item status)
.bf-feed__badgeThe status badge on a marked item
.bf-feed__item--setA card whose item status is set
.bf-feed__load-moreThe "Load more" button

For example, to match your own brand color instead of the default blue and square off the cards:

css
.bf-feed__item {
  border-radius: 0;
}
.bf-feed__reaction--active {
  border-color: #16a34a;
  color: #16a34a;
}

Option 2: your own UI with fetch() ​

For cards, a map or anything else, call the same public endpoints the widget uses. No key, no proxy, straight from the browser:

html
<div id="items"></div>
<script type="module">
  const FORM_ID = '11111111-1111-4111-8111-111111111111' // your form's ID
  const res = await fetch(`https://f.bootform.com/forms/${FORM_ID}/feed?per_page=100`)
  const { items } = await res.json()

  const list = document.getElementById('items')
  for (const item of items) {
    const card = document.createElement('article')
    const photo = item.files.find((f) => f.mime.startsWith('image/'))
    if (photo) {
      const img = document.createElement('img')
      img.src = photo.url
      img.alt = item.fields.description ?? ''
      img.loading = 'lazy'
      card.append(img)
    }
    const text = document.createElement('p')
    text.textContent = item.fields.description ?? '' // textContent, never innerHTML: values are visitor input
    card.append(text)
    list.append(card)
  }
</script>

Treat every field value as untrusted visitor input: insert it with textContent (or your framework's normal escaping), never as HTML.

Reactions, reports and item status:

POST https://f.bootform.com/forms/{form_id}/feed/{submission_id}/react    { "type": "up" }   (or "down")
POST https://f.bootform.com/forms/{form_id}/feed/{submission_id}/report   (no body)
POST https://f.bootform.com/forms/{form_id}/feed/{submission_id}/status   (no body; needs an item status on the form)

Limits and timing ​

WhatLimit
per_pageDefault 50, maximum 200
OrderNewest first (created_at)
Feed requests60 per minute per IP address, per form
File requests600 per minute per IP address, per form
ReactionsOne per IP address per submission (sending the other type switches it), remembered for 90 days after the last one; one react request per submission per minute
ReportsOne per IP address per submission, ever
Item status marksOne per IP address per submission, ever
Delay after approving or rejectingUp to 30 seconds for the feed, about a minute for a file link (responses are cached)
Delay after changing the feed's settingsNone: public fields and turning the feed on or off apply to the next request

Going over a limit returns 429 with code RATE_LIMITED.

"One per visitor" means one per IP address. There's no cookie and no account, so people sharing a network (an office, a school, some mobile carriers) share one reaction, one report and one status mark, and someone who switches networks can react again. If one stray mark shouldn't flip an item, raise the item status threshold. Good enough for a sense of what visitors think. Not a vote you could hold an election on.

Keep API keys out of your page ​

API keys can do everything on your account: there are no read-only or per-form keys. Never put one in client-side JavaScript, a static site's source, or a public repository. The feed exists so you never have to: it is the public, read-only view of exactly the submissions you approved.