Accessing Previews
Learn how to display v0 chat previews from the Platform API v2
Use chats.getPreview to display the live preview for a chat in your own product.
Preview access is designed to go through your backend. The API returns a preview URL and a short-lived preview token. Browsers cannot attach that token as a custom header when loading an iframe, so your iframe should point at your own proxy route, not directly at the preview URL.
Your proxy is responsible for:
- calling
chats.getPreview - caching the returned preview while it is valid
- passing iframe requests to the SDK preview helper
- clearing the cached preview when v0 asks for a refresh
- returning a loading page while the preview is not ready
Proxy flow
- Your frontend renders an iframe that points to your backend.
- Your backend gets or refreshes the preview for the chat.
- Your backend passes the request, preview, fallback URL, and iframe path to
fetchPreview. - The SDK forwards the request to
preview.urlwithx-v0-preview-token. - If v0 returns
x-v0-preview-refresh: 1, the SDK calls your refresh callback and redirects the iframe to your fallback URL. - Your fallback URL returns a loading page that retries the iframe URL until
chats.getPreviewreturns a preview again.
<iframe src="/api/v0-preview/chat_abc123/" />Example proxy
The example below uses an in-memory cache for clarity. Use Redis or another shared cache in production.
The proxy route must be a catch-all route because iframe navigations, assets, and in-app requests all need to flow through the same proxy.
import { fetchPreview, v0 } from 'v0'
type Preview = NonNullable<
Awaited<ReturnType<typeof v0.chats.getPreview>>['data']
>
const previewCache = new Map<string, Preview>()
async function getPreview(chatId: string) {
const cached = previewCache.get(chatId)
const now = Date.now()
if (cached && new Date(cached.expiresAt).getTime() - now > 60_000) {
return cached
}
const { data: preview } = await v0.chats.getPreview({ chatId })
if (preview) {
previewCache.set(chatId, preview)
} else {
previewCache.delete(chatId)
}
return preview
}
export async function proxyPreviewRequest(
request: Request,
chatId: string,
path: string[],
) {
const proxyUrl = new URL(request.url)
const preview = await getPreview(chatId)
return fetchPreview({
request,
preview,
path,
fallbackUrl: `/api/v0-preview/${chatId}/loading?returnTo=${encodeURIComponent(
proxyUrl.pathname + proxyUrl.search,
)}`,
onPreviewRefresh: () => {
previewCache.delete(chatId)
},
})
}In a Next.js app, the iframe src should point at a route like app/api/v0-preview/[chatId]/[[...path]]/route.ts. Pass the catch-all path param to fetchPreview so asset requests and client-side navigations are forwarded to the same path under the preview URL.
The fallbackUrl should point at a loading route that eventually retries the original iframe URL. For example, the loading route can render a loading state and refresh back to the returnTo URL after a short delay.
fetchPreview does not call chats.getPreview or manage your cache. It only forwards a request when you pass a preview, redirects to fallbackUrl when you pass null, and calls onPreviewRefresh before redirecting when v0 asks for a fresh preview.
Refresh behavior
Preview details are temporary:
tokenis valid untilexpiresAturlcan become stale if the underlying sandbox restarts
Use expiresAt to refresh before the token expires. Separately, if the preview response includes x-v0-preview-refresh: 1, fetchPreview treats it as a proxy signal. It calls onPreviewRefresh and redirects to fallbackUrl instead of rendering that response in the iframe.
If chats.getPreview returns null, the sandbox is not ready yet. Pass null to fetchPreview; it will redirect to fallbackUrl. Your fallback route should render a loading page and retry the proxy route with backoff or a timed refresh.
Do not send your v0 API key to the preview URL or expose it to the browser. The API key is only for calling the Platform API; preview access uses the short-lived x-v0-preview-token header.