API¶
REST over HTTPS, JSON in and out. Everything the dashboard can do, the API can do, under the same permissions.
There are two APIs and the difference matters:
| Authenticated API | Public plane | |
|---|---|---|
| Prefix | /api/v1/ |
/api/public/v1/ |
| Who calls it | your backend, the CLI, an MCP client | your visitors' browsers |
| Credentials | token or OAuth2 | none — a public key identifies, it never authorizes |
| Reads | anything you have a scope for | approved + consented items only |
| CORS | your configured origins | any origin |
If you are writing server code, you want this section. If you are writing front-end code that posts what a visitor typed, you want the public plane.
The machine-readable contract¶
The OpenAPI 3 schema is generated from the code itself, so it cannot drift from what the server does:
| Schema | https://withfeedback.com/api/schema/ |
| Swagger UI | https://withfeedback.com/api/docs/ |
| ReDoc | https://withfeedback.com/api/redoc/ |
Every operation has a stable operationId, which is what client generators key
on. These pages explain the shape and the rules; the schema is the reference
for exact fields.
Resource paths are tenant-scoped¶
Almost every path carries the team, and usually the project:
That is deliberate rather than decorative. A resource is only reachable through the team that owns it, so a token belonging to one team cannot read another team's data by guessing an id — the mismatch is a 404, not a 403, because whether a record exists is itself not something to leak.
GET /api/v1/teams/ lists the teams you belong to. GET /api/v1/me/ returns
who you are.
Errors¶
Standard HTTP status codes. The body carries a detail string, and often a
machine-readable code.
| Status | Means | What to do |
|---|---|---|
400 |
The request body is wrong | Read the field errors; do not retry unchanged |
401 |
No credentials, or they are invalid | Re-authenticate |
402 |
Quota exceeded, or your plan lacks the feature | Check code; upgrade or wait for the month to roll |
403 |
Authenticated, but not allowed | Wrong scope, or your role lacks the permission |
404 |
Not found — or not yours | Check the team and project in the path |
409 |
Conflict with current state | Read the state and decide; retrying identically will not help |
413 |
Body too large | Send less |
429 |
Rate limited | Back off and retry |
503 |
A dependency is unavailable | Retry with backoff |
402 is not a failure of your code
A 402 means the account ran out of something it pays for, or is on a plan
without the feature. The code field says which. Treat it as a state to
surface to a human, not an error to retry in a loop.
Idempotency and retries¶
Anything that spends money or changes state is designed so a retry is safe:
- Completing a video upload is idempotent — repeat calls return the current state and do not start a second transcode.
- Submitting a video draft twice reports success and changes nothing; the first submit is the one that counts.
- Moderation transitions are compare-and-set. Approving something already approved is not an error, and does not fire a second webhook.
Rate limits¶
Per-IP, and they differ by surface because the surfaces cost different amounts:
| Scope | Limit |
|---|---|
| Authenticated requests | 1000/hour per user |
| Unauthenticated | 100/hour |
| Public reads (widget payloads) | 120/min |
| Public writes (submissions, survey responses) | 10/min |
| Public uploads (reserve, complete) | 8/min |
A 429 carries no penalty beyond the wait. Repeatedly tripping the abuse
controls on the public plane is different — see
the public plane.
Next¶
- Authentication — tokens, OAuth2, and which to pick
- Scopes — the full list, and how they combine with roles
- Webhooks — every event, and how to verify a delivery
- Public plane — the browser-facing half