# Bancos (referência)

## GET `/v1/projects/{p}/databases`

Lista os bancos do projeto

Bancos do projeto, do mais antigo para o mais novo (`size_mb` do estado armazenado).

Escopo exigido: `projects:read`.

Escopo exigido: `projects:read` · operationId: `listDatabases`

| Parâmetro | Em | Tipo | Obrig. | Descrição |
|---|---|---|---|---|
| `p` | path | string | sim | Id (26 caracteres) ou slug do projeto. Projeto de outro time ou fora dos `project_ids` do token ⇒ `404`. |
| `cursor` | query | string | não | Cursor opaco devolvido em `next_cursor` da página anterior. |
| `limit` | query | integer | não | Itens por página (1 a 200). |

### Resposta 200

Página (mais novo/antigo primeiro conforme o recurso).

| Campo | Tipo | Obrig. | Descrição |
|---|---|---|---|
| `next_cursor` | string\|null | sim | Passe em `cursor` para a próxima página; `null` ao fim. |
| `has_more` | boolean | sim | Há mais páginas. |
| `items` | array<object> | sim |  |
| `items[].id` | string | sim | Id do serviço `kind=database`. |
| `items[].project_id` | string | sim |  |
| `items[].name` | string | sim |  |
| `items[].kind` | enum(database) | sim |  |
| `items[].engine` | enum(postgres, mariadb, sqlite) | sim |  |
| `items[].status` | enum(provisioning, ready, read_only, error, deleting, running, idle, deploying, failed) | sim | Estado da instância. |
| `items[].size_mb` | integer | sim |  |
| `items[].quota_mb` | integer | sim |  |
| `items[].read_only` | boolean | sim | `true` quando o banco passou da cota. |
| `items[].host` | string | sim | Host público do motor do banco, com URL própria por motor no nó (PostgreSQL `db01.pgsql.<domínio>`, MariaDB `db01.mariadb.<domínio>`, sqlite `<banco>.db01.sqlite.<domínio>`); é o host das variáveis `DATABASE_URL`/`PGHOST`/`MYSQL_HOST`/`LIBSQL_URL`. |
| `items[].port` | integer | sim |  |
| `items[].created_at` | string | sim |  |

```json
{
  "items": [
    {
      "id": "01jb7k4a5q8r7t2v4w6x8y0a9z",
      "project_id": "01jb7k3m5q8r7t2v4w6x8y0a1b",
      "name": "db",
      "kind": "database",
      "engine": "postgres",
      "status": "ready",
      "size_mb": 42,
      "quota_mb": 1024,
      "read_only": false,
      "host": "db01.pgsql.nebularws.com",
      "port": 5432,
      "created_at": "2026-10-10T14:03:11Z"
    }
  ],
  "next_cursor": null,
  "has_more": false
}
```

### Erros comuns

Corpo `{"error", "message", "details"?}` (veja [Autenticação e visão geral](/referencia/autenticacao/) e a [tabela de erros](/erros/tabela/)): `400` Entrada inválida (nome de arquivo fora das regras, cursor malformado, formulário sem `file`, nome de galeria vazio ou longo); `401` Token ausente, inválido, expirado ou revogado. A resposta é genérica de propósito; `403` O token não tem o escopo exigido pela operação; `404` Recurso inexistente ou pertencente a outro time (indistinguíveis de propósito); `429` Limite de requisições excedido. Espere `Retry-After` segundos; `500` Falha interna (`internal`). O detalhe fica só no log do servidor; repita depois, e informe o `Idempotency-Key` ao suporte se persistir; `503` Serviço (ex.: scanner ou borda) temporariamente indisponível. Tente de novo após `Retry-After`.

## POST `/v1/projects/{p}/databases`

Cria um banco gerenciado

Cria um serviço `kind=database` e provisiona a instância (`postgres`, `mariadb` ou `sqlite`). As credenciais não vêm na resposta: apps do projeto as recebem como variáveis `${{db.VAR}}`; para vê-las use `POST …/credentials`.

Escopo exigido: `databases:write`.

Escopo exigido: `databases:write` · operationId: `createDatabase`

| Parâmetro | Em | Tipo | Obrig. | Descrição |
|---|---|---|---|---|
| `p` | path | string | sim | Id (26 caracteres) ou slug do projeto. Projeto de outro time ou fora dos `project_ids` do token ⇒ `404`. |
| `Idempotency-Key` | header | string | não | Chave de idempotência (8 a 128 caracteres `[A-Za-z0-9._:-]`; use um UUID). Mesma chave + mesmo pedido em 24 h ⇒ a mesma resposta, com `Idempotent-Replay: true`; mesma chave + pedido diferente ⇒ `422 idempotency_conflict`. Falhas nunca são gravadas, então a repetição executa de verdade. |

### Corpo (application/json)

| Campo | Tipo | Obrig. | Descrição |
|---|---|---|---|
| `name` | string | sim |  |
| `engine` | enum(postgres, mariadb, sqlite) | sim |  |
| `quota_mb` | integer | sim |  |

### Resposta 201

Banco criado.

| Campo | Tipo | Obrig. | Descrição |
|---|---|---|---|
| `id` | string | sim | Id do serviço `kind=database`. |
| `project_id` | string | sim |  |
| `name` | string | sim |  |
| `kind` | enum(database) | sim |  |
| `engine` | enum(postgres, mariadb, sqlite) | sim |  |
| `status` | enum(provisioning, ready, read_only, error, deleting, running, idle, deploying, failed) | sim | Estado da instância. |
| `size_mb` | integer | sim |  |
| `quota_mb` | integer | sim |  |
| `read_only` | boolean | sim | `true` quando o banco passou da cota. |
| `host` | string | sim | Host público do motor do banco, com URL própria por motor no nó (PostgreSQL `db01.pgsql.<domínio>`, MariaDB `db01.mariadb.<domínio>`, sqlite `<banco>.db01.sqlite.<domínio>`); é o host das variáveis `DATABASE_URL`/`PGHOST`/`MYSQL_HOST`/`LIBSQL_URL`. |
| `port` | integer | sim |  |
| `created_at` | string | sim |  |

```json
{
  "id": "01jb7k4a5q8r7t2v4w6x8y0a9z",
  "project_id": "01jb7k3m5q8r7t2v4w6x8y0a1b",
  "name": "db",
  "kind": "database",
  "engine": "postgres",
  "status": "ready",
  "size_mb": 42,
  "quota_mb": 1024,
  "read_only": false,
  "host": "db01.pgsql.nebularws.com",
  "port": 5432,
  "created_at": "2026-10-10T14:03:11Z"
}
```

### Resposta 409

`name_taken`.

| Campo | Tipo | Obrig. | Descrição |
|---|---|---|---|
| `error` | enum(unauthorized, forbidden, not_found, payload_too_large, quota_exceeded, unsupported_type, class_not_allowed, extension_mismatch, malware_detected, svg_unsafe, archive_encrypted, archive_bomb, archive_unsupported, rate_limited, unavailable, idempotency_conflict, invalid_request, unsupported_media_type, bad_json, too_large, authz_unavailable, database_unavailable, restore_not_removable, gallery_not_empty, bad_body, internal, name_taken, slug_taken, project_quota_exceeded, project_not_empty, deploy_in_progress, nothing_to_rollback, not_cancelable, sensitive_lowered, duplicate_variable, domain_taken, volume_exists, volume_in_use, volume_attached, backup_not_ready, unresolved_reference, reference_cycle, invalid_reference, reference_too_deep, value_too_large) | sim | Código estável para tratamento programático. A tabela completa está na descrição geral. |
| `message` | string | sim | Mensagem legível em pt-BR. |
| `details` | object | não | Presente em alguns `422` (ex.: `unresolved_reference` e `reference_cycle` trazem `details.references`). |
| `details.references` | array<string> | não | Referências problemáticas (só nomes, nunca valores). |
| `details.retry_after_seconds` | integer | não | Em `422 rate_limited` (ex.: verificação manual de domínio): segundos até poder tentar de novo. |

```json
{
  "error": "name_taken",
  "message": "Já existe um serviço com esse nome no projeto."
}
```

### Resposta 422

`quota_exceeded` ou `idempotency_conflict`.

| Campo | Tipo | Obrig. | Descrição |
|---|---|---|---|
| `error` | enum(unauthorized, forbidden, not_found, payload_too_large, quota_exceeded, unsupported_type, class_not_allowed, extension_mismatch, malware_detected, svg_unsafe, archive_encrypted, archive_bomb, archive_unsupported, rate_limited, unavailable, idempotency_conflict, invalid_request, unsupported_media_type, bad_json, too_large, authz_unavailable, database_unavailable, restore_not_removable, gallery_not_empty, bad_body, internal, name_taken, slug_taken, project_quota_exceeded, project_not_empty, deploy_in_progress, nothing_to_rollback, not_cancelable, sensitive_lowered, duplicate_variable, domain_taken, volume_exists, volume_in_use, volume_attached, backup_not_ready, unresolved_reference, reference_cycle, invalid_reference, reference_too_deep, value_too_large) | sim | Código estável para tratamento programático. A tabela completa está na descrição geral. |
| `message` | string | sim | Mensagem legível em pt-BR. |
| `details` | object | não | Presente em alguns `422` (ex.: `unresolved_reference` e `reference_cycle` trazem `details.references`). |
| `details.references` | array<string> | não | Referências problemáticas (só nomes, nunca valores). |
| `details.retry_after_seconds` | integer | não | Em `422 rate_limited` (ex.: verificação manual de domínio): segundos até poder tentar de novo. |

```json
{
  "error": "quota_exceeded",
  "message": "Limite de serviços do plano atingido."
}
```

### Erros comuns

Corpo `{"error", "message", "details"?}` (veja [Autenticação e visão geral](/referencia/autenticacao/) e a [tabela de erros](/erros/tabela/)): `400` Entrada inválida (nome de arquivo fora das regras, cursor malformado, formulário sem `file`, nome de galeria vazio ou longo); `401` Token ausente, inválido, expirado ou revogado. A resposta é genérica de propósito; `403` O token não tem o escopo exigido pela operação; `404` Recurso inexistente ou pertencente a outro time (indistinguíveis de propósito); `413` Corpo JSON acima do limite da rota (`too_large`); `415` O corpo precisa ser `application/json` (`unsupported_media_type`); `429` Limite de requisições excedido. Espere `Retry-After` segundos; `500` Falha interna (`internal`). O detalhe fica só no log do servidor; repita depois, e informe o `Idempotency-Key` ao suporte se persistir; `503` Serviço (ex.: scanner ou borda) temporariamente indisponível. Tente de novo após `Retry-After`.

## GET `/v1/projects/{p}/databases/{s}`

Consulta um banco

Estado, tamanho e cota.

Escopo exigido: `projects:read`.

Escopo exigido: `projects:read` · operationId: `getDatabase`

| Parâmetro | Em | Tipo | Obrig. | Descrição |
|---|---|---|---|---|
| `p` | path | string | sim | Id (26 caracteres) ou slug do projeto. Projeto de outro time ou fora dos `project_ids` do token ⇒ `404`. |
| `s` | path | string | sim | Id ou nome do serviço `kind=database`. Serviço que não é banco ⇒ `404`. |

### Resposta 200

Banco.

| Campo | Tipo | Obrig. | Descrição |
|---|---|---|---|
| `id` | string | sim | Id do serviço `kind=database`. |
| `project_id` | string | sim |  |
| `name` | string | sim |  |
| `kind` | enum(database) | sim |  |
| `engine` | enum(postgres, mariadb, sqlite) | sim |  |
| `status` | enum(provisioning, ready, read_only, error, deleting, running, idle, deploying, failed) | sim | Estado da instância. |
| `size_mb` | integer | sim |  |
| `quota_mb` | integer | sim |  |
| `read_only` | boolean | sim | `true` quando o banco passou da cota. |
| `host` | string | sim | Host público do motor do banco, com URL própria por motor no nó (PostgreSQL `db01.pgsql.<domínio>`, MariaDB `db01.mariadb.<domínio>`, sqlite `<banco>.db01.sqlite.<domínio>`); é o host das variáveis `DATABASE_URL`/`PGHOST`/`MYSQL_HOST`/`LIBSQL_URL`. |
| `port` | integer | sim |  |
| `created_at` | string | sim |  |

```json
{
  "id": "01jb7k4a5q8r7t2v4w6x8y0a9z",
  "project_id": "01jb7k3m5q8r7t2v4w6x8y0a1b",
  "name": "db",
  "kind": "database",
  "engine": "postgres",
  "status": "ready",
  "size_mb": 42,
  "quota_mb": 1024,
  "read_only": false,
  "host": "db01.pgsql.nebularws.com",
  "port": 5432,
  "created_at": "2026-10-10T14:03:11Z"
}
```

### Erros comuns

Corpo `{"error", "message", "details"?}` (veja [Autenticação e visão geral](/referencia/autenticacao/) e a [tabela de erros](/erros/tabela/)): `401` Token ausente, inválido, expirado ou revogado. A resposta é genérica de propósito; `403` O token não tem o escopo exigido pela operação; `404` Recurso inexistente ou pertencente a outro time (indistinguíveis de propósito); `429` Limite de requisições excedido. Espere `Retry-After` segundos; `500` Falha interna (`internal`). O detalhe fica só no log do servidor; repita depois, e informe o `Idempotency-Key` ao suporte se persistir; `503` Serviço (ex.: scanner ou borda) temporariamente indisponível. Tente de novo após `Retry-After`.

## PATCH `/v1/projects/{p}/databases/{s}`

Altera a cota

Altera `quota_mb`.

Escopo exigido: `databases:write`.

Escopo exigido: `databases:write` · operationId: `updateDatabase`

| Parâmetro | Em | Tipo | Obrig. | Descrição |
|---|---|---|---|---|
| `p` | path | string | sim | Id (26 caracteres) ou slug do projeto. Projeto de outro time ou fora dos `project_ids` do token ⇒ `404`. |
| `s` | path | string | sim | Id ou nome do serviço `kind=database`. Serviço que não é banco ⇒ `404`. |

### Corpo (application/json)

| Campo | Tipo | Obrig. | Descrição |
|---|---|---|---|
| `quota_mb` | integer | sim |  |

### Resposta 200

Banco atualizado.

| Campo | Tipo | Obrig. | Descrição |
|---|---|---|---|
| `id` | string | sim | Id do serviço `kind=database`. |
| `project_id` | string | sim |  |
| `name` | string | sim |  |
| `kind` | enum(database) | sim |  |
| `engine` | enum(postgres, mariadb, sqlite) | sim |  |
| `status` | enum(provisioning, ready, read_only, error, deleting, running, idle, deploying, failed) | sim | Estado da instância. |
| `size_mb` | integer | sim |  |
| `quota_mb` | integer | sim |  |
| `read_only` | boolean | sim | `true` quando o banco passou da cota. |
| `host` | string | sim | Host público do motor do banco, com URL própria por motor no nó (PostgreSQL `db01.pgsql.<domínio>`, MariaDB `db01.mariadb.<domínio>`, sqlite `<banco>.db01.sqlite.<domínio>`); é o host das variáveis `DATABASE_URL`/`PGHOST`/`MYSQL_HOST`/`LIBSQL_URL`. |
| `port` | integer | sim |  |
| `created_at` | string | sim |  |

```json
{
  "id": "01jb7k4a5q8r7t2v4w6x8y0a9z",
  "project_id": "01jb7k3m5q8r7t2v4w6x8y0a1b",
  "name": "db",
  "kind": "database",
  "engine": "postgres",
  "status": "ready",
  "size_mb": 42,
  "quota_mb": 1024,
  "read_only": false,
  "host": "db01.pgsql.nebularws.com",
  "port": 5432,
  "created_at": "2026-10-10T14:03:11Z"
}
```

### Resposta 422

`quota_exceeded`: a cota não comporta a operação.

| Campo | Tipo | Obrig. | Descrição |
|---|---|---|---|
| `error` | enum(unauthorized, forbidden, not_found, payload_too_large, quota_exceeded, unsupported_type, class_not_allowed, extension_mismatch, malware_detected, svg_unsafe, archive_encrypted, archive_bomb, archive_unsupported, rate_limited, unavailable, idempotency_conflict, invalid_request, unsupported_media_type, bad_json, too_large, authz_unavailable, database_unavailable, restore_not_removable, gallery_not_empty, bad_body, internal, name_taken, slug_taken, project_quota_exceeded, project_not_empty, deploy_in_progress, nothing_to_rollback, not_cancelable, sensitive_lowered, duplicate_variable, domain_taken, volume_exists, volume_in_use, volume_attached, backup_not_ready, unresolved_reference, reference_cycle, invalid_reference, reference_too_deep, value_too_large) | sim | Código estável para tratamento programático. A tabela completa está na descrição geral. |
| `message` | string | sim | Mensagem legível em pt-BR. |
| `details` | object | não | Presente em alguns `422` (ex.: `unresolved_reference` e `reference_cycle` trazem `details.references`). |
| `details.references` | array<string> | não | Referências problemáticas (só nomes, nunca valores). |
| `details.retry_after_seconds` | integer | não | Em `422 rate_limited` (ex.: verificação manual de domínio): segundos até poder tentar de novo. |

```json
{
  "error": "quota_exceeded",
  "message": "Limite de serviços do plano atingido."
}
```

### Erros comuns

Corpo `{"error", "message", "details"?}` (veja [Autenticação e visão geral](/referencia/autenticacao/) e a [tabela de erros](/erros/tabela/)): `400` Entrada inválida (nome de arquivo fora das regras, cursor malformado, formulário sem `file`, nome de galeria vazio ou longo); `401` Token ausente, inválido, expirado ou revogado. A resposta é genérica de propósito; `403` O token não tem o escopo exigido pela operação; `404` Recurso inexistente ou pertencente a outro time (indistinguíveis de propósito); `413` Corpo JSON acima do limite da rota (`too_large`); `415` O corpo precisa ser `application/json` (`unsupported_media_type`); `429` Limite de requisições excedido. Espere `Retry-After` segundos; `500` Falha interna (`internal`). O detalhe fica só no log do servidor; repita depois, e informe o `Idempotency-Key` ao suporte se persistir; `503` Serviço (ex.: scanner ou borda) temporariamente indisponível. Tente de novo após `Retry-After`.

## DELETE `/v1/projects/{p}/databases/{s}`

Apaga o banco

Apaga o banco. Corpo opcional `{"final_backup": true|false}` (padrão **true**: faz um backup antes). Idempotente: repetir para um banco que este time apagou pela API devolve `204`.

Escopo exigido: `databases:write`.

Escopo exigido: `databases:write` · operationId: `deleteDatabase`

| Parâmetro | Em | Tipo | Obrig. | Descrição |
|---|---|---|---|---|
| `p` | path | string | sim | Id (26 caracteres) ou slug do projeto. Projeto de outro time ou fora dos `project_ids` do token ⇒ `404`. |
| `s` | path | string | sim | Id ou nome do serviço `kind=database`. Serviço que não é banco ⇒ `404`. |

### Corpo (application/json)

| Campo | Tipo | Obrig. | Descrição |
|---|---|---|---|
| `final_backup` | boolean | não | Faz um backup antes de apagar (padrão `true`). |

### Resposta 204

Removido (ou já removido anteriormente: repetir é `204`).

### Erros comuns

Corpo `{"error", "message", "details"?}` (veja [Autenticação e visão geral](/referencia/autenticacao/) e a [tabela de erros](/erros/tabela/)): `400` Entrada inválida (nome de arquivo fora das regras, cursor malformado, formulário sem `file`, nome de galeria vazio ou longo); `401` Token ausente, inválido, expirado ou revogado. A resposta é genérica de propósito; `403` O token não tem o escopo exigido pela operação; `404` Recurso inexistente ou pertencente a outro time (indistinguíveis de propósito); `413` Corpo JSON acima do limite da rota (`too_large`); `415` O corpo precisa ser `application/json` (`unsupported_media_type`); `429` Limite de requisições excedido. Espere `Retry-After` segundos; `500` Falha interna (`internal`). O detalhe fica só no log do servidor; repita depois, e informe o `Idempotency-Key` ao suporte se persistir; `503` Serviço (ex.: scanner ou borda) temporariamente indisponível. Tente de novo após `Retry-After`.

## GET `/v1/projects/{p}/databases/{s}/backups`

Lista os backups

Backups do mais novo para o mais antigo.

Escopo exigido: `projects:read`.

Escopo exigido: `projects:read` · operationId: `listBackups`

| Parâmetro | Em | Tipo | Obrig. | Descrição |
|---|---|---|---|---|
| `p` | path | string | sim | Id (26 caracteres) ou slug do projeto. Projeto de outro time ou fora dos `project_ids` do token ⇒ `404`. |
| `s` | path | string | sim | Id ou nome do serviço `kind=database`. Serviço que não é banco ⇒ `404`. |
| `cursor` | query | string | não | Cursor opaco devolvido em `next_cursor` da página anterior. |
| `limit` | query | integer | não | Itens por página (1 a 200). |

### Resposta 200

Página (mais novo/antigo primeiro conforme o recurso).

| Campo | Tipo | Obrig. | Descrição |
|---|---|---|---|
| `next_cursor` | string\|null | sim | Passe em `cursor` para a próxima página; `null` ao fim. |
| `has_more` | boolean | sim | Há mais páginas. |
| `items` | array<object> | sim |  |
| `items[].id` | string | sim |  |
| `items[].at` | string | sim |  |
| `items[].size` | integer | sim | Bytes. |
| `items[].status` | string | sim | Estado do backup (ex.: `done`). |

```json
{
  "items": [
    {
      "id": "01jb7kb15q8r7t2v4w6x8y0a1j",
      "at": "2026-10-10T14:03:11Z",
      "size": 44040192,
      "status": "done"
    }
  ],
  "next_cursor": null,
  "has_more": false
}
```

### Erros comuns

Corpo `{"error", "message", "details"?}` (veja [Autenticação e visão geral](/referencia/autenticacao/) e a [tabela de erros](/erros/tabela/)): `400` Entrada inválida (nome de arquivo fora das regras, cursor malformado, formulário sem `file`, nome de galeria vazio ou longo); `401` Token ausente, inválido, expirado ou revogado. A resposta é genérica de propósito; `403` O token não tem o escopo exigido pela operação; `404` Recurso inexistente ou pertencente a outro time (indistinguíveis de propósito); `429` Limite de requisições excedido. Espere `Retry-After` segundos; `500` Falha interna (`internal`). O detalhe fica só no log do servidor; repita depois, e informe o `Idempotency-Key` ao suporte se persistir; `503` Serviço (ex.: scanner ou borda) temporariamente indisponível. Tente de novo após `Retry-After`.

## POST `/v1/projects/{p}/databases/{s}/backups`

Inicia um backup

Backup sob demanda, assíncrono: acompanhe em `GET …/jobs/{job_id}`.

Escopo exigido: `databases:write`.

Escopo exigido: `databases:write` · operationId: `createBackup`

| Parâmetro | Em | Tipo | Obrig. | Descrição |
|---|---|---|---|---|
| `p` | path | string | sim | Id (26 caracteres) ou slug do projeto. Projeto de outro time ou fora dos `project_ids` do token ⇒ `404`. |
| `s` | path | string | sim | Id ou nome do serviço `kind=database`. Serviço que não é banco ⇒ `404`. |
| `Idempotency-Key` | header | string | não | Chave de idempotência (8 a 128 caracteres `[A-Za-z0-9._:-]`; use um UUID). Mesma chave + mesmo pedido em 24 h ⇒ a mesma resposta, com `Idempotent-Replay: true`; mesma chave + pedido diferente ⇒ `422 idempotency_conflict`. Falhas nunca são gravadas, então a repetição executa de verdade. |

### Resposta 202

Backup iniciado.

| Campo | Tipo | Obrig. | Descrição |
|---|---|---|---|
| `job_id` | string | sim |  |
| `backup_id` | string | não | Só no backup. |

```json
{
  "job_id": "01jb7k8a5q8r7t2v4w6x8y0a6g",
  "backup_id": "01jb7kb15q8r7t2v4w6x8y0a1j"
}
```

### Resposta 422

`idempotency_conflict`.

| Campo | Tipo | Obrig. | Descrição |
|---|---|---|---|
| `error` | enum(unauthorized, forbidden, not_found, payload_too_large, quota_exceeded, unsupported_type, class_not_allowed, extension_mismatch, malware_detected, svg_unsafe, archive_encrypted, archive_bomb, archive_unsupported, rate_limited, unavailable, idempotency_conflict, invalid_request, unsupported_media_type, bad_json, too_large, authz_unavailable, database_unavailable, restore_not_removable, gallery_not_empty, bad_body, internal, name_taken, slug_taken, project_quota_exceeded, project_not_empty, deploy_in_progress, nothing_to_rollback, not_cancelable, sensitive_lowered, duplicate_variable, domain_taken, volume_exists, volume_in_use, volume_attached, backup_not_ready, unresolved_reference, reference_cycle, invalid_reference, reference_too_deep, value_too_large) | sim | Código estável para tratamento programático. A tabela completa está na descrição geral. |
| `message` | string | sim | Mensagem legível em pt-BR. |
| `details` | object | não | Presente em alguns `422` (ex.: `unresolved_reference` e `reference_cycle` trazem `details.references`). |
| `details.references` | array<string> | não | Referências problemáticas (só nomes, nunca valores). |
| `details.retry_after_seconds` | integer | não | Em `422 rate_limited` (ex.: verificação manual de domínio): segundos até poder tentar de novo. |

```json
{
  "error": "idempotency_conflict",
  "message": "Esta Idempotency-Key já foi usada com outro conteúdo. Gere uma chave nova."
}
```

### Erros comuns

Corpo `{"error", "message", "details"?}` (veja [Autenticação e visão geral](/referencia/autenticacao/) e a [tabela de erros](/erros/tabela/)): `400` Entrada inválida (nome de arquivo fora das regras, cursor malformado, formulário sem `file`, nome de galeria vazio ou longo); `401` Token ausente, inválido, expirado ou revogado. A resposta é genérica de propósito; `403` O token não tem o escopo exigido pela operação; `404` Recurso inexistente ou pertencente a outro time (indistinguíveis de propósito); `429` Limite de requisições excedido. Espere `Retry-After` segundos; `500` Falha interna (`internal`). O detalhe fica só no log do servidor; repita depois, e informe o `Idempotency-Key` ao suporte se persistir; `503` Serviço (ex.: scanner ou borda) temporariamente indisponível. Tente de novo após `Retry-After`.

## POST `/v1/projects/{p}/databases/{s}/credentials`

Revela as credenciais de conexão

Devolve as credenciais em claro. **Nunca em `GET`**, nunca em cache, e a leitura é auditada. O token precisa do escopo `env:reveal` e ter sido criado com confirmação de identidade (step-up); sem isso, `403`.

Escopo exigido: `env:reveal`.

Escopo exigido: `env:reveal` · operationId: `revealDatabaseCredentials`

| Parâmetro | Em | Tipo | Obrig. | Descrição |
|---|---|---|---|---|
| `p` | path | string | sim | Id (26 caracteres) ou slug do projeto. Projeto de outro time ou fora dos `project_ids` do token ⇒ `404`. |
| `s` | path | string | sim | Id ou nome do serviço `kind=database`. Serviço que não é banco ⇒ `404`. |

### Resposta 200

Credenciais.

| Campo | Tipo | Obrig. | Descrição |
|---|---|---|---|
| `credentials` | array<object> | sim |  |
| `credentials[].name` | string | sim |  |
| `credentials[].value` | string | sim |  |

```json
{
  "credentials": [
    {
      "name": "DB_HOST",
      "value": "db"
    },
    {
      "name": "DB_PASSWORD",
      "value": "valor-exibido-uma-vez"
    }
  ]
}
```

### Erros comuns

Corpo `{"error", "message", "details"?}` (veja [Autenticação e visão geral](/referencia/autenticacao/) e a [tabela de erros](/erros/tabela/)): `401` Token ausente, inválido, expirado ou revogado. A resposta é genérica de propósito; `403` O token não tem o escopo exigido pela operação; `404` Recurso inexistente ou pertencente a outro time (indistinguíveis de propósito); `429` Limite de requisições excedido. Espere `Retry-After` segundos; `500` Falha interna (`internal`). O detalhe fica só no log do servidor; repita depois, e informe o `Idempotency-Key` ao suporte se persistir; `503` Serviço (ex.: scanner ou borda) temporariamente indisponível. Tente de novo após `Retry-After`.

## POST `/v1/projects/{p}/databases/{s}/restore`

Restaura um backup

Inicia um restore. `target=new` (padrão) cria um banco **ao lado** do atual, sem substituí-lo: o serviço continua apontando para o banco de origem e o novo aparece em `GET …/restores` já com `status: restoring`. `same` **sobrescreve os dados vivos** (no painel exige step-up). Acompanhe em `GET …/jobs/{job_id}` ou em `GET …/restores`.

Escopo exigido: `databases:write`.

Escopo exigido: `databases:write` · operationId: `restoreDatabase`

| Parâmetro | Em | Tipo | Obrig. | Descrição |
|---|---|---|---|---|
| `p` | path | string | sim | Id (26 caracteres) ou slug do projeto. Projeto de outro time ou fora dos `project_ids` do token ⇒ `404`. |
| `s` | path | string | sim | Id ou nome do serviço `kind=database`. Serviço que não é banco ⇒ `404`. |
| `Idempotency-Key` | header | string | não | Chave de idempotência (8 a 128 caracteres `[A-Za-z0-9._:-]`; use um UUID). Mesma chave + mesmo pedido em 24 h ⇒ a mesma resposta, com `Idempotent-Replay: true`; mesma chave + pedido diferente ⇒ `422 idempotency_conflict`. Falhas nunca são gravadas, então a repetição executa de verdade. |

### Corpo (application/json)

| Campo | Tipo | Obrig. | Descrição |
|---|---|---|---|
| `backup_id` | string | sim |  |
| `target` | enum(new, same) | não | `new` (padrão) cria ao lado, sem substituir o banco atual; `same` sobrescreve o banco vivo (no painel exige step-up). |
| `new_db_name` | string | não | Ignorado: o nome do banco restaurado é gerado pelo servidor (`n_…`) e vem em `db_name` de `GET …/restores`. |

### Resposta 202

Restore iniciado.

| Campo | Tipo | Obrig. | Descrição |
|---|---|---|---|
| `job_id` | string | sim |  |
| `backup_id` | string | não | Só no backup. |

```json
{
  "job_id": "01jb7kc15q8r7t2v4w6x8y0a2k"
}
```

### Resposta 409

`backup_not_ready`: o backup ainda não terminou.

| Campo | Tipo | Obrig. | Descrição |
|---|---|---|---|
| `error` | enum(unauthorized, forbidden, not_found, payload_too_large, quota_exceeded, unsupported_type, class_not_allowed, extension_mismatch, malware_detected, svg_unsafe, archive_encrypted, archive_bomb, archive_unsupported, rate_limited, unavailable, idempotency_conflict, invalid_request, unsupported_media_type, bad_json, too_large, authz_unavailable, database_unavailable, restore_not_removable, gallery_not_empty, bad_body, internal, name_taken, slug_taken, project_quota_exceeded, project_not_empty, deploy_in_progress, nothing_to_rollback, not_cancelable, sensitive_lowered, duplicate_variable, domain_taken, volume_exists, volume_in_use, volume_attached, backup_not_ready, unresolved_reference, reference_cycle, invalid_reference, reference_too_deep, value_too_large) | sim | Código estável para tratamento programático. A tabela completa está na descrição geral. |
| `message` | string | sim | Mensagem legível em pt-BR. |
| `details` | object | não | Presente em alguns `422` (ex.: `unresolved_reference` e `reference_cycle` trazem `details.references`). |
| `details.references` | array<string> | não | Referências problemáticas (só nomes, nunca valores). |
| `details.retry_after_seconds` | integer | não | Em `422 rate_limited` (ex.: verificação manual de domínio): segundos até poder tentar de novo. |

### Resposta 422

`idempotency_conflict`.

| Campo | Tipo | Obrig. | Descrição |
|---|---|---|---|
| `error` | enum(unauthorized, forbidden, not_found, payload_too_large, quota_exceeded, unsupported_type, class_not_allowed, extension_mismatch, malware_detected, svg_unsafe, archive_encrypted, archive_bomb, archive_unsupported, rate_limited, unavailable, idempotency_conflict, invalid_request, unsupported_media_type, bad_json, too_large, authz_unavailable, database_unavailable, restore_not_removable, gallery_not_empty, bad_body, internal, name_taken, slug_taken, project_quota_exceeded, project_not_empty, deploy_in_progress, nothing_to_rollback, not_cancelable, sensitive_lowered, duplicate_variable, domain_taken, volume_exists, volume_in_use, volume_attached, backup_not_ready, unresolved_reference, reference_cycle, invalid_reference, reference_too_deep, value_too_large) | sim | Código estável para tratamento programático. A tabela completa está na descrição geral. |
| `message` | string | sim | Mensagem legível em pt-BR. |
| `details` | object | não | Presente em alguns `422` (ex.: `unresolved_reference` e `reference_cycle` trazem `details.references`). |
| `details.references` | array<string> | não | Referências problemáticas (só nomes, nunca valores). |
| `details.retry_after_seconds` | integer | não | Em `422 rate_limited` (ex.: verificação manual de domínio): segundos até poder tentar de novo. |

```json
{
  "error": "idempotency_conflict",
  "message": "Esta Idempotency-Key já foi usada com outro conteúdo. Gere uma chave nova."
}
```

### Erros comuns

Corpo `{"error", "message", "details"?}` (veja [Autenticação e visão geral](/referencia/autenticacao/) e a [tabela de erros](/erros/tabela/)): `400` Entrada inválida (nome de arquivo fora das regras, cursor malformado, formulário sem `file`, nome de galeria vazio ou longo); `401` Token ausente, inválido, expirado ou revogado. A resposta é genérica de propósito; `403` O token não tem o escopo exigido pela operação; `404` Recurso inexistente ou pertencente a outro time (indistinguíveis de propósito); `413` Corpo JSON acima do limite da rota (`too_large`); `415` O corpo precisa ser `application/json` (`unsupported_media_type`); `429` Limite de requisições excedido. Espere `Retry-After` segundos; `500` Falha interna (`internal`). O detalhe fica só no log do servidor; repita depois, e informe o `Idempotency-Key` ao suporte se persistir; `503` Serviço (ex.: scanner ou borda) temporariamente indisponível. Tente de novo após `Retry-After`.

## GET `/v1/projects/{p}/databases/{s}/restores`

Lista os bancos criados por restore

Bancos criados por restores `target=new` ao lado do banco, de **qualquer motor**, com o estado de cada um (`restoring`, `ready` ou `failed`, com `error` quando falhou). Postgres e MariaDB: o banco restaurado (`db_name`) fica na mesma instância, host e porta, e usa as **mesmas credenciais** do banco de origem. SQLite: nasce uma instância própria (`url`; gere um token novo com a rotação).

Escopo exigido: `projects:read`.

Escopo exigido: `projects:read` · operationId: `listRestores`

| Parâmetro | Em | Tipo | Obrig. | Descrição |
|---|---|---|---|---|
| `p` | path | string | sim | Id (26 caracteres) ou slug do projeto. Projeto de outro time ou fora dos `project_ids` do token ⇒ `404`. |
| `s` | path | string | sim | Id ou nome do serviço `kind=database`. Serviço que não é banco ⇒ `404`. |
| `cursor` | query | string | não | Cursor opaco devolvido em `next_cursor` da página anterior. |
| `limit` | query | integer | não | Itens por página (1 a 200). |

### Resposta 200

Página (mais novo/antigo primeiro conforme o recurso).

| Campo | Tipo | Obrig. | Descrição |
|---|---|---|---|
| `next_cursor` | string\|null | sim | Passe em `cursor` para a próxima página; `null` ao fim. |
| `has_more` | boolean | sim | Há mais páginas. |
| `items` | array<object> | sim |  |
| `items[].id` | string | sim |  |
| `items[].job_id` | string | sim |  |
| `items[].backup_id` | string | sim |  |
| `items[].db_name` | string | sim | Nome do banco restaurado (postgres/mariadb: na mesma instância da origem; sqlite: o id da instância nova). |
| `items[].status` | enum(restoring, ready, failed) | sim | `restoring` enquanto o job roda; `ready` quando pronto para uso; `failed` com `error`. |
| `items[].error` | object | não |  |
| `items[].error.code` | string | sim | Código estável da falha: `deploy_failed` (genérico), `unresolved_reference`, `reference_cycle`, `image_requires_root`, `crashed_after_start` e os do build: `build_clone_failed`, `build_detect_failed`, `build_install_failed`, `build_script_failed`, `build_copy_failed`, `build_activate_failed`, `activate_readonly_write` (o app tentou gravar sob `/app`, somente leitura: use um volume nessa pasta ou `/tmp`), `activate_app_crashed` (o app encerrou logo ao iniciar; a mensagem traz a primeira linha de erro do log), `activate_oom` (o app usou mais memória que o limite do serviço e foi encerrado), `activate_no_http` (o app estável e rodando não respondeu na porta esperada), `team_app_limit` (o time já roda o máximo de apps do plano; a mensagem traz N de M: pare ou remova um app, ou peça ao operador para ampliar o limite), `build_volume_unavailable`, `build_oom`, `build_timeout`, `build_storage_error` (falha interna de armazenamento da plataforma, não do repositório), `build_disk_full` (espaço de build cheio) e `build_storage_unavailable` (nenhum armazenamento de build utilizável na plataforma; não é falta de espaço). O código vem gravado com o deploy; `message` traz a causa em português (sem segredos nem caminhos do servidor). |
| `items[].error.message` | string | sim |  |
| `items[].error.details` | any | não | Dados extras do erro (qualquer JSON). |
| `items[].host` | string | sim |  |
| `items[].port` | integer | sim |  |
| `items[].url` | string | não | Só sqlite. |
| `items[].created_at` | string | sim |  |

```json
{
  "items": [
    {
      "id": "01jb7k9a5q8r7t2v4w6x8y0a7h",
      "job_id": "01jb7kc15q8r7t2v4w6x8y0a2k",
      "backup_id": "01jb7kb15q8r7t2v4w6x8y0a1j",
      "db_name": "n_k7p2m4q6r8t2v4w6",
      "status": "ready",
      "host": "db01.pgsql.example.com",
      "port": 5432,
      "created_at": "2026-10-10T14:03:11Z"
    }
  ],
  "next_cursor": null,
  "has_more": false
}
```

### Erros comuns

Corpo `{"error", "message", "details"?}` (veja [Autenticação e visão geral](/referencia/autenticacao/) e a [tabela de erros](/erros/tabela/)): `400` Entrada inválida (nome de arquivo fora das regras, cursor malformado, formulário sem `file`, nome de galeria vazio ou longo); `401` Token ausente, inválido, expirado ou revogado. A resposta é genérica de propósito; `403` O token não tem o escopo exigido pela operação; `404` Recurso inexistente ou pertencente a outro time (indistinguíveis de propósito); `429` Limite de requisições excedido. Espere `Retry-After` segundos; `500` Falha interna (`internal`). O detalhe fica só no log do servidor; repita depois, e informe o `Idempotency-Key` ao suporte se persistir; `503` Serviço (ex.: scanner ou borda) temporariamente indisponível. Tente de novo após `Retry-After`.

## POST `/v1/projects/{p}/databases/{s}/rotate`

Rotaciona as credenciais

Gera novas credenciais e marca os serviços dependentes para novo deploy.

Escopo exigido: `databases:write`.

Escopo exigido: `databases:write` · operationId: `rotateDatabase`

| Parâmetro | Em | Tipo | Obrig. | Descrição |
|---|---|---|---|---|
| `p` | path | string | sim | Id (26 caracteres) ou slug do projeto. Projeto de outro time ou fora dos `project_ids` do token ⇒ `404`. |
| `s` | path | string | sim | Id ou nome do serviço `kind=database`. Serviço que não é banco ⇒ `404`. |

### Resposta 200

Banco.

| Campo | Tipo | Obrig. | Descrição |
|---|---|---|---|
| `id` | string | sim | Id do serviço `kind=database`. |
| `project_id` | string | sim |  |
| `name` | string | sim |  |
| `kind` | enum(database) | sim |  |
| `engine` | enum(postgres, mariadb, sqlite) | sim |  |
| `status` | enum(provisioning, ready, read_only, error, deleting, running, idle, deploying, failed) | sim | Estado da instância. |
| `size_mb` | integer | sim |  |
| `quota_mb` | integer | sim |  |
| `read_only` | boolean | sim | `true` quando o banco passou da cota. |
| `host` | string | sim | Host público do motor do banco, com URL própria por motor no nó (PostgreSQL `db01.pgsql.<domínio>`, MariaDB `db01.mariadb.<domínio>`, sqlite `<banco>.db01.sqlite.<domínio>`); é o host das variáveis `DATABASE_URL`/`PGHOST`/`MYSQL_HOST`/`LIBSQL_URL`. |
| `port` | integer | sim |  |
| `created_at` | string | sim |  |

```json
{
  "id": "01jb7k4a5q8r7t2v4w6x8y0a9z",
  "project_id": "01jb7k3m5q8r7t2v4w6x8y0a1b",
  "name": "db",
  "kind": "database",
  "engine": "postgres",
  "status": "ready",
  "size_mb": 42,
  "quota_mb": 1024,
  "read_only": false,
  "host": "db01.pgsql.nebularws.com",
  "port": 5432,
  "created_at": "2026-10-10T14:03:11Z"
}
```

### Erros comuns

Corpo `{"error", "message", "details"?}` (veja [Autenticação e visão geral](/referencia/autenticacao/) e a [tabela de erros](/erros/tabela/)): `401` Token ausente, inválido, expirado ou revogado. A resposta é genérica de propósito; `403` O token não tem o escopo exigido pela operação; `404` Recurso inexistente ou pertencente a outro time (indistinguíveis de propósito); `429` Limite de requisições excedido. Espere `Retry-After` segundos; `500` Falha interna (`internal`). O detalhe fica só no log do servidor; repita depois, e informe o `Idempotency-Key` ao suporte se persistir; `503` Serviço (ex.: scanner ou borda) temporariamente indisponível. Tente de novo após `Retry-After`.

## GET `/v1/projects/{p}/databases/{s}/jobs/{j}`

Consulta um job de backup/restore (polling)

`queued → running → done|failed`. Em restore `target=new`, `new_instance` descreve o banco criado ao lado e seu estado (sem credencial). Em job com falha, `error.code` é estável (`backup_failed`, `backup_tls_failed`, `restore_failed`, …) e `error.message` está em português claro, sem segredos.

Escopo exigido: `projects:read`.

Escopo exigido: `projects:read` · operationId: `getDatabaseJob`

| Parâmetro | Em | Tipo | Obrig. | Descrição |
|---|---|---|---|---|
| `p` | path | string | sim | Id (26 caracteres) ou slug do projeto. Projeto de outro time ou fora dos `project_ids` do token ⇒ `404`. |
| `s` | path | string | sim | Id ou nome do serviço `kind=database`. Serviço que não é banco ⇒ `404`. |
| `j` | path | string | sim | Id do job de backup/restore (`job_id`). |

### Resposta 200

Job.

| Campo | Tipo | Obrig. | Descrição |
|---|---|---|---|
| `id` | string | sim |  |
| `kind` | enum(backup, restore) | sim |  |
| `status` | enum(queued, running, done, failed) | sim |  |
| `error` | object | não |  |
| `error.code` | string | sim | Código estável da falha: `deploy_failed` (genérico), `unresolved_reference`, `reference_cycle`, `image_requires_root`, `crashed_after_start` e os do build: `build_clone_failed`, `build_detect_failed`, `build_install_failed`, `build_script_failed`, `build_copy_failed`, `build_activate_failed`, `activate_readonly_write` (o app tentou gravar sob `/app`, somente leitura: use um volume nessa pasta ou `/tmp`), `activate_app_crashed` (o app encerrou logo ao iniciar; a mensagem traz a primeira linha de erro do log), `activate_oom` (o app usou mais memória que o limite do serviço e foi encerrado), `activate_no_http` (o app estável e rodando não respondeu na porta esperada), `team_app_limit` (o time já roda o máximo de apps do plano; a mensagem traz N de M: pare ou remova um app, ou peça ao operador para ampliar o limite), `build_volume_unavailable`, `build_oom`, `build_timeout`, `build_storage_error` (falha interna de armazenamento da plataforma, não do repositório), `build_disk_full` (espaço de build cheio) e `build_storage_unavailable` (nenhum armazenamento de build utilizável na plataforma; não é falta de espaço). O código vem gravado com o deploy; `message` traz a causa em português (sem segredos nem caminhos do servidor). |
| `error.message` | string | sim |  |
| `error.details` | any | não | Dados extras do erro (qualquer JSON). |
| `result` | any | não | Resultado do job quando `done` (qualquer JSON). |
| `new_instance` | object | não | Banco criado por um restore `target=new` (qualquer motor). Nunca carrega credencial (postgres/mariadb usam as da origem). |
| `new_instance.id` | string | sim |  |
| `new_instance.job_id` | string | sim |  |
| `new_instance.backup_id` | string | sim |  |
| `new_instance.db_name` | string | sim | Nome do banco restaurado (postgres/mariadb: na mesma instância da origem; sqlite: o id da instância nova). |
| `new_instance.status` | enum(restoring, ready, failed) | sim | `restoring` enquanto o job roda; `ready` quando pronto para uso; `failed` com `error`. |
| `new_instance.error` | object | não |  |
| `new_instance.error.code` | string | sim | Código estável da falha: `deploy_failed` (genérico), `unresolved_reference`, `reference_cycle`, `image_requires_root`, `crashed_after_start` e os do build: `build_clone_failed`, `build_detect_failed`, `build_install_failed`, `build_script_failed`, `build_copy_failed`, `build_activate_failed`, `activate_readonly_write` (o app tentou gravar sob `/app`, somente leitura: use um volume nessa pasta ou `/tmp`), `activate_app_crashed` (o app encerrou logo ao iniciar; a mensagem traz a primeira linha de erro do log), `activate_oom` (o app usou mais memória que o limite do serviço e foi encerrado), `activate_no_http` (o app estável e rodando não respondeu na porta esperada), `team_app_limit` (o time já roda o máximo de apps do plano; a mensagem traz N de M: pare ou remova um app, ou peça ao operador para ampliar o limite), `build_volume_unavailable`, `build_oom`, `build_timeout`, `build_storage_error` (falha interna de armazenamento da plataforma, não do repositório), `build_disk_full` (espaço de build cheio) e `build_storage_unavailable` (nenhum armazenamento de build utilizável na plataforma; não é falta de espaço). O código vem gravado com o deploy; `message` traz a causa em português (sem segredos nem caminhos do servidor). |
| `new_instance.error.message` | string | sim |  |
| `new_instance.error.details` | any | não | Dados extras do erro (qualquer JSON). |
| `new_instance.host` | string | sim |  |
| `new_instance.port` | integer | sim |  |
| `new_instance.url` | string | não | Só sqlite. |
| `new_instance.created_at` | string | sim |  |

```json
{
  "id": "01jb7k8a5q8r7t2v4w6x8y0a6g",
  "kind": "backup",
  "status": "done",
  "result": {
    "size": 44040192
  }
}
```

### Erros comuns

Corpo `{"error", "message", "details"?}` (veja [Autenticação e visão geral](/referencia/autenticacao/) e a [tabela de erros](/erros/tabela/)): `401` Token ausente, inválido, expirado ou revogado. A resposta é genérica de propósito; `403` O token não tem o escopo exigido pela operação; `404` Recurso inexistente ou pertencente a outro time (indistinguíveis de propósito); `429` Limite de requisições excedido. Espere `Retry-After` segundos; `500` Falha interna (`internal`). O detalhe fica só no log do servidor; repita depois, e informe o `Idempotency-Key` ao suporte se persistir; `503` Serviço (ex.: scanner ou borda) temporariamente indisponível. Tente de novo após `Retry-After`.

## DELETE `/v1/projects/{p}/databases/{s}/restores/{r}`

Apaga um banco de restore

Idempotente (repetir ⇒ `204`). Em postgres/mariadb o banco restaurado vive na instância da origem e só sai com ela: a resposta é `409 restore_not_removable`, exceto para um restore `failed` (só o registro é removido).

Escopo exigido: `databases:write`.

Escopo exigido: `databases:write` · operationId: `deleteRestore`

| Parâmetro | Em | Tipo | Obrig. | Descrição |
|---|---|---|---|---|
| `p` | path | string | sim | Id (26 caracteres) ou slug do projeto. Projeto de outro time ou fora dos `project_ids` do token ⇒ `404`. |
| `s` | path | string | sim | Id ou nome do serviço `kind=database`. Serviço que não é banco ⇒ `404`. |
| `r` | path | string | sim | Id da instância criada por um restore `target=new`. |

### Resposta 204

Removido (ou já removido anteriormente: repetir é `204`).

### Resposta 409

`restore_not_removable`: o banco restaurado divide a instância da origem e ainda não pode ser removido sozinho.

| Campo | Tipo | Obrig. | Descrição |
|---|---|---|---|
| `error` | enum(unauthorized, forbidden, not_found, payload_too_large, quota_exceeded, unsupported_type, class_not_allowed, extension_mismatch, malware_detected, svg_unsafe, archive_encrypted, archive_bomb, archive_unsupported, rate_limited, unavailable, idempotency_conflict, invalid_request, unsupported_media_type, bad_json, too_large, authz_unavailable, database_unavailable, restore_not_removable, gallery_not_empty, bad_body, internal, name_taken, slug_taken, project_quota_exceeded, project_not_empty, deploy_in_progress, nothing_to_rollback, not_cancelable, sensitive_lowered, duplicate_variable, domain_taken, volume_exists, volume_in_use, volume_attached, backup_not_ready, unresolved_reference, reference_cycle, invalid_reference, reference_too_deep, value_too_large) | sim | Código estável para tratamento programático. A tabela completa está na descrição geral. |
| `message` | string | sim | Mensagem legível em pt-BR. |
| `details` | object | não | Presente em alguns `422` (ex.: `unresolved_reference` e `reference_cycle` trazem `details.references`). |
| `details.references` | array<string> | não | Referências problemáticas (só nomes, nunca valores). |
| `details.retry_after_seconds` | integer | não | Em `422 rate_limited` (ex.: verificação manual de domínio): segundos até poder tentar de novo. |

### Erros comuns

Corpo `{"error", "message", "details"?}` (veja [Autenticação e visão geral](/referencia/autenticacao/) e a [tabela de erros](/erros/tabela/)): `401` Token ausente, inválido, expirado ou revogado. A resposta é genérica de propósito; `403` O token não tem o escopo exigido pela operação; `404` Recurso inexistente ou pertencente a outro time (indistinguíveis de propósito); `429` Limite de requisições excedido. Espere `Retry-After` segundos; `500` Falha interna (`internal`). O detalhe fica só no log do servidor; repita depois, e informe o `Idempotency-Key` ao suporte se persistir; `503` Serviço (ex.: scanner ou borda) temporariamente indisponível. Tente de novo após `Retry-After`.
