The public plane¶
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 returns201and 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¶
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.