REST API reference
The SDK is the fastest way to integrate, but everything it does is plain HTTP. Use this reference when you're calling Pluralize from a language without an SDK, debugging a request, or building a server-side integration by hand.
- Base URL:
https://pluralize.app - Content type:
application/jsonon every request with a body (except file upload, which ismultipart/form-data). - Errors: non-2xx responses return
{ "error": "<code>", "message": "<text>" }. See Errors & troubleshooting for the full code list.
Authentication
There are two ways to authenticate a call, depending on who is acting.
App key — x-pluralize-key
Identifies your app. Required on the unauthenticated tenant flows (signup, login, refresh,
password reset, email verification). The publishable key is safe to ship in the
browser — it names your app, it is not a secret. Get it from the dashboard or
pluralize app create / pluralize env pull --key.
POST /api/v1/auth/login
x-pluralize-key: plz_pk_live_xxxxxxxx
content-type: application/jsonTenant bearer — Authorization + x-pluralize-app
Identifies a signed-in tenant. Required on every tenant-scoped endpoint (their data, files, billing, account). Send the access token and the app id:
GET /api/v1/auth/me
authorization: Bearer <accessToken>
x-pluralize-app: <appId>Access tokens expire after 15 minutes; refresh them with the refresh token (90-day sliding
window). The SDK does this for you automatically on a 401.
Auth endpoints
| Method & path | Auth | Body | Returns |
| --- | --- | --- | --- |
| POST /api/v1/auth/signup | key | { email, password, locale?, inviteToken?, sendEmail? } | 201 { tenant, accessToken, refreshToken, expiresIn, refreshExpiresIn } |
| POST /api/v1/auth/login | key | { email, password } | 200 { tenant, accessToken, refreshToken, expiresIn, refreshExpiresIn } |
| POST /api/v1/auth/refresh | key | { refreshToken } | 200 { accessToken, refreshToken, expiresIn, refreshExpiresIn } |
| POST /api/v1/auth/logout | bearer | { refreshToken } | 204 |
| GET /api/v1/auth/me | bearer | — | 200 { tenant } |
| POST /api/v1/auth/verify-email | key | { token } | 200 { ok: true } |
| POST /api/v1/auth/resend-verification | bearer | { locale? } | 200 { ok: true } |
| POST /api/v1/auth/request-password-reset | key | { email, sendEmail? } | 204 (or 200 { resetToken, locale } when sendEmail: false) |
| POST /api/v1/auth/reset-password | key | { token, password } | 200 { ok: true } |
| POST /api/v1/auth/introspect | key | { include?: ["subscription","tenant"] } + optional bearer | 200 { active, tenantId, appId, exp, parentTenantId, subscription?, tenant? } |
request-password-reset always returns the same shape whether or not the email exists
(anti-enumeration). See Password reset.
Social login (OAuth)
Browser redirect flow, not a JSON call. Send the browser to:
GET /api/v1/auth/oauth/{provider}?appId={appId}&redirect={yourUrl}&inviteToken={optional}
provider is one of google, github, apple, microsoft, facebook, discord,
twitter. Pluralize bounces through the provider and returns to your redirect with the
tokens in the URL fragment (#plz_oauth=success&plz_access_token=...&plz_refresh_token=...).
The SDK reads and clears that fragment automatically. See
Social login.
Data endpoints
All require the tenant bearer. {collection} is any alphanumeric collection name.
| Method & path | Body / query | Returns |
| --- | --- | --- |
| POST /api/v1/data/{collection} | { data } | 201 { record } |
| GET /api/v1/data/{collection} | ?filter=&sort=&limit=&offset= | 200 { records, total, limit, offset } |
| GET /api/v1/data/{collection}/{id} | — | 200 { record } / 404 |
| PUT /api/v1/data/{collection}/{id} | { data } (merge-patch) | 200 { record } / 404 |
| DELETE /api/v1/data/{collection}/{id} | — | 204 |
| POST /api/v1/data/{collection}/upsert | { data } against a configured unique field | 200/201 { record, created } |
| POST /api/v1/data/{collection}/{id}/share | { expiresInSeconds? } | 201 { shareToken } |
| DELETE /api/v1/data/{collection}/{id}/share | — | 200 { revoked } |
| GET /api/v1/public/shared/{token} | no auth | 200 { record } / 410 if expired |
filter and sort are URL-encoded JSON. Operators: $eq, $ne, $gt, $gte, $lt,
$lte, $in. See Data for filtering, unique fields, and share links.
Files endpoints
All require the tenant bearer. Max 10 MB; HTML/SVG/JS MIME types are rejected.
| Method & path | Body | Returns |
| --- | --- | --- |
| POST /api/v1/files/upload | multipart/form-data with a file part | 201 { file } |
| GET /api/v1/files/{id} | — | 200 { file } |
| DELETE /api/v1/files/{id} | — | 204 |
file is { id, url, pathname, sizeBytes, mimeType, originalFilename, createdAt }. The
url is a public CDN URL. See Files.
Billing & usage endpoints
| Method & path | Auth | Body | Returns |
| --- | --- | --- | --- |
| GET /api/v1/billing/plans | key | — | 200 { plans } |
| POST /api/v1/billing/checkout | bearer | { planId, successUrl, cancelUrl, currency? } | 200 { url } |
| POST /api/v1/billing/portal | bearer | { returnUrl } | 200 { url } |
| GET /api/v1/billing/subscription | bearer | — | 200 { subscription } (or null) |
| GET /api/v1/billing/entitlements | bearer | — | 200 { entitlements } |
| GET /api/v1/billing/usage/{featureKey} | bearer | ?period=monthly\|daily | 200 { count, period, limit } |
| POST /api/v1/billing/usage/{featureKey}/increment | bearer | { by?, period? } | 200 { count, period, limit } / 402 limit_reached |
subscription carries a source of polar, gift, trial, or family. See
Billing and Server-side for gating patterns.
Account, invites & family endpoints
All require the tenant bearer unless noted.
| Method & path | Body | Returns |
| --- | --- | --- |
| POST /api/v1/tenants/me/delete | — | deletes the account; fires the tenant.deleted webhook |
| POST /api/v1/tenants/me/invite-tokens | { note?, expiresInSeconds? } | { token, quota } (plaintext once) |
| GET /api/v1/tenants/me/invite-tokens | — | { tokens, quota } |
| DELETE /api/v1/tenants/me/invite-tokens/{id} | — | { quota } |
| POST /api/v1/auth/family-invite/consume | { token, email, password, locale? } — key auth | new or email_exists result |
| POST /api/v1/tenants/me/family-invites | { expiresInSeconds?, note? } | minted family invite (owner only) |
| GET /api/v1/tenants/me/family-invites | — | { invites, seats } (owner only) |
| DELETE /api/v1/tenants/me/family-invites/{id} | — | revoked (owner only) |
| GET /api/v1/tenants/me/family/members | — | { members } (owner only) |
| POST /api/v1/tenants/me/family/remove-member | { member_tenant_id } | { ok, removedMemberTenantId } (owner only) |
| POST /api/v1/tenants/me/family/leave | — | { ok } (member only) |
| POST /api/v1/tenants/me/replace-with-family | { token } | { ok, parentTenantId, accessToken, refreshToken, expiresIn } |
The SDK wraps all of these as app.auth.* methods — see Authentication.
Object shapes
The objects returned by the endpoints above, as the SDK types them. Timestamps are ISO 8601 strings.
interface TenantInfo { // `tenant` on signup / login / me
id: string;
email: string;
emailVerifiedAt: string | null; // ISO timestamp once verified, else null
createdAt: string;
}
interface DataRecord { // `record` on data reads/writes
id: string;
data: Record<string, unknown>;
createdAt: string;
updatedAt: string;
}
interface FindResult { // GET /api/v1/data/{collection}
records: DataRecord[];
total: number;
limit: number;
offset: number;
}
interface FileInfo { // `file` on upload / get
id: string;
url: string; // public CDN URL
pathname: string;
sizeBytes: number;
mimeType: string;
originalFilename: string | null;
createdAt: string;
}
interface ShareToken { // `shareToken` on POST .../{id}/share
id: string;
token: string; // read it back via GET /api/v1/public/shared/{token}
expiresAt: string | null; // null = never expires
createdAt: string;
}
interface Subscription { // GET /api/v1/billing/subscription (or null)
planId: string;
planName: string;
planSlug: string;
status: 'active' | 'past_due' | 'canceled' | 'trialing' | 'unpaid';
currentPeriodEnd: string | null;
cancelAtPeriodEnd: boolean;
source?: 'polar' | 'gift' | 'trial' | 'family'; // treat undefined as 'polar'
giftExpiresAt?: string | null; // when source === 'gift' (null = permanent)
trialUntil?: string | null; // when source === 'trial'
familyOwnerEmail?: string | null; // when source === 'family'
}
interface PlanInfo { // GET /api/v1/billing/plans → { plans: PlanInfo[] }
id: string;
name: string;
slug: string;
description: string | null;
priceCents: number;
currency: string;
interval: 'month' | 'year';
sortOrder: number;
entitlements: Record<string, boolean | number>;
}entitlements (from GET /api/v1/billing/entitlements) is a flat
Record<string, boolean | number> — a boolean gates a feature on/off, a number is a
limit. See Server-side for how to enforce them.
App configuration
Per-app settings — CORS origins, OAuth providers, plans and entitlements, signup mode,
unique fields, webhooks — are managed from the dashboard or the
CLI, not from the tenant-facing API key. They are authenticated with your
developer session, not the app key, so they're not part of the public surface above.
To watch your own usage against your developer plan, call GET /api/v1/developer/usage
with your developer session (see Pricing).