Shared

Read the organization (API key)

GET
/api/v1/organization

The calling organization's own profile and settings. Shared, and this is the operation that makes the rest of the surface navigable: businessType here is how a program learns which engine it is talking to, and therefore which of the two sets of endpoints below apply to it. Guarding it to one vertical would make that undiscoverable.

A singleton, so the envelope is { data: { … } } with an object and there is no nextCursor, no limit and no cursor. One envelope, always data, so a caller unwraps every v1 response the same way.

timezone is the org's own IANA zone, and it is what the wall-clock times on GET /api/v1/service-periods are expressed in and what startsAt/endsAt on the two booking lists are expressed in too. A program reading bookings needs this endpoint to interpret them correctly, so a key granted only appointments:read or reservations:read cannot resolve them; grant organization:read alongside. See the timestamps section of docs/api-keys.md. currency and the deposit* fields are what a program needs to quote a deposit without guessing.

There is deliberately no organization:write scope to pair with this. Migration 0022 exists because a tenant could edit its own slug, status and business_type outside the app; handing that back to a long-lived program-held credential would undo it. solvintia_client_id is withheld: it links this org to a record in a different Solvintia product and means nothing here.

Authorization

bearerApiKey organization:read
AuthorizationBearer <token>

The scheme for programmatic callers: partner integrations, a customer's own scripts, the planned skill and MCP server. Accepted by the /api/v1/* operations and by nothing else; the rest of this document is session-cookie only. Every :read scope below has a GET /api/v1/<resource> behind it, and a static check fails the build if a scope is added without one. Full detail lives in docs/api-keys.md; the load-bearing parts are:

  • Transport. Authorization: Bearer sk_live_… only. No query parameter, no custom header, no cookie fallback: a credential in a URL ends up in access logs, referrers and browser history.
  • A key is a program with scopes, not a human with a role. It carries no admin/staff role at all; scopes are the entire authorization model, and an admin issuing a key is deliberately delegating their own authority.
  • Scopes are resource:action, where resource is exactly the /api/<resource> folder name and action is read or write. write does NOT imply read: they are compared by exact string.
  • The engine boundary is enforced twice. Engine scopes are filtered at issuance against the org's own business_type (an appointments org cannot be granted reservations:*), and again at use time by the gate's vertical argument. wrong_vertical is its own 403 code.
  • There is no scope for key management. A key can never mint, list or revoke another key, so a leaked key cannot be used to establish persistence. organization is read-only for the same reason migration 0022 exists.
  • Errors carry a machine-readable code alongside error: missing_authorization, invalid_key, key_revoked, key_expired, organization_inactive (all 401); insufficient_scope, wrong_vertical, plan_required (403); rate_limited (429). Match on code, never on the sentence. Note this is a RICHER error body than the {error} every session-authenticated route in this document returns.
  • Rate limiting is per key, a fixed 60-second window defaulting to 120 requests/minute, with Retry-After and X-RateLimit-* headers on a 429. Session-authenticated routes have no rate limiting in application code, deliberately: the rest of the app is covered at the edge by a Vercel WAF rate-limit rule, which is not billed for what it blocks and never reaches a function. See docs/api-keys.md.
  • One envelope, one paging scheme. Every list answers { data: [...], nextCursor }; the singleton answers { data: {...} }. nextCursor is keyset (seek) paging, opaque, and null exactly when there are no more rows. Every field is camelCase, unlike the database columns underneath.
  • /api/v1/ is a real version segment, not decoration. There is no v2 yet. When one ships, v1 keeps answering unchanged for a notice window of at least 6 months, and every v1 response carries Deprecation and Sunset headers (RFC 9745, RFC 8594) for that whole window. Only a security fix can shorten it. See docs/api-keys.md.

In: header

Scope: organization:read

Response Body

application/json

application/json

application/json

application/json

application/json

curl -X GET "https://example.com/api/v1/organization"
{  "data": {    "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",    "name": "string",    "slug": "string",    "industry": "string",    "status": "string",    "timezone": "string",    "businessType": "appointments",    "tagline": "string",    "currency": "string",    "logoUrl": "string",    "backgroundImageUrl": "string",    "accentColor": "string",    "bookingTheme": "string",    "bookingThemeToggleEnabled": true,    "notifyEmailEnabled": true,    "notifySmsEnabled": true,    "depositEnabled": true,    "depositType": "fixed",    "depositAmountCents": 0,    "depositPercentage": 0,    "depositPerPerson": true,    "depositMinPartySize": 0,    "depositRefundWindowHours": 0,    "guestManageEnabled": true,    "guestManageCutoffHours": 0,    "createdAt": "2019-08-24T14:15:22Z"  }}

List clients (API key) GET

The client/guest directory, for a program. **Shared by both engines** and gated with no vertical for that reason: a client record means the same thing to a salon and a restaurant, which is why the dashboard screen behind it is one shared route too. There is consequently no `wrong_vertical` failure here. Read-only, like every `/api/v1` operation. As above, the handler's explicit `company_id` filter is the entire tenant boundary: removing it would leak every organization's client list. Ordered newest first. No date filters: a client directory is not time-shaped, and there is no search parameter either, deliberately: a `?q=` would be a third `.or()` call site to sanitize and pagination already lets a caller walk the whole list.

Send product feedback (bug report, idea, or general) to Solvintia POST

Reached from NavUser's account menu, present for every signed-in dashboard login regardless of role or vertical: deliberately NOT the guest post-booking review at /api/feedback/{token} above, a different route on a different table for a different audience. Written through the caller's own RLS-scoped client, not service-role: book_feedback_submissions' `tenant_insert_own` policy (migration 0182) already enforces `company_id` and `user_id` both matching the JWT, so this route trusts the database rather than re-checking what it already checks. On success, also fires a best-effort alert email to `ALERT_EMAIL || NOTIFICATIONS_FROM_EMAIL` (the same operator address /api/cron/queue-health alerts to), synchronously rather than through book_job_queue: this is low-volume internal alerting, not a customer-facing send that needs retry resilience. A Resend failure here never turns a successful submission into an error response; the row is already saved. Private inbox by design: nothing this route returns, and nothing /superadmin/feedback shows, is ever surfaced back to the submitter beyond the 200 itself.