Server API and OpenAPI
Customer Portal uses Nitro file-based handlers. The assembled application generates its API reference from the handlers present in portal core and every enabled feature layer.
Discover a deployed API
Each running portal exposes:
| Route | Purpose |
|---|---|
/api-docs | Scalar interactive API browser. |
/api-docs/swagger | Swagger UI for the same document. |
/api-docs/openapi.json | Ordered OpenAPI document enriched with Better Auth operations and shared contracts. |
/api-docs/openapi.raw.json | Nitro's unmerged generated document, intended for internal assembly. |
Use the ordered /api-docs/openapi.json document for clients and reviews. It reflects the layers installed in that deployment, so it is more authoritative than a copied endpoint list.
Current product surface
At the documented master revision, the assembled repository contains 238 Nitro API handler files. The count is a review signal rather than an API version: a catch-all handler can contribute multiple OpenAPI operations, and a handler can respond to one method only.
| Owning layer | Handler files | Route families | Primary responsibility |
|---|---|---|---|
core | 3 | /api/auth/**, /api/notifications | Better Auth transport, current permissions, and notification feed |
ui | 0 | — | Neutral presentation only |
authentication | 0 | — | Uses core identity handlers and owns only account-entry UI |
organizations | 8 | /api/organizations/**, /api/profile | Profile, invitations, organization details, and email-provider credentials |
clients | 13 | /api/clients/** | CLIENT profiles, memberships, invitations, archival, selection, and module activation |
administration | 27 | /api/admin/** | System users, organizations, invitations, and centralized email administration |
saas-configuration | 5 | /api/admin/portal-settings/**, /api/portal/** | Portal settings, onboarding completion, and public configuration |
service-requests | 9 | /api/service-requests/** | Customer and organization-administrator request workflows |
timesheets | 57 | /api/timesheets/** | Entry, timers, setup, approvals, reporting, and client review access |
invoices | 32 | /api/invoices/** | Invoices, settings, PDFs, delivery, payments, attachments, viewers, and client access |
invoice-timesheets | 2 | /api/invoice-timesheets/** | Approved-time sources and atomic source-backed invoice creation |
products | 45 | /api/products/**, /api/store/** | Store settings, catalog, checkout, orders, purchases, media, and delivery |
planning | 37 | /api/planning/**, /api/store/planning/** | Availability, booking holds, appointments, provider connections, and synchronization |
invoice-products | 0 | — | Product-order invoice integration through registered server contracts |
preset | 0 | — | Composition only |
kit | 0 | — | Configuration and migration CLI only |
CI compares these layer and handler counts with the pinned public repository tree. When the product source pin changes, a mismatch requires the inventory and relevant narrative documentation to be reviewed together. For the exact operations, parameters, and response schemas in a deployment, use its generated OpenAPI document.
Authentication and tenancy
Most product handlers are session APIs rather than token-based public APIs. A protected feature operation should:
- require an authenticated session;
- derive the active organization from that session;
- evaluate a typed feature action against the user's organization role;
- scope every tenant-owned query by the returned organization ID;
- validate path, query, and body input before repository access.
import { definePortalRouteMeta, requireFeatureAccess } from '@nuxt-customer-portal/core/server'
import { exampleFeature } from '../../../shared/feature'
import { createExampleSchema } from '../../utils/example-validation'
definePortalRouteMeta({
openAPI: {
tags: ['Example'],
operationId: 'exampleCreate',
summary: 'Create an example record',
description: 'Creates a record in the active organization.'
},
body: createExampleSchema
})
export default defineEventHandler(async (event) => {
const { session, organizationId } = await requireFeatureAccess(event, exampleFeature.policy, 'create')
const input = createExampleSchema.parse(await readBody(event))
return createExample({
...input,
organizationId,
createdById: session.user.id
})
})
Do not accept organizationId from a normal browser request when the active session already determines the organization. PROVIDER-side client operations require an active PROVIDER membership and never use a global administrator bypass.
OpenAPI metadata
Every contributor-owned handler should call definePortalRouteMeta() beside the route with:
- one stable, unique
operationId; - a domain tag such as
TimesheetsorService requests; - an imperative summary;
- a description that states authentication, tenant scope, and the relevant permission;
- request and response schemas when Nitro cannot infer enough detail.
Keep the metadata beside the handler so moving or removing a layer also moves or removes its API description.
Error semantics
Core adapters establish these baseline responses:
| Status | Meaning |
|---|---|
400 | The session has no active organization or validated input is invalid. |
401 | No authenticated user session. |
403 | The user lacks organization membership or the required feature action. |
404 | A scoped record does not exist or is not visible in the active organization. |
409 | The requested transition conflicts with current domain state. |
Feature repositories should avoid revealing whether a record exists in another organization. Validation failures should be useful to the caller without exposing secrets, SQL, or credentials.
Stability
The generated OpenAPI document describes the current deployment; it is not yet a versioned public API guarantee. Pin the Customer Portal commit used by an integration and review the document when upgrading. See Compatibility and releases.
