OpenAPI spec
openapi: 3.0.3
info:
title: Unbase API
description: >
Unbase is a zero-config SQL (SQLite) database over HTTP, built on
Cloudflare Workers + Durable Objects. `POST /v1/projects` returns a
project and a bearer token in a single call; that token (the project's
secret key) is the permanent credential for the project's data plane.
Each project has two keys:
- **Secret key** (a.k.a. "service role"), format `<projectId>.<sig>` —
full SQL read/write plus admin. Send it as `Authorization: Bearer
<secretKey>`. Never expose it in a browser.
- **Anon key** (a.k.a. "publishable"), format `<projectId>.pk.<sig>` —
safe to embed in a client app. It unlocks the project's per-project
Auth endpoints (`/v1/projects/{id}/auth/*`) and read-only access to
Storage objects (`/v1/projects/{id}/storage/*`), and is sent in an
`apikey` header.
Project ids look like `unbase_xxxxxxxxxxxx`, account ids like `acct_...`,
and Auth end-user ids like `user_...`.
version: "2.0.0"
contact:
url: https://api.unbase.dev
servers:
- url: https://api.unbase.dev
description: Production
tags:
- name: projects
description: Create, claim, and delete projects
- name: sql
description: Run SQL against a project
- name: usage
description: Usage and export
- name: account
description: Log in and manage projects
- name: auth
description: >
Per-project Auth service — authenticate the end users of an app built
on a project. Callers authenticate with the project's anon key in an
`apikey` header (the secret key works too). Access tokens are HS256 JWTs
signed with the project's own JWT secret.
- name: storage
description: >
Per-project file storage, backed by Cloudflare R2. Uploads and deletes
require the secret key; reads (download and list) accept the anon key
too, sent as `Authorization: Bearer <key>`. Objects are private — there
is no public/anonymous URL.
- name: leaderboards
description: >
Per-project high-score tables for HTML games. Game routes (start/submit
a run, read a board) take the anon key in the `apikey` header and are
subject to the config's origin allowlist; config and moderation routes
take the secret key. The browser SDK is served at GET /v1/leaderboard.js.
See /docs/leaderboards.md for the config format and rules library.
paths:
/v1/projects:
post:
operationId: createProject
tags: [projects]
summary: Create a new project
description: >
Creates a new anonymous project and returns its id, URL, a bearer
token (the secret key), and an anon key. The token is shown exactly
once in this response and is not retrievable afterwards. No
authentication required.
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
turnstileToken:
type: string
description: >
Cloudflare Turnstile token from the browser creation
flow. Required in production, where a missing or
invalid token returns 403. Scripts should create
projects through POST /v1/account/projects with an
account session instead, which needs no Turnstile.
additionalProperties: false
responses:
"201":
description: Project created
content:
application/json:
schema:
$ref: "#/components/schemas/CreateProjectResponse"
"403":
$ref: "#/components/responses/Forbidden"
"429":
description: Too many projects created from this IP this hour
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"503":
description: Shared free-tier capacity temporarily saturated
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/v1/claim:
post:
operationId: initiateClaim
tags: [projects]
summary: Initiate a magic-link claim (step 1 of 2)
description: >
Step 1 of the two-step claim flow. Confirms the project exists,
then mints a signed, 30-minute magic-link token and emails a claim
link ({SITE_URL}/claim/verify?token=...) to the given address via
Resend. This does NOT claim the project — the recipient must
open the link, which drives POST /v1/claim/verify. Requiring the
email owner to open the link proves control of the address before
the claim is committed. No authentication required (the caller must
supply the projectId). When no Resend API key is configured (keyless
dev mode) no email is sent and the full claim URL is returned inline
as `devLink`; do not run keyless in production.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [id, email]
properties:
id:
type: string
description: The projectId to claim
example: unbase_7f3k9q2m1x8a
email:
type: string
format: email
additionalProperties: false
responses:
"200":
description: >
Claim link sent (or, in keyless dev mode, returned inline as
devLink)
content:
application/json:
schema:
$ref: "#/components/schemas/InitiateClaimResponse"
"400":
description: Missing id or email
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"404":
description: Project not found
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/v1/claim/verify:
post:
operationId: verifyClaim
tags: [projects]
summary: Complete a magic-link claim (step 2 of 2)
description: >
Step 2 of the two-step claim flow. Verifies the magic-link token's
HMAC signature and 30-minute expiry, then permanently attaches the
anonymous project to the email-based account, upgrading its plan
from "anonymous" to "free" and removing the 7-day expiry. Creates
the account if it doesn't already exist (idempotent by email). The
project's existing bearer token continues to work unchanged. No
authentication required — the token itself is the proof.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [token]
properties:
token:
type: string
description: The token from the magic link.
additionalProperties: false
responses:
"200":
description: Claim completed
content:
application/json:
schema:
$ref: "#/components/schemas/ClaimResponse"
"400":
description: >
Missing token, or the token is malformed, tampered, or expired
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/v1/auth/login:
post:
operationId: requestLogin
tags: [account]
summary: Request an account login magic link
description: >
Sends a magic link to the given email for the Unbase dashboard /
account. Verifying it (see /v1/auth/verify) logs the user in,
creating their account on first login — so this doubles as sign-up.
This is the account/console login, distinct from the per-project
Auth service under /v1/projects/{id}/auth. No authentication
required. In keyless dev mode (no Resend key) the link is returned
inline as `devLink` instead of being emailed.
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [email]
properties:
email:
type: string
format: email
additionalProperties: false
responses:
"200":
description: Link sent (or returned inline in dev mode)
content:
application/json:
schema:
$ref: "#/components/schemas/MagicLinkResponse"
"400":
$ref: "#/components/responses/BadRequest"
/v1/auth/verify:
post:
operationId: verifyLogin
tags: [account]
summary: Complete account login and receive a session token
description: >
Verifies an account login magic link's HMAC signature and 30-minute
expiry, gets-or-creates the account for that email, and returns a
long-lived (30-day) account session token. Present that token as a
bearer credential on the /v1/account/* endpoints.
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [token]
properties:
token:
type: string
additionalProperties: false
responses:
"200":
description: Logged in
content:
application/json:
schema:
$ref: "#/components/schemas/SessionResponse"
"400":
$ref: "#/components/responses/BadRequest"
/v1/account:
get:
operationId: getAccount
tags: [account]
summary: Get the authenticated account
security:
- sessionAuth: []
responses:
"200":
description: Account details
content:
application/json:
schema:
$ref: "#/components/schemas/AccountResponse"
"401":
$ref: "#/components/responses/Unauthorized"
/v1/account/projects:
get:
operationId: listAccountProjects
tags: [account]
summary: List the account's projects
description: >
Returns every project owned by the authenticated account, each with
a freshly minted working bearer token and anon key (keys are
deterministic, so this is safe — the session already proves
ownership).
security:
- sessionAuth: []
responses:
"200":
description: The account's projects
content:
application/json:
schema:
$ref: "#/components/schemas/AccountProjectList"
"401":
$ref: "#/components/responses/Unauthorized"
post:
operationId: createAccountProject
tags: [account]
summary: Create a new project
description: >
Creates a new project owned by the account, on the account's plan,
and returns its endpoint, secret token, and anon key. Enforces the
plan's max-projects cap.
security:
- sessionAuth: []
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
name:
type: string
description: Optional human-friendly project name (max 64 chars).
additionalProperties: false
responses:
"201":
description: Project created
content:
application/json:
schema:
$ref: "#/components/schemas/CreatedProjectResponse"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
description: Plan project limit reached
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/v1/account/projects/{projectId}:
patch:
operationId: renameAccountProject
tags: [account]
summary: Rename a project
description: >
Updates the display name of a project the authenticated account owns.
The name is trimmed and capped at 64 chars; an empty (or whitespace)
name clears it back to null, and the dashboard then falls back to the
project id. Scoped to the account — renaming a project the account
does not own returns 404.
security:
- sessionAuth: []
parameters:
- name: projectId
in: path
required: true
schema:
type: string
description: The project's id (e.g. `unbase_...`).
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
name:
type: string
description: New project name (max 64 chars; empty clears it).
additionalProperties: false
responses:
"200":
description: The updated project id and name
content:
application/json:
schema:
type: object
required: [id, name]
properties:
id:
type: string
name:
type: string
nullable: true
"401":
$ref: "#/components/responses/Unauthorized"
"404":
description: No such project owned by this account
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/v1/stripe/webhook:
post:
operationId: stripeWebhook
tags: [projects]
summary: Apply paid-plan upgrades/downgrades from Stripe
description: >
Receives Stripe webhook events and applies paid-plan changes.
Authenticated by the Stripe signature (NOT a bearer token): the
Stripe-Signature header is verified via HMAC-SHA256 against
STRIPE_WEBHOOK_SECRET with a 5-minute timestamp tolerance. The
request body is the raw Stripe event JSON. On
checkout.session.completed the customer email (customer_email or
customer_details.email) and target plan (metadata.plan, "founder"
or "pro", set on the Stripe Payment Link) are read and the account
plus all its projects are upgraded to that plan. On
customer.subscription.deleted the account is downgraded to "free",
but only if metadata.email is present on the event.
security: []
parameters:
- name: Stripe-Signature
in: header
required: true
schema:
type: string
description: >
Stripe's `t=...,v1=...` signature over the raw request body.
requestBody:
required: true
description: Raw Stripe event JSON.
content:
application/json:
schema:
type: object
additionalProperties: true
responses:
"200":
description: >
Event received. `applied` is true when a plan change was made,
false for acknowledged-but-no-op events.
content:
application/json:
schema:
$ref: "#/components/schemas/StripeWebhookResponse"
"400":
description: Bad or missing Stripe signature
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"503":
description: STRIPE_WEBHOOK_SECRET is not configured
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/v1/projects/{id}/query:
post:
operationId: runQuery
tags: [sql]
summary: Run a single SQL statement
security:
- secretKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/Statement"
responses:
"200":
description: Statement executed
headers:
X-Unbase-Rows-Read:
schema:
type: string
description: Mirrors rowsRead in the response body
X-Unbase-Rows-Written:
schema:
type: string
description: Mirrors rowsWritten in the response body
X-Unbase-Limit-Warning:
schema:
type: string
enum: ["true"]
description: >
Present only when the account is at or over 100% of its
plan's monthly quota (read, write, or storage).
content:
application/json:
schema:
$ref: "#/components/schemas/QueryResult"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"413":
$ref: "#/components/responses/PayloadTooLarge"
"503":
$ref: "#/components/responses/ServiceUnavailable"
/v1/projects/{id}/batch:
post:
operationId: runBatch
tags: [sql]
summary: Run multiple SQL statements as one atomic transaction
security:
- secretKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [statements]
properties:
statements:
type: array
minItems: 1
items:
$ref: "#/components/schemas/Statement"
additionalProperties: false
responses:
"200":
description: All statements executed (all-or-nothing)
headers:
X-Unbase-Rows-Read:
schema:
type: string
X-Unbase-Rows-Written:
schema:
type: string
X-Unbase-Limit-Warning:
schema:
type: string
enum: ["true"]
content:
application/json:
schema:
$ref: "#/components/schemas/BatchResult"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"413":
$ref: "#/components/responses/PayloadTooLarge"
"503":
$ref: "#/components/responses/ServiceUnavailable"
/v1/projects/{id}/usage:
get:
operationId: getUsage
tags: [usage]
summary: Get plan, status, and current-month usage
security:
- secretKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
responses:
"200":
description: Usage snapshot
content:
application/json:
schema:
$ref: "#/components/schemas/UsageResponse"
"401":
$ref: "#/components/responses/Unauthorized"
/v1/projects/{id}/export:
get:
operationId: exportProject
tags: [usage]
summary: Download a plain-SQL logical dump of the project
description: >
Returns a plain-text SQL dump (CREATE TABLE + INSERT INTO
statements wrapped in a transaction) — not a binary .sqlite file.
Fully replayable with `sqlite3 restored.db < dump.sql`.
security:
- secretKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
responses:
"200":
description: SQL dump
headers:
Content-Disposition:
schema:
type: string
example: 'attachment; filename="unbase_7f3k9q2m1x8a.sql"'
content:
application/sql:
schema:
type: string
format: binary
"401":
$ref: "#/components/responses/Unauthorized"
"404":
$ref: "#/components/responses/NotFound"
/v1/projects/{id}/tables:
get:
operationId: listTables
tags: [sql]
summary: List the project's tables, columns, and row counts
description: >
Schema introspection for building a table browser. Returns user
tables only — internal bookkeeping (_unbase_*), SQLite (sqlite_*),
and Cloudflare (_cf_*) tables are excluded.
security:
- secretKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
responses:
"200":
description: The project's tables
content:
application/json:
schema:
$ref: "#/components/schemas/TablesResponse"
"401":
$ref: "#/components/responses/Unauthorized"
/v1/projects/{id}:
delete:
operationId: deleteProject
tags: [projects]
summary: Permanently delete a project and its exports
security:
- secretKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
responses:
"204":
description: Deleted (no content)
"401":
$ref: "#/components/responses/Unauthorized"
/v1/projects/{id}/storage/objects:
get:
operationId: listStorageObjects
tags: [storage]
summary: List a project's storage objects
security:
- secretKey: []
- anonApiKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
responses:
"200":
description: The project's storage objects
content:
application/json:
schema:
$ref: "#/components/schemas/StorageObjectList"
"401":
$ref: "#/components/responses/Unauthorized"
/v1/projects/{id}/storage/objects/{key}:
put:
operationId: uploadStorageObject
tags: [storage]
summary: Upload (or overwrite) an object
description: >
The request body is the raw file bytes — not multipart/form-data.
`Content-Type` is stored and returned on download; it defaults to
`application/octet-stream` if omitted. The key may contain `/` to
namespace objects into folders, e.g. `avatars/user.png`. Capped at
100 MB per object, subject to the plan's file-storage quota.
security:
- secretKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
- $ref: "#/components/parameters/StorageKey"
requestBody:
required: true
content:
"*/*":
schema:
type: string
format: binary
responses:
"201":
description: Object stored
content:
application/json:
schema:
$ref: "#/components/schemas/StorageObject"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"413":
$ref: "#/components/responses/PayloadTooLarge"
get:
operationId: downloadStorageObject
tags: [storage]
summary: Download an object
security:
- secretKey: []
- anonApiKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
- $ref: "#/components/parameters/StorageKey"
responses:
"200":
description: Object bytes, with the Content-Type stored at upload time
content:
"*/*":
schema:
type: string
format: binary
"401":
$ref: "#/components/responses/Unauthorized"
"404":
$ref: "#/components/responses/NotFound"
delete:
operationId: deleteStorageObject
tags: [storage]
summary: Delete an object
security:
- secretKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
- $ref: "#/components/parameters/StorageKey"
responses:
"204":
description: Deleted (no content)
"401":
$ref: "#/components/responses/Unauthorized"
"404":
$ref: "#/components/responses/NotFound"
/v1/projects/{id}/auth/signup:
post:
operationId: authSignup
tags: [auth]
summary: Sign up an end user with email + password
description: >
Registers a new end user of the project and returns a Session.
Authenticate the request with the project's anon key in the `apikey`
header (the secret key also works). Password must be at least 8
characters.
security:
- anonApiKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [email, password]
properties:
email:
type: string
format: email
password:
type: string
minLength: 8
additionalProperties: false
responses:
"201":
description: User created; Session returned
content:
application/json:
schema:
$ref: "#/components/schemas/Session"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"409":
description: A user with that email already exists
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/v1/projects/{id}/auth/signin:
post:
operationId: authSignin
tags: [auth]
summary: Sign in an end user with email + password
description: >
Verifies an end user's email + password and returns a Session.
Authenticate the request with the project's anon key in the `apikey`
header (the secret key also works).
security:
- anonApiKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [email, password]
properties:
email:
type: string
format: email
password:
type: string
additionalProperties: false
responses:
"200":
description: Signed in; Session returned
content:
application/json:
schema:
$ref: "#/components/schemas/Session"
"400":
$ref: "#/components/responses/BadRequest"
"401":
description: Invalid credentials or missing/invalid apikey
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/v1/projects/{id}/auth/magiclink:
post:
operationId: authMagicLink
tags: [auth]
summary: Send an end user a passwordless magic link
description: >
Emails the end user a passwordless sign-in link pointing at
`redirectTo?token=...` (falls back to
`<SITE_URL>/auth/callback?token=...` when redirectTo is omitted). The
project owner's app then calls .../auth/verify with the token to
obtain a Session. Authenticate the request with the project's anon
key in the `apikey` header. In keyless dev mode the link is returned
inline as `devLink`.
security:
- anonApiKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [email]
properties:
email:
type: string
format: email
redirectTo:
type: string
format: uri
description: >
URL the emailed link points at; the single-use token is
appended as a `token` query parameter. Defaults to
`<SITE_URL>/auth/callback`.
additionalProperties: false
responses:
"200":
description: Link sent (or returned inline in dev mode)
content:
application/json:
schema:
$ref: "#/components/schemas/MagicLinkResponse"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
/v1/projects/{id}/auth/verify:
post:
operationId: authVerify
tags: [auth]
summary: Verify a magic-link token and receive a Session
description: >
Completes a passwordless sign-in by verifying the token from a magic
link and returning a Session. Creates the end user on first verify.
Authenticate the request with the project's anon key in the `apikey`
header.
security:
- anonApiKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [token]
properties:
token:
type: string
additionalProperties: false
responses:
"200":
description: Verified; Session returned
content:
application/json:
schema:
$ref: "#/components/schemas/Session"
"400":
description: Missing, malformed, or expired token
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"401":
$ref: "#/components/responses/Unauthorized"
/v1/projects/{id}/auth/token:
post:
operationId: authRefreshToken
tags: [auth]
summary: Exchange a refresh token for a new Session
description: >
Rotates a refresh token for a fresh Session (new access + refresh
token). Refresh tokens are single-use — each call invalidates the
one presented. Authenticate the request with the project's anon key
in the `apikey` header.
security:
- anonApiKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [refreshToken]
properties:
refreshToken:
type: string
additionalProperties: false
responses:
"200":
description: New Session issued
content:
application/json:
schema:
$ref: "#/components/schemas/Session"
"400":
$ref: "#/components/responses/BadRequest"
"401":
description: Invalid, expired, or already-used refresh token
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/v1/projects/{id}/auth/password:
post:
operationId: authChangePassword
tags: [auth]
summary: Change the signed-in end user's password
description: >
Changes the password of the end user identified by their access token.
Present the access token as `Authorization: Bearer <accessToken>`
together with the project's anon key in the `apikey` header. A user who
already has a password must supply the correct `currentPassword`;
passwordless (magic-link-only) users omit it to set one for the first
time. The new password must be at least 8 characters. The change revokes
the user's other sessions and returns a fresh Session.
security:
- userAccessToken: []
anonApiKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [newPassword]
properties:
currentPassword:
type: string
description: >
Required when the user already has a password; omitted by
passwordless users setting one for the first time.
newPassword:
type: string
minLength: 8
additionalProperties: false
responses:
"200":
description: Password changed; a fresh Session is returned
content:
application/json:
schema:
$ref: "#/components/schemas/Session"
"400":
$ref: "#/components/responses/BadRequest"
"401":
description: Missing/invalid access token or incorrect current password
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
/v1/projects/{id}/auth/recover:
post:
operationId: authRecover
tags: [auth]
summary: Send an end user a password-reset link
description: >
Starts a forgot-password flow: emails the end user a reset link
pointing at `redirectTo?token=...` (falls back to
`<SITE_URL>/auth/reset?token=...` when redirectTo is omitted). The
project owner's app then calls .../auth/reset with the token and a new
password. Authenticate the request with the project's anon key in the
`apikey` header. Always reports success without revealing whether an
account exists. In keyless dev mode the link is returned inline as
`devLink`.
security:
- anonApiKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [email]
properties:
email:
type: string
format: email
redirectTo:
type: string
format: uri
description: >
URL the emailed link points at; the single-use reset token
is appended as a `token` query parameter. Defaults to
`<SITE_URL>/auth/reset`.
additionalProperties: false
responses:
"200":
description: Reset link sent (or returned inline in dev mode)
content:
application/json:
schema:
$ref: "#/components/schemas/MagicLinkResponse"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
/v1/projects/{id}/auth/reset:
post:
operationId: authResetPassword
tags: [auth]
summary: Complete a password reset with a reset token
description: >
Completes a forgot-password flow: verifies the token from a reset link,
sets the new password on the matching account, revokes all prior
sessions, and returns a fresh Session. The new password must be at
least 8 characters. Authenticate the request with the project's anon
key in the `apikey` header.
security:
- anonApiKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [token, newPassword]
properties:
token:
type: string
newPassword:
type: string
minLength: 8
additionalProperties: false
responses:
"200":
description: Password reset; Session returned
content:
application/json:
schema:
$ref: "#/components/schemas/Session"
"400":
description: >
Missing/short new password, or a missing, malformed, expired, or
unknown-account reset token
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
"401":
$ref: "#/components/responses/Unauthorized"
/v1/projects/{id}/auth/user:
post:
operationId: authGetUser
tags: [auth]
summary: Get the current end user
description: >
Returns the end user identified by the access token. Present the
access token as `Authorization: Bearer <accessToken>` together with
the project's anon key in the `apikey` header. The request body is an
empty JSON object.
security:
- userAccessToken: []
anonApiKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
requestBody:
required: false
content:
application/json:
schema:
type: object
additionalProperties: false
responses:
"200":
description: The current end user
content:
application/json:
schema:
type: object
required: [user]
properties:
user:
$ref: "#/components/schemas/AuthUser"
"401":
$ref: "#/components/responses/Unauthorized"
/v1/projects/{id}/auth/logout:
post:
operationId: authLogout
tags: [auth]
summary: Revoke a refresh token (sign out)
description: >
Invalidates the given refresh token so it can no longer be exchanged.
Authenticate the request with the project's anon key in the `apikey`
header.
security:
- anonApiKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [refreshToken]
properties:
refreshToken:
type: string
additionalProperties: false
responses:
"204":
description: Logged out (no content)
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
/v1/projects/{id}/auth/users:
get:
operationId: authListUsers
tags: [auth]
summary: List the project's end users (admin)
description: >
Admin-only. Returns every end user registered with the project.
Requires the project's secret key as `Authorization: Bearer
<secretKey>` — the anon key is rejected here.
security:
- secretKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
responses:
"200":
description: The project's end users
content:
application/json:
schema:
type: object
required: [users]
properties:
users:
type: array
items:
$ref: "#/components/schemas/AuthUser"
"401":
$ref: "#/components/responses/Unauthorized"
/v1/projects/{id}/auth/settings:
get:
operationId: authGetSettings
tags: [auth]
summary: Get the project's Auth settings (admin)
description: >
Admin-only. Returns the project's Auth configuration, including the
JWT secret used to sign end-user access tokens — so the project owner
can verify those tokens in their own backend. Requires the project's
secret key as `Authorization: Bearer <secretKey>`.
security:
- secretKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
responses:
"200":
description: Auth settings
content:
application/json:
schema:
$ref: "#/components/schemas/AuthSettings"
"401":
$ref: "#/components/responses/Unauthorized"
/v1/leaderboard.js:
get:
operationId: getLeaderboardSdk
tags: [leaderboards]
summary: The browser SDK (defines window.UnbaseLeaderboard)
security: []
responses:
"200":
description: JavaScript
content:
application/javascript:
schema:
type: string
/v1/projects/{id}/leaderboards:
get:
operationId: getLeaderboardOverview
tags: [leaderboards]
summary: Config, boards with current-period entry counts, 24h run stats, open flags and bans
security:
- secretKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
responses:
"200":
description: Overview
content:
application/json:
schema:
type: object
"401":
$ref: "#/components/responses/Unauthorized"
/v1/projects/{id}/leaderboards/config:
get:
operationId: getLeaderboardConfig
tags: [leaderboards]
summary: Read the leaderboard config ({ config, isDefault })
security:
- secretKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
responses:
"200":
description: The config
content:
application/json:
schema:
type: object
properties:
config:
$ref: "#/components/schemas/LeaderboardConfig"
isDefault:
type: boolean
"401":
$ref: "#/components/responses/Unauthorized"
put:
operationId: putLeaderboardConfig
tags: [leaderboards]
summary: Replace the leaderboard config
description: >
Changing a board's sort order re-ranks its stored rows; removing a
board deletes its rows. Returns 400 invalid_config naming the problem.
security:
- secretKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/LeaderboardConfig"
responses:
"200":
description: The saved (normalized) config
content:
application/json:
schema:
type: object
properties:
config:
$ref: "#/components/schemas/LeaderboardConfig"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
/v1/projects/{id}/leaderboards/device:
post:
operationId: leaderboardDevice
tags: [leaderboards]
summary: The player's anonymous id, kept in a partitioned cookie
description: >
For games whose iframe can't use localStorage (cross-site iframes
with third-party storage blocked). With no body, it returns the id
stored in the partitioned `ulb_<projectId>` cookie, minting one if
there's none. With `{ device }`, it adopts that id (used after a
save-code claim). Call it with `credentials: "include"`. This is the
only route that answers credentialed CORS from any origin (except
`null`).
security:
- anonApiKey: []
- secretKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
device: { type: string, pattern: "^[A-Za-z0-9_-]{8,64}$" }
responses:
"200":
description: "{ device, name } (the last display name this player used, or null), plus a Set-Cookie with the partitioned id cookie"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
/v1/projects/{id}/leaderboards/runs/start:
post:
operationId: startLeaderboardRun
tags: [leaderboards]
summary: Start a run and get a single-use token
description: >
Rate-limited to 30 starts/minute per device and 300/hour per IP (429
with Retry-After). Tokens expire after 6 hours.
security:
- anonApiKey: []
- secretKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [device]
properties:
device:
type: string
pattern: "^[A-Za-z0-9_-]{8,64}$"
responses:
"201":
description: Run started
content:
application/json:
schema:
type: object
properties:
runId: { type: string }
token: { type: string }
startedAt: { type: integer, description: Unix ms }
"403":
description: origin_not_allowed
"429":
description: rate_limited
/v1/projects/{id}/leaderboards/runs/submit:
post:
operationId: submitLeaderboardRun
tags: [leaderboards]
summary: Submit a run's score
security:
- anonApiKey: []
- secretKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [token, name, values]
properties:
token: { type: string }
name: { type: string, maxLength: 20 }
values:
type: object
additionalProperties: { type: number }
responses:
"200":
description: Accepted
content:
application/json:
schema:
type: object
properties:
accepted: { type: boolean }
runId: { type: string }
rank:
type: object
additionalProperties: { type: integer }
newBest:
type: object
additionalProperties: { type: boolean }
top:
type: object
additionalProperties:
type: array
items:
$ref: "#/components/schemas/LeaderboardRow"
"400":
description: bad_name or invalid_values
"404":
description: run_not_found
"409":
description: run_used
"410":
description: run_expired
"422":
description: implausible (failed the duration check or a rule)
/v1/projects/{id}/leaderboards/runs:
get:
operationId: listLeaderboardRuns
tags: [leaderboards]
summary: Recent submitted runs, with rejection reasons (kept 14 days)
security:
- secretKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
- name: status
in: query
schema:
type: string
enum: [accepted, rejected, blocked, expired, deleted, started]
- name: limit
in: query
schema: { type: integer, maximum: 200 }
responses:
"200":
description: Runs
content:
application/json:
schema:
type: object
/v1/projects/{id}/leaderboards/runs/{runId}:
delete:
operationId: deleteLeaderboardRun
tags: [leaderboards]
summary: Remove a run from every board
security:
- secretKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
- name: runId
in: path
required: true
schema: { type: string }
responses:
"200":
description: "{ deleted: n }"
/v1/projects/{id}/leaderboards/boards/{board}/top:
get:
operationId: getLeaderboardTop
tags: [leaderboards]
summary: Top rows of a board
security:
- anonApiKey: []
- secretKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
- $ref: "#/components/parameters/BoardSlug"
- name: limit
in: query
schema: { type: integer, default: 10, maximum: 100 }
- $ref: "#/components/parameters/LeaderboardPeriod"
- name: device
in: query
description: Marks the caller's own row with `me`, and includes it if shadow-banned.
schema: { type: string }
responses:
"200":
description: Board rows
content:
application/json:
schema:
type: object
properties:
board: { type: string }
period: { type: string }
periodKey: { type: string }
rows:
type: array
items:
$ref: "#/components/schemas/LeaderboardRow"
"404":
$ref: "#/components/responses/NotFound"
/v1/projects/{id}/leaderboards/boards/{board}/around:
get:
operationId: getLeaderboardAround
tags: [leaderboards]
summary: A device's row and its neighbours
security:
- anonApiKey: []
- secretKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
- $ref: "#/components/parameters/BoardSlug"
- name: device
in: query
required: true
schema: { type: string }
- name: span
in: query
schema: { type: integer, default: 5, maximum: 10 }
- $ref: "#/components/parameters/LeaderboardPeriod"
responses:
"200":
description: "{ rank: integer|null, rows: LeaderboardRow[] }"
/v1/projects/{id}/leaderboards/boards/{board}/entries:
get:
operationId: listLeaderboardEntries
tags: [leaderboards]
summary: Every row of a board period (any visibility, private fields, device ids)
security:
- secretKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
- $ref: "#/components/parameters/BoardSlug"
- $ref: "#/components/parameters/LeaderboardPeriod"
- name: limit
in: query
schema: { type: integer, default: 50, maximum: 200 }
- name: offset
in: query
schema: { type: integer, default: 0 }
responses:
"200":
description: Rows
content:
application/json:
schema:
type: object
/v1/projects/{id}/leaderboards/bans:
get:
operationId: listLeaderboardBans
tags: [leaderboards]
summary: List banned devices
security:
- secretKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
responses:
"200":
description: "{ bans: [{ device, kind, reason, createdAt }] }"
/v1/projects/{id}/leaderboards/bans/{device}:
put:
operationId: banLeaderboardDevice
tags: [leaderboards]
summary: Shadow-ban or block a device
security:
- secretKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
- name: device
in: path
required: true
schema: { type: string }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [kind]
properties:
kind: { type: string, enum: [shadow, block] }
reason: { type: string }
responses:
"200":
description: Banned
delete:
operationId: unbanLeaderboardDevice
tags: [leaderboards]
summary: Lift a ban; the device's rows become public again
security:
- secretKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
- name: device
in: path
required: true
schema: { type: string }
responses:
"200":
description: Unbanned
/v1/projects/{id}/leaderboards/flags:
get:
operationId: listLeaderboardFlags
tags: [leaderboards]
summary: Accepted runs that tripped a soft (flag) rule
security:
- secretKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
- name: resolved
in: query
description: Include resolved flags too.
schema: { type: boolean }
responses:
"200":
description: "{ flags: [{ id, runId, device, name, values, reason, createdAt, resolved }] }"
/v1/projects/{id}/leaderboards/flags/{flagId}/resolve:
post:
operationId: resolveLeaderboardFlag
tags: [leaderboards]
summary: Mark a flag as reviewed
security:
- secretKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
- name: flagId
in: path
required: true
schema: { type: string }
responses:
"200":
description: Resolved
"404":
$ref: "#/components/responses/NotFound"
/v1/projects/{id}/leaderboards/progress:
get:
operationId: getProgress
tags: [leaderboards]
summary: A device's stats, unlocked and locked unlocks, and progress rank
security:
- anonApiKey: []
- secretKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
- name: device
in: query
required: true
schema: { type: string }
responses:
"200":
description: "{ stats, unlocked, locked, rank, total }"
"400":
description: progression_disabled
/v1/projects/{id}/leaderboards/progress/top:
get:
operationId: getProgressTop
tags: [leaderboards]
summary: The progress board (most unlocks; earliest last unlock wins ties)
security:
- anonApiKey: []
- secretKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
- name: limit
in: query
schema: { type: integer }
- name: device
in: query
schema: { type: string }
description: Marks the caller's row with `me`
responses:
"200":
description: "{ rows: [{ rank, name, unlocks, total, lastUnlockAt, me? }], total }"
"404":
description: The progress board is disabled
/v1/projects/{id}/leaderboards/progress/around:
get:
operationId: getProgressAround
tags: [leaderboards]
summary: A device's progress-board row and its neighbours
security:
- anonApiKey: []
- secretKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
- name: device
in: query
required: true
schema: { type: string }
- name: span
in: query
schema: { type: integer }
responses:
"200":
description: OK
/v1/projects/{id}/leaderboards/devices/link:
post:
operationId: createSaveCode
tags: [leaderboards]
summary: Create a single-use save transfer code (15 min)
security:
- anonApiKey: []
- secretKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [device]
properties:
device: { type: string }
responses:
"200":
description: "{ code: \"XXXX-XXXX\", expiresAt }"
"404":
description: The device has nothing to transfer
"429":
description: rate_limited (5 per device per hour)
/v1/projects/{id}/leaderboards/devices/claim:
post:
operationId: claimSaveCode
tags: [leaderboards]
summary: Adopt the player behind a save code
security:
- anonApiKey: []
- secretKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [code, device]
properties:
code: { type: string, description: Case-insensitive; dashes optional }
device: { type: string }
responses:
"200":
description: "{ device: <original device id>, progress }"
"400":
description: bad_code (unknown, used or expired)
"429":
description: rate_limited (10 attempts per IP per hour)
/v1/projects/{id}/leaderboards/progress/summary:
get:
operationId: getProgressSummary
tags: [leaderboards]
summary: Players with progress and per-unlock earned counts
security:
- secretKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
responses:
"200":
description: OK
/v1/projects/{id}/leaderboards/progress/players:
get:
operationId: listProgressPlayers
tags: [leaderboards]
summary: Players ranked by progress, or searched by name / device id
security:
- secretKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
- name: q
in: query
schema: { type: string }
- name: limit
in: query
schema: { type: integer }
responses:
"200":
description: OK
/v1/projects/{id}/leaderboards/progress/devices/{device}:
get:
operationId: getDeviceProgress
tags: [leaderboards]
summary: A device's stats, unlocks (with source and run), profile and ban
security:
- secretKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
- name: device
in: path
required: true
schema: { type: string }
responses:
"200":
description: OK
delete:
operationId: resetDeviceProgress
tags: [leaderboards]
summary: Delete a device's stats, unlocks and profile (leaderboard rows stay)
security:
- secretKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
- name: device
in: path
required: true
schema: { type: string }
responses:
"200":
description: OK
/v1/projects/{id}/leaderboards/progress/devices/{device}/unlocks:
post:
operationId: grantUnlock
tags: [leaderboards]
summary: Grant an unlock (source owner)
security:
- secretKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
- name: device
in: path
required: true
schema: { type: string }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [id]
properties:
id: { type: string }
responses:
"200":
description: OK
/v1/projects/{id}/leaderboards/progress/devices/{device}/unlocks/{unlockId}:
delete:
operationId: revokeUnlock
tags: [leaderboards]
summary: Revoke an unlock
security:
- secretKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
- name: device
in: path
required: true
schema: { type: string }
- name: unlockId
in: path
required: true
schema: { type: string }
responses:
"200":
description: OK
/v1/projects/{id}/leaderboards/progress/backfill:
post:
operationId: backfillProgress
tags: [leaderboards]
summary: Grant unlocks existing players already qualify for (paginated)
security:
- secretKey: []
parameters:
- $ref: "#/components/parameters/ProjectId"
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
cursor: { type: string }
limit: { type: integer, maximum: 5000 }
responses:
"200":
description: "{ processed, granted, nextCursor }"
components:
securitySchemes:
secretKey:
type: http
scheme: bearer
description: >
The project's secret key (service role), of the form
`${projectId}.${signature}`, returned once by POST /v1/projects. It
is the permanent full-access credential for that project's data
plane and admin Auth endpoints; a key only authorizes the projectId
embedded in it. Never expose it in a browser.
anonApiKey:
type: apiKey
in: header
name: apikey
description: >
The project's anon (publishable) key, of the form
`${projectId}.pk.${signature}`. Safe to embed in a client app. Sent
in the `apikey` header, it unlocks the per-project Auth endpoints
only. The secret key is also accepted in this header.
userAccessToken:
type: http
scheme: bearer
description: >
An end user's access token — an HS256 JWT signed with the project's
own JWT secret, returned in a Session by the Auth endpoints. Used on
POST /v1/projects/{id}/auth/user. Claims:
{ sub: userId, iss: projectId, role: "authenticated", email, iat, exp }.
sessionAuth:
type: http
scheme: bearer
description: >
Account session token returned by POST /v1/auth/verify. Long-lived
(30 days) and scoped to the account, not a single project. Used on
the /v1/account/* endpoints.
parameters:
ProjectId:
name: id
in: path
required: true
schema:
type: string
example: unbase_7f3k9q2m1x8a
description: The projectId to operate on.
BoardSlug:
name: board
in: path
required: true
schema:
type: string
example: daily
description: A board slug from the leaderboard config.
LeaderboardPeriod:
name: period
in: query
schema:
type: string
default: current
description: >
"current", or a past period key: "all", "2026-10-02" (daily),
"2026-W40" (weekly), "2026-10" (monthly) or a season id.
StorageKey:
name: key
in: path
required: true
schema:
type: string
example: avatars/user.png
description: >
The object's key within the project. May contain `/` to namespace
objects into folders; each path segment must be percent-encoded.
responses:
BadRequest:
description: Malformed request
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
Unauthorized:
description: Missing, invalid, or mismatched credential
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
Forbidden:
description: >
Quota hard-limit exceeded (writes only), account suspended, or the
statement referenced a reserved _unbase_* table
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
NotFound:
description: Unknown route or project
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
PayloadTooLarge:
description: Response would exceed the 2 MB response cap
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
ServiceUnavailable:
description: >
Shared free-tier capacity temporarily saturated (circuit
breaker); writes only, reads unaffected
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
schemas:
CreateProjectResponse:
type: object
required: [projectId, url, token, anonKey]
properties:
projectId:
type: string
example: unbase_7f3k9q2m1x8a
url:
type: string
format: uri
example: https://api.unbase.dev/v1/projects/unbase_7f3k9q2m1x8a
token:
type: string
description: >
The project's secret key. Shown exactly once. Store it
immediately.
example: unbase_7f3k9q2m1x8a.AbCdEf123...
anonKey:
type: string
description: >
The project's anon (publishable) key. Safe to embed in a client
app; unlocks the project's Auth endpoints only.
example: unbase_7f3k9q2m1x8a.pk.AbCdEf123...
InitiateClaimResponse:
type: object
required: [sent]
properties:
sent:
type: boolean
description: >
True when a claim email was sent (Resend key configured);
false in keyless dev mode.
devLink:
type: string
format: uri
description: >
Present only in keyless dev mode — the full claim URL, returned
inline because no email was sent.
example: https://unbase.dev/claim/verify?token=eyJ...
ClaimResponse:
type: object
required: [projectId, accountId, plan]
properties:
projectId:
type: string
example: unbase_7f3k9q2m1x8a
accountId:
type: string
example: acct_9k2m1x8a7f3k
plan:
type: string
enum: [free]
MagicLinkResponse:
type: object
required: [sent]
properties:
sent:
type: boolean
description: True when the link was emailed.
devLink:
type: string
description: >
Present only in keyless dev mode — the link that would have been
emailed, returned inline so the flow stays usable.
SessionResponse:
type: object
required: [token, accountId, email, plan]
properties:
token:
type: string
description: The account session bearer token (30-day expiry).
accountId:
type: string
example: acct_9k2m1x8a7f3k
email:
type: string
format: email
plan:
type: string
enum: [free, founder, pro]
AccountResponse:
type: object
required: [accountId, email, plan]
properties:
accountId:
type: string
example: acct_9k2m1x8a7f3k
email:
type: string
format: email
plan:
type: string
enum: [free, founder, pro]
AccountProjectSummary:
type: object
required: [id, plan, status, sizeBytes, url, token, anonKey]
properties:
id:
type: string
example: unbase_7f3k9q2m1x8a
name:
type: string
nullable: true
plan:
type: string
enum: [anonymous, free, founder, pro]
status:
type: string
enum: [active, limited, suspended]
createdAt:
type: integer
format: int64
nullable: true
lastUsedAt:
type: integer
format: int64
nullable: true
sizeBytes:
type: integer
url:
type: string
format: uri
example: https://api.unbase.dev/v1/projects/unbase_7f3k9q2m1x8a
token:
type: string
description: The project's secret key.
anonKey:
type: string
description: The project's anon (publishable) key.
AccountProjectList:
type: object
required: [projects]
properties:
projects:
type: array
items:
$ref: "#/components/schemas/AccountProjectSummary"
CreatedProjectResponse:
type: object
required: [projectId, url, token, anonKey, plan]
properties:
projectId:
type: string
example: unbase_7f3k9q2m1x8a
name:
type: string
nullable: true
url:
type: string
format: uri
example: https://api.unbase.dev/v1/projects/unbase_7f3k9q2m1x8a
token:
type: string
description: The project's secret key.
anonKey:
type: string
description: The project's anon (publishable) key.
plan:
type: string
enum: [free, founder, pro]
TablesResponse:
type: object
required: [tables]
properties:
tables:
type: array
items:
type: object
required: [name, rowCount, columns]
properties:
name:
type: string
rowCount:
type: integer
columns:
type: array
items:
type: object
required: [name, type, notnull, pk]
properties:
name:
type: string
type:
type: string
notnull:
type: boolean
pk:
type: boolean
StorageObject:
type: object
required: [key, size, contentType, createdAt, updatedAt]
properties:
key:
type: string
example: avatars/user.png
size:
type: integer
description: Size in bytes.
contentType:
type: string
example: image/png
createdAt:
type: integer
description: Unix ms timestamp of first upload.
updatedAt:
type: integer
description: Unix ms timestamp of the most recent upload (same as createdAt if never overwritten).
StorageObjectList:
type: object
required: [objects, totalBytes, count]
properties:
objects:
type: array
items:
$ref: "#/components/schemas/StorageObject"
totalBytes:
type: integer
description: Sum of size across all of the project's objects.
count:
type: integer
StripeWebhookResponse:
type: object
required: [received, applied]
properties:
received:
type: boolean
example: true
applied:
type: boolean
description: >
True when a plan change was applied; false for
acknowledged-but-no-op events.
plan:
type: string
description: The plan applied (present only when applied is true).
enum: [free, founder, pro]
projects:
type: integer
description: >
Number of projects updated (present only when applied is true).
Statement:
type: object
required: [sql]
properties:
sql:
type: string
example: "SELECT * FROM todos WHERE id = ?"
params:
type: array
description: Positional values bound to `?` placeholders.
items: {}
additionalProperties: false
QueryResult:
type: object
required: [rows, rowsRead, rowsWritten]
properties:
rows:
type: array
items:
type: object
additionalProperties: true
rowsRead:
type: integer
rowsWritten:
type: integer
BatchResult:
type: object
required: [results, rowsRead, rowsWritten]
properties:
results:
type: array
items:
$ref: "#/components/schemas/QueryResult"
rowsRead:
type: integer
description: Sum of rowsRead across all statements
rowsWritten:
type: integer
description: Sum of rowsWritten across all statements
UsageResponse:
type: object
required:
[plan, status, sizeBytes, usageMonth, rowsRead, rowsWritten]
properties:
plan:
type: string
enum: [anonymous, free, founder, pro]
status:
type: string
enum: [active, limited, suspended]
sizeBytes:
type: integer
usageMonth:
type: string
description: Calendar month these counters apply to, YYYY-MM.
example: "2026-07"
rowsRead:
type: integer
description: Row reads so far this calendar month.
rowsWritten:
type: integer
description: Row writes so far this calendar month.
AuthUser:
type: object
required: [id, email, createdAt]
properties:
id:
type: string
example: user_3m1x8a7f3k9q
email:
type: string
format: email
emailConfirmedAt:
type: string
format: date-time
nullable: true
description: >
When the email was confirmed (e.g. via magic link). Null until
confirmed.
createdAt:
type: string
format: date-time
lastSignInAt:
type: string
format: date-time
nullable: true
Session:
type: object
required:
[accessToken, tokenType, expiresIn, expiresAt, refreshToken, user]
description: >
Returned by the Auth signup/signin/verify/token endpoints. The
accessToken is an HS256 JWT signed with the project's own JWT secret
(retrievable via .../auth/settings), so the project owner can verify
end-user tokens in their own backend. Claims:
{ sub: userId, iss: projectId, role: "authenticated", email, iat, exp }.
properties:
accessToken:
type: string
description: HS256 JWT signed with the project's JWT secret.
tokenType:
type: string
enum: [bearer]
expiresIn:
type: integer
description: Access-token lifetime in seconds.
example: 3600
expiresAt:
type: integer
format: int64
description: Unix epoch (seconds) at which the access token expires.
refreshToken:
type: string
description: Single-use refresh token; rotated on each /auth/token call.
user:
$ref: "#/components/schemas/AuthUser"
AuthSettings:
type: object
required: [jwtSecret, userCount, anonKey, authUrl]
properties:
jwtSecret:
type: string
description: >
The project's JWT secret used to sign end-user access tokens.
Use it to verify HS256 access-token signatures in your backend.
userCount:
type: integer
description: Number of end users registered with the project.
anonKey:
type: string
description: The project's anon (publishable) key.
authUrl:
type: string
format: uri
description: Base URL of the project's Auth service.
example: https://api.unbase.dev/v1/projects/unbase_7f3k9q2m1x8a/auth
LeaderboardRow:
type: object
properties:
rank: { type: integer, description: Ties share a rank }
name: { type: string }
values:
type: object
description: Public fields only
additionalProperties: { type: number }
at: { type: integer, description: Unix ms }
me: { type: boolean, description: Present on the requesting device's own row }
LeaderboardConfig:
type: object
required: [fields, boards]
properties:
fields:
type: array
maxItems: 8
items:
type: object
required: [name, min, max]
properties:
name: { type: string, pattern: "^[a-z][a-z0-9_]{0,31}$" }
type: { type: string, enum: [int, float], default: int }
min: { type: number }
max: { type: number }
public: { type: boolean, default: true }
duration: { type: boolean, description: "Seconds of play; must not exceed the real run length + 5s" }
boards:
type: array
maxItems: 10
items:
type: object
required: [slug, sort]
properties:
slug: { type: string, pattern: "^[a-z0-9][a-z0-9_-]{0,31}$" }
sort:
type: array
minItems: 1
maxItems: 3
items:
type: array
description: '[field, "asc" | "desc"]'
items: { type: string }
period: { type: string, enum: [all, daily, weekly, monthly, season], default: all }
season: { type: string }
keep: { type: string, enum: [best, all], default: best }
rules:
type: array
maxItems: 20
items:
type: object
description: >
One of min_per, max_per, max_linear, max_rate, cap_delta (see
/docs/leaderboards.md). `flag: true` accepts and queues for
review instead of rejecting.
allowedOrigins:
type: array
maxItems: 20
items: { type: string }
description: Empty = any origin. Exact, https://*.example.com wildcards, or "itch".
progression:
type: object
description: >
Optional. { stats: [{ name, agg: count|sum|max|min|last, field?,
public? }] (≤16), unlocks: [{ id, name, description?, hidden?,
requires?, when }] (≤64), board?: { enabled } }. A condition is
{ run|stat: name, gte|gt|lte|lt|eq: n } or { all|any: [...] }.
See /docs/leaderboards.md#progression.
ErrorResponse:
type: object
required: [error]
properties:
error:
type: object
required: [code, message]
properties:
code:
type: string
enum:
- bad_request
- unauthorized
- forbidden
- not_found
- conflict
- payload_too_large
- rate_limited
- service_unavailable
- internal_error
message:
type: string