ZTS Docs

Public API (REST, SDK, MCP)

External API surfaces — REST, OpenAPI, SDK, CLI, and MCP — and how they relate to tRPC.

Zero To Shipped exposes one tRPC router (@zts/trpc) through several transports. Pick the surface that matches your client.

SurfaceURLAuthBest for
tRPC/api/trpcSession cookie / Better AuthWeb, mobile, extension (first-party)
REST/api/restx-api-key: zts_…Integrations, webhooks, scripts
OpenAPI/api/openapi.jsonSpec onlySDK generation, API explorers
Swagger UI/api/docsBrowserInteractive REST docs
MCP/api/mcpOAuth + API keysCursor and other MCP hosts
Auth OpenAPI/api/auth/referenceVariesBetter Auth routes

What is exposed

Only procedures tagged with apiExposure() in packages/trpc/src/openapi-meta.ts appear on REST, in packages/sdk/openapi.json, and as MCP tools. Today that covers user profile, user preferences, and utImage.delete (Images tag).

Not exposed (app-internal, session-only tRPC): admin.*, auth.changePassword, polar.*, and other routers without apiExposure().

REST and OpenAPI

REST routes come from tRPC procedures that use apiExposure() (sets both OpenAPI and MCP meta):

.meta(
  apiExposure({
    description: "Get the authenticated user's profile",
    mcpName: "get_current_user",
    path: "/users/me",
    summary: "Get current user",
    tags: ["Users"],
  })
)

Handler: apps/web/src/app/api/rest/[...path]/route.ts
Live spec: /api/openapi.json (REST base {NEXT_PUBLIC_APP_URL}/api/rest)
Swagger UI: /api/docs (loads the same spec; use Authorize with your zts_ API key as x-api-key)
Committed spec: packages/sdk/openapi.json (regenerated by @zts/sdk build)

API keys

REST (and MCP tool calls that use keys) authenticate with the x-api-key header. Keys are issued via the Better Auth API key plugin (zts_ prefix, configured in packages/auth/src/index.ts).

Create keys in the app UI or through Better Auth’s API key endpoints after migrations are applied (pnpm db:migrate).

Example:

curl -X POST "$BASE_URL/api/rest/users/me" \
  -H "x-api-key: zts_your_key_here" \
  -H "Content-Type: application/json"

TypeScript SDK (@zts/sdk)

packages/sdk ships a generated client (@hey-api/openapi-ts) with two API classes: Users and Images.

Regenerate after changing exposed procedures:

pnpm sdk:build
# runs @zts/trpc generate:openapi → packages/sdk/openapi.json → codegen → tsc

Configure once (defaults baseUrl to http://localhost:3000/api/rest):

import { configure, Users, Images } from "@zts/sdk";
 
configure({ apiKey: process.env.ZTS_API_KEY! });
 
const { data: me } = await Users.userGetCurrentUser({ body: {} });
 
const { data: prefs } = await Users.userGetPreferences({ body: {} });
 
await Users.userUpdateProfile({
  body: { name: "Ada", username: "ada", bio: "", timezone: "UTC" },
});
 
await Images.utImageDelete({ path: { id: "image-id" } });

Other Users methods: userGetUserForEditingProfile, userUpdatePreference, userGetSinglePreference, userMarkUserAsOnboarded, userResetUserOnboarding.

Env helpers: ZTS_API_KEY (required), optional ZTS_BASE_URL (full REST prefix, e.g. https://demo.zerotoshipped.com/api/rest). Or initializeFromEnv() from @zts/sdk.

Use the SDK for integrations and workers — not first-party web/mobile (use tRPC). See packages/sdk/README.md in the product repo.

CLI (zts)

packages/cli publishes the zts binary (request, me, preferences, config).

Build:

pnpm cli:build

Environment:

VariablePurpose
ZTS_API_KEYzts_… API key (required)
ZTS_BASE_URLREST base URL (default http://localhost:3000/api/rest)
export ZTS_API_KEY=zts_...
export ZTS_BASE_URL=http://localhost:3000/api/rest   # optional
 
# Raw REST (paths are relative to /api/rest)
zts request POST /users/me '{}'
zts request DELETE /images/your-image-id
 
# Shortcuts
zts me
zts preferences
zts config          # prints baseUrl + whether ZTS_API_KEY is set

From the monorepo without linking: pnpm --filter @zts/cli run dev me. After cd packages/cli && npm link, use zts globally.

MCP (Cursor and agents)

MCP exposes the same apiExposure() procedures as REST (shared mcpName / OpenAPI path).

Endpoint: /api/mcp (implemented in apps/web/src/app/api/[transport]/route.ts using trpc-to-mcp).

Cursor configuration

{
  "mcpServers": {
    "zts": {
      "url": "http://localhost:3000/api/mcp"
    }
  }
}

Auth flow

  1. Create an API key in the app (zts_ prefix).
  2. MCP clients discover OAuth via /.well-known/oauth-protected-resource (see routes under apps/web/src/app/.well-known/).
  3. Tool calls use the issued OAuth token or API key.

Better Auth’s MCP plugin (packages/auth) and OAuth helpers under apps/web/src/server/mcp/ back this flow. After pulling schema changes for API keys / MCP OAuth tables, run pnpm db:migrate.

Product repo reference: docs/MCP_SETUP.md in the zerotoshipped repository.

Boundaries (what to use when)

flowchart LR
  subgraph firstParty [First-party apps]
    Web[apps/web]
    Mobile[apps/mobile]
    Ext[apps/extension]
  end
  subgraph external [External / automation]
    SDK[@zts/sdk]
    CLI[zts CLI]
    MCP[MCP host]
    REST[HTTP client]
  end
  TRPC["/api/trpc"]
  RESTAPI["/api/rest"]
  MCPAPI["/api/mcp"]
  Web --> TRPC
  Mobile --> TRPC
  Ext --> TRPC
  SDK --> RESTAPI
  CLI --> RESTAPI
  REST --> RESTAPI
  MCP --> MCPAPI
  TRPC --> Router["packages/trpc appRouter"]
  RESTAPI --> Router
  MCPAPI --> Router
  • Inside the monorepo UI apps → tRPC + shared @zts/trpc types.
  • Partner integrations, cron scripts, serverless workers → REST + SDK or CLI + API key.
  • IDE agents (Cursor) → MCP + OAuth/API keys; keep tool surface minimal via .meta({ mcp: { enabled: true } }).

Adding a new public endpoint

  1. Implement the procedure in packages/trpc/src/routers/.
  2. Add .meta(apiExposure({ … })) with Zod v4 inputs (use emptyInput for no-body POSTs).
  3. Use protectedProcedure so ctx.session or ctx.apiKey is required.
  4. Regenerate: pnpm --filter @zts/trpc run generate:openapi then pnpm sdk:build.

See Better Auth for session-based access on tRPC and Database for migrations when auth tables change.

On this page