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

ModeUse forQuery shape
CursorGalleries, Load more, scroll loadingcursor + limit
PageTables with page numberspage + perPage
OffsetPipelines or offset-based widgetsoffset + 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.

TS

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.

TS

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.

TS

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.

TS

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