Authentication
All endpoints except /signup, /login, /regions, and /errors require an API key.
Include your API key in the Authorization header:
Authorization: Bearer YOUR_API_KEY
Authentication Errors
{
"success": false,
"error": "Authorization header required"
}
Error Codes
All error responses use a standard code field. Use these codes for programmatic error handling instead of parsing error messages.
| Code | HTTP Status | Description |
| ok |
200 |
Request succeeded |
| created |
201 |
Resource was created |
| bad_request |
400 |
Invalid input or missing required fields |
| auth_required |
401 |
Authorization header missing |
| unauthorized |
401 |
Invalid API key |
| forbidden |
403 |
Tier limit reached, wrong project, or operation not allowed for isolation level |
| role_required |
403 |
API key role is insufficient for this endpoint (e.g. read or write key calling an admin-only endpoint like /admin/query) |
| scope_denied |
403 |
API key scope does not include the requested tenant or workspace (e.g. tenant-scoped key attempting to access a different tenant) |
| permission_denied |
403 |
Database role denied the operation (e.g. read role attempting INSERT) |
| not_found |
404 |
Resource does not exist |
| conflict |
409 |
Resource already exists, or operation conflicts with current state |
| rate_limited |
429 |
Too many requests |
| internal_error |
500 |
Server error |
Example Error Responses
{
"success": false,
"http_status": 409,
"code": "conflict",
"error": "Tenant 'acme' is in trash. Use POST /tenants/acme/restore or DELETE /tenants/acme?hard=true"
}
{
"success": false,
"http_status": 403,
"code": "forbidden",
"error": "PITR is only available for L2 (dedicated) tenants"
}
Rate Limits
Rate limits protect shared infrastructure and ensure fair usage. Limits are applied in layers — from IP-level DDoS protection to tier-based API quotas.
IP Rate Limiting
All requests are rate-limited by IP address before authentication. This protects against abuse and DDoS. Exceeding the limit repeatedly results in a temporary ban.
| Parameter | Value | Description |
| Requests per second |
100 |
Maximum requests per second from a single IP address |
| Ban threshold |
5 violations |
After 5 rate limit violations, the IP is temporarily banned |
| Ban duration |
5 minutes |
Banned IPs receive 429 for all requests during this period |
Tier-Based Limits
After authentication, API requests are counted against your project's tier. Backup requests have a separate hourly limit. Dedicated tenants bypass the backup rate limit since backups run on their own server.
| Limit | Window | Scope | Description |
| API requests |
Per minute |
Project |
Limit varies by tier. Upgrade at tenantsdb.com/billing. |
| Backup requests |
Per hour |
Project |
Limits manual backup frequency. Dedicated tenants are exempt. |
Response
Rate-limited requests return HTTP 429 Too Many Requests with a Retry-After header indicating when to retry (1 second for rate limits, 300 seconds for bans).
Retry-After: 1
Content-Type: application/json
{
"success": false,
"error": "rate limit exceeded: max 60 requests per minute"
}
IP bans apply across both the API and database proxy connections. A ban from excessive API requests also blocks proxy connections from the same IP, and vice versa.
List available cloud regions for dedicated tenant provisioning. This endpoint is public and does not require authentication.
Shared databases are hosted in EMEA. For region selection, create or migrate tenants to dedicated isolation.
Request
curl -s https://api.tenantsdb.com/regions
Response
{
"regions": [
{
"zone": "eu-central",
"label": "Europe",
"is_default": true
},
{
"zone": "ap-southeast",
"label": "Asia Pacific"
},
{
"zone": "us-east",
"label": "US East"
},
{
"zone": "us-west",
"label": "US West"
}
]
}
Use the zone value when creating dedicated tenants or migrating to a specific region. If no region is specified, the default region is used.
List all error codes and their HTTP status mappings. Public endpoint — no authentication required. Useful for building client libraries.
Request
curl -s https://api.tenantsdb.com/errors
Response
{
"codes": {
"ok": { "status": 200, "description": "Request succeeded" },
"created": { "status": 201, "description": "Resource was created" },
"bad_request": { "status": 400, "description": "Invalid input or missing fields" },
"auth_required": { "status": 401, "description": "Authorization header missing" },
"unauthorized": { "status": 401, "description": "Invalid API key" },
"forbidden": { "status": 403, "description": "Tier limit reached or wrong project" },
"role_required": { "status": 403, "description": "API key role insufficient for endpoint" },
"scope_denied": { "status": 403, "description": "API key scope excludes this tenant or workspace" },
"permission_denied": { "status": 403, "description": "Database role denied the operation" },
"not_found": { "status": 404, "description": "Resource does not exist" },
"conflict": { "status": 409, "description": "Already exists or operation in progress" },
"rate_limited": { "status": 429, "description": "Too many requests" },
"internal_error": { "status": 500, "description": "Server error" }
}
}
Create a new account. Returns an API key for your first project.
Request Body
| Field | Type | | Description |
| email |
string |
required |
Email address |
| password |
string |
required |
Password (min 8 characters) |
| project_name |
string |
optional |
Project name (default: "My Project") |
Response
{
"success": true,
"http_status": 201,
"code": "created",
"api_key": "tdb_sk_a91de156...",
"project_id": "tdb_2abf90d3"
}
Login to existing account. Returns all projects with API keys and a session token for dashboard use.
Request Body
| Field | Type | | Description |
| email |
string |
required |
Email address |
| password |
string |
required |
Password |
Response
{
"success": true,
"http_status": 200,
"code": "ok",
"session_token": "tdb_sess_a91de156...",
"projects": [
{
"project_id": "tdb_2abf90d3",
"name": "Healthcare SaaS",
"api_key": "tdb_sk_a91de156..."
}
]
}
The session_token is used internally by the TenantsDB dashboard. Use the api_key from each project for API access.
Invalidate the current session token. Pass the session token in the Authorization header.
Response
{
"success": true,
"message": "Logged out"
}
Projects
Projects are the top-level organizational boundary. Each project has its own workspaces, blueprints, tenants, and API keys.
List all projects for the authenticated customer.
{
"success": true,
"http_status": 200,
"code": "ok",
"count": 2,
"projects": [
{
"project_id": "tdb_2abf90d3",
"name": "Healthcare SaaS",
"api_key": "tdb_sk_a91de156...",
"api_key_count": 2,
"workspaces": 3,
"tenants": 45,
"database_types": ["PostgreSQL", "MongoDB"],
"created_at": "2026-01-17T20:12:06Z"
},
{
"project_id": "tdb_43cd4942",
"name": "E-commerce Platform",
"api_key": "tdb_sk_42d5be1a...",
"api_key_count": 1,
"workspaces": 1,
"tenants": 0,
"database_types": ["PostgreSQL"],
"created_at": "2026-01-17T20:12:24Z"
}
]
}
Create a new project. Returns a new API key.
Request Body
| Field | Type | | Description |
| name |
string |
required |
Project name |
Response
{
"success": true,
"http_status": 201,
"code": "created",
"project_id": "tdb_43cd4942",
"name": "E-commerce Platform",
"api_key": "tdb_sk_42d5be1a...",
"proxy_password": "tdb_a9b55759ef905535",
"message": "Project created. Save your API key - it won't be shown again!"
}
Switch to a different project. Returns the API key for the target project. Accepts project ID or project name.
Response
{
"success": true,
"http_status": 200,
"code": "ok",
"project_id": "tdb_43cd4942",
"name": "E-commerce Platform",
"api_key": "tdb_sk_42d5be1a...",
"proxy_password": "tdb_a9b55759ef905535"
}
Delete a project and all its resources (workspaces, blueprints, tenants, API keys).
Response
{
"success": true,
"http_status": 200,
"code": "ok",
"message": "Project 'tdb_43cd4942' deleted successfully"
}
API Keys
Manage API keys for the current project.
Every API key has explicit scope and role. Scope controls what the key can touch (the whole project, a workspace, or specific tenants). Role controls what operations are permitted (admin, write, or read). The database role enforces operations natively at the SQL/Mongo/Redis layer for defense in depth.
| Field | Values | Description |
| scope_type |
project | workspace | tenant |
Where the key applies. project covers everything in the project; workspace and tenant narrow access using scope_values. |
| scope_values |
[] | ["wsname", ...] | ["t1", "t2", ...] |
Workspace names or tenant IDs. OR semantics: a key with ["t1","t2"] can access either tenant. Must be empty when scope_type=project. |
| role |
admin | write | read |
admin grants full access including cross-tenant fan-out via /admin/query. write permits read + write DML. read is read-only. |
The mcp role exists as well, but it is reserved for OAuth-managed keys assigned at consent time. Manual creation with role=mcp is rejected with bad_request.
List all API keys for the current project.
Response
{
"success": true,
"http_status": 200,
"code": "ok",
"count": 3,
"api_keys": [
{
"id": 118,
"key": "tdb_sk_1a28fba2...",
"project_id": "tdb_701dc142",
"project_name": "My Saas",
"key_name": "Production",
"tier": "scale",
"is_active": true,
"scope_type": "project",
"scope_values": [],
"role": "admin",
"created_at": "2026-04-27T21:20:55Z",
"last_used_at": "2026-04-28T09:14:02Z",
"oauth_managed": false
},
{
"id": 122,
"key_name": "Reporting",
"scope_type": "workspace",
"scope_values": ["orders"],
"role": "read",
"oauth_managed": false
},
{
"id": 125,
"key_name": "OAuth: Claude",
"scope_type": "project",
"scope_values": [],
"role": "mcp",
"oauth_managed": true
}
]
}
The full key value is returned in this response. Treat it as a credential. last_used_at may be omitted for keys that have never been used.
Returns the email and project for the API key authenticating the request. Used by
tdb whoami and dashboard "current session" displays. Does not return key, role, or scope — for those, see
GET /apikeys.
Response
{
"success": true,
"http_status": 200,
"code": "ok",
"email": "[email protected]",
"project_id": "tdb_701dc142",
"project_name": "My Saas"
}
Generate a new API key. All four fields are required — there are no defaults.
Request Body
| Field | Type | | Description |
| name |
string |
required |
Human-readable label (e.g. "Production", "CI/CD", "Reporting"). |
| role |
string |
required |
admin, write, or read. |
| scope_type |
string |
required |
project, workspace, or tenant. |
| scope_values |
array<string> |
conditional |
Workspace names or tenant IDs. Must be empty when scope_type=project; must contain at least one entry when workspace or tenant. |
Examples
{
"name": "Production",
"role": "admin",
"scope_type": "project",
"scope_values": []
}
{
"name": "Reporting",
"role": "read",
"scope_type": "workspace",
"scope_values": ["orders"]
}
{
"name": "Acme + Globex Sync",
"role": "write",
"scope_type": "tenant",
"scope_values": ["acme", "globex"]
}
Response
{
"success": true,
"http_status": 201,
"code": "created",
"api_key": "tdb_sk_1a28fba2a972b58c0d9051c658d18d5505b8f04cd8b8526ca61fa643f34e183b",
"proxy_password": "tdb_646145765b597e4f",
"project_id": "tdb_701dc142",
"name": "Production",
"scope_type": "project",
"scope_values": [],
"role": "admin",
"note": "Use api_key for HTTP API, proxy_password for database connections"
}
The full api_key and proxy_password are returned exactly once. Save both before closing the response. Subsequent GET /apikeys calls expose the api_key value but never the proxy_password.
Validation errors
{
"success": false,
"code": "bad_request",
"error": "Failed to generate API key: invalid scope: scope_values must be empty for scope_type=project"
}
{
"success": false,
"code": "bad_request",
"error": "role \"mcp\" is reserved for OAuth integrations and cannot be created manually"
}
Revoke an API key. Returns 409 Conflict if the key is OAuth-managed — revoke the OAuth connection instead, which cascades the key deletion.
Response — success
{
"success": true,
"http_status": 200,
"code": "ok",
"message": "API key successfully revoked"
}
Response — OAuth-managed key
{
"success": false,
"code": "oauth_managed",
"error": "This key is managed by an OAuth connection. Revoke the connection in Connected Apps to delete it."
}
Workspaces
Workspaces are development environments where you design your database schema. Each workspace operates in one of two modes:
tenant
Tenant Mode
Schema changes are captured as versioned blueprints. Deploy blueprints to create isolated tenant databases. This is the default mode.
control
Control Mode
A managed database with full DDL access. No blueprints, versioning, or tenants. Ideal for your application's control plane (users, billing, config).
List all workspaces. Each workspace includes a mode field (tenant or control).
Response
{
"workspaces": [
{
"id": "ws_abc123",
"name": "main",
"mode": "tenant",
"database": "postgresql",
"created_at": "2025-01-15T10:30:00Z",
"version": 3
},
{
"id": "ws_def456",
"name": "control-plane",
"mode": "control",
"database": "postgresql",
"created_at": "2025-01-15T11:00:00Z"
}
],
"count": 2
}
The version field is only present for tenant mode workspaces.
Create a workspace. Automatically creates a linked blueprint (tenant mode only).
Request Body
| Field | Type | | Description |
| name |
string |
required |
Workspace name. Cannot contain __ (reserved separator). |
| database |
string |
required |
PostgreSQL, MySQL, MongoDB, or Redis |
| mode |
string |
required |
tenant or control. Tenant mode uses blueprints and deployments. Control mode creates a standalone managed database. |
Response — Tenant Mode
{
"success": true,
"http_status": 201,
"code": "created",
"id": "myapp",
"mode": "tenant",
"blueprint": "myapp",
"database": "PostgreSQL",
"connection": {
"host": "pg.tenantsdb.com",
"port": 5432,
"database": "myapp_workspace",
"user": "tdb_2abf90d3",
"password": "tdb_d2bf66ed7898c448"
},
"connection_string": "postgresql://tdb_2abf90d3:[email protected]:5432/myapp_workspace?sslmode=require"
}
Response — Control Mode
Control mode workspaces omit the blueprint field. The workspace is a standalone managed database — no blueprints, versioning, or tenant deployment.
{
"success": true,
"http_status": 201,
"code": "created",
"id": "controlplane",
"mode": "control",
"database": "PostgreSQL",
"connection": {
"host": "pg.tenantsdb.com",
"port": 5432,
"database": "controlplane_workspace",
"user": "tdb_2abf90d3",
"password": "tdb_d2bf66ed7898c448"
},
"connection_string": "postgresql://tdb_2abf90d3:[email protected]:5432/controlplane_workspace?sslmode=require"
}
Connection Strings by Database
TLS is enabled for all database types. The connection string format varies:
| Database | TLS Parameter | Example |
| PostgreSQL |
?sslmode=require |
postgresql://user:pass@host:5432/db?sslmode=require |
| MySQL |
TLS required |
mysql://user:pass@host:3306/db |
| MongoDB |
?tls=true |
mongodb://user:pass@host:27017/db?authMechanism=PLAIN&directConnection=true&tls=true |
| Redis |
rediss:// scheme |
rediss://workspace:pass@host:6379/0 |
All connections are encrypted via TLS. PostgreSQL uses
sslmode=require, MySQL TLS is configured per driver (see
Connections), MongoDB uses
tls=true, and Redis uses the
rediss:// URI scheme.
Get workspace details including schema and connection info.
Response
{
"id": "ws_abc123",
"name": "main",
"mode": "tenant",
"database": "postgresql",
"schema": ["CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL)"],
"schema_error": null,
"undeployed_changes": 2,
"connection": {
"host": "proxy.tenantsdb.com",
"port": 5432,
"database": "ws_abc123",
"username": "ws_abc123",
"password": "••••••••"
},
"connection_string": "postgresql://ws_abc123:••••••••@proxy.tenantsdb.com:5432/ws_abc123?sslmode=require",
"settings": {
"query_timeout_ms": 30000,
"max_rows_per_query": 1000,
"max_connections": 10
},
"version": 3
}
undeployed_changes and version are only present for tenant mode workspaces.
Delete a workspace. For tenant mode, returns 409 Conflict if tenants are deployed to its blueprint. Control mode workspaces can always be deleted.
Response — success
{
"message": "workspace deleted",
"details": {
"workspace": "main",
"mode": "tenant",
"versions_deleted": 3,
"deletion_type": "full",
"physical_db": true
}
}
Response — 409 Conflict (tenants deployed)
{
"message": "cannot delete workspace with deployed tenants",
"reason": "tenants_deployed",
"deployed_to": ["acme", "globex"],
"action_required": "Delete all tenants first or use a different workspace"
}
Run a query in the workspace dev database. DDL statements (CREATE TABLE, ALTER TABLE, etc.) are allowed and automatically tracked as blueprint versions for deployment to tenants.
Request Body
| Field | Type | | Description |
| query |
string |
required |
The SQL, MongoDB, or Redis query to execute. |
Response
{
"columns": ["id", "name", "email"],
"rows": [
[1, "Alice", "[email protected]"]
],
"row_count": 1,
"execution_time": "8ms"
}
Import schema from template, JSON, external database, or URL.
Request Body
| Field | Type | | Description |
| source |
string |
required |
json, database, template, or url |
| template |
string |
optional |
Template name: ecommerce, saas, blog, fintech |
| tables |
array |
optional |
Table definitions (for JSON source, SQL databases) |
| collections |
array |
optional |
Collection definitions (for JSON source, MongoDB) |
| connection |
object |
optional |
Database connection (for database source) |
| url |
string |
optional |
URL to JSON schema (for url source) |
From Template
{
"source": "template",
"template": "fintech"
}
From Database
{
"source": "database",
"connection": {
"type": "postgresql",
"host": "db.example.com",
"port": 5432,
"database": "mydb",
"user": "admin",
"password": "secret"
}
}
Response
{
"success": true,
"http_status": 200,
"code": "ok",
"message": "Schema imported to workspace 'myapp'",
"source": "template",
"created": ["users", "accounts", "transactions"],
"skipped": [],
"errors": [],
"details": {
"workspace": "myapp",
"database_type": "PostgreSQL",
"statements_executed": 3,
"statements_skipped": 0,
"statements_failed": 0,
"statements_total": 3
}
}
Get the current schema of a workspace (tables, columns, indexes).
Response
{
"tables": [
{
"name": "users",
"columns": [
{ "name": "id", "type": "SERIAL", "nullable": false },
{ "name": "name", "type": "TEXT", "nullable": false }
],
"indexes": ["users_pkey"]
}
]
}
Get pending DDL changes ready for deployment.
Tenant mode only. Returns 400 Bad Request for control mode workspaces (no blueprint versioning).
Remove a DDL change from the deploy queue. Returns 404 Not Found if the DDL does not exist or has already been deployed.
Response
{
"success": true,
"http_status": 200,
"code": "ok",
"message": "DDL 42 removed from deploy queue"
}
Revert a DDL change in the workspace by executing the reverse statement. Only works for undeployed DDLs. Supports CREATE TABLE → DROP TABLE, CREATE INDEX → DROP INDEX, ALTER TABLE ADD COLUMN → DROP COLUMN, CREATE COLLECTION → DROP COLLECTION.
Response
{
"success": true,
"http_status": 200,
"code": "ok",
"message": "Reverted: DROP TABLE IF EXISTS users;"
}
Cannot revert DDLs already deployed to tenants. Create a new migration instead.
Get current settings for a workspace. Available fields depend on the database type.
Response
{
"success": true,
"http_status": 200,
"code": "ok",
"database_type": "PostgreSQL",
"settings": {
"query_timeout_ms": 30000,
"max_rows_per_query": 10000,
"max_connections": 100
},
"available_fields": ["query_timeout_ms", "max_rows_per_query", "max_connections"]
}
PostgreSQL/MySQL/MongoDB: query_timeout_ms, max_rows_per_query, max_connections. Redis: default_ttl, max_keys, patterns.
Update workspace settings. Supports partial updates — only provided fields are changed.
Request Body (PostgreSQL / MySQL / MongoDB)
{
"query_timeout_ms": 30000,
"max_rows_per_query": 10000,
"max_connections": 100
}
Request Body (Redis)
{
"default_ttl": 3600,
"max_keys": 50000,
"patterns": {
"cache:*": { "ttl": 300 },
"session:*": { "ttl": 86400 }
}
}
Response
{
"success": true,
"http_status": 200,
"code": "ok",
"settings": { ... },
"message": "Settings saved. Deploy to apply to tenants.",
"pending_deploy": true,
"database_type": "PostgreSQL"
}
For tenant mode, the message is "Settings saved. Deploy to apply to tenants." with pending_deploy: true. For control mode, the message is "Settings saved." with no pending deploy.
Migrating from an existing database? The data import endpoints (
import-data,
import-data/analyze,
import-full) are covered in the
Quick Start Guide →
Blueprints
Blueprints are versioned schema snapshots created from workspaces. They define what gets deployed to tenant databases.
List all blueprints.
Response
{
"blueprints": [
{
"name": "main",
"database": "postgresql",
"workspace_id": "ws_abc123",
"current_version": 3,
"tenant_count": 12
}
],
"count": 1
}
Get blueprint schema and deployment status.
Response
{
"name": "main",
"database": "postgresql",
"workspace_id": "ws_abc123",
"current_version": 3,
"schema": [
"CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL)"
],
"tenant_count": 12
}
List all versions of a blueprint.
Response
{
"versions": [
{
"version": 3,
"ddl_count": 2,
"deployed_at": "2025-02-01T10:00:00Z"
},
{
"version": 2,
"ddl_count": 1,
"deployed_at": "2025-01-20T14:30:00Z"
}
],
"count": 2
}
Tenants
Tenants are your customers' isolated database instances. Each tenant gets its own physical database, created from a blueprint.
L1
Shared
Multi-tenant pool. Cost-effective, instant provisioning. Each tenant gets a separate database on a shared server.
L2
Dedicated
Own VM. Full physical isolation, dedicated resources. Zero-downtime migration via native replication.
List all tenants for the current project.
Response
{
"tenants": [
{
"tenant_id": "acme",
"status": "active",
"databases": [
{
"blueprint": "main",
"database_type": "postgresql",
"isolation_level": "L1"
}
],
"created_at": "2025-01-20T14:00:00Z"
}
],
"count": 1
}
Create a new tenant with database(s) from blueprint(s). Returns HTTP 201 Created.
Only available for tenant mode workspaces. Returns 400 Bad Request if the blueprint belongs to a control mode workspace.
L2 dedicated VMs require a paid plan. Free tier accounts receive
403 forbidden.
Upgrade →
Request Body
| Field | Type | | Description |
| tenant_id |
string |
required |
Tenant name (lowercase letters, numbers, underscores only). Cannot contain __. |
| databases |
array |
required |
Array of {blueprint, isolation_level, region}. Region is optional and applies to dedicated tenants only. |
Response — Shared (instant)
Shared tenants are created instantly. The status is ready and the connection string is available immediately.
{
"success": true,
"http_status": 201,
"code": "created",
"tenant_id": "acme",
"status": "ready",
"databases": [
{
"blueprint": "fintech",
"database_type": "PostgreSQL",
"isolation_level": 1,
"connection": {
"database": "fintech__acme",
"connection_string": "postgresql://tdb_2abf90d3:[email protected]:5432/fintech__acme?sslmode=require"
}
}
]
}
Tenant Connection Strings by Database
The connection string format in the response depends on the database type:
"mongodb://tdb_2abf90d3:[email protected]:27017/fintech__acme?authMechanism=PLAIN&directConnection=true&tls=true"
Redis uses a different auth scheme: tenant_id:api_key instead of project_id:proxy_password. The database field is always 0.
Response — Dedicated (async)
Dedicated tenants require a server to be provisioned. The response returns immediately with status: provisioning. Poll GET /tenants/{id} until the status becomes ready.
{
"success": true,
"http_status": 201,
"code": "created",
"tenant_id": "acme",
"status": "provisioning",
"databases": [
{
"blueprint": "fintech",
"database_type": "PostgreSQL",
"isolation_level": 2,
"region": "eu-central"
}
]
}
Do not attempt to connect until status is ready. Provisioning typically takes 1–2 minutes depending on region.
Get tenant details with all database connections.
Response
{
"tenant_id": "acme",
"status": "active",
"databases": [
{
"blueprint": "main",
"database_type": "postgresql",
"isolation_level": "L1",
"connection": {
"host": "proxy.tenantsdb.com",
"port": 5432,
"database": "acme",
"username": "acme",
"password": "••••••••"
},
"connection_string": "postgresql://acme:••••••••@proxy.tenantsdb.com:5432/acme?sslmode=require",
"version": 3
}
],
"recovery_undo": {
"available": true,
"original_host": "vm-pg-01",
"expires_at": "2025-02-01T14:00:00Z",
"expires_in": "23h 45m",
"command": "tdb tenants recover-undo acme"
},
"created_at": "2025-01-20T14:00:00Z"
}
recovery_undo is only present when a recent recovery can be reversed.
Run a DML query against a single tenant database. Accepts admin, write, and read roles. The database role enforces the actual permission at the SQL/Mongo/Redis layer, so a read key attempting an INSERT returns 403 permission_denied from the database itself.
For cross-tenant fan-out (
all_tenants: true), use
POST /admin/query instead. That endpoint is restricted to
role=admin.
Request Body
| Field | Type | | Description |
| blueprint |
string |
required |
Blueprint name. Used to resolve the tenant's database type. |
| query |
string |
required |
The SQL/MongoDB/Redis query to execute. DDL statements are blocked — deploy schema through blueprints. |
Request
curl -X POST https://api.tenantsdb.com/tenants/acme/query \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"blueprint": "fintech",
"query": "SELECT id, name, balance FROM accounts WHERE balance > 1000"
}'
Response — success
{
"success": true,
"http_status": 200,
"code": "ok",
"tenant": "acme",
"result": {
"count": 2,
"rows": [
{ "id": 1, "name": "Alice", "balance": 5000 },
{ "id": 2, "name": "Bob", "balance": 2500 }
]
}
}
Response — permission denied (read role attempting write)
{
"success": false,
"http_status": 403,
"code": "permission_denied",
"error": "query failed: pq: permission denied for table users"
}
Response — scope denied
{
"success": false,
"code": "scope_denied",
"error": "credential scoped to tenants [planbtest1], attempted \"acme\""
}
Response — DDL blocked
{
"success": false,
"http_status": 400,
"code": "bad_request",
"error": "DDL not allowed on tenant databases. Deploy schema through blueprints."
}
Soft-delete a tenant. Add ?hard=true for permanent deletion. Returns 409 Conflict if tenant is provisioning or migrating.
Restore a soft-deleted tenant. L1 tenants get databases recreated instantly. L2 tenants are restored from VM snapshots.
Response
{
"success": true,
"http_status": 200,
"code": "ok",
"message": "Tenant 'acme' restored successfully",
"tenant": {
"tenant_id": "acme",
"status": "ready"
}
}
Only tenants with status deleted can be restored. Returns 409 Conflict if tenant has a different status.
Suspend a tenant (block all queries). Only ready tenants can be suspended. Returns 409 Conflict if wrong status.
Resume a suspended tenant. Only suspended tenants can be resumed. Returns 409 Conflict if wrong status.
Migrate a tenant between shared and dedicated infrastructure, or change regions. Uses native database replication for zero-downtime migrations.
L2 dedicated VMs require a paid plan. Free tier accounts receive
403 forbidden.
Upgrade →
Request Body
| Field | Type | | Description |
| isolation_level |
int |
required |
Target level: 1 (shared) or 2 (dedicated) |
| blueprint |
string |
required |
Which database blueprint to migrate |
| region |
string |
optional |
Target region zone (e.g., eu-central, us-east). See GET /regions. |
Response — Level Change (L1 → L2)
{
"success": true,
"http_status": 200,
"code": "ok",
"tenant_id": "acme",
"blueprint": "fintech",
"status": "migrating",
"from": 1,
"to": 2,
"message": "Migration in progress for 'fintech'. Check tenant status for completion."
}
Response — Region Change (L2 → L2)
{
"success": true,
"http_status": 200,
"code": "ok",
"tenant_id": "acme",
"blueprint": "fintech",
"status": "migrating",
"from": 2,
"to": 2,
"change": "region",
"from_region": "eu-central",
"to_region": "us-east",
"message": "Migration in progress for 'fintech'. Check tenant status for completion."
}
Status Lifecycle
| Status | Queries | Description |
| syncing |
✓ Working |
Data is being replicated. Your application continues running normally on the original server. |
| migrating |
Paused |
Brief cutover (~2 seconds). Queries are held until routing switches. |
| ready |
✓ Working |
Migration complete. Tenant is live on the new server. Connection strings are unchanged. |
If replication encounters an issue, TenantsDB automatically falls back to a safe backup-and-restore approach. A safety backup is always taken before migration begins.
Trigger an on-demand backup to S3. Backs up all databases for the tenant.
Response
{
"success": true,
"http_status": 200,
"code": "ok",
"message": "Backup scheduled for tenant: acme",
"tenant_id": "acme",
"type": "manual",
"timestamp": "2026-02-15T14-30-00",
"paths": ["postgresql/manual/2026-02-15T14-30-00/tdb_2abf90d3_tenant_acme.sql.gz"]
}
List all backups for a tenant across all database types.
Response
{
"success": true,
"http_status": 200,
"code": "ok",
"tenant_id": "acme",
"count": 3,
"backups": [
{
"type": "manual",
"date": "2026-02-15T14-30-00",
"database_type": "postgresql",
"storage_path": "postgresql/manual/2026-02-15T14-30-00/tdb_2abf90d3_tenant_acme.sql.gz",
"size_bytes": 524288,
"created_at": "2026-02-14T03:00:00Z"
}
]
}
Restore a tenant from a backup using the exact storage path. Use GET /tenants/{id}/backups to list available paths. For point-in-time recovery, use POST /tenants/{id}/recover (L2 only).
Request Body
| Field | Type | | Description |
| storage_path |
string |
required |
Exact S3 path from backups list |
| storage_path |
string |
optional* |
Exact S3 path from backups list |
Use tdb tenants backups {id} to get the exact storage_path value.
Response
{
"success": true,
"http_status": 200,
"code": "ok",
"message": "Rollback scheduled for tenant: acme",
"tenant_id": "acme",
"status": "restoring",
"paths": ["postgresql/manual/2026-02-15T14-30-00/tdb_2abf90d3_tenant_acme.sql.gz"]
}
Point-in-time recovery. Creates a new VM from the target timestamp, preserving the original VM for 24 hours. L2 (dedicated) tenants only. Returns 403 Forbidden for L1 tenants.
Request Body
| Field | Type | | Description |
| timestamp |
string |
required |
ISO 8601 timestamp (e.g., "2026-02-03T17:10:59Z") |
Response
{
"success": true,
"http_status": 200,
"code": "ok",
"message": "Recovery initiated for tenant 'acme' to 2026-02-03T17:10:59Z. Original VM preserved for 24h.",
"tenant": {
"tenant_id": "acme",
"status": "recovering",
"target_timestamp": "2026-02-03T17:10:59Z"
}
}
Undo a point-in-time recovery by swapping back to the original VM. Available within 24 hours of recovery. L2 only.
Response
{
"success": true,
"http_status": 200,
"code": "ok",
"message": "Recovery undone for tenant 'acme'. Original VM restored.",
"tenant": {
"tenant_id": "acme",
"status": "ready"
}
}
The 24-hour undo window starts when recovery is initiated. After expiry, the original VM is destroyed.
Get deployment history for a tenant. Shows which blueprint versions have been applied.
Query Parameters
| Param | Type | | Description |
| limit |
integer |
optional |
Max results (default: all) |
Response
{
"success": true,
"http_status": 200,
"code": "ok",
"tenant_id": "acme",
"count": 3,
"deployments": [ ... ]
}
Get query execution logs.
Query Parameters
| Param | Type | | Description |
| limit |
integer |
optional |
Max results (default: 50, max: 1000) |
| offset |
integer |
optional |
Pagination offset |
| type |
string |
optional |
Filter: error, slow |
| search |
string |
optional |
Query text search |
| source |
string |
optional |
Filter by source: proxy, api |
| since |
string |
optional |
Time filter: 1h, 24h, 7d, 30d |
Response
{
"success": true,
"http_status": 200,
"code": "ok",
"tenant_id": "acme",
"logs": [
{
"query": "SELECT * FROM users WHERE active = true",
"source": "proxy",
"success": true,
"duration_ms": 4.2,
"created_at": "2026-02-15T14:30:00Z"
}
],
"count": 50,
"has_more": true
}
Get aggregated query statistics: total queries, success/failure counts, average duration, slow query count, and breakdown by source.
Response
{
"success": true,
"http_status": 200,
"code": "ok",
"tenant_id": "acme",
"total_queries": 48230,
"successful": 47891,
"failed": 339,
"by_source": {
"proxy": 45000,
"api": 3230
},
"avg_duration_ms": 12.4,
"slow_queries": 18
}
Get performance metrics (query count, latency, success rate).
Deployments
Deploy blueprint schema changes to tenant databases. Runs DDL migrations across all or selected tenants.
List all deployment jobs.
Response
{
"deployments": [
{
"id": "dep_abc123",
"blueprint_name": "main",
"version": 3,
"status": "completed",
"tenants_total": 12,
"tenants_completed": 12,
"created_at": "2025-02-01T10:00:00Z"
}
],
"count": 1
}
Create a new deployment.
Request Body
| Field | Type | | Description |
| blueprint_name |
string |
required |
Blueprint to deploy |
| version |
string |
optional |
Version to deploy |
| deploy_all |
boolean |
optional |
Deploy to all tenants |
Get deployment status and progress.
Response
{
"id": "dep_abc123",
"blueprint_name": "main",
"version": 3,
"status": "in_progress",
"tenants_total": 12,
"tenants_completed": 8,
"tenants_failed": 0,
"results": [
{ "tenant_id": "acme", "status": "completed", "duration": "120ms" },
{ "tenant_id": "globex", "status": "pending" }
],
"created_at": "2025-02-01T10:00:00Z"
}
Admin Query
Run data queries directly on tenant databases. DDL statements are blocked. Schema changes must go through workspaces and blueprints.
Run a query directly on one or all tenant databases. Restricted to keys with role=admin because all_tenants: true can read or modify every tenant in the project.
For per-tenant queries with non-admin keys, use
POST /tenants/{tenantID}/query instead. That endpoint accepts
admin,
write, and
read roles, with the database role enforced at the SQL/Mongo/Redis layer.
Request Body
| Field | Type | | Description |
| blueprint |
string |
required |
Blueprint name. Used to resolve database type and route to tenant databases. |
| query |
string |
required |
The SQL/MongoDB/Redis query to execute. DDL statements are blocked. |
| tenant_id |
string |
optional* |
Target a specific tenant. Provide this or all_tenants. |
| all_tenants |
boolean |
optional* |
Run query on all tenants. Provide this or tenant_id. |
Either tenant_id or all_tenants: true must be provided. Not available for control mode workspaces. Use workspace query instead.
Response — single tenant
{
"tenant_id": "acme",
"columns": ["id", "name", "email"],
"rows": [
[1, "Alice", "[email protected]"]
],
"row_count": 1,
"execution_time": "12ms"
}
Response — all tenants
{
"results": [
{
"tenant_id": "acme",
"columns": ["id", "name"],
"rows": [[1, "Alice"]],
"row_count": 1
}
],
"total_tenants": 1,
"execution_time": "45ms"
}
Response — non-admin key
{
"success": false,
"http_status": 403,
"code": "role_required",
"error": "this endpoint requires one of the following roles: admin",
"required_roles": ["admin"],
"current_role": "read"
}
Response — DDL blocked
{
"success": false,
"http_status": 400,
"code": "bad_request",
"error": "DDL not allowed on tenant databases. Deploy schema through blueprints."
}