Skip to content

The public plane

https://withfeedback.com/api/public/v1/

This is the half your visitors' browsers talk to. Use it when you are building your own collection UI instead of using our widget or hosted pages.

It is unauthenticated, cookie-free and open to any origin. It has to be — the code calling it runs on your site, and shipping a credential to a browser is shipping it to everybody.

The public key identifies. It never authorizes.

Your project's public key appears in every embed, so treat it as public information, not a secret. On its own it lets a caller post a submission and read approved, consented items. It does not let anyone read your pending queue, your contacts or your settings, and it cannot spend your video quota unless you have switched video on somewhere (see Video).

Endpoints

Method Path Does
GET /widgets/{widget_id}/ Widget config plus approved items
POST /submissions/ Submit feedback or a testimonial
POST /survey-responses/ Submit survey or NPS answers
POST /events/ Impression beacon (analytics only)
POST /uploads/videos/ Reserve a direct video upload
POST /uploads/videos/{id}/complete/ Confirm the upload landed

Submitting

await fetch("https://withfeedback.com/api/public/v1/submissions/", {
  method: "POST",
  credentials: "omit",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    public_key: "YOUR_PUBLIC_KEY",
    kind: "testimonial",           // or "feedback"
    text: "It saved us a week.",
    rating: 5,
    consent_display: true,         // may we show this publicly?
    name: "Jane Roe",
    email: "jane@example.com",
    page_url: location.href,
    form_started_at: startedAt,    // epoch seconds, stamped when you rendered
    website: honeypotValue,        // your decoy field
  }),
});

201 means accepted. It does not mean published — the item is pending until a person approves it.

The two anti-bot fields

Both are optional, and both are worth sending:

  • website — a hidden decoy input. Humans leave it empty; bots fill it. A non-empty value returns 201 and creates nothing, deliberately, so a bot learns nothing from the response.
  • form_started_at — epoch seconds when you rendered the form. A submission faster than three seconds is treated the same way.

Consent is a question you have to ask

consent_display defaults to false. If you do not ask, you collect feedback you may read but not publish. Whatever wording you put next to the checkbox is what the consent record should say, so ask plainly.

Reading approved content

GET /api/public/v1/widgets/{widget_id}/

Returns the widget's config and a page of approved items. The response is built by one allowlisted serializer whose exact key set is regression-tested, so nothing you did not ask for can appear in it — no email addresses, no internal metadata, no pending items.

A video URL is only present when the asset is genuinely published. Having the files exist is not enough; a half-finished publish cannot leak a clip.

Abuse controls

The public plane is the internet-facing surface, so it is layered:

Control Bound
Per-IP throttles 120/min reads, 10/min writes, 8/min uploads
Per-project caps submissions per hour and per day
Honeypot + minimum fill time obvious automation
Body size cap 64 KB
IP blocklist repeated violations lock an address out for 24 hours
CAPTCHA optional per project — see below

A 429 is a wait. Repeated violations — honeypot hits, oversized bodies — are counted, and ten of them blocklist the address from the whole public plane for a day. Normal traffic never reaches that.

CAPTCHA

Projects can switch on reCAPTCHA v3 (require_captcha). It is deliberately asymmetric:

  • On submissions and survey responses the score is recorded as a signal on the item. It never refuses. A privacy extension or a bad minute at Google must not cost a customer a real testimonial, and a public write only needs the public key anyway — a bot would post to the API instead of the page.
  • On the video reservation from a hosted page it is a hard gate. That call spends non-refundable video seconds and starts a transcode before any human sees anything, and a page we serve can always mint a token.

Pass the token as captcha_token. If you are building your own front end on your own domain, our key cannot mint a token there — see Video for what that means in practice.

CORS

Any origin, no credentials. That combination is the safe one and it is enforced: the server refuses to start configured to send credentials on this namespace, because "any origin" plus "with cookies" is origin reflection with a login attached.