API Reference
/
Pagination Guide
Pagination Guide
Moshpit lists Integration Splats with JSON body pagination metadata. The REST API supports cursor, numbered page, and offset pagination so host apps can use the pattern that matches their UI and backend.
The API intentionally does not use HTTP Link headers. The design keeps browser
and Next.js examples simple while still following the same concepts used by
JSON:API cursor pagination,
GitHub REST pagination,
and RFC 8288 Web Linking.
Choose a mode
| Mode | Use for | Query shape |
|---|---|---|
| Cursor | Galleries, Load more, scroll loading | cursor + limit |
| Page | Tables with page numbers | page + perPage |
| Offset | Pipelines or offset-based widgets | offset + limit |
Do not mix modes. page=1&limit=20, cursor=...&page=2, and
offset=20&perPage=20 all return 400.
Keep the secret server-side
Never expose msk_... in browser JavaScript. Public pages should call your own
backend, and your backend should call Moshpit.
TypeScript
// app/api/moshpit/splats/route.ts
import { NextRequest, NextResponse } from 'next/server';
const allowed = new Set([
'cursor',
'limit',
'page',
'perPage',
'offset',
'includeTotal',
'sort',
'dir',
'embeddable',
'visibility',
'owned',
]);
export async function GET(request: NextRequest) {
const upstreamUrl = new URL('https://moshpit.studio/api/v1/splats');
for (const [key, value] of request.nextUrl.searchParams) {
if (allowed.has(key)) upstreamUrl.searchParams.set(key, value);
}
const externalUserId = await getCurrentHostUserId(request); // optional
const headers: Record<string, string> = {
Authorization: `Bearer ${process.env.MOSHPIT_SECRET_KEY}`,
'X-Moshpit-Public-Key': process.env.MOSHPIT_PUBLIC_KEY!,
};
if (externalUserId) {
headers['X-Moshpit-External-User-Id'] = externalUserId;
}
const upstream = await fetch(upstreamUrl, { headers, cache: 'no-store' });
return new NextResponse(await upstream.text(), {
status: upstream.status,
headers: { 'Content-Type': 'application/json' },
});
}Cursor Load more
Use cursor mode for public galleries. Pass visibility=public so the page is
filled by public results even when a signed-in external user also has private
splats.
TypeScript
let cursor: string | null = null;
let hasMore = true;
async function loadMore() {
if (!hasMore) return;
const params = new URLSearchParams({
visibility: 'public',
embeddable: 'true',
limit: '20',
});
if (cursor) params.set('cursor', cursor);
const data = await fetch(`/api/moshpit/splats?${params}`).then((r) =>
r.json()
);
appendCards(data.splats);
cursor = data.pagination.nextCursor;
hasMore = data.pagination.hasMore;
}For scroll loading, trigger the same function from an IntersectionObserver
attached to a sentinel element below the grid. Disable the observer while a
request is in flight and when hasMore is false.
Numbered pages
Use page mode for account management tables. Keep page and perPage in the
URL so refreshes, browser back/forward, and shared links preserve the table
state.
TypeScript
const params = new URLSearchParams({
owned: 'true',
page: String(page),
perPage: String(perPage),
sort: 'updatedAt',
dir: 'desc',
});
const data = await fetch(`/api/moshpit/splats?${params}`).then((r) => r.json());
renderRows(data.splats);
renderPagination({
page: data.pagination.page,
perPage: data.pagination.perPage,
total: data.pagination.total,
totalPages: data.pagination.totalPages,
hasMore: data.pagination.hasMore,
});If totalPages is 0, show the empty state and disable page controls.
Offset loading
Use offset mode when an existing table or data export works in absolute row positions. Cursor mode is preferred for changing datasets because it is stable when new splats are created while the user is browsing.
TypeScript
let offset = 0;
const limit = 50;
async function fetchWindow() {
const data = await fetch(
`/api/moshpit/splats?offset=${offset}&limit=${limit}`
).then((r) => r.json());
offset = data.pagination.nextOffset ?? offset;
return data.splats;
}Visibility and ownership
For public galleries, send both visibility=public and the optional
X-Moshpit-External-User-Id header. The visibility filter keeps pages full of
public items, while the external-user header preserves isOwner for public
splats owned by the signed-in host user.
For account libraries, send owned=true with X-Moshpit-External-User-Id so
the table contains only the signed-in host user's Integration Splats. Use
isOwner before showing edit or delete actions in any mixed public gallery.
What's next
- GET /v1/splats — full request and response reference.
- POST embed-sessions — mint a viewer or editor session for a selected splat.