Work with a site's two input channels — widget feedback and question batches — from your own code: sync feedback into another tool, triage it as you go, publish a batch of questions to a client, and read their answers back. Connecting an AI agent instead? See the MCP page.
Two secret tokens per site, generated separately in Setup → Data API and each shown only once. One scope model everywhere — REST and MCP:
sk_… | Read. Feedback export, question batches and their answers. |
wk_… | Read + write. Everything sk_ can read, plus updating feedback status and creating question batches. A leaked read token still cannot change anything. |
Send either as a bearer token:
Authorization: Bearer sk_your_secret_tokenUnlike the publishable widget key, both tokens are secrets — keep them server-side, never in client JS. Each is scoped to exactly one site and individually rotatable from the dashboard; the old token stops working immediately.
GET https://feedback.latentedge.io/api/v1/export/feedbacksince | Cursor from a previous response's nextCursor. Returns only rows after it. Omit on the first call. |
limit | Page size. Default 50, max 200. |
type | Filter by feedback, feature_request, or bug. |
status | Filter by new, triaged, or resolved. |
curl -s "https://feedback.latentedge.io/api/v1/export/feedback?limit=50" \
-H "Authorization: Bearer sk_your_secret_token"Every response is wrapped in { success, data }. For this endpoint data holds the page:
{
"success": true,
"data": {
"feedback": [
{
"id": "0f2c…",
"siteId": "c7b3…",
"type": "bug",
"message": "The export button 404s",
"rating": null,
"pageUrl": "https://yoursite.com/reports",
"status": "new",
"createdAt": "2026-06-27 19:04:11"
}
],
"nextCursor": "MjAyNi0wNi0yNyAxOTowNDoxMXww0f2c",
"hasMore": false
}
}nextCursor — pass it back as ?since= on your next call. It is always returned so you can keep polling for rows created after the latest one.hasMore — true when more rows are immediately available (keep paging right away); false when you've caught up.Store nextCursor between runs and pass it as since. You'll only ever receive rows you haven't seen, in stable order. Poll at whatever interval suits you — empty polls are cheap, so there's no need to hammer it. A minute is plenty for most workflows; if you need lower latency, ask us about webhooks.
// Poll for new feedback on an interval and persist the cursor to resume later.
const ENDPOINT = 'https://feedback.latentedge.io/api/v1/export/feedback';
const TOKEN = process.env.FEEDBACK_READ_TOKEN; // sk_...
let cursor = loadCursor(); // your own storage; null on first run
async function poll() {
const url = new URL(ENDPOINT);
if (cursor) url.searchParams.set('since', cursor);
url.searchParams.set('limit', '100');
const res = await fetch(url, { headers: { Authorization: 'Bearer ' + TOKEN } });
if (!res.ok) throw new Error('export failed: ' + res.status);
const { data } = await res.json();
for (const item of data.feedback) {
handleFeedback(item); // do your thing
}
if (data.nextCursor) {
cursor = data.nextCursor;
saveCursor(cursor); // resume from here next run
}
}
setInterval(poll, 60_000); // once a minute is plentyTriage from your own tooling with the write token: mark an item triaged once you've reviewed it or resolved once it's addressed.
curl -s -X PUT "https://feedback.latentedge.io/api/v1/export/feedback/FEEDBACK_ID" \
-H "Authorization: Bearer wk_your_write_token" \
-H "Content-Type: application/json" \
-d '{"status": "triaged"}'A read token gets a 401 here; an id belonging to another site is a plain 404.
The outbound channel: publish a set of questions, send the share link to a client or stakeholder, and they answer in the browser with no account. These endpoints let your code (or your agent) send batches and read the answers back.
GET /export/questions | List the site's batches with status (draft → sent → opened → answered) and question/answer counts. A draft was created in the dashboard and not sent yet, so its link is not live. Share links are never included — they are each batch's access control. |
GET /export/questions/{'{id}'} | One batch: questions beside the client's answers, with per-answer updated timestamps. |
GET /export/questions/{'{id}'}/link | A sent batch's share URL, for when you no longer have the one the create call returned. Write token only — the link lets whoever holds it answer the batch, so a read token cannot fetch one. A draft has no live link and answers 409. |
POST /questions | Create a batch (write token only). Body: { title, introHtml?, notifyEmail?, sendNow?, questions: [{n, title, html}] }. Creates a draft by default - it returns { id, status } with no link, and the owner reviews and sends it from the dashboard. Pass sendNow: true to publish immediately and get the share URL back, which you can also fetch again later from the endpoint above. Submit notifications default to the site owner's email. |
curl -s "https://feedback.latentedge.io/api/v1/export/questions" \
-H "Authorization: Bearer sk_your_secret_token"Each site's token is limited to 60 requests per minute. Over that you get a 429 response — back off and retry on your next interval. Polling once a minute uses one request, so you have plenty of headroom.
The same tokens also drive a remote MCP server, so Claude (or any MCP client) can summarize and triage feedback, send question batches, and read back answers through tools instead of you writing polling code. Setup and the full tool list live on the MCP page.
Embedding the widget instead? See the install guide. Questions? Get in touch.