SDKs & integrations
REST API
Call OrbitNest over HTTPS with your project API keys. Base URL, headers, errors, and the app-facing database, storage, function, realtime and auth endpoints.
Everything the SDKs do is a plain HTTPS request with a JSON body. Use the REST API from languages without an SDK, from scripts, or when you need full control over headers.
Base URL
https://api.orbitnest.io/apiApp-facing endpoints identify the project by its slug. Watch the path segment: some groups use project (singular) and others projects (plural).
| Area | Path prefix |
|---|---|
| Database | /api/project/:slug/database |
| Storage | /api/project/:slug/storage |
| Public files | /api/public/:slug/storage |
| Edge functions | /api/projects/:slug/functions/v1 |
| Realtime (REST) | /api/projects/:slug/realtime |
| End-user auth | /api/projects/:slug/auth |
| Realtime (WebSocket) | wss://api.orbitnest.io/realtime/v1/ws?project=:slug (no /api) |
Studio, the CLI and the MCP server use separate admin endpoints, such as /api/projects/:projectId/database/sql and /api/admin/projects/:projectId/.... Those take a project UUID and a Studio account session, not an API key, and aren't meant for apps.
Authentication
Each project has two API keys, shown on the project's API Keys page. Both are JWTs that carry the project slug and a role. See API keys & JWT.
| Key | Use it in | Can do |
|---|---|---|
anon | Browsers and mobile apps | Reads, inserts and single-statement SELECT/INSERT SQL, subject to RLS. Updates and deletes need a user access token as well. |
service_role | Servers only | Everything, including DDL, multi-statement SQL, bulk inserts and RLS management. Bypasses RLS on the SQL and query endpoints, table data reads, single-row writes and bulk inserts. |
Keep the service-role key on the server
Never ship the service-role key in a browser or mobile app. Anyone who has it can read and change all of your data.
Send the key in one of these headers:
| Header | Notes |
|---|---|
Authorization: Bearer <api key> | Works on every app-facing endpoint. |
apikey: <api key> | Database and storage endpoints. On database endpoints it takes precedence over Authorization; on storage endpoints Authorization wins. Allowed by CORS. |
x-api-key: <api key> | Database, storage and function endpoints. Not allowed by CORS, so don't use it from browsers. |
curl "https://api.orbitnest.io/api/project/my-app/database/tables/list" \
-H "Authorization: Bearer $ORBITNEST_ANON_KEY"User requests
To act as a signed-in end user on the database endpoints, send the anon key in apikey and the user's access token in Authorization. The API verifies the token and runs table data reads, row inserts, updates and deletes, and bulk updates and deletes as the authenticated role, so RLS policies can use auth.uid(). The SQL and query endpoints ignore the user token.
curl -X PUT "https://api.orbitnest.io/api/project/my-app/database/tables/posts/rows/$POST_ID" \
-H "apikey: $ORBITNEST_ANON_KEY" \
-H "Authorization: Bearer $USER_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"title": "Edited title"}'- Only an access token counts. Refresh tokens, MFA challenge tokens, expired tokens and tokens from a signed-out session are not used. The request then continues with the anon key alone: reads run without a user, and updates and deletes return
401. - The user token is only applied together with the anon key. With the service-role key it is ignored.
- A user access token on its own isn't an API key: database endpoints reject it. Storage endpoints accept a user access token directly in
Authorization.
Get access tokens from the auth endpoints.
Errors
Every error response has the same shape:
{
"statusCode": 400,
"timestamp": "2026-10-10T12:00:00.000Z",
"path": "/api/project/my-app/database/tables/posts/rows",
"method": "POST",
"error": "Bad Request",
"message": "Constraint violation: null value in column \"title\" violates not-null constraint"
}Some errors add a machine-readable code. message can be a string or, for request validation errors, an array of strings.
| Status | Typical cause |
|---|---|
400 | Invalid input, a SQL or constraint error (the PostgreSQL message is included), an invalid filter, or a row update or delete that matched no row or was denied by RLS |
401 | Missing or invalid API key, a revoked key, a key for another project, or an operation that needs the service-role key or a user token |
402 | A plan quota is used up. The body has code: "QUOTA_EXCEEDED", quota (database, storage, functions, invocations or projects), plan, limit and usage. See Database size quota. |
403 | SQL rejected by the SQL checks, a protected auth_* table, a plan restriction (code: "PLAN_REQUIRED"), or permission denied by PostgreSQL |
404 | Unknown project, table or function |
429 | Rate limit exceeded on auth or analytics ingestion endpoints |
5xx | A server or infrastructure failure |
Database
| Method | Path | Key | Description |
|---|---|---|---|
GET | /api/project/:slug/database/tables/list | Any | Table names, as an array of strings |
GET | /api/project/:slug/database/tables?table=:table | Any | Column metadata and rls_enabled for each table. Omit table for all tables. |
GET | /api/project/:slug/database/tables/:table/data | Any | Rows, with paging, sorting and filters |
POST | /api/project/:slug/database/tables/:table/rows | Any | Insert one row |
PUT | /api/project/:slug/database/tables/:table/rows/:id | Service role, or anon plus user | Update the row whose id matches |
DELETE | /api/project/:slug/database/tables/:table/rows/:id | Service role, or anon plus user | Delete the row whose id matches |
POST | /api/project/:slug/database/tables/:table/rows/bulk | Service role | Insert many rows |
PUT | /api/project/:slug/database/tables/:table/rows/bulk | Service role, or anon plus user | Update many rows in one transaction |
DELETE | /api/project/:slug/database/tables/:table/rows/bulk | Service role, or anon plus user | Delete many rows in one transaction |
POST | /api/project/:slug/database/sql | Any (anon: SELECT/INSERT only) | Run SQL |
POST | /api/project/:slug/database/query | Any | Structured SELECT |
POST | /api/project/:slug/database/tables/:table/rls/enable | Service role | Enable RLS |
POST | /api/project/:slug/database/tables/:table/rls/disable | Service role | Disable RLS |
GET | /api/project/:slug/database/tables/:table/policies | Service role | List policies |
POST | /api/project/:slug/database/tables/:table/policies | Service role | Create a policy. See Database → RLS. |
DELETE | /api/project/:slug/database/tables/:table/policies/:name | Service role | Delete a policy |
The table data endpoint and the row endpoints can't be used on the built-in auth_* tables. Use the auth endpoints instead.
Read rows
GET /api/project/:slug/database/tables/:table/data accepts these query parameters:
| Parameter | Default | Description |
|---|---|---|
page | 1 | 1-based page number |
limit | 50 | Rows per page, 1 to 100 |
from, to | 0-based, inclusive row range. When both are set they replace page and limit and return exactly rows from through to, up to 100 rows. | |
sortBy | Column to sort by. Must exist on the table. | |
sortOrder | ASC | ASC or DESC, case-insensitive |
filter | column:operator:value. Repeat the parameter to add filters. Filters are combined with AND. |
Filter operators {#filters}
| Operator | SQL | Example |
|---|---|---|
eq | = | filter=status:eq:active |
neq (or ne) | != | filter=status:neq:archived |
gt | > | filter=views:gt:100 |
gte (or ge) | >= | filter=views:gte:100 |
lt | < | filter=price:lt:20 |
lte (or le) | <= | filter=price:lte:20 |
like | LIKE | filter=title:like:Intro% |
ilike | ILIKE | filter=title:ilike:%launch% |
in | IN (…), comma-separated | filter=status:in:draft,review |
is | IS NULL, or = true / = false | filter=deleted_at:is:null |
- The filter is split at its first two colons, so the value can contain
:, as infilter=created_at:gte:2026-10-01T00:00:00Z. - These return
400with a message instead of being ignored: a column that doesn't exist on the table, a filter without two colons, an unknown operator, and anisvalue other thannull,trueorfalse. - Values are sent as bound text parameters and cast by PostgreSQL. URL-encode them, for example
%as%25. - This is not PostgREST syntax:
?status=eq.activeand?order=are ignored.
curl -G "https://api.orbitnest.io/api/project/my-app/database/tables/posts/data" \
-H "Authorization: Bearer $ORBITNEST_ANON_KEY" \
--data-urlencode "filter=published:eq:true" \
--data-urlencode "filter=title:ilike:%launch%" \
--data-urlencode "sortBy=created_at" \
--data-urlencode "sortOrder=DESC" \
--data-urlencode "limit=20"{
"success": true,
"table_name": "posts",
"total_rows": 42,
"returned_rows": 20,
"page": 1,
"limit": 20,
"columns": [
{ "name": "id", "type": "uuid", "is_primary_key": true },
{ "name": "title", "type": "text", "is_primary_key": false }
],
"data": [{ "id": "8f0c…", "title": "Launch week" }],
"count": 42
}Who the query runs as depends on the credentials:
- Anon key alone: the project database role, with RLS and no user context.
- Anon key plus a user access token: the
authenticatedrole with the user's claims, so per-userSELECTpolicies apply.total_rowscounts only the rows the user can see. - Service-role key: RLS is bypassed.
With the anon key, auth_* tables return 403.
Write rows
Insert a row by sending its columns as a JSON object. Objects and arrays are stored as JSON in json and jsonb columns.
curl -X POST "https://api.orbitnest.io/api/project/my-app/database/tables/posts/rows" \
-H "Authorization: Bearer $ORBITNEST_SERVICE_KEY" \
-H "Content-Type: application/json" \
-d '{"title": "Hello", "body": "World", "tags": ["intro"]}'{
"success": true,
"rows_affected": 1,
"message": "Row inserted successfully",
"data": [{ "id": "8f0c…", "title": "Hello", "body": "World", "tags": ["intro"] }]
}- Insert returns
201. Update returns200with the same shape and the updated row indata. - Update and delete match on the table's
idcolumn. If no row matches, or RLS denies the change, the response is400withRow not found, or update denied by row-level security policy. - Delete returns
{ "success": true }. - With the anon key and no user token, inserts run without a user context.
- With the anon key, with or without a user token,
auth_*tables return403. - When the workspace is over its database size quota, inserts and updates return
402. Deletes still work.
Bulk operations
All three bulk endpoints take a JSON array.
| Endpoint | Body | Response |
|---|---|---|
POST …/rows/bulk | Rows to insert. Every object must have the same keys. | { success, rows_affected, message, data } |
PUT …/rows/bulk | [{ "where": { "id": "…" }, "data": { "status": "done" } }]. Each where matches columns by equality. | { success, rows_affected, message } |
DELETE …/rows/bulk | [{ "id": "…" }, { "status": "archived" }]. Each object is one equality condition. | { success, rows_affected, message } |
Bulk updates and deletes run in one transaction: if one item fails, none are applied. Bulk insert with the service-role key bypasses RLS. Bulk updates and deletes run as the project database role, so RLS applies to them even with the service-role key; with the anon key plus a user token they run with the user's claims.
Run SQL
POST /api/project/:slug/database/sql takes sql and optional positional params for $1, $2 and so on.
curl -X POST "https://api.orbitnest.io/api/project/my-app/database/sql" \
-H "Authorization: Bearer $ORBITNEST_ANON_KEY" \
-H "Content-Type: application/json" \
-d '{"sql": "SELECT id, title FROM posts WHERE published = $1 LIMIT 10", "params": [true]}'{
"success": true,
"execution_time_ms": 4,
"rows_affected": 10,
"columns": [{ "name": "id", "type": "uuid" }, { "name": "title", "type": "text" }],
"data": [{ "id": "8f0c…", "title": "Launch week" }],
"message": "Query executed successfully. 10 row(s) returned."
}- With the anon key, the SQL must be a single statement that starts with
SELECT,SHOW,DESCRIBEorINSERT. Anything else, including a leading comment or aWITHclause, returns400, and a second statement after;returns403. Anon SQL runs as the project database role, with RLS and no user context, even when a user token is sent. - With the service-role key, any SQL that passes the SQL checks runs, and RLS is bypassed. You can send several statements, including
BEGIN; … COMMIT;, as one atomic script if you leave outparams. A multi-statement script returnsdata: [],columns: []androws_affected: 0. - Statements that can grow the database return
402while the workspace is over its database size quota. - The body can be up to 200 KB.
Structured query
POST /api/project/:slug/database/query builds a SELECT from JSON. where matches columns by equality and combines them with AND.
curl -X POST "https://api.orbitnest.io/api/project/my-app/database/query" \
-H "Authorization: Bearer $ORBITNEST_ANON_KEY" \
-H "Content-Type: application/json" \
-d '{
"table": "posts",
"select": ["id", "title"],
"where": { "published": true },
"orderBy": { "column": "created_at", "direction": "desc" },
"limit": 10,
"offset": 0
}'The response is { "rows": [...], "total": 10 }, where total is the number of rows returned. Table and column names must be plain identifiers, direction is asc or desc, and limit and offset are integers from 0 to 1,000,000.
Storage
Details, bucket settings and image transforms are on the Storage page.
| Method | Path | Description |
|---|---|---|
GET | /api/project/:slug/storage/:bucket?prefix=&limit= | List files (default limit 100) |
POST | /api/project/:slug/storage/:bucket/upload | Upload a file as multipart/form-data with fields file, path and optional upsert. Up to 50 MB. |
GET | /api/project/:slug/storage/:bucket/download/*path | Download a file |
GET | /api/project/:slug/storage/buckets/:bucket/objects/*path | Download a file (alias) |
GET | /api/project/:slug/storage/:bucket/public-url/*path | Get a file's public URL |
DELETE | /api/project/:slug/storage/:bucket | Delete files. Body: { "paths": ["…"] }. Users can delete only their own files. |
POST | /api/project/:slug/storage/:bucket/create | Create a bucket (service role) |
DELETE | /api/project/:slug/storage/:bucket/delete | Delete a bucket (service role) |
GET | /api/public/:slug/storage/:bucket/*path | Serve a file from a public bucket, no key needed. Accepts width, height, format, quality and fit for images. |
Edge functions
Invoke a deployed function with POST, GET, PUT or DELETE. Send an API key as Authorization: Bearer or x-api-key. The function's response is returned as is. See Edge functions.
curl -X POST "https://api.orbitnest.io/api/projects/my-app/functions/v1/hello" \
-H "Authorization: Bearer $ORBITNEST_ANON_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Ada"}'Realtime
The WebSocket protocol is described on the Realtime page. These REST endpoints take the key as Authorization: Bearer:
| Method | Path | Key | Description |
|---|---|---|---|
GET | /api/projects/:slug/realtime/tables | Any | List realtime-enabled tables |
POST | /api/projects/:slug/realtime/tables | Service role | Enable realtime. Body: { "table": "messages", "schema": "public" } |
DELETE | /api/projects/:slug/realtime/tables/:schema/:table | Service role | Disable realtime |
POST | /api/projects/:slug/realtime/broadcast | Service role | Broadcast. Body: { "channel", "event", "payload" } |
Auth
End-user sign-up and sign-in endpoints don't need an API key. Endpoints marked "User" need the user's access token as Authorization: Bearer; a refresh token or MFA challenge token is rejected there. Rate limits are per IP address: 120 requests per minute for refresh, verify, signout, signout-all, GET user and GET mfa/factors, and 10 per minute for the other endpoints. See Authentication for request bodies and flows.
| Method | Path | Description |
|---|---|---|
POST | /api/projects/:slug/auth/signup-with-email | Start sign-up; emails a one-time code |
POST | /api/projects/:slug/auth/verify-signup | Finish sign-up with the code |
POST | /api/projects/:slug/auth/signin | Sign in with { "email", "password" } |
POST | /api/projects/:slug/auth/signin-with-email | Start passwordless sign-in |
POST | /api/projects/:slug/auth/verify-signin | Finish passwordless sign-in |
POST | /api/projects/:slug/auth/sms/send-otp | Send an SMS code |
POST | /api/projects/:slug/auth/sms/verify-otp | Sign in with the SMS code |
POST | /api/projects/:slug/auth/verify-mfa | Finish a sign-in that requires a TOTP code |
POST | /api/projects/:slug/auth/refresh | Exchange { "refresh_token" } for new tokens |
POST | /api/projects/:slug/auth/recover | Email a password reset code |
POST | /api/projects/:slug/auth/reset-password | Set a new password with the code |
GET | /api/projects/:slug/auth/user | User: get the profile |
PUT | /api/projects/:slug/auth/user | User: update the profile |
DELETE | /api/projects/:slug/auth/user | User: delete the account |
POST | /api/projects/:slug/auth/signout | User: end the current session |
POST | /api/projects/:slug/auth/signout-all | User: end all sessions |
A successful sign-in returns access_token, refresh_token, expires_in and user, or an MFA challenge when the user has TOTP enabled.
Limits and other behavior
- Requests time out after 60 seconds. Edge function invocations get longer, based on the function's timeout.
- SQL request bodies can be up to 200 KB, and file uploads up to 50 MB.
- Table data pages hold at most 100 rows.
- Responses carry
Cache-Control: private, no-storeunless an endpoint sets its own caching. - Browsers can call the API from the project's App URLs (Project Settings),
*.orbitnest.io,localhostand127.0.0.1. Add your web app's origin under App URLs. GET /api/project/:slug/healthreports the project's status without a key.- The hosted API doesn't serve Swagger UI or an OpenAPI document.