# Projetos (referência)

## GET `/v1/projects`

Lista os projetos

Projetos do time do token (ou só os permitidos, se o token for restrito), do mais novo para o mais antigo.

Escopo exigido: `projects:read`.

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

| Parâmetro | Em | Tipo | Obrig. | Descrição |
|---|---|---|---|---|
| `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 projeto (26 caracteres). |
| `items[].name` | string | sim |  |
| `items[].slug` | string | sim |  |
| `items[].team_id` | string | sim |  |
| `items[].created_at` | string | sim |  |

```json
{
  "items": [
    {
      "id": "01jb7k3m5q8r7t2v4w6x8y0a1b",
      "name": "Loja Acme",
      "slug": "loja-acme",
      "team_id": "01jb7h0000000000000000team",
      "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; `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`

Cria um projeto

Cria um projeto vazio no time. O projeto nasce com um serviço `web` padrão. Um token **restrito a projetos específicos** não pode criar (`403`): o novo ficaria fora da lista dele. Time inativo ⇒ `403`.

Escopo exigido: `projects:write`.

Escopo exigido: `projects:write` · operationId: `createProject`

| Parâmetro | Em | Tipo | Obrig. | Descrição |
|---|---|---|---|---|
| `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 |  |
| `slug` | string | não | Opcional; gerado a partir do nome quando omitido. |

### Resposta 201

Projeto criado.

| Campo | Tipo | Obrig. | Descrição |
|---|---|---|---|
| `id` | string | sim | Id do projeto (26 caracteres). |
| `name` | string | sim |  |
| `slug` | string | sim |  |
| `team_id` | string | sim |  |
| `created_at` | string | sim |  |

```json
{
  "id": "01jb7k3m5q8r7t2v4w6x8y0a1b",
  "name": "Loja Acme",
  "slug": "loja-acme",
  "team_id": "01jb7h0000000000000000team",
  "created_at": "2026-10-10T14:03:11Z"
}
```

### Resposta 409

`slug_taken` (slug em uso) ou `project_quota_exceeded` (limite de projetos do time).

| 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

`idempotency_conflict`: a mesma `Idempotency-Key` com outro corpo.

| 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; `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}`

Detalhe do projeto

Devolve o projeto. `{p}` aceita o id ou o slug.

Escopo exigido: `projects:read`.

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

| 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`. |

### Resposta 200

Projeto.

| Campo | Tipo | Obrig. | Descrição |
|---|---|---|---|
| `id` | string | sim | Id do projeto (26 caracteres). |
| `name` | string | sim |  |
| `slug` | string | sim |  |
| `team_id` | string | sim |  |
| `created_at` | string | sim |  |

```json
{
  "id": "01jb7k3m5q8r7t2v4w6x8y0a1b",
  "name": "Loja Acme",
  "slug": "loja-acme",
  "team_id": "01jb7h0000000000000000team",
  "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}`

Renomeia o projeto

Renomeia o projeto (o slug não muda).

Escopo exigido: `projects:write`.

Escopo exigido: `projects:write` · operationId: `updateProject`

| 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`. |

### Corpo (application/json)

| Campo | Tipo | Obrig. | Descrição |
|---|---|---|---|
| `name` | string | não |  |

### Resposta 200

Projeto atualizado.

| Campo | Tipo | Obrig. | Descrição |
|---|---|---|---|
| `id` | string | sim | Id do projeto (26 caracteres). |
| `name` | string | sim |  |
| `slug` | string | sim |  |
| `team_id` | string | sim |  |
| `created_at` | string | sim |  |

```json
{
  "id": "01jb7k3m5q8r7t2v4w6x8y0a1b",
  "name": "Loja Acme",
  "slug": "loja-acme",
  "team_id": "01jb7h0000000000000000team",
  "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/)): `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}`

Apaga o projeto

Apaga o projeto com serviços, containers, volumes, bancos e variáveis. **Irreversível.** Idempotente: repetir a chamada (pelo **id ou pelo slug**) para um projeto que este time já apagou pela API devolve `204`; um id ou slug que nunca foi visível ao token é `404`.

Escopo exigido: `projects:write`.

Escopo exigido: `projects:write` · operationId: `deleteProject`

| 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`. |

### Resposta 204

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

### Resposta 409

`project_not_empty`: ainda há recursos que impedem a remoçã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. |

### 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`.
