JavaScript SDK
@pinwren/sdk — a small, typed client for the REST API for Node, Deno, Bun and edge runtimes.
@pinwren/sdk (MIT) wraps /api/v1 with typed methods. Its types are generated from the same Zod
schemas the server validates with, so the client and your instance agree on every field. It has no dependencies,
ships ESM and CommonJS, and uses the global fetch (Node 20+).
npm install @pinwren/sdkCreate a client
import { Pinwren } from "@pinwren/sdk";
export const pinwren = new Pinwren({
url: "https://feedback.example.com", // your instance
token: process.env.PINWREN_TOKEN!, // pwt_… from Settings › API tokens
});Prop
Type
createClient(options) is a shorthand for new Pinwren(options). Keep tokens on the server — never ship one to a
browser bundle.
Feedback
// One page, newest first
const { items, nextCursor } = await pinwren.feedback.list({ status: "open", kind: "bug", limit: 50 });
// Every match, following the cursor for you
for await (const item of pinwren.feedback.iterate({ status: "active", priority: "urgent" })) {
console.log(`#${item.number} ${item.title ?? item.body}`);
}
// Full detail: thread, files, timeline, diagnostics
const detail = await pinwren.feedback.get(id);
// Triage (needs feedback:write)
await pinwren.feedback.update(id, { status: "in_progress", assigneeIds: [userId] });
// Resolve, with an internal note saying what fixed it (needs feedback:write, and comments:write for the note)
await pinwren.feedback.resolve(id, "Fixed in https://github.com/acme/site/pull/2210");
// Steps, failing requests and console errors for reproducing a bug report
const repro = await pinwren.feedback.reproduction(id);
console.log(repro.markdown);
// An agent-ready brief and a draft Playwright test (masked data only; replay and screenshot are linked)
const brief = await pinwren.feedback.brief(id);
console.log(brief.markdown, brief.commands.claude);
const test = await pinwren.feedback.reproTest(id);
console.log(test.caveats, test.code);| Method | Returns | Scope |
|---|---|---|
feedback.list(query?) | { items, nextCursor } | feedback:read |
feedback.iterate(query?) | An async iterator over every matching item | feedback:read |
feedback.get(id) | The item with comments, assets, activity, diagnostics | feedback:read |
feedback.update(id, patch) | The updated item | feedback:write |
feedback.resolve(id, note?, fix?) | The updated item; fix is { releaseId } or { nextRelease: true } | feedback:write |
feedback.reproduction(id) | { feedbackId, markdown, steps, failingRequests, errors, replay } | feedback:read |
feedback.brief(id) | { markdown, hints, commands, links, privacy } | feedback:read |
feedback.reproTest(id) | { filename, code, caveats, stats } — a draft, not a verified test | feedback:read |
labels.list(projectId) | The project's labels with live item counts | feedback:read |
releases.list(projectId) | The project's releases with fixed and regression counts | feedback:read |
releases.create(projectId, r) | Record a release ({ version, commit?, deployedAt?, notes? }); the same version updates it → { release, created, adopted } | releases:write |
customFields.list(projectId) | The project's custom form fields (key, type, options, audience, kinds) | feedback:read |
views.list() | Saved inbox views you can use | feedback:read |
stats.get({ days?, projectId? }) | Totals, breakdowns and a daily series | feedback:read |
query takes projectId, status (a status, "active" or "all"), kind, priority, assigneeId, roundId,
q (full-text), page (part of the page URL), since (ISO 8601), limit (1–100) and cursor. patch takes
status, priority, title, assigneeIds and roundId.
Comments, projects, rounds, files
// A public reply the reporter sees on the page (default visibility is "internal")
await pinwren.comments.create(id, { body: "Thanks — fixed!", visibility: "public" });
const projects = await pinwren.projects.list(); // with open counts
const rounds = await pinwren.rounds.list(projectId);
await pinwren.rounds.update(roundId, { status: "closed" });
// Screenshots, attachments and replays, as bytes
const { contentType, bytes } = await pinwren.assets.get(assetId);| Method | Returns |
|---|---|
comments.create(feedbackId, c) | The comment (comments:write) |
projects.list() | An array of projects with openCount |
rounds.list(projectId) | The project's review rounds |
rounds.get(id) | One round, with its counts |
rounds.update(id, patch) | { name?, dueAt?, status? } (feedback:write) |
assets.get(id) | { contentType, bytes } |
assets.url(id) | The asset's URL (fetch it with the same Bearer token) |
request(method, path, init?) | Raw Response for any /api/v1 path (throws on errors) |
Errors
Every failure throws a PinwrenError with the HTTP status, the stable code from the
problem details and the server's message. A network failure or timeout has status 0 and code
NETWORK_ERROR; an error answer without a code (a failed validation, or a proxy's error page) has code
HTTP_ERROR.
import { PinwrenError } from "@pinwren/sdk";
try {
await pinwren.feedback.update(id, { status: "resolved" });
} catch (err) {
if (err instanceof PinwrenError && err.code === "INSUFFICIENT_SCOPE") {
console.error("This token can't change feedback — add feedback:write");
} else throw err;
}Types
All wire types are exported: Feedback, FeedbackDetail, FeedbackListItem, FeedbackListQuery,
FeedbackUpdate, FeedbackStatus, FeedbackKind, Priority, Comment, CommentCreate, Project, Round,
RoundUpdate, Reproduction and more.
import type { FeedbackListItem, FeedbackStatus } from "@pinwren/sdk";