API reference
Every client method, React hook, type and error code in panelui-studio, and the HTTP endpoints behind them.
createStudio
import { createStudio } from 'panelui-studio';
const studio = createStudio(config);| Option | Type | Default |
|---|---|---|
publishableKey | string | Required. The project's pk_… key. |
baseUrl | string | https://studio.panelui.dev/api/v1 |
storage | StudioStorage | In an app: secure storage, then AsyncStorage, then memory. On the web: localStorage, then memory. |
device | DeviceContext | Detected from the device or browser; anything passed wins |
timeoutMs | number | 10000. Uploads get three times this. |
fetch | typeof fetch | The global fetch |
Client
| Method | Returns | |
|---|---|---|
register() | Installation | Registers this install if it isn't yet. Every other call does this for you. |
reset() | void | Forgets the install; the next call registers a new one. |
identify({ id, email?, name? }) | { userId } | Attaches one of your users to this install. |
feedback.submit(input) | SubmitFeedbackResult | Sends a report. See Feedback. |
feedback.list() | FeedbackSummary[] | This install's reports and support conversations, newest first (latest 50). |
feedback.form() | FormField[] | The project's custom form. Empty on Free or without fields. |
support.start(message) | StartSupportResult | Starts a support conversation. Takes a string or a StartSupportInput. |
announcements.list() | Announcement[] | Live announcements for this install. |
announcements.seen(id) | void | Records a view. |
announcements.dismiss(id) | void | Hides it for this install for good. |
conversations.messages(id) | ConversationMessage[] | A thread, oldest first. Marks replies as read. |
conversations.send(id, body) | ConversationMessage | Adds the user's reply. |
uploads.screenshot(input) | string | Uploads a screenshot from a uri or a file; returns its attachment id. See ScreenshotInput. |
React
From panelui-studio/react.
| Export | |
|---|---|
<StudioProvider publishableKey="…"> | Creates one client for the tree. Also takes client={studio} to share one you made. |
useStudio() | The client from the nearest provider. |
useFeedbackList() | { items, isLoading, error, reload, submit } |
useConversation(id, { pollMs? }) | { messages, isLoading, isSending, error, send, reload }. Polls every 15 s by default; pollMs: 0 turns it off. |
useFeedbackForm() | { fields, isLoading, error } |
useAnnouncements() | { announcements, isLoading, error, seen, dismiss, reload } |
Types
type SubmitFeedbackInput = {
message: string;
rating?: number; // 1–5
tags?: string[]; // up to 5
screen?: string;
appVersion?: string;
build?: string;
metadata?: Record<string, unknown>; // Pro and Max
attachmentIds?: string[]; // 1 on Free, 3 on Pro and Max
fields?: Record<string, string | number | boolean>; // Pro and Max
country?: string; // ISO-3166 alpha-2
};
type FormField = {
key: string;
label: string;
type: 'text' | 'textarea' | 'choice' | 'checkbox' | 'rating';
options: string[]; // for choice
required: boolean;
};
type StartSupportInput = Omit<SubmitFeedbackInput, 'rating' | 'tags'>;
type Announcement = {
id: string;
title: string;
body: string;
cta: { label: string; url: string } | null;
endsAt: string | null;
seen: boolean;
createdAt: string;
};
type SubmitFeedbackResult = {
feedback: { id: string; number: number; status: FeedbackStatus; createdAt: string };
warnings: string[];
};
type FeedbackStatus = 'new' | 'in_progress' | 'resolved' | 'archived';
type FeedbackSummary = {
id: string;
number: number;
kind: 'feedback' | 'support';
message: string;
status: FeedbackStatus;
createdAt: string;
lastMessageAt: string | null;
replies: number;
};
type ConversationMessage = {
id: string;
author: 'user' | 'developer';
body: string;
createdAt: string;
};
type ScreenshotInput =
| { uri: string; contentType?: ScreenshotContentType; byteSize?: number } // a file uri, or anything fetch reads
| { file: Blob; contentType?: ScreenshotContentType; byteSize?: number }; // a File from <input type="file">, or any Blob
type ScreenshotContentType = 'image/png' | 'image/jpeg' | 'image/webp';
type DeviceContext = {
platform?: 'ios' | 'android' | 'web' | 'other';
osName?: string;
osVersion?: string;
model?: string; // the browser and its major version on the web
locale?: string;
appVersion?: string;
country?: string; // ISO-3166 alpha-2
};
type StudioStorage = {
getItem(key: string): Promise<string | null> | string | null;
setItem(key: string, value: string): Promise<void> | void;
removeItem(key: string): Promise<void> | void;
};Storage helpers: secureStore() and asyncStorage() (apps only; on the web
they throw), browserStorage() (localStorage, falling back to memory),
memoryStorage() and defaultStorage().
Builds
panelui-studio ships two builds behind one set of types. React Native and
Expo resolve the native build through the package's react-native export
condition; everything else — Next.js, Vite, Expo's web target, Node — gets the
browser build, which has no React Native imports. panelui-studio/react is a
client module and uses whichever build the app resolved.
Errors
Every failure is a StudioError with code, status, and retryAfter when
rate limited. isStudioError(error) narrows an unknown catch.
| Code | Status | Meaning |
|---|---|---|
bad_request | 400 | Validation failed; the message says which field. |
invalid_json | 400 | The body wasn't JSON. |
unauthorized | 401 | Missing, unknown or revoked publishable key. |
invalid_installation | 401 | The install token is unknown. reset() and retry. |
quota_exceeded | 402 | The workspace reached this month's hard limit. |
feature_unavailable | 403 | The workspace's plan doesn't include this. |
storage_full | 403 | Screenshot storage is full; send the report without it. |
not_found | 404 | Not found, or not this install's. |
payload_too_large | 413 | Body over 64 KB, or a screenshot over 5 MB. |
rate_limited | 429 | Too many requests; wait retryAfter seconds. |
internal_error | 500 | Nothing was recorded; retrying is safe. |
storage_unavailable | 503 | File storage isn't available. |
network_error, timeout | — | The request never got an answer. |
HTTP
For platforms without the SDK, the same API is plain JSON over HTTPS with open
CORS and no cookies; browsers can read Retry-After. Every request carries
Authorization: Bearer pk_…; all but registration also carry
X-Studio-Installation: it_….
| Endpoint | |
|---|---|
POST /installations | Register an install. Returns { installationId, installationToken } once. |
POST /identify | { id, email?, name? } |
POST /feedback | Submit a report. 201 with { feedback, warnings }. |
GET /feedback | This install's reports. |
GET /feedback/{id}/messages | A thread. |
POST /feedback/{id}/messages | { body } — the user's reply. |
POST /uploads | { contentType, byteSize } → a presigned PUT URL valid for 5 minutes. |
GET /form | The project's custom form fields. |
POST /support | Starts a support conversation. 201 with { conversation, warnings }. |
GET /announcements | Live announcements for this install. |
POST /announcements/{id}/seen | Records a view. |
POST /announcements/{id}/dismiss | Hides it for this install. |
Errors come back as { "error": { "code": "…", "message": "…" } } with the
statuses above.