# Galerias e arquivos (referência)

## GET `/v1/galleries`

Lista as galerias visíveis ao token

Retorna as galerias do time do token (ou apenas as permitidas, se o token for restrito).
Escopo exigido: `assets:read`.

Escopo exigido: `assets:read` · operationId: `listGalleries`

| Parâmetro | Em | Tipo | Obrig. | Descrição |
|---|---|---|---|---|
| `project` | query | string | não | Filtra por id de projeto. |
| `cursor` | query | string | não | Cursor opaco devolvido em `next_cursor` da página anterior. |

### Resposta 200

Página de galerias.

| 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[].key` | string | sim | Chave pública imutável usada nas URLs. |
| `items[].name` | string | sim |  |
| `items[].bucket` | object | sim |  |
| `items[].bucket.id` | string | sim |  |
| `items[].bucket.label` | string | sim | Rótulo do host. |
| `items[].bucket.kind` | enum(shared, dedicated) | sim |  |
| `items[].bucket.host` | string | sim |  |
| `items[].project` | object \| null | sim |  |
| `items[].assets_count` | integer | sim |  |
| `items[].bytes` | integer | sim |  |
| `items[].created_at` | string | sim |  |

```json
{
  "items": [
    {
      "id": "gal_01J9ZK5Q",
      "key": "marcas-k7p2",
      "name": "Marcas",
      "bucket": {
        "id": "bkt_01J9ZG00",
        "label": "media",
        "kind": "shared",
        "host": "media.nebularcdn.com"
      },
      "project": {
        "id": "prj_01J9ZK3M",
        "name": "Site Acme",
        "slug": "site-acme"
      },
      "assets_count": 12,
      "bytes": 4823910,
      "created_at": "2026-10-09T14: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; `503` Serviço (ex.: scanner ou borda) temporariamente indisponível. Tente de novo após `Retry-After`.

## POST `/v1/galleries`

Cria uma galeria

Cria a galeria no bucket padrão do time. A `key` pública é gerada pelo servidor e é imutável.
Escopo exigido: `galleries:manage`. Um token **restrito a galerias específicas** não pode criar
galerias (a nova ficaria fora da lista dele): recebe `403 forbidden`.

Escopo exigido: `galleries:manage` · operationId: `createGallery`

### Corpo (application/json)

| Campo | Tipo | Obrig. | Descrição |
|---|---|---|---|
| `name` | string | sim |  |
| `project_id` | string | não | Projeto ao qual a galeria será associada (organização apenas). |

```json
{
  "name": "Marcas",
  "project_id": "prj_01J9ZK3M"
}
```

### Resposta 201

Galeria criada.

| Campo | Tipo | Obrig. | Descrição |
|---|---|---|---|
| `id` | string | sim |  |
| `key` | string | sim | Chave pública imutável usada nas URLs. |
| `name` | string | sim |  |
| `bucket` | object | sim |  |
| `bucket.id` | string | sim |  |
| `bucket.label` | string | sim | Rótulo do host. |
| `bucket.kind` | enum(shared, dedicated) | sim |  |
| `bucket.host` | string | sim |  |
| `project` | object \| null | sim |  |
| `assets_count` | integer | sim |  |
| `bytes` | integer | sim |  |
| `created_at` | string | sim |  |

```json
{
  "id": "gal_01J9ZK5Q",
  "key": "marcas-k7p2",
  "name": "Marcas",
  "bucket": {
    "id": "bkt_01J9ZG00",
    "label": "media",
    "kind": "shared",
    "host": "media.nebularcdn.com"
  },
  "project": {
    "id": "prj_01J9ZK3M",
    "name": "Site Acme",
    "slug": "site-acme"
  },
  "assets_count": 0,
  "bytes": 0,
  "created_at": "2026-10-09T14: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); `429` Limite de requisições excedido. Espere `Retry-After` segundos; `503` Serviço (ex.: scanner ou borda) temporariamente indisponível. Tente de novo após `Retry-After`.

## GET `/v1/galleries/{key}/assets`

Lista os arquivos de uma galeria

Lista arquivos com URLs e status de varredura. Escopo exigido: `assets:read`.

Escopo exigido: `assets:read` · operationId: `listAssets`

| Parâmetro | Em | Tipo | Obrig. | Descrição |
|---|---|---|---|---|
| `key` | path | string | sim | Chave pública imutável da galeria (ex.: `marcas-k7p2`). |
| `prefix` | query | string | não | Retorna apenas nomes que começam com este prefixo. |
| `cursor` | query | string | não | Cursor opaco devolvido em `next_cursor` da página anterior. |

### Resposta 200

Página de arquivos.

| 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[].name` | string | sim |  |
| `items[].gallery_key` | string | sim |  |
| `items[].current` | object | sim |  |
| `items[].current.id` | string | sim |  |
| `items[].current.hash` | string | sim | SHA-256 hexadecimal do conteúdo. |
| `items[].current.bytes` | integer | sim |  |
| `items[].current.content_type` | string | sim | Detectado pelo conteúdo. |
| `items[].current.class` | enum(image, svg, font, media, document, markup, archive, binary) | sim | Classe do arquivo, detectada pelo conteúdo. Define como a CDN serve o arquivo (ver descrição geral). |
| `items[].current.width` | integer\|null | sim | Somente imagens. |
| `items[].current.height` | integer\|null | sim | Somente imagens. |
| `items[].current.variants` | array<string> | sim | Larguras de variantes geradas (imagens). |
| `items[].current.scan` | object | sim |  |
| `items[].current.scan.status` | enum(pending, clean, rejected, quarantined) | sim | `pending`: aguardando varredura, não publicado. `clean`: publicado. `rejected`: recusado. `quarantined`: detectado na varredura diária; deixou de ser servido (451). |
| `items[].current.scan.reason` | string\|null | sim | Motivo da recusa ou quarentena. |
| `items[].current.scan.engine` | string\|null | sim |  |
| `items[].current.scan.scanned_at` | string\|null | sim |  |
| `items[].current.url` | string\|null | sim | URL **imutável** (`https://<bucket>.nebularcdn.com/<gallery_key>/<hash[0:12]>/<name>`), com `Cache-Control: public, max-age=31536000, immutable`; `null` enquanto o arquivo não estiver `clean`. Para imagens aceita `?w=640` (largura) e `?q=` (qualidade); o formato (AVIF/WebP) é negociado por `Accept` com `Vary: Accept`. Se a segregação de downloads estiver ligada, classes de download usam outro domínio; use sempre a URL devolvida pela API, sem montá-la. |
| `items[].current.uploaded_by` | object | sim |  |
| `items[].current.uploaded_by.kind` | enum(user, token) | sim |  |
| `items[].current.uploaded_by.name` | string | sim |  |
| `items[].current.created_at` | string | sim |  |
| `items[].alias_url` | string\|null | sim | URL **alias** (`https://<bucket>.nebularcdn.com/<gallery_key>/<name>`), sempre a versão atual (`max-age=60, stale-while-revalidate=86400`); `null` enquanto não `clean`. Também aceita `?w=640`, ex.: `https://media.nebularcdn.com/marcas-k7p2/logos/acme.png?w=640`. |
| `items[].versions_count` | integer | sim |  |
| `items[].created_at` | string | sim |  |

```json
{
  "items": [
    {
      "id": "ast_01J9ZK8W",
      "name": "logos/acme.png",
      "gallery_key": "marcas-k7p2",
      "current": {
        "id": "ver_01J9ZK8X",
        "hash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
        "bytes": 48213,
        "content_type": "image/png",
        "class": "image",
        "width": 1200,
        "height": 630,
        "variants": [
          "320",
          "640",
          "1280"
        ],
        "scan": {
          "status": "clean",
          "reason": null,
          "engine": "clamav",
          "scanned_at": "2026-10-09T14:05:02Z"
        },
        "url": "https://media.nebularcdn.com/marcas-k7p2/9f86d081884c/logos/acme.png",
        "uploaded_by": {
          "kind": "token",
          "name": "agente-deploy"
        },
        "created_at": "2026-10-09T14:05:01Z"
      },
      "alias_url": "https://media.nebularcdn.com/marcas-k7p2/logos/acme.png",
      "versions_count": 1,
      "created_at": "2026-10-09T14:05:01Z"
    }
  ],
  "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; `503` Serviço (ex.: scanner ou borda) temporariamente indisponível. Tente de novo após `Retry-After`.

## POST `/v1/galleries/{key}/assets`

Envia ou versiona um arquivo

Envia um arquivo (multipart). Se o nome já existir, cria uma nova versão; o alias passa a apontar
para ela. Escopo exigido: `assets:write`.

- **201**: varredura concluída e arquivo `clean` — já está publicado.
- **202**: o arquivo está `pending` (scanner ocupado ou fora do ar) e **não foi publicado**. Consulte
  `GET /v1/galleries/{key}/assets/{name}` (campo `asset.current.scan.status`) periodicamente (comece com 2 s, com backoff) até
  `scan.status` sair de `pending`. Não reenvie o arquivo.
- **422**: arquivo recusado pela varredura (`malware_detected`, `svg_unsafe`, `archive_encrypted`,
  `archive_bomb`, `archive_unsupported`). Não tente de novo com o mesmo conteúdo.

Exemplo: `curl -H "Authorization: Bearer $NBK" -H "Idempotency-Key: $(uuidgen)" -F file=@logo.png
-F name=logos/acme.png https://api.nebularws.com/v1/galleries/marcas-k7p2/assets`

Escopo exigido: `assets:write` · operationId: `uploadAsset`

| Parâmetro | Em | Tipo | Obrig. | Descrição |
|---|---|---|---|---|
| `key` | path | string | sim | Chave pública imutável da galeria (ex.: `marcas-k7p2`). |
| `Idempotency-Key` | header | string | não | Chave de idempotência (até 255 caracteres; use um UUID). Mesma chave + mesmo corpo ⇒ mesma resposta. Mesma chave + corpo diferente ⇒ `409 idempotency_conflict`. |

### Corpo (multipart/form-data)

| Campo | Tipo | Obrig. | Descrição |
|---|---|---|---|
| `file` | string | sim | Conteúdo do arquivo (até 25 MB). |
| `name` | string | não | Nome dentro da galeria: até 4 segmentos separados por `/`, `[a-z0-9._-]`, até 200 caracteres, sem `..`, sem segmento iniciando com `.` e com o primeiro segmento diferente de 12 caracteres hexadecimais. Inválido ⇒ `400 invalid_request`. Se omitido, usa o nome do arquivo enviado normalizado. |

### Resposta 201

Arquivo publicado (`scan.status = clean`).

| Campo | Tipo | Obrig. | Descrição |
|---|---|---|---|
| `id` | string | sim |  |
| `name` | string | sim |  |
| `gallery_key` | string | sim |  |
| `current` | object | sim |  |
| `current.id` | string | sim |  |
| `current.hash` | string | sim | SHA-256 hexadecimal do conteúdo. |
| `current.bytes` | integer | sim |  |
| `current.content_type` | string | sim | Detectado pelo conteúdo. |
| `current.class` | enum(image, svg, font, media, document, markup, archive, binary) | sim | Classe do arquivo, detectada pelo conteúdo. Define como a CDN serve o arquivo (ver descrição geral). |
| `current.width` | integer\|null | sim | Somente imagens. |
| `current.height` | integer\|null | sim | Somente imagens. |
| `current.variants` | array<string> | sim | Larguras de variantes geradas (imagens). |
| `current.scan` | object | sim |  |
| `current.scan.status` | enum(pending, clean, rejected, quarantined) | sim | `pending`: aguardando varredura, não publicado. `clean`: publicado. `rejected`: recusado. `quarantined`: detectado na varredura diária; deixou de ser servido (451). |
| `current.scan.reason` | string\|null | sim | Motivo da recusa ou quarentena. |
| `current.scan.engine` | string\|null | sim |  |
| `current.scan.scanned_at` | string\|null | sim |  |
| `current.url` | string\|null | sim | URL **imutável** (`https://<bucket>.nebularcdn.com/<gallery_key>/<hash[0:12]>/<name>`), com `Cache-Control: public, max-age=31536000, immutable`; `null` enquanto o arquivo não estiver `clean`. Para imagens aceita `?w=640` (largura) e `?q=` (qualidade); o formato (AVIF/WebP) é negociado por `Accept` com `Vary: Accept`. Se a segregação de downloads estiver ligada, classes de download usam outro domínio; use sempre a URL devolvida pela API, sem montá-la. |
| `current.uploaded_by` | object | sim |  |
| `current.uploaded_by.kind` | enum(user, token) | sim |  |
| `current.uploaded_by.name` | string | sim |  |
| `current.created_at` | string | sim |  |
| `alias_url` | string\|null | sim | URL **alias** (`https://<bucket>.nebularcdn.com/<gallery_key>/<name>`), sempre a versão atual (`max-age=60, stale-while-revalidate=86400`); `null` enquanto não `clean`. Também aceita `?w=640`, ex.: `https://media.nebularcdn.com/marcas-k7p2/logos/acme.png?w=640`. |
| `versions_count` | integer | sim |  |
| `created_at` | string | sim |  |

```json
{
  "id": "ast_01J9ZK8W",
  "name": "logos/acme.png",
  "gallery_key": "marcas-k7p2",
  "current": {
    "id": "ver_01J9ZK8X",
    "hash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
    "bytes": 48213,
    "content_type": "image/png",
    "class": "image",
    "width": 1200,
    "height": 630,
    "variants": [
      "320",
      "640",
      "1280"
    ],
    "scan": {
      "status": "clean",
      "reason": null,
      "engine": "clamav",
      "scanned_at": "2026-10-09T14:05:02Z"
    },
    "url": "https://media.nebularcdn.com/marcas-k7p2/9f86d081884c/logos/acme.png",
    "uploaded_by": {
      "kind": "token",
      "name": "agente-deploy"
    },
    "created_at": "2026-10-09T14:05:01Z"
  },
  "alias_url": "https://media.nebularcdn.com/marcas-k7p2/logos/acme.png",
  "versions_count": 1,
  "created_at": "2026-10-09T14:05:01Z"
}
```

### Resposta 202

Aceito, mas **ainda não publicado** (`scan.status = pending`). Consulte o `GET` do arquivo até
`scan.status` sair de `pending`. Enquanto `pending`, `content_type`/`class` são provisórios
(`application/octet-stream`/`binary`) e `width`/`height` são `null`; os valores reais só aparecem após `clean`.

| Campo | Tipo | Obrig. | Descrição |
|---|---|---|---|
| `id` | string | sim |  |
| `name` | string | sim |  |
| `gallery_key` | string | sim |  |
| `current` | object | sim |  |
| `current.id` | string | sim |  |
| `current.hash` | string | sim | SHA-256 hexadecimal do conteúdo. |
| `current.bytes` | integer | sim |  |
| `current.content_type` | string | sim | Detectado pelo conteúdo. |
| `current.class` | enum(image, svg, font, media, document, markup, archive, binary) | sim | Classe do arquivo, detectada pelo conteúdo. Define como a CDN serve o arquivo (ver descrição geral). |
| `current.width` | integer\|null | sim | Somente imagens. |
| `current.height` | integer\|null | sim | Somente imagens. |
| `current.variants` | array<string> | sim | Larguras de variantes geradas (imagens). |
| `current.scan` | object | sim |  |
| `current.scan.status` | enum(pending, clean, rejected, quarantined) | sim | `pending`: aguardando varredura, não publicado. `clean`: publicado. `rejected`: recusado. `quarantined`: detectado na varredura diária; deixou de ser servido (451). |
| `current.scan.reason` | string\|null | sim | Motivo da recusa ou quarentena. |
| `current.scan.engine` | string\|null | sim |  |
| `current.scan.scanned_at` | string\|null | sim |  |
| `current.url` | string\|null | sim | URL **imutável** (`https://<bucket>.nebularcdn.com/<gallery_key>/<hash[0:12]>/<name>`), com `Cache-Control: public, max-age=31536000, immutable`; `null` enquanto o arquivo não estiver `clean`. Para imagens aceita `?w=640` (largura) e `?q=` (qualidade); o formato (AVIF/WebP) é negociado por `Accept` com `Vary: Accept`. Se a segregação de downloads estiver ligada, classes de download usam outro domínio; use sempre a URL devolvida pela API, sem montá-la. |
| `current.uploaded_by` | object | sim |  |
| `current.uploaded_by.kind` | enum(user, token) | sim |  |
| `current.uploaded_by.name` | string | sim |  |
| `current.created_at` | string | sim |  |
| `alias_url` | string\|null | sim | URL **alias** (`https://<bucket>.nebularcdn.com/<gallery_key>/<name>`), sempre a versão atual (`max-age=60, stale-while-revalidate=86400`); `null` enquanto não `clean`. Também aceita `?w=640`, ex.: `https://media.nebularcdn.com/marcas-k7p2/logos/acme.png?w=640`. |
| `versions_count` | integer | sim |  |
| `created_at` | string | sim |  |

```json
{
  "id": "ast_01J9ZK8W",
  "name": "logos/acme.png",
  "gallery_key": "marcas-k7p2",
  "current": {
    "id": "ver_01J9ZK8X",
    "hash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
    "bytes": 48213,
    "content_type": "application/octet-stream",
    "class": "binary",
    "width": null,
    "height": null,
    "variants": [],
    "scan": {
      "status": "pending",
      "reason": null,
      "engine": null,
      "scanned_at": null
    },
    "url": null,
    "uploaded_by": {
      "kind": "token",
      "name": "agente-deploy"
    },
    "created_at": "2026-10-09T14:05:01Z"
  },
  "alias_url": null,
  "versions_count": 1,
  "created_at": "2026-10-09T14:05:01Z"
}
```

### Resposta 409

Mesma `Idempotency-Key` usada com um corpo diferente.

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

### Resposta 413

Arquivo acima de 25 MB (ou 40 MP para imagens), ou cota do time excedida
(`payload_too_large` / `quota_exceeded`). Reduza o arquivo ou exclua arquivos antigos.

| 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": "payload_too_large",
  "message": "O arquivo excede o limite de 25 MB."
}
```

### Resposta 415

Tipo não reconhecido (`unsupported_type`), classe de arquivo não permitida no bucket
(`class_not_allowed`) ou extensão divergente do conteúdo detectado (`extension_mismatch`).
Corrija o nome ou o arquivo.

| 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": "unsupported_type",
  "message": "Este tipo de arquivo não é permitido neste bucket."
}
```

### Resposta 422

Recusado pela varredura: `malware_detected` (a mensagem traz o nome da detecção),
`svg_unsafe`, `archive_encrypted`, `archive_bomb` ou `archive_unsupported`. A amostra não é guardada. Não reenvie o mesmo conteúdo.

| 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": "malware_detected",
  "message": "Arquivo recusado: Win.Test.EICAR_HDB-1 detectado."
}
```

### 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; `503` Serviço (ex.: scanner ou borda) temporariamente indisponível. Tente de novo após `Retry-After`.

## GET `/v1/galleries/{key}/assets/{name}`

Detalhe do arquivo, versões, URLs e varredura

Retorna a versão atual, o histórico de versões e o status de varredura. Use para consultar um upload
`pending` até que `scan.status` mude. Escopo exigido: `assets:read`.

Escopo exigido: `assets:read` · operationId: `getAsset`

| Parâmetro | Em | Tipo | Obrig. | Descrição |
|---|---|---|---|---|
| `key` | path | string | sim | Chave pública imutável da galeria (ex.: `marcas-k7p2`). |
| `name` | path | string | sim | Nome do arquivo na galeria (até 4 segmentos separados por `/`, URL-encoded ou não, ex.: `logos%2Facme.png`). |

### Resposta 200

Arquivo.

| Campo | Tipo | Obrig. | Descrição |
|---|---|---|---|
| `asset` | object | sim |  |
| `asset.id` | string | sim |  |
| `asset.name` | string | sim |  |
| `asset.gallery_key` | string | sim |  |
| `asset.current` | object | sim |  |
| `asset.current.id` | string | sim |  |
| `asset.current.hash` | string | sim | SHA-256 hexadecimal do conteúdo. |
| `asset.current.bytes` | integer | sim |  |
| `asset.current.content_type` | string | sim | Detectado pelo conteúdo. |
| `asset.current.class` | enum(image, svg, font, media, document, markup, archive, binary) | sim | Classe do arquivo, detectada pelo conteúdo. Define como a CDN serve o arquivo (ver descrição geral). |
| `asset.current.width` | integer\|null | sim | Somente imagens. |
| `asset.current.height` | integer\|null | sim | Somente imagens. |
| `asset.current.variants` | array<string> | sim | Larguras de variantes geradas (imagens). |
| `asset.current.scan` | object | sim |  |
| `asset.current.scan.status` | enum(pending, clean, rejected, quarantined) | sim | `pending`: aguardando varredura, não publicado. `clean`: publicado. `rejected`: recusado. `quarantined`: detectado na varredura diária; deixou de ser servido (451). |
| `asset.current.scan.reason` | string\|null | sim | Motivo da recusa ou quarentena. |
| `asset.current.scan.engine` | string\|null | sim |  |
| `asset.current.scan.scanned_at` | string\|null | sim |  |
| `asset.current.url` | string\|null | sim | URL **imutável** (`https://<bucket>.nebularcdn.com/<gallery_key>/<hash[0:12]>/<name>`), com `Cache-Control: public, max-age=31536000, immutable`; `null` enquanto o arquivo não estiver `clean`. Para imagens aceita `?w=640` (largura) e `?q=` (qualidade); o formato (AVIF/WebP) é negociado por `Accept` com `Vary: Accept`. Se a segregação de downloads estiver ligada, classes de download usam outro domínio; use sempre a URL devolvida pela API, sem montá-la. |
| `asset.current.uploaded_by` | object | sim |  |
| `asset.current.uploaded_by.kind` | enum(user, token) | sim |  |
| `asset.current.uploaded_by.name` | string | sim |  |
| `asset.current.created_at` | string | sim |  |
| `asset.alias_url` | string\|null | sim | URL **alias** (`https://<bucket>.nebularcdn.com/<gallery_key>/<name>`), sempre a versão atual (`max-age=60, stale-while-revalidate=86400`); `null` enquanto não `clean`. Também aceita `?w=640`, ex.: `https://media.nebularcdn.com/marcas-k7p2/logos/acme.png?w=640`. |
| `asset.versions_count` | integer | sim |  |
| `asset.created_at` | string | sim |  |
| `versions` | array<object> | sim |  |
| `versions[].id` | string | sim |  |
| `versions[].hash` | string | sim | SHA-256 hexadecimal do conteúdo. |
| `versions[].bytes` | integer | sim |  |
| `versions[].content_type` | string | sim | Detectado pelo conteúdo. |
| `versions[].class` | enum(image, svg, font, media, document, markup, archive, binary) | sim | Classe do arquivo, detectada pelo conteúdo. Define como a CDN serve o arquivo (ver descrição geral). |
| `versions[].width` | integer\|null | sim | Somente imagens. |
| `versions[].height` | integer\|null | sim | Somente imagens. |
| `versions[].variants` | array<string> | sim | Larguras de variantes geradas (imagens). |
| `versions[].scan` | object | sim |  |
| `versions[].scan.status` | enum(pending, clean, rejected, quarantined) | sim | `pending`: aguardando varredura, não publicado. `clean`: publicado. `rejected`: recusado. `quarantined`: detectado na varredura diária; deixou de ser servido (451). |
| `versions[].scan.reason` | string\|null | sim | Motivo da recusa ou quarentena. |
| `versions[].scan.engine` | string\|null | sim |  |
| `versions[].scan.scanned_at` | string\|null | sim |  |
| `versions[].url` | string\|null | sim | URL **imutável** (`https://<bucket>.nebularcdn.com/<gallery_key>/<hash[0:12]>/<name>`), com `Cache-Control: public, max-age=31536000, immutable`; `null` enquanto o arquivo não estiver `clean`. Para imagens aceita `?w=640` (largura) e `?q=` (qualidade); o formato (AVIF/WebP) é negociado por `Accept` com `Vary: Accept`. Se a segregação de downloads estiver ligada, classes de download usam outro domínio; use sempre a URL devolvida pela API, sem montá-la. |
| `versions[].uploaded_by` | object | sim |  |
| `versions[].uploaded_by.kind` | enum(user, token) | sim |  |
| `versions[].uploaded_by.name` | string | sim |  |
| `versions[].created_at` | string | sim |  |

```json
{
  "asset": {
    "id": "ast_01J9ZK8W",
    "name": "logos/acme.png",
    "gallery_key": "marcas-k7p2",
    "current": {
      "id": "ver_01J9ZK8X",
      "hash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
      "bytes": 48213,
      "content_type": "image/png",
      "class": "image",
      "width": 1200,
      "height": 630,
      "variants": [
        "320",
        "640",
        "1280"
      ],
      "scan": {
        "status": "clean",
        "reason": null,
        "engine": "clamav",
        "scanned_at": "2026-10-09T14:05:02Z"
      },
      "url": "https://media.nebularcdn.com/marcas-k7p2/9f86d081884c/logos/acme.png",
      "uploaded_by": {
        "kind": "token",
        "name": "agente-deploy"
      },
      "created_at": "2026-10-09T14:05:01Z"
    },
    "alias_url": "https://media.nebularcdn.com/marcas-k7p2/logos/acme.png",
    "versions_count": 1,
    "created_at": "2026-10-09T14:05:01Z"
  },
  "versions": [
    {
      "id": "ver_01J9ZK8X",
      "hash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
      "bytes": 48213,
      "content_type": "image/png",
      "class": "image",
      "width": 1200,
      "height": 630,
      "variants": [
        "320",
        "640",
        "1280"
      ],
      "scan": {
        "status": "clean",
        "reason": null,
        "engine": "clamav",
        "scanned_at": "2026-10-09T14:05:02Z"
      },
      "url": "https://media.nebularcdn.com/marcas-k7p2/9f86d081884c/logos/acme.png",
      "uploaded_by": {
        "kind": "token",
        "name": "agente-deploy"
      },
      "created_at": "2026-10-09T14:05:01Z"
    }
  ]
}
```

### 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; `503` Serviço (ex.: scanner ou borda) temporariamente indisponível. Tente de novo após `Retry-After`.

## DELETE `/v1/galleries/{key}/assets/{name}`

Exclui o arquivo

Exclui o arquivo e purga o cache da CDN. O backup retém os bytes por 30 dias. Escopo exigido:
`assets:write`.

Escopo exigido: `assets:write` · operationId: `deleteAsset`

| Parâmetro | Em | Tipo | Obrig. | Descrição |
|---|---|---|---|---|
| `key` | path | string | sim | Chave pública imutável da galeria (ex.: `marcas-k7p2`). |
| `name` | path | string | sim | Nome do arquivo na galeria (até 4 segmentos separados por `/`, URL-encoded ou não, ex.: `logos%2Facme.png`). |

### Resposta 204

Excluído.

### 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; `503` Serviço (ex.: scanner ou borda) temporariamente indisponível. Tente de novo após `Retry-After`.
