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);
OptionTypeDefault
publishableKeystringRequired. The project's pk_… key.
baseUrlstringhttps://studio.panelui.dev/api/v1
storageStudioStorageIn an app: secure storage, then AsyncStorage, then memory. On the web: localStorage, then memory.
deviceDeviceContextDetected from the device or browser; anything passed wins
timeoutMsnumber10000. Uploads get three times this.
fetchtypeof fetchThe global fetch

Client

MethodReturns
register()InstallationRegisters this install if it isn't yet. Every other call does this for you.
reset()voidForgets 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)SubmitFeedbackResultSends 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)StartSupportResultStarts a support conversation. Takes a string or a StartSupportInput.
announcements.list()Announcement[]Live announcements for this install.
announcements.seen(id)voidRecords a view.
announcements.dismiss(id)voidHides it for this install for good.
conversations.messages(id)ConversationMessage[]A thread, oldest first. Marks replies as read.
conversations.send(id, body)ConversationMessageAdds the user's reply.
uploads.screenshot(input)stringUploads 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.

CodeStatusMeaning
bad_request400Validation failed; the message says which field.
invalid_json400The body wasn't JSON.
unauthorized401Missing, unknown or revoked publishable key.
invalid_installation401The install token is unknown. reset() and retry.
quota_exceeded402The workspace reached this month's hard limit.
feature_unavailable403The workspace's plan doesn't include this.
storage_full403Screenshot storage is full; send the report without it.
not_found404Not found, or not this install's.
payload_too_large413Body over 64 KB, or a screenshot over 5 MB.
rate_limited429Too many requests; wait retryAfter seconds.
internal_error500Nothing was recorded; retrying is safe.
storage_unavailable503File 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 /installationsRegister an install. Returns { installationId, installationToken } once.
POST /identify{ id, email?, name? }
POST /feedbackSubmit a report. 201 with { feedback, warnings }.
GET /feedbackThis install's reports.
GET /feedback/{id}/messagesA thread.
POST /feedback/{id}/messages{ body } — the user's reply.
POST /uploads{ contentType, byteSize } → a presigned PUT URL valid for 5 minutes.
GET /formThe project's custom form fields.
POST /supportStarts a support conversation. 201 with { conversation, warnings }.
GET /announcementsLive announcements for this install.
POST /announcements/{id}/seenRecords a view.
POST /announcements/{id}/dismissHides it for this install.

Errors come back as { "error": { "code": "…", "message": "…" } } with the statuses above.

On this page