Authorization header:Authorization: Bearer YOUR_API_KEY
/signup, /login, /regions, and /errors require an API key.{
"success": false,
"error": "Authorization header required"
}
success, http_status, and code. Endpoint-specific fields are merged alongside.{
"success": true,
"http_status": 200,
"code": "ok",
"tenant_id": "wayne",
"status": "ready"
}
{
"success": false,
"http_status": 404,
"code": "not_found",
"error": "Tenant not found: wayne"
}
| Field | Type | Description |
|---|---|---|
| success | boolean | Whether the request succeeded |
| http_status | integer | HTTP status code (mirrors the response header) |
| code | string | Machine-readable status code (e.g., ok, created, not_found) |
| error | string | Human-readable error message (only present on errors) |
http_status field always matches the HTTP response status code. Use code for programmatic error handling and error for display messages.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 |
{
"success": false,
"http_status": 409,
"code": "conflict",
"error": "Tenant 'wayne' is in trash. Use POST /tenants/wayne/restore or DELETE /tenants/wayne?hard=true"
}
{
"success": false,
"http_status": 403,
"code": "forbidden",
"error": "PITR is only available for L2 (dedicated) tenants"
}
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 |
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. |
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"
}
curl -s https://api.tenantsdb.com/regions
{
"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"
}
]
}
zone value when creating dedicated tenants or migrating to a specific region. If no region is specified, the default region is used.curl -s https://api.tenantsdb.com/errors
{
"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" }
}
}
| Field | Type | Description | |
|---|---|---|---|
| string | required | Email address | |
| password | string | required | Password (min 8 characters) |
| project_name | string | optional | Project name (default: "My Project") |
{
"success": true,
"http_status": 201,
"code": "created",
"api_key": "tdb_sk_a91de156...",
"project_id": "tdb_2abf90d3"
}
| Field | Type | Description | |
|---|---|---|---|
| string | required | Email address | |
| password | string | required | Password |
{
"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..."
}
]
}
session_token is used internally by the TenantsDB dashboard. Use the api_key from each project for API access.{
"success": true,
"message": "Logged out"
}
{
"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"
}
]
}
api_key is the full project-scoped admin key. Truncated here for readability; the actual response contains the complete value. workspaces and tenants are live counts (deleted tenants excluded). database_types is the distinct set of database types across the project's workspaces.api_key and its proxy_password. Save both: this is the only response that exposes them.| Field | Type | Description | |
|---|---|---|---|
| name | string | required | Project name |
{
"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!"
}
api_key is the full project-scoped admin key (shown truncated). Use it for HTTP API authentication. Use proxy_password for wire-protocol database connections.{id} URL parameter accepts either a project ID (tdb_*) or a project name. The dashboard and CLI use this to switch context.{
"success": true,
"http_status": 200,
"code": "ok",
"project_id": "tdb_43cd4942",
"name": "E-commerce Platform",
"api_key": "tdb_sk_42d5be1a...",
"proxy_password": "tdb_a9b55759ef905535"
}
{id} URL parameter accepts either a project ID or a project name. Only the project's owner may rename it.| Field | Type | Description | |
|---|---|---|---|
| name | string | required | New project name |
{
"success": true,
"http_status": 200,
"code": "ok",
"project_id": "tdb_43cd4942",
"name": "New Project Name",
"message": "Project renamed to 'New Project Name'"
}
403 forbidden if the caller is not the project owner.{
"success": true,
"http_status": 200,
"code": "ok",
"message": "Project 'tdb_43cd4942' deleted successfully"
}
400 bad_request with message "Cannot delete your only project. Rename it or create another project first." when this is the customer's last project. Returns 403 forbidden if the caller is not the project owner.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. |
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.{
"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
}
]
}
key value is returned in this response (truncated above for display). Treat it as a credential. last_used_at is omitted for keys that have never been used. Entries 2 and 3 are shown abbreviated; the actual API returns all fields on every entry.X-Project-ID header. Each entry includes project_id and project_name for disambiguation.Same item shape as GET /apikeys. The count reflects keys across all projects.
tdb whoami and dashboard "current session" displays. Does not return key, role, or scope. For those, see GET /apikeys.{
"success": true,
"http_status": 200,
"code": "ok",
"email": "[email protected]",
"project_id": "tdb_701dc142",
"project_name": "My Saas"
}
| 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. |
{
"name": "Production",
"role": "admin",
"scope_type": "project",
"scope_values": []
}
{
"name": "Reporting",
"role": "read",
"scope_type": "workspace",
"scope_values": ["orders"]
}
{
"name": "wayne + Globex Sync",
"role": "write",
"scope_type": "tenant",
"scope_values": ["wayne", "globex"]
}
{
"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"
}
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.{
"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"
}
409 Conflict if the key is OAuth-managed. To delete an OAuth-managed key, revoke the OAuth connection instead, which cascades the key deletion.{
"success": true,
"http_status": 200,
"code": "ok",
"message": "API key successfully revoked"
}
{
"success": false,
"code": "oauth_managed",
"error": "This key is managed by an OAuth connection. Revoke the connection in Connected Apps to delete it."
}
proxy_password or any workspace-scoped proxy_password created for that blueprint. See Credential Scopes.mode field (tenant or control).{
"success": true,
"http_status": 200,
"code": "ok",
"count": 2,
"workspaces": [
{
"id": "main",
"name": "main",
"mode": "tenant",
"database": "PostgreSQL",
"created_at": "2026-01-15T10:30:00Z",
"version": "1.2"
},
{
"id": "controlplane",
"name": "controlplane",
"mode": "control",
"database": "PostgreSQL",
"created_at": "2026-01-15T11:00:00Z"
}
]
}
version field is only present for tenant mode workspaces and tracks the latest blueprint version as a string (e.g. "1.2").| Field | Type | Description | |
|---|---|---|---|
| name | string | required | Workspace name. Lowercase letters, numbers, underscores only. Cannot contain __ (reserved separator). |
| database | string | required | One of: PostgreSQL, MySQL, MongoDB, Redis |
| mode | string | required | tenant or control. Tenant mode uses blueprints and deployments. Control mode creates a standalone managed database. |
{
"success": true,
"http_status": 201,
"code": "created",
"id": "myapp",
"mode": "tenant",
"blueprint": "myapp",
"database": "PostgreSQL",
"message": "Workspace 'myapp' created. Connect via proxy to build your schema.",
"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"
}
MySQL workspace responses include a tls field with driver guidance.
{
"success": true,
"http_status": 201,
"code": "created",
"id": "myapp",
"mode": "tenant",
"blueprint": "myapp",
"database": "MySQL",
"message": "Workspace 'myapp' created. Connect via proxy to build your schema.",
"connection": {
"host": "mysql.tenantsdb.com",
"port": 3306,
"database": "myapp_workspace",
"user": "tdb_2abf90d3",
"password": "tdb_d2bf66ed7898c448"
},
"connection_string": "mysql://tdb_2abf90d3:[email protected]:3306/myapp_workspace",
"tls": "required, see docs.tenantsdb.com/connections#mysql"
}
Control mode workspaces omit the blueprint field. The workspace is a standalone managed database with no blueprints, versioning, or tenant deployment.
{
"success": true,
"http_status": 201,
"code": "created",
"id": "controlplane",
"mode": "control",
"database": "PostgreSQL",
"message": "Workspace 'controlplane' created. Connect via proxy to build your schema.",
"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"
}
TLS is enabled for all database types. The connection string format varies. For Redis, the username field carries the blueprint name (not the project_id).
| 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://{blueprint}:pass@host:6379/0 |
sslmode=require, MySQL TLS is configured per driver (see MySQL Connections), MongoDB uses tls=true, and Redis uses the rediss:// URI scheme. For Redis workspaces, the username field carries the blueprint name, not the project_id.{id} URL parameter is the workspace name.{
"success": true,
"http_status": 200,
"code": "ok",
"id": "main",
"name": "main",
"mode": "tenant",
"version": "1.2",
"database": "PostgreSQL",
"schema": ["CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL)"],
"undeployed_changes": 2,
"connection": {
"host": "pg.tenantsdb.com",
"port": 5432,
"database": "main_workspace",
"user": "tdb_2abf90d3",
"password": "tdb_d2bf66ed7898c448"
},
"connection_string": "postgresql://tdb_2abf90d3:[email protected]:5432/main_workspace?sslmode=require",
"settings": {
"query_timeout_ms": 30000,
"max_rows_per_query": 1000,
"max_connections": 10
}
}
version and undeployed_changes are only present for tenant mode workspaces. version is the latest blueprint version as a string. schema_error appears in place of schema when schema retrieval fails. For MySQL workspaces a tls field is included with driver guidance.409 Conflict if tenants are deployed to its blueprint. Control mode workspaces can always be deleted.{
"success": true,
"http_status": 200,
"code": "ok",
"message": "Workspace 'main' deleted successfully",
"details": {
"workspace": "main",
"mode": "tenant",
"versions_deleted": ["1.0", "1.1", "1.2"],
"deletion_type": "hard",
"metadata": "archived",
"physical_db": "permanently destroyed"
}
}
{
"success": false,
"http_status": 409,
"code": "conflict",
"message": "Cannot delete workspace 'main'",
"reason": "Blueprint 'main' deployed to 2 production tenants",
"deployed_to": ["wayne", "globex"],
"action_required": "Delete these production tenants first, then try again"
}
| Field | Type | Description | |
|---|---|---|---|
| query | string | required | The SQL, MongoDB, or Redis query to execute. |
Tenant-mode responses include blueprint metadata and an actions map pointing at related endpoints.
{
"success": true,
"http_status": 200,
"code": "ok",
"result": { ... },
"row_count": 1,
"truncated": false,
"max_rows_limit": 10000,
"blueprint": "main",
"version": "1.2",
"undeployed_changes": 2,
"actions": {
"view_schema": "GET /workspaces/main",
"add_more": "POST /workspaces/main/queries",
"check_diff": "GET /workspaces/main/diff",
"deploy": "POST /deployments"
}
}
Control-mode responses omit blueprint metadata and have fewer entries in actions.
{
"success": true,
"http_status": 200,
"code": "ok",
"result": { ... },
"row_count": 1,
"truncated": false,
"max_rows_limit": 10000,
"actions": {
"view_schema": "GET /workspaces/controlplane",
"add_more": "POST /workspaces/controlplane/queries"
}
}
result shape depends on the database type and query. For large result sets, result_url and result_size_bytes may replace the inline result. truncated is true when output was cut to max_rows_limit.| 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) |
{
"source": "template",
"template": "fintech"
}
{
"source": "database",
"connection": {
"type": "postgresql",
"host": "db.example.com",
"port": 5432,
"database": "mydb",
"user": "admin",
"password": "secret"
}
}
{
"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
}
}
{
"tables": [
{
"name": "users",
"columns": [
{ "name": "id", "type": "SERIAL", "nullable": false },
{ "name": "name", "type": "TEXT", "nullable": false }
],
"indexes": ["users_pkey"]
}
]
}
400 Bad Request for control mode workspaces (no blueprint versioning).404 Not Found if the DDL does not exist or has already been deployed.{
"success": true,
"http_status": 200,
"code": "ok",
"message": "DDL 42 removed from deploy queue"
}
{
"success": true,
"http_status": 200,
"code": "ok",
"message": "Reverted: DROP TABLE IF EXISTS users;"
}
{
"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"]
}
query_timeout_ms, max_rows_per_query, max_connections. Redis: default_ttl, max_keys, patterns.{
"query_timeout_ms": 30000,
"max_rows_per_query": 10000,
"max_connections": 100
}
{
"default_ttl": 3600,
"max_keys": 50000,
"patterns": {
"cache:*": { "ttl": 300 },
"session:*": { "ttl": 86400 }
}
}
{
"success": true,
"http_status": 200,
"code": "ok",
"settings": { ... },
"message": "Settings saved. Deploy to apply to tenants.",
"pending_deploy": true,
"database_type": "PostgreSQL"
}
pending_deploy: true. For control mode, the message is "Settings saved." with no pending deploy.import-data, import-data/analyze, import-full) are covered in the Quick Start Guide.{
"success": true,
"http_status": 200,
"code": "ok",
"blueprints": [
{
"id": 12,
"name": "main",
"version": "1.2",
"workspace_id": "main",
"database_type": "PostgreSQL",
"project_id": "tdb_2abf90d3",
"created_at": "2026-01-20T14:00:00Z",
"deployed_to": ["wayne", "globex"],
"ddl_statements": [
"CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL)"
],
"tenant_count": 12
}
]
}
workspace_id is the workspace name (workspaces are identified by name). version is a string (e.g. "1.2"). database_type is one of PostgreSQL, MySQL, MongoDB, Redis. deployed_to lists tenant IDs currently running this blueprint. tenant_count is computed from tenant_databases.Note: this endpoint uses database (not database_type) and workspace (not workspace_id). Field names differ from the list endpoint.
{
"success": true,
"http_status": 200,
"code": "ok",
"name": "main",
"version": "1.2",
"status": "deployed",
"database": "PostgreSQL",
"workspace": "main",
"routing_field": "tenant_id",
"created_at": "2026-01-20T14:00:00Z",
"schema": [
"CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL)"
],
"settings": {
"query_timeout_ms": 30000,
"max_rows_per_query": 10000,
"max_connections": 100
},
"deployment_status": {
"deployed": true,
"total_ddl_statements": 5,
"deployed_to_count": 2,
"ready_to_deploy": true
}
}
status is "draft" if the blueprint has never been deployed, otherwise "deployed". settings are inherited from the source workspace. schema is rendered for display; the exact shape depends on database type.If schema rendering fails (e.g. the workspace database is unreachable), the response degrades to a flat object with schema_error instead of schema. This fallback response does not include BaseResponse fields and created_at is a Unix timestamp integer instead of an RFC 3339 string.
{
"name": "main",
"version": "1.2",
"database": "PostgreSQL",
"workspace": "main",
"created_at": 1769000400,
"schema_error": "workspace database not reachable"
}
{
"success": false,
"http_status": 404,
"code": "not_found",
"error": "Blueprint not found: main"
}
{
"success": true,
"http_status": 200,
"code": "ok",
"blueprint": "main",
"latest": "1.2",
"total_versions": 2,
"versions": [
{
"version": "1.2",
"created_at": "2026-02-01T10:00:00Z",
"deployed_to_count": 2,
"ddl_count": 2
},
{
"version": "1.1",
"created_at": "2026-01-20T14:30:00Z",
"deployed_to_count": 1,
"ddl_count": 1
}
]
}
Returns 404 if the blueprint does not exist or has no versions.
{
"success": false,
"http_status": 404,
"code": "not_found",
"error": "No versions found for blueprint: main"
}
proxy_password embedded in the response connection string.proxy_password, generated when the tenant is created. The project proxy_password cannot connect to a tenant database through the wire proxy. Save the per-tenant value alongside the tenant record in your application, keyed by tenant_id. See Credential Scopes.?include_deleted=true to include soft-deleted tenants.{
"success": true,
"http_status": 200,
"code": "ok",
"count": 1,
"tenants": [
{
"tenant_id": "wayne",
"status": "ready",
"databases": [
{
"blueprint": "fintech",
"database_type": "PostgreSQL",
"isolation_level": 1
}
],
"created_at": "2026-01-20T14:00:00Z"
}
]
}
ready, provisioning, migrating, syncing, suspended, restoring, recovering, deleted, partially_deleted. Connection strings are not included in list responses. Fetch a specific tenant with GET /tenants/{id} to retrieve the connection.HTTP 201 Created. The response includes a per-tenant proxy_password embedded in the connection string.tenant mode workspaces. Returns 400 Bad Request if the blueprint belongs to a control mode workspace.403 forbidden. Upgrade.| Field | Type | Description | |
|---|---|---|---|
| tenant_id | string | required | 1 to 30 chars. Must start with a lowercase letter. Allowed: a-z 0-9 _ -. No consecutive separators (__ or --). Cannot end with a separator. Reserved words rejected. |
| databases | array | required | Array of {blueprint, isolation_level, region}. isolation_level defaults to 1 (shared). region applies to L2 only. |
Shared tenants are created instantly. Status is ready and the connection string is available immediately.
{
"success": true,
"http_status": 201,
"code": "created",
"tenant_id": "wayne",
"status": "ready",
"databases": [
{
"blueprint": "fintech",
"database_type": "PostgreSQL",
"isolation_level": 1,
"connection": {
"database": "fintech__wayne",
"connection_string": "postgresql://tdb_2abf90d3:[email protected]:5432/fintech__wayne?sslmode=require"
}
}
]
}
tdb_4f2c9d1ab7e8350c in this example) is unique to this tenant. A second POST /tenants call returns a different value. Subsequent GET /tenants/{id} responses include the same connection string. Project proxy_password and sk_ keys are rejected at the wire proxy when used to connect to a tenant database.The connection object contains database, connection_string, and (for MySQL only) a tls note. The password slot is always the per-tenant proxy_password.
{
"database": "fintech__wayne",
"connection_string": "postgresql://tdb_2abf90d3:[email protected]:5432/fintech__wayne?sslmode=require"
}
{
"database": "fintech__wayne",
"connection_string": "mysql://tdb_2abf90d3:[email protected]:3306/fintech__wayne",
"tls": "required, see docs.tenantsdb.com/connections#mysql"
}
{
"database": "fintech__wayne",
"connection_string": "mongodb://tdb_2abf90d3:[email protected]:27017/fintech__wayne?authMechanism=PLAIN&directConnection=true&tls=true"
}
{
"database": "0",
"connection_string": "rediss://wayne:[email protected]:6379/0"
}
tenant_id as the username (instead of project_id). The password is still the per-tenant proxy_password. The database field is always 0.Dedicated tenants require a server to be provisioned. The response returns immediately with status: provisioning and no connection object. Poll GET /tenants/{id} until status becomes ready to retrieve the connection.
{
"success": true,
"http_status": 201,
"code": "created",
"tenant_id": "wayne",
"status": "provisioning",
"databases": [
{
"blueprint": "fintech",
"database_type": "PostgreSQL",
"isolation_level": 2,
"region": "eu-central"
}
]
}
status is ready. Provisioning typically takes 1 to 2 minutes depending on region.proxy_password embedded in connection_string that the original create response returned.{
"success": true,
"http_status": 200,
"code": "ok",
"tenant_id": "wayne",
"status": "ready",
"databases": [
{
"blueprint": "fintech",
"database_type": "PostgreSQL",
"isolation_level": 1,
"connection": {
"database": "fintech__wayne",
"connection_string": "postgresql://tdb_2abf90d3:[email protected]:5432/fintech__wayne?sslmode=require"
}
}
],
"recovery_undo": {
"available": true,
"original_host": "vm-pg-01",
"expires_at": "2026-02-01T14:00:00Z",
"expires_in": "23h 45m",
"command": "tdb tenants recover-undo wayne"
},
"created_at": "2026-01-20T14:00:00Z"
}
recovery_undo only appears when a recent point-in-time recovery can still be reversed. region appears on L2-dedicated databases. connection is omitted when the tenant is not ready (provisioning, migrating, deleted).admin, write, and read roles. The role is enforced by the native database at the SQL/Mongo/Redis layer.POST /admin/query. That endpoint requires role=admin.| Field | Type | Description | |
|---|---|---|---|
| blueprint | string | required | Blueprint name. Used to resolve the tenant's database type. |
| query | string | required | The SQL, MongoDB, or Redis query to execute. DDL statements are blocked. Deploy schema through blueprints. |
curl -X POST https://api.tenantsdb.com/tenants/wayne/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"
}'
The result shape depends on the database type. SQL returns rows; MongoDB returns documents; Redis returns command-specific values.
{
"success": true,
"http_status": 200,
"code": "ok",
"tenant": "wayne",
"result": { ... }
}
The key's role is read but the query writes. The native database rejects it at the role-user layer; the error body carries the underlying database message.
{
"success": false,
"http_status": 403,
"code": "permission_denied",
"error": "query failed: pq: permission denied for table accounts"
}
{
"success": false,
"http_status": 403,
"code": "permission_denied",
"error": "query failed: Error 1142 (42000): INSERT command denied to user 'tdb_u_xxxxxxxxxxxxxxxx_r'@'...' for table 'accounts'"
}
The key's scope_type is tenant or workspace and its scope_values do not include this tenant. Project-scoped keys are not rejected by this endpoint (they hit the wire proxy's project-scoped block instead).
{
"success": false,
"code": "scope_denied",
"error": "credential scoped to tenants [globex], attempted \"wayne\""
}
DDL is blocked for every role on tenant databases. Schema changes go through workspaces.
{
"success": false,
"http_status": 400,
"code": "bad_request",
"error": "DDL not allowed on tenant databases. Deploy schema through blueprints. See: https://docs.tenantsdb.com/getting-started.html#schema-evolution"
}
?hard=true for permanent deletion. Returns 409 Conflict if the tenant is provisioning or migrating.{
"success": true,
"http_status": 200,
"code": "ok",
"message": "Tenant 'wayne' restored successfully",
"tenant": {
"tenant_id": "wayne",
"status": "ready"
}
}
deleted can be restored. Returns 409 Conflict for other statuses.ready tenants can be suspended. Returns 409 Conflict for other statuses.suspended tenants can be resumed. Returns 409 Conflict for other statuses.403 forbidden. Upgrade.| 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. |
{
"success": true,
"http_status": 200,
"code": "ok",
"tenant_id": "wayne",
"blueprint": "fintech",
"status": "migrating",
"from": 1,
"to": 2,
"message": "Migration in progress for 'fintech'. Check tenant status for completion."
}
{
"success": true,
"http_status": 200,
"code": "ok",
"tenant_id": "wayne",
"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 | Queries | Description |
|---|---|---|
| syncing | ✓ Working | Data is being replicated. Your application continues running normally on the original server. |
| migrating | Paused | Brief cutover (about 2 seconds). Queries are held until routing switches. |
| ready | ✓ Working | Migration complete. Tenant is live on the new server. The per-tenant proxy_password and connection string are unchanged. |
{
"success": true,
"http_status": 200,
"code": "ok",
"message": "Backup scheduled for tenant: wayne",
"tenant_id": "wayne",
"type": "manual",
"timestamp": "2026-02-15T14-30-00",
"paths": ["postgresql/manual/2026-02-15T14-30-00/tdb_2abf90d3_tenant_wayne.sql.gz"]
}
{
"success": true,
"http_status": 200,
"code": "ok",
"tenant_id": "wayne",
"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_wayne.sql.gz",
"size_bytes": 524288,
"created_at": "2026-02-14T03:00:00Z"
}
]
}
GET /tenants/{id}/backups to list available paths. For point-in-time recovery, use POST /tenants/{id}/recover (L2 only).| Field | Type | Description | |
|---|---|---|---|
| storage_path | string | required | Exact S3 path from backups list |
tdb tenants backups {id} to get the exact storage_path value.{
"success": true,
"http_status": 200,
"code": "ok",
"message": "Rollback scheduled for tenant: wayne",
"tenant_id": "wayne",
"status": "restoring",
"paths": ["postgresql/manual/2026-02-15T14-30-00/tdb_2abf90d3_tenant_wayne.sql.gz"]
}
403 Forbidden for L1 tenants.| Field | Type | Description | |
|---|---|---|---|
| timestamp | string | required | ISO 8601 timestamp (e.g., "2026-02-03T17:10:59Z") |
{
"success": true,
"http_status": 200,
"code": "ok",
"message": "Recovery initiated for tenant 'wayne' to 2026-02-03T17:10:59Z. Original VM preserved for 24h.",
"tenant": {
"tenant_id": "wayne",
"status": "recovering",
"target_timestamp": "2026-02-03T17:10:59Z"
}
}
{
"success": true,
"http_status": 200,
"code": "ok",
"message": "Recovery undone for tenant 'wayne'. Original VM restored.",
"tenant": {
"tenant_id": "wayne",
"status": "ready"
}
}
| Param | Type | Description | |
|---|---|---|---|
| limit | integer | optional | Max results (default: all) |
{
"success": true,
"http_status": 200,
"code": "ok",
"tenant_id": "wayne",
"count": 3,
"deployments": [ ... ]
}
| 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 |
{
"success": true,
"http_status": 200,
"code": "ok",
"tenant_id": "wayne",
"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
}
{
"success": true,
"http_status": 200,
"code": "ok",
"tenant_id": "wayne",
"total_queries": 48230,
"successful": 47891,
"failed": 339,
"by_source": {
"proxy": 45000,
"api": 3230
},
"avg_duration_ms": 12.4,
"slow_queries": 18
}
{
"success": true,
"http_status": 200,
"code": "ok",
"count": 1,
"jobs": [
{
"job_id": "dep_abc123",
"blueprint_id": "main",
"version": "1.2",
"status": "completed",
"total_tenants": 12,
"completed_tenants": 12,
"failed_tenants": 0,
"created_at": "2026-02-01T10:00:00Z"
}
]
}
pending, running, completed, failed. version is a string (e.g. "1.2"), not a number.HTTP 201 Created immediately with a job_id and status_url to poll. Migration runs asynchronously.| Field | Type | Description | |
|---|---|---|---|
| blueprint_id | string | required | Blueprint to deploy (the blueprint's name). |
| version | string | optional | Blueprint version to deploy (e.g. "1.2"). Defaults to the latest version. |
| tenant_ids | array | optional | Specific tenants to deploy to. If omitted or empty, deploys to all tenants using this blueprint. |
curl -X POST https://api.tenantsdb.com/deployments \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"blueprint_id": "main",
"version": "1.2"
}'
{
"success": true,
"http_status": 201,
"code": "created",
"deployment": {
"job_id": "dep_abc123",
"blueprint_id": "main",
"version": "1.2",
"status": "running",
"status_url": "/deployments/dep_abc123",
"total_tenants": 12,
"created_at": "2026-02-01T10:00:00Z"
}
}
deployment object. Use status_url (or GET /deployments/{job_id}) to poll progress.deployment).{
"success": true,
"http_status": 200,
"code": "ok",
"job_id": "dep_abc123",
"blueprint_id": "main",
"status": "running",
"progress": 0.667,
"progress_display": "8/12 tenants",
"total_tenants": 12,
"completed_tenants": 8,
"failed_tenants": 0
}
When tenants fail, the response includes an errors array. There is no per-tenant results array; failures are summarized as strings.
{
"success": true,
"http_status": 200,
"code": "ok",
"job_id": "dep_abc123",
"blueprint_id": "main",
"status": "failed",
"progress": 1.0,
"progress_display": "12/12 tenants",
"total_tenants": 12,
"completed_tenants": 10,
"failed_tenants": 2,
"errors": [
"tenant 'globex': constraint violation on accounts.email_unique",
"tenant 'stark': timeout after 30s"
]
}
pending, running, completed, failed. progress is a float between 0 and 1. progress_display is the human-readable form. errors is omitted when there are no failures.role=admin because all_tenants: true can read or modify every tenant in the project.POST /tenants/{tenantID}/query instead. That endpoint accepts admin, write, and read roles, with the database role enforced at the SQL/Mongo/Redis layer.| Field | Type | Description | |
|---|---|---|---|
| blueprint | string | required | Blueprint name. Used to resolve database type and route to tenant databases. |
| query | string | required | The SQL, MongoDB, or 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 of the blueprint. Provide this or tenant_id. |
tenant_id or all_tenants: true must be provided. Not available for control mode workspaces. Use workspace query instead.When tenant_id is set, the response wraps the query output in a result field. The exact result shape depends on the database type and query.
{
"success": true,
"http_status": 200,
"code": "ok",
"tenant": "wayne",
"result": { ... }
}
When all_tenants: true, the response contains one entry per tenant in results. Each entry has tenant and result (on success) or tenant and error (on failure).
{
"success": true,
"http_status": 200,
"code": "ok",
"count": 3,
"results": [
{
"tenant": "wayne",
"result": { ... }
},
{
"tenant": "globex",
"result": { ... }
},
{
"tenant": "stark",
"error": "connection timeout"
}
]
}
success is true as long as the request itself was valid. Individual tenants may still fail with errors in their entry. Check each results[] entry for error to detect per-tenant failures.Keys with role write, read, or any other non-admin role are rejected by the requireRole middleware before reaching the handler.
{
"success": false,
"code": "role_required",
"error": "this endpoint requires one of the following roles: admin",
"required_roles": ["admin"],
"current_role": "read"
}
DDL is blocked for every role on tenant databases. Schema changes go through workspaces.
{
"success": false,
"http_status": 400,
"code": "bad_request",
"error": "DDL not allowed on tenant databases. Deploy schema through blueprints. See: https://docs.tenantsdb.com/getting-started.html#schema-evolution"
}
role=admin because all_tenants: true can read or modify every tenant in the project.POST /tenants/{tenantID}/query instead. That endpoint accepts admin, write, and read roles, with the database role enforced at the SQL/Mongo/Redis layer.| Field | Type | Description | |
|---|---|---|---|
| blueprint | string | required | Blueprint name. Used to resolve database type and route to tenant databases. |
| query | string | required | The SQL, MongoDB, or 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 of the blueprint. Provide this or tenant_id. |
tenant_id or all_tenants: true must be provided. Not available for control mode workspaces. Use workspace query instead.When tenant_id is set, the response wraps the query output in a result field. The exact result shape depends on the database type and query.
{
"success": true,
"http_status": 200,
"code": "ok",
"tenant": "wayne",
"result": { ... }
}
When all_tenants: true, the response contains one entry per tenant in results. Each entry has tenant and result (on success) or tenant and error (on failure).
{
"success": true,
"http_status": 200,
"code": "ok",
"count": 3,
"results": [
{
"tenant": "wayne",
"result": { ... }
},
{
"tenant": "globex",
"result": { ... }
},
{
"tenant": "stark",
"error": "connection timeout"
}
]
}
success is true as long as the request itself was valid. Individual tenants may still fail with errors in their entry. Check each results[] entry for error to detect per-tenant failures.Keys with role write, read, or any other non-admin role are rejected by the requireRole middleware before reaching the handler.
{
"success": false,
"code": "role_required",
"error": "this endpoint requires one of the following roles: admin",
"required_roles": ["admin"],
"current_role": "read"
}
DDL is blocked for every role on tenant databases. Schema changes go through workspaces.
{
"success": false,
"http_status": 400,
"code": "bad_request",
"error": "DDL not allowed on tenant databases. Deploy schema through blueprints. See: https://docs.tenantsdb.com/getting-started.html#schema-evolution"
}