# UniSupri Marketplace API · Documentação completa
Fonte: https://api-docs.samdevel.com.br/llms-full.txt
Esta página contém a concatenação de todos os guias da API. Para a referência OpenAPI completa, veja:
- JSON: https://api-docs.samdevel.com.br/openapi.json
- YAML: https://api-docs.samdevel.com.br/openapi.yaml
---
=================================================================
SOURCE: https://api-docs.samdevel.com.br/guides/primeiros-passos.md
=================================================================
# Introdução
Autenticar uma integração tem três passos: criar uma credencial, trocar a credencial por um token de acesso e usar o token na primeira chamada.
---
## 1. Criar uma credencial de integração
Toda integração começa por uma credencial. A credencial é um par **username + senha** que pertence à sua loja. Você pode criar quantas precisar (uma por ERP, hub, dashboard, etc.) e revogar individualmente.
1. Acesse o **Portal do Vendedor**.
2. No menu lateral, vá em **Configurações → API & Integrações**.
3. Clique em **Nova credencial** (canto superior direito).
4. Dê um nome descritivo da aplicação que vai usar a credencial. Exemplos: `ERP`, `Hub de integração`, `Dashboard de BI`, `Webhook de notificações`.
5. Confirme. A tela exibe a credencial recém-criada:
- **Username**: gerado automaticamente no formato `slug-da-loja@xxxxxxxx` (ex.: `loja-xpto@12345678`).
- **Senha**: string aleatória de 20 caracteres.
> ⚠️ A senha aparece **uma única vez**. Copie e guarde em local seguro (cofre de senhas, gerenciador de segredos do seu sistema). Se perder, dá pra gerar uma nova pelo botão **Trocar senha** na linha da credencial, mas a anterior deixa de funcionar.
---
## 2. Obter um token de acesso
Com a credencial em mãos, o seu sistema externo faz uma chamada **autenticada por username + senha** pra obter um token de acesso. Esse token é o que vai no `Authorization: Bearer` de todas as chamadas seguintes.
```bash
curl -X POST https://api.sandbox.samdevel.com.br/api/integration/auth \
-H 'Content-Type: application/json' \
-d '{
"username": "loja-xpto@12345678",
"password": "Xk9p2Lm7nQ4rT8vW3yZ1"
}'
```
Resposta:
```json
{
"success": true,
"data": {
"token": "sk_live_a1b2c3d4e5f6...",
"expires_at": "2026-06-19T12:34:56-03:00"
}
}
```
- O token vale por **30 dias**.
- Cada chamada ao `/api/integration/auth` emite um token **novo** e invalida o anterior. Esse é o mecanismo de rotação: não acumule tokens; gere um novo só quando o atual estiver perto de expirar.
> Detalhes completos da autenticação (limites, erros, troca de senha) estão no [guia de Autenticação](/guia/autenticacao).
---
## 3. Fazer sua primeira chamada
Use o token obtido como `Bearer` em qualquer endpoint da API. Para confirmar que está tudo certo, consulte a sessão da sua loja:
```bash
curl https://api.sandbox.samdevel.com.br/api/stores/auth/session \
-H "Authorization: Bearer sk_live_a1b2c3d4e5f6..."
```
Você deve receber os dados públicos da loja autenticada (id, nome, status, empresa).
=================================================================
SOURCE: https://api-docs.samdevel.com.br/guides/autenticacao.md
=================================================================
# Autenticação
A API do marketplace UniSupri usa autenticação **bearer token** em todos os endpoints de seller. O token é obtido a partir de uma credencial (username + senha) criada no Portal do Vendedor. Leia [Primeiros Passos](/guia/primeiros-passos) se ainda não criou a sua.
---
## Modelo
```mermaid
sequenceDiagram
autonumber
participant I as Integrador
participant A as API
I->>A: POST /integration/auth { username, password }
A-->>I: { token, expires_at }
I->>A: GET /api/* + Bearer sk_live_*
A-->>I: 200 OK
```
**Dois conceitos distintos:**
| Conceito | Onde mora | Quem usa | Quando renova |
|----------|-----------|----------|---------------|
| **Credencial** (`username` + `senha`) | Portal do Vendedor | Operador humano | Manualmente, quando suspeita de vazamento ou rotação programada |
| **Token de acesso** (`sk_live_*`) | Memória do ERP | Sistema externo | Automaticamente via `/api/integration/auth`, antes de expirar |
A credencial **fica parada** no seu cofre de senhas e não vai em chamadas de API. O token é o que circula em cada requisição.
---
## Obter um token
POST/api/integration/auth
Endpoint **público**, não exige `Authorization`. A própria credencial (no body) é a forma de autenticação.
### Request
```json
{
"username": "loja-xpto@12345678",
"password": "Xk9p2Lm7nQ4rT8vW3yZ1"
}
```
### Response 200
```json
{
"success": true,
"data": {
"token": "sk_live_a1b2c3d4e5f6...",
"expires_at": "2026-06-19T12:34:56-03:00"
}
}
```
### Rotação
Cada chamada bem-sucedida ao `/api/integration/auth`:
- Emite um **token novo**.
- Substitui o token anterior. O antigo deixa de funcionar imediatamente.
- Repopula `expires_at` para `agora + 30 dias`.
Implicação prática: **só existe 1 token ativo por credencial**. Se você roda dois sistemas que precisam de tokens independentes, crie **duas credenciais separadas** no portal.
### Quando renovar
Renove **antes** de expirar. Recomendamos disparar uma nova chamada quando faltarem 24-48h pra expiração. Você não vai encontrar um endpoint de refresh-token separado, e isso é proposital: pra manter o fluxo simples, a renovação é a própria chamada ao `/api/integration/auth` — a mesma que você já usa pra obter o token. Um endpoint só, que serve tanto pra primeira emissão quanto pra renovar. Se o token expirar e você fizer uma chamada com ele, a API responde **401** e o ERP deve renovar e tentar de novo.
---
## Usar o token
Em qualquer endpoint do seller, envie o token no cabeçalho `Authorization`:
```bash
curl https://api.sandbox.samdevel.com.br/api/products \
-H "Authorization: Bearer sk_live_a1b2c3d4e5f6..."
```
A API valida:
1. **Formato:** o token começa com `sk_live_` e tem comprimento esperado.
2. **Existência:** existe uma credencial associada a este token.
3. **Ativação:** a credencial não foi revogada (`active = false`).
4. **Validade:** `expires_at` ainda não passou.
Falha em qualquer uma → resposta **401 Unauthorized**.
---
## Confirmar a autenticação
GET/api/stores/auth/session
Use para validar que o token está autenticando a loja correta. A resposta traz os dados da loja (id, nome, código, status, tipo, limites e features), a `company` associada (CNPJ, razão social, regime tributário) e `auth_type: "integration_token"`.
---
## Limites de taxa
O `/api/integration/auth` é protegido contra brute-force:
- **5 tentativas por minuto** por par `(IP, username)`.
- Estourou o limite: **429 Too Many Requests**, com `Retry-After` em segundos.
O contador é por par IP+username; tentativas legítimas de outras lojas/IPs não são afetadas.
> Este limite é só da emissão de token. O limite das **demais rotas de integração** está em [Limites de uso](/guia/limites).
---
## Erros possíveis
| Status | Quando acontece | O que fazer |
|--------|-----------------|-------------|
| **401** no `/integration/auth` | Username inexistente, senha errada **ou** credencial inativa | Verifique username/senha. A mensagem é genérica de propósito e não revela qual dos três casos é. Se persistir, confirme no portal que a credencial está ativa. |
| **401** em endpoints autenticados | Token ausente, malformado, revogado ou expirado | Renove o token via `/integration/auth`. Se o erro persistir após renovar, a credencial pode ter sido revogada; verifique no portal. |
| **422** no `/integration/auth` | `username` ou `password` ausente / vazio | Garanta que os dois campos vão no body como strings não-vazias. |
| **429** no `/integration/auth` | Mais de 5 tentativas/minuto pra mesma credencial+IP | Aguarde o `Retry-After` segundos antes de tentar novamente. Cache o token e não chame o auth em loop. |
---
## Trocar a senha
Quando trocar:
- Você suspeita que a senha vazou.
- Rotação preventiva periódica (recomendado a cada 90 dias).
- Trocou de mãos a operação do ERP.
Como trocar:
1. Portal do Vendedor → **Configurações → API & Integrações**.
2. Localize a credencial na lista.
3. Clique no ícone de **chave** (`🔑 Trocar senha`).
4. Escolha **Gerar aleatória** (recomendado) ou **Definir manualmente** (mínimo 12 caracteres).
5. Copie a nova senha. Ela aparece **uma única vez**.
**Importante:** trocar a senha **não invalida o token corrente**. O ERP que está usando o token atual continua funcionando até a próxima expiração ou até você revogar a credencial inteira. Se você precisa **invalidar imediatamente** (suspeita de comprometimento), revogue a credencial (lixeira) e crie uma nova.
---
## Revogar uma credencial
Portal do Vendedor → **Configurações → API & Integrações** → ícone de **lixeira** na linha da credencial.
Efeito imediato:
- Username e senha deixam de autenticar no `/integration/auth`.
- O token corrente (se ainda estiver dentro do prazo) **continua válido até a expiração**, porque a checagem do middleware é pelo `token`, e o registro inteiro foi removido.
Se precisa cortar acesso de imediato, faça nessa ordem:
1. Marque a credencial como inativa (botão futuro) **ou** delete.
2. Acompanhe `last_used_at` para confirmar que o ERP parou de chamar.
---
## Boas práticas
- **Cache o token.** Não chame `/integration/auth` antes de cada requisição; emita um token e use-o pelos 30 dias.
- **Renove proativamente.** Agende um job que renove o token quando faltarem ~24h.
- **Trate 401 como sinal de renovação.** Se uma chamada autenticada retornar 401, renove o token e tente uma vez. Se ainda falhar, alerte um humano.
- **Uma credencial por sistema.** Não compartilhe credenciais entre ERPs diferentes; a rotação substitui o token, e isso quebra o sistema que ficou pra trás.
- **Não logue a senha nem o token em arquivos de log persistentes.** Use mascaramento (`sk_live_...XXXX`).
- **Cofre de senhas.** Guarde `username` + `password` em um gerenciador de segredos (1Password, Vault, AWS Secrets Manager, etc.), nunca em código-fonte commitado.
=================================================================
SOURCE: https://api-docs.samdevel.com.br/guides/limites.md
=================================================================
# Limites de uso
A API de integração aplica um limite de requisições por token, para proteger a plataforma contra loops e abuso. O limite é generoso: o uso normal de um ERP, hub ou automação não encosta nele.
---
## O limite
| Chamadas | Limite | Janela | Conta por |
|---|---|---|---|
| Rotas autenticadas pelo token de integração | 300 | 1 minuto | token (`sk_live_*`) |
| `POST /api/integration/auth` (emitir token) | 5 | 1 minuto | ver [Autenticação](/guia/autenticacao) |
O limite de 300/min vale para **todas as rotas** que você chama com o bearer token de integração (produtos, pedidos, estoque, etc.), somadas. A contagem é **por token**: cada credencial tem seu próprio balde, então separar sistemas em credenciais distintas também separa os limites.
> Emitir o token (`/integration/auth`) tem um limite próprio, mais estrito, porque é a porta de entrada — detalhes em [Autenticação](/guia/autenticacao). Algumas rotas pontuais também podem ter um limite próprio; nesses casos vale o menor.
---
## Headers
Toda resposta de uma rota de integração traz o estado do seu balde:
| Header | Significado |
|---|---|
| `X-RateLimit-Limit` | Teto da janela (ex.: `300`). |
| `X-RateLimit-Remaining` | Quantas chamadas ainda cabem na janela atual. |
Use o `Remaining` para se auto-regular: se estiver baixo, espace as chamadas antes de bater no teto.
---
## Quando você estoura: 429
Passou do teto, a API responde **429 Too Many Requests** com o envelope padrão:
```json
{
"success": false,
"code": 429,
"message_code": "TOO_MANY_REQUESTS",
"description": "Muitas requisições em pouco tempo. Aguarde antes de tentar novamente."
}
```
Junto vem o header **`Retry-After`**, com os segundos que faltam para a janela reabrir. O tratamento correto:
1. Leia o `Retry-After`.
2. Espere esse tempo — tentar antes não adianta, só renova o bloqueio.
3. Tente a chamada de novo.
---
## Boas práticas
- **Respeite o `Retry-After`.** Reenviar antes do prazo só consome tentativas e mantém você bloqueado.
- **Não faça polling agressivo.** Para acompanhar jobs assíncronos (DANFE, etiquetas), 3–10s entre chamadas basta.
- **Cache o token.** Não chame `/integration/auth` a cada requisição — ele tem o limite mais estrito (5/min). Emita um token e reutilize pelos 30 dias (ver [Autenticação](/guia/autenticacao)).
- **Uma credencial por sistema.** Além de organizar, isola o limite: um sistema em loop não derruba os outros.
- **Trate 429 como sinal de espera, não de falha.** Faça backoff e siga.
=================================================================
SOURCE: https://api-docs.samdevel.com.br/guides/midia.md
=================================================================
# Mídia
A **Mídia** é o utilitário de upload de imagens **compartilhado** da API. Em vez de cada recurso ter seu próprio upload, você sobe a imagem **uma vez** aqui, recebe um identificador e depois **vincula** esse identificador ao recurso final. Como ela serve a vários domínios ao mesmo tempo (produtos, anúncios e o que vier), faz mais sentido documentá-la num lugar só do que repetir o mesmo fluxo dentro de cada guia.
> Pré-requisitos: você precisa de um token válido. Veja [Autenticação](/guia/autenticacao).
Hoje quem consome a Mídia é:
- **Produtos** — a galeria do produto ([Produtos › Galeria](/guia/produtos#galeria)).
- **Anúncios** — as imagens da publicação ([Anúncios](/guia/anuncios)).
---
## Como funciona
O upload é **síncrono e em uma única chamada** (`multipart/form-data`): você manda o arquivo e, na mesma resposta, recebe a imagem já processada. O servidor converte tudo para **WebP**, gera automaticamente as **variações de tamanho** (`thumbnail`, `sm`, `md`, `lg`, `xl`) e devolve as URLs públicas de cada uma — você não precisa redimensionar nada do seu lado.
O identificador da imagem é o campo `id` (um **ULID**). É esse valor que você usa como `image_id` ao vincular a um produto ou anúncio, e como `{file_hash}` nas rotas de detalhe e exclusão.
---
## Ciclo de vida
A imagem nasce **solta** (`in_use: false`, sem `reference_id`). Ela só passa a contar como "em uso" quando você a vincula a um recurso — por exemplo, ao adicioná-la à galeria com `POST /api/products/{id}/gallery` passando o `id` como `image_id`.
- **Solta:** imagens sem vínculo são removidas por uma rotina de limpeza da plataforma (janela de ~72h). Suba a imagem só quando for usá-la em seguida.
- **Em uso:** uma imagem `in_use: true` fica protegida — qualquer tentativa de excluir ou alterar retorna **409**. Não é pra te atrapalhar: é uma trava de segurança pra você não derrubar, sem querer, uma foto que está aparecendo num produto ou anúncio publicado. Quando quiser mesmo remover, tire o vínculo no recurso primeiro (ex.: remova da galeria do produto) e aí a exclusão libera.
---
## Subir uma imagem
POST/api/media/upload
Envie como `multipart/form-data` no campo `image`:
| Regra | Valor |
|-------|-------|
| Campo | `image` (arquivo) |
| Formatos aceitos | `image/jpeg`, `image/png`, `image/webp` |
| Tamanho máximo | 2 MB |
```bash
curl -X POST https://api.sandbox.samdevel.com.br/api/media/upload \
-H "Authorization: Bearer sk_live_a1b2c3d4e5f6..." \
-F "image=@/caminho/para/foto.jpg"
```
Resposta **201**. O arquivo já vem convertido para WebP, com uma URL por tamanho:
```json
{
"success": true,
"data": {
"id": "01K8PBIMG00001VWXYZ12345678",
"original_source": "https://cdn.unisupri.com/products/2026/05/28/original_01K8PBIMG00001VWXYZ12345678.webp",
"urls": [
{ "name": "thumbnail", "size": "135x135", "url": "https://cdn.unisupri.com/.../thumbnail_01K8PBIMG....webp" },
{ "name": "sm", "size": "200x200", "url": "https://cdn.unisupri.com/.../sm_01K8PBIMG....webp" },
{ "name": "md", "size": "400x400", "url": "https://cdn.unisupri.com/.../md_01K8PBIMG....webp" },
{ "name": "lg", "size": "800x800", "url": "https://cdn.unisupri.com/.../lg_01K8PBIMG....webp" },
{ "name": "xl", "size": "1600x1600", "url": "https://cdn.unisupri.com/.../xl_01K8PBIMG....webp" }
],
"in_use": false,
"reference_id": null,
"reference_model": null,
"created_at": "2026-05-28T10:00:00-03:00",
"updated_at": "2026-05-28T10:00:00-03:00"
}
}
```
---
## Listar imagens
GET/api/media
Lista paginada das imagens da sua loja. Filtros:
| Parâmetro | Para quê |
|-----------|----------|
| `filter[reference_id]` | Imagens vinculadas a um recurso específico (ID do produto/anúncio). |
| `filter[reference_model]` | Tipo do recurso vinculado: `product` (produto), `publication` (anúncio) ou `store` (loja). |
| `filter[in_use]` | `true` (já vinculadas) ou `false` (soltas). |
| `sort` | `created_at` ou `updated_at` (prefixe com `-` para descendente). |
| `per_page` | Itens por página (1–100, default 15). |
---
## Detalhes de uma imagem
GET/api/media/{file_hash}
Retorna os mesmos dados do upload para a imagem cujo `id` você informar. **404** se ela não existir ou não pertencer à sua loja.
---
## Excluir uma imagem
DELETE/api/media/{file_hash}
Remove o registro e os arquivos no storage. Retorna **204**. Se a imagem estiver em uso por um produto ou anúncio (`in_use: true`), retorna **409** — desvincule antes.
=================================================================
SOURCE: https://api-docs.samdevel.com.br/guides/loja.md
=================================================================
# Guia da Loja
Gerencie a página pública da sua loja, identidade visual e avaliações.
> **🚧 BETA**: esta API ainda está em revisão. Os endpoints listados aqui funcionam, mas **schemas, nomes de campos e comportamentos podem mudar** antes da versão estável. Recomendamos integrar com flexibilidade: trate respostas como dicionários, não dependa rigidamente de campos opcionais.
---
## Página da Loja
A página pública é o que o comprador vê ao acessar a sua loja no marketplace. Tem três pedaços que você gerencia por API: **textos descritivos**, **logo** e **imagem de fundo**.
> **⚠️ Por enquanto, só pelo portal.** Os endpoints de Página da Loja exigem escopos que hoje ainda não são atribuíveis a tokens de integração — chamadas com token `sk_live_*` retornam **403**. Essa parte da API, por ora, só funciona pelo portal do seller (as rotas de [Avaliações](#avaliações) funcionam normalmente com token de integração). Esse é parte do motivo de a API da Loja seguir em BETA.
### Consultar
GET/api/stores/page
Retorna tudo: dados da loja, da `company`, textos descritivos e URLs públicas do logo/background.
### Textos descritivos
PUT/api/stores/page
Quatro campos opcionais. Envie só os que quer atualizar; os omitidos não mudam. Pra **limpar** um campo, mande `null` — mas repare na leitura de volta: os campos de texto (`short_description`, `description`, `full_description`) voltam como string vazia (`""`); só `foundation_date` retorna `null` de verdade.
| Campo | Limite | Para que serve |
|-------|--------|----------------|
| `short_description` | 255 chars | Subtítulo curto, exibido logo abaixo do nome da loja. |
| `description` | 500 chars | Resumo na seção "Sobre" da página. |
| `full_description` | sem limite | Texto longo da aba "Sobre" expandida. Suporta múltiplos parágrafos. |
| `foundation_date` | data ISO | Data de fundação, vira "Loja desde 2015" no cabeçalho. |
Exemplo:
```json
{
"short_description": "Tecnologia no atacado",
"description": "Loja referência em atacado de tecnologia desde 2015.",
"foundation_date": "2015-05-20"
}
```
### Logo e imagem de fundo
Ambos são enviados via `multipart/form-data` no campo `image` direto nos endpoints da própria loja. Aqui você não usa o fluxo de Mídia (`/api/media/upload`), e é de propósito: logo e fundo são peças únicas da loja — não uma galeria — então não faz sentido subir, guardar um `id` e vincular depois. Você manda o arquivo direto no endpoint e ele já substitui o anterior. Especificação:
| Item | Valor |
|------|-------|
| Campo do upload | `image` |
| Tipos aceitos | `jpeg`, `png`, `jpg`, `gif`, `webp` |
| Tamanho máximo | 5 MB |
| Comportamento | Substitui o arquivo anterior (sem versionamento). A URL pública é a mesma. |
Após o upload, a URL final aparece no `GET /api/stores/page`. Pode levar alguns segundos pra CDN propagar.
#### Logo
POST/api/stores/page/logoDELETE/api/stores/page/logo
Exibido no cabeçalho da página da loja e nos cards de listagem do marketplace. Recomendado: imagem quadrada (1:1), fundo transparente (PNG/WebP), mínimo 256×256.
#### Background
POST/api/stores/page/backgroundDELETE/api/stores/page/background
Banner horizontal no topo da página da loja. Recomendado: largura mínima 1920px, proporção ~3:1 (ex.: 1920×640). Sem texto importante na imagem, porque partes podem ser cortadas em telas estreitas.
---
## Avaliações
Nota da loja calculada automaticamente a partir das avaliações dos pedidos entregues.
### Resumo
GET/api/stores/rating
### Histórico
GET/api/stores/rating/history
=================================================================
SOURCE: https://api-docs.samdevel.com.br/guides/produtos.md
=================================================================
# Visão geral
Cadastro do produto base: especificações, galeria, enums e o fluxo completo de cadastro. **Preços e estoque** têm guias próprios.
| Tema | Guia |
|---|---|
| Tabela de preços (`default` / `wholesale`) | [Produtos: preços](/guia/produtos-precos) |
| Movimentação e saldo de estoque | [Produtos: estoque](/guia/produtos-estoque) |
---
## Conceitos
**Unidade de medida (`base_unit`):** `un`, `pc`, `cx`, `pct`, `kg`, `lt`, `mt`, `m2`, `m3`.
**Tipo de preço (`price_type`):** `default` (valor único) ou `wholesale` (até 5 faixas progressivas por quantidade). Detalhes em [Preços](/guia/produtos-precos).
**Disponibilidade:** `is_available_for_sale` controla a visibilidade do produto no catálogo. `is_industrializable` indica produto fabricado sob demanda.
**Dimensões (obrigatórias para frete):** `dimensions.height`, `dimensions.width`, `dimensions.length` (cm) e `dimensions.weight` (kg). Todos os valores devem ser no mínimo 0.001.
O objeto que `GET /products/{id}` devolve — produto, preços e estoque juntos:
```jsonc
{
"product": {
"id": "01H8X…", // ID do produto
"name": "Produto Exemplo A",
"sku": "SKU-001", // único por loja
"base_unit": "un", // ← Unidade de medida
"price_type": "default", // ← Tipo de preço (default | wholesale)
"dimensions": {…}, // ← Dimensões (cm) + peso (kg)
"is_available_for_sale": true, // ← Disponibilidade no catálogo
"is_industrializable": false, // fabricado sob demanda
"auto_wholesale_pricing": false, // atacado por % de desconto (ver Preços)
"allow_sale_without_stock": false, // venda sem estoque (sob encomenda)
"quantity": 100, // estoque físico
"reserved_quantity": 5, // reservado em pedidos abertos
"available_quantity": 95, // disponível p/ venda
"thumbnail": { // imagem principal
"id": "01JMZ…", // ID da imagem
"resources": [{…}] // variações (name, size, url)
},
"price": 50.0 // preço-base (faixa min_quantity=1)
},
"prices": [{…}], // faixas de preço (ver Preços)
"stock": [{…}] // saldo por local (ver Estoque)
}
```
---
## Produto
| Campo | Descrição |
|-------|-----------|
| `id` | ULID, gerado automaticamente. |
| `name` | Nome (máx. 255 caracteres). |
| `sku` | Código único por loja (máx. 50 caracteres). |
| `base_unit` | Unidade de medida. |
| `price_type` | `default` ou `wholesale`. |
| `is_available_for_sale` | Visível no catálogo. |
| `is_industrializable` | Fabricado sob demanda. |
| `allow_sale_without_stock` | Permite venda sem saldo em estoque (sob encomenda). |
| `auto_wholesale_pricing` | Faixas de atacado derivadas automaticamente do % de desconto. Detalhes em [Preços](/guia/produtos-precos). |
| `dimensions` | Altura, largura, comprimento (cm) e peso (kg). |
> O `sku` é definido na criação e não pode ser alterado: se vier no `PUT /api/products/{id}`, é simplesmente ignorado.
> Sublojas em **modo espelho** não criam SKU próprio — o catálogo vem da matriz. O `POST /api/products` responde `422 SUBSTORE_CANNOT_CREATE_SKU`.
---
## Galeria
Cada produto suporta até **6 imagens**. A primeira é a thumbnail por padrão; se ela for removida, a próxima imagem assume o lugar.
### Fluxo de upload
1. Suba o arquivo via [Mídia](/guia/midia). Ele retorna o `image_id`.
2. Vincule à galeria com `POST /api/products/{id}/gallery`.
### Endpoints
---
## Fluxo: cadastro completo
| Etapa | Ação | Onde |
|-------|------|------|
| 1 | Consultar enums | `GET /api/global/enums/product/units` |
| 2 | Criar produto | `POST /api/products` |
| 3 | Definir preço (`default` ou `wholesale`) | [Produtos: preços](/guia/produtos-precos) |
| 4 | Definir estoque inicial | [Produtos: estoque](/guia/produtos-estoque) |
| 5 | Upload de imagens | `POST /api/media/upload` ([Mídia](/guia/midia)) |
| 6 | Vincular à galeria | `POST /api/products/{id}/gallery` |
Depois disso, o produto está pronto para virar **[anúncio](/guia/anuncios)** e ir ao marketplace.
=================================================================
SOURCE: https://api-docs.samdevel.com.br/guides/produtos-precos.md
=================================================================
# Preços
Tabela de preços do produto. Dois modelos: **único** (`default`) ou **progressivo por quantidade** (`wholesale`, até 5 faixas).
> Pré-requisitos: leia [Produtos: visão geral](/guia/produtos). O `price_type` é definido no próprio produto (`POST /api/products`); este guia trata só da tabela de preços por trás dele.
---
## Conceitos
**`price_type`** vive no produto e dita o comportamento da tabela:
| `price_type` | Faixas permitidas | `min_quantity` |
|---|---|---|
| `default` | Exatamente **1** | Sempre `1` |
| `wholesale` | **1 a 5** progressivas | `1`, depois valores crescentes |
**Faixa** é cada linha da tabela: par `(min_quantity, price)`. A faixa que vale numa venda é a de maior `min_quantity` cujo valor é `≤` quantidade comprada.
A chave de cada faixa é o `min_quantity`. É por isso que `PUT` e `DELETE` recebem ele no path.
> **Quer voltar de `wholesale` para `default`?** Só depois de deixar uma faixa só. Como o `default` é, por definição, uma tabela de uma linha, a plataforma não tem como adivinhar qual das suas faixas de atacado deve sobreviver — então a decisão fica com você: apague as faixas extras primeiro e aí a troca é liberada.
`GET /products/{id}/prices` devolve sempre um **array de faixas** — o que muda é o conteúdo conforme o `price_type`.
**Padrão (`default`)** — exatamente 1 faixa, sem medidas:
```jsonc
[
{
"min_quantity": 1, // sempre 1 no default
"value": 89.9, // preço unitário único
"discount_percent": null, // não se aplica
"package_weight": null, // medidas ficam null no default
"package_height": null,
"package_width": null,
"package_length": null
}
]
```
**Atacado (`wholesale`)** — 1 a 5 faixas progressivas, cada uma com as medidas da embalagem:
```jsonc
[
{
"min_quantity": 1, // faixa base: preço e medidas de UMA unidade
"value": 50.0,
"discount_percent": null, // no atacado automático a base fica 0; o % vai nas faixas seguintes
"package_weight": 1.2,
"package_height": 10.0,
"package_width": 12.0,
"package_length": 15.0
},
{
"min_quantity": 10, // faixa de atacado: a partir de 10 un
"value": 45.0, // preço unitário menor
"discount_percent": null,
"package_weight": 12.5, // medidas da EMBALAGEM fechada
"package_height": 30.0,
"package_width": 40.0,
"package_length": 50.0
}
]
```
---
## Preço padrão (`default`)
Produto vendido por valor unitário. Uma única faixa, sempre com `min_quantity = 1`.
### Criar
POST/api/products/{id}/prices
```json
{
"min_quantity": 1,
"price": 89.90
}
```
### Atualizar
PUT/api/products/{id}/prices/1
```json
{
"price": 79.90
}
```
---
## Preço de atacado (`wholesale`)
Tabela progressiva por volume, até 5 faixas. Cada faixa tem o **preço unitário** vendido **a partir** daquela quantidade.
> **Recomendado: precifique por desconto (`%`), não por valor fixo.** Ligando o `auto_wholesale_pricing`, você informa só o `discount_percent` de cada faixa e deixa o preço ser derivado do preço base. A gestão fica **automática**: você reajusta o preço base num lugar só e **todas as faixas se recalculam sozinhas** — sem reabrir faixa por faixa e sem risco de a tabela ficar inconsistente. Os dois modos estão documentados abaixo, mas comece pelo [Atacado automático](#atacado-automatico).
Exemplo de tabela montada:
| Faixa | `min_quantity` | `price` (unitário) | Quando vale |
|---|---|---|---|
| 1 | 1 | R$ 50,00 | 1 a 9 unidades |
| 2 | 10 | R$ 45,00 | 10 a 49 unidades |
| 3 | 50 | R$ 40,00 | 50 unidades ou mais |
A faixa base (`min_quantity = 1`) é sempre o preço unitário. As faixas de atacado são as de `min_quantity ≥ 2`.
### Medidas da embalagem (obrigatórias)
Toda faixa de atacado representa uma **embalagem fechada** (caixa/fardo) — é com ela que a plataforma calcula o frete da venda fracionada. Por isso, a **primeira faixa de atacado precisa trazer as medidas completas** da embalagem:
| Campo | Unidade | Regra |
|---|---|---|
| `package_weight` | kg | > 0 |
| `package_height` | cm | > 0 |
| `package_width` | cm | > 0 |
| `package_length` | cm | > 0 |
Os quatro campos são **tudo-ou-nada**: ou você manda os quatro, ou nenhum. Mandar só parte deles é recusado com `422`.
Depois que existe **uma** faixa medida, as faixas seguintes podem ser criadas **sem** as medidas — elas herdam a embalagem de referência. O produto só não pode ficar sem nenhuma faixa medida: por isso a remoção da última faixa com medidas (havendo outras faixas dependendo dela) também é recusada com `422`.
> A faixa base (`min_quantity = 1`) carrega as medidas de **uma unidade** (o produto avulso); as faixas de atacado (`min_quantity ≥ 2`) carregam as medidas da **embalagem fechada** vendida naquela faixa. Assim o frete sai correto tanto na venda unitária quanto no fardo.
### Adicionar uma faixa (modo manual)
POST/api/products/{id}/prices
No modo manual você informa o `price` de cada faixa diretamente — e fica responsável por reajustar faixa a faixa quando o preço mudar. (Para uma gestão mais simples, prefira o [Atacado automático](#atacado-automatico) — a abordagem recomendada — em que você precifica por desconto.)
Faixa base (`min_quantity = 1`) — só o preço. As medidas de **uma unidade** vivem no cadastro do produto (`dimensions`, ver [Produtos: visão geral](/guia/produtos)) e são apenas projetadas na leitura da faixa base — enviar `package_*` aqui não tem efeito:
```json
{
"min_quantity": 1,
"price": 50.00
}
```
Primeira faixa de atacado — com as medidas da **embalagem fechada**:
```json
{
"min_quantity": 10,
"price": 45.00,
"package_weight": 12.5,
"package_height": 30.0,
"package_width": 40.0,
"package_length": 50.0
}
```
Faixas seguintes podem omitir as medidas (herdam a referência):
```json
{
"min_quantity": 50,
"price": 40.00
}
```
### Reajustar uma faixa existente
PUT/api/products/{id}/prices/10
O corpo aceita o `price` e/ou as medidas. O merge parcial vale só para as medidas e o `discount_percent` — **o `price` não**: se você não mandar, ele é zerado (no modo manual e na faixa base). Sempre reenvie o `price` no `PUT`.
```json
{
"price": 42.50
}
```
### Remover uma faixa
DELETE/api/products/{id}/prices/10
---
## Atacado automático
**É a abordagem recomendada para o atacado:** o preço passa a ser gerido num ponto só, o que mantém a tabela sempre coerente.
O produto tem um interruptor `auto_wholesale_pricing` (definido no próprio produto, junto do `price_type`). Quando **ligado**, você para de informar o `price` das faixas de atacado e passa a informar só o **desconto** (`discount_percent`) — o preço de cada faixa é **derivado do preço unitário base menos o desconto**.
O `discount_percent` é sempre **relativo ao preço base**. O que fica salvo na faixa é o desconto, não um valor fixo: por isso, **se você mudar o preço base, todas as faixas de atacado são recalculadas** aplicando de novo o desconto de cada uma. Você ajusta o preço num lugar só.
Exemplo: preço base R$ 50,00, faixa de 50 un com `discount_percent: 20` → preço da faixa = R$ 40,00. Se depois o base virar R$ 60,00, a mesma faixa passa sozinha para R$ 48,00.
Regras desse modo:
- O **preço base** (`min_quantity = 1`) precisa existir antes de criar faixas de atacado — é dele que sai o cálculo. Sem ele, a API responde `422`.
- O `discount_percent` vai de `0` a menos de `100`.
- Os descontos precisam ser **progressivos**: faixa de quantidade maior exige desconto maior que a anterior (o preço sempre cai conforme a quantidade sobe). Caso contrário, `422`.
Faixa de atacado por desconto (em vez de `price`):
```json
{
"min_quantity": 50,
"discount_percent": 20,
"package_weight": 12.5,
"package_height": 30.0,
"package_width": 40.0,
"package_length": 50.0
}
```
> Comparando os dois modos para a mesma faixa: no **manual** você manda `"price": 40.00`; no **automático** você manda `"discount_percent": 20` e a API calcula o preço a partir do base. No modo manual o `discount_percent` é ignorado.
---
## Listar a tabela
GET/api/products/{id}/prices
```json
{
"success": true,
"message_code": "SUCCESS",
"data": [
{
"value": 50.00,
"min_quantity": 1,
"discount_percent": null,
"package_weight": 1.2,
"package_height": 10.0,
"package_width": 12.0,
"package_length": 15.0
},
{
"value": 45.00,
"min_quantity": 10,
"discount_percent": null,
"package_weight": 12.5,
"package_height": 30.0,
"package_width": 40.0,
"package_length": 50.0
}
]
}
```
Cada faixa traz o valor (`value`), o `min_quantity`, o `discount_percent` (preenchido no atacado automático) e as medidas da embalagem. Já vem ordenado por `min_quantity` ascendente, pronto pra renderizar na tela do comprador.
=================================================================
SOURCE: https://api-docs.samdevel.com.br/guides/produtos-estoque.md
=================================================================
# Estoque
Movimentação e consulta de saldo. O estoque tem três visões: o que **existe** fisicamente, o que está **reservado** em carrinhos/pedidos abertos, e o que sobra **disponível** para nova venda.
> Pré-requisitos: leia [Produtos: visão geral](/guia/produtos). O produto precisa existir antes de receber estoque; a rota é por `productId`.
---
## Conceitos
| Campo | O que é |
|---|---|
| `quantity` | Estoque físico total na sua loja. |
| `reserved_quantity` | Já comprometido em carrinhos/pedidos em aberto. |
| `available_quantity` | O que sobra pra venda: `quantity - reserved_quantity`. |
**Regra:** o catálogo respeita o `available_quantity`. Se ele zera, o produto deixa de aparecer pra novas compras, mesmo que ainda exista fisicamente.
O objeto que `GET /products/{id}/stock` devolve — total consolidado + saldo por localização:
```jsonc
{
"total": 150, // soma das quantidades em todos os locais
"stocks": [ // ← saldo por localização
{
"id": 321, // ID do registro de saldo
"product_sku": {…}, // resumo do produto dono do saldo
"location_stock": {…}, // a localização deste saldo
"quantity": 100, // físico
"reserved_quantity": 5, // reservado em pedidos abertos
"available_quantity": 95 // o que sobra pra venda
}
],
"locations": [{…}] // metadados dos locais (name, status, location_type…)
}
```
### Tipos de movimentação
A rota de movimentação aceita 3 modos, controlados pelas flags booleanas `increment` e `decrement`:
| Flags enviadas | Efeito sobre `quantity` |
|---|---|
| nenhuma | Sobrescreve com o valor enviado. Útil pra **acerto de inventário**. |
| `increment: true` | Soma. Útil pra **entrada de fornecedor**. |
| `decrement: true` | Subtrai. Útil pra **baixa manual** (perda, avaria, venda fora do canal). |
Mandar as duas flags juntas é recusado com `400`.
> Os 3 modos só mexem em `quantity` — e você pode estar se perguntando como ajustar o `reserved_quantity`. A resposta é: você não precisa. Ele é cuidado pela plataforma, que reserva e libera saldo sozinha conforme os pedidos abrem e fecham. Isso evita que duas vendas simultâneas briguem pela mesma unidade, então deixe essa conta com a gente e foque só no estoque físico.
---
## Consultar saldo
GET/api/products/{id}/stock
```json
{
"data": {
"total": 150,
"stocks": [
{
"id": 321,
"product_sku": {
"id": "01H81AV32307PVBSV4RXF15EK9",
"name": "Caneta Esferográfica Azul",
"sku": "CAN-AZ-01"
},
"location_stock": {
"location_id": "01H81AV32307PVBSV4RXF15LOC",
"store_id": 42,
"name": "Matriz - São Paulo",
"status": "available",
"location_type": "seller_location",
"allows_pickup": false,
"allows_production": false
},
"quantity": 100,
"reserved_quantity": 5,
"available_quantity": 95
}
],
"locations": [
{
"location_id": "01H81AV32307PVBSV4RXF15LOC",
"store_id": 42,
"name": "Matriz - São Paulo",
"status": "available",
"location_type": "seller_location",
"allows_pickup": false,
"allows_production": false
}
]
}
}
```
> No exemplo, o `product_sku` aparece resumido — na prática ele vem com o cadastro completo do produto (preços, thumbnail, dimensões…).
---
## Movimentar
POST/api/products/{id}/stock
O `location_id` é **obrigatório** — diz em qual local o saldo se move. Sem flag nenhuma, o valor enviado **sobrescreve** a quantidade:
```json
{
"location_id": "01H81AV32307PVBSV4RXF15LOC",
"quantity": 150
}
```
### Exemplos por cenário
**Recebi 40 unidades do fornecedor:**
```json
{
"location_id": "01H81AV32307PVBSV4RXF15LOC",
"quantity": 40,
"increment": true
}
```
**Inventário deu 95 (era 100):**
```json
{
"location_id": "01H81AV32307PVBSV4RXF15LOC",
"quantity": 95
}
```
**Avaria, baixar 3:**
```json
{
"location_id": "01H81AV32307PVBSV4RXF15LOC",
"quantity": 3,
"decrement": true
}
```
=================================================================
SOURCE: https://api-docs.samdevel.com.br/guides/anuncios.md
=================================================================
# Guia de Anúncios
Cadastrar um produto não o coloca à venda — quem aparece na vitrine é o **anúncio**. Este guia mostra os dois caminhos para vender no marketplace:
1. **Publicar um anúncio próprio** — você cria a página do produto do zero: categoria, atributos, imagens, descrição.
2. **Ofertar em um anúncio de catálogo** — o anúncio já existe na plataforma; você só pluga o seu SKU como uma oferta.
---
## Pré-requisitos
- **[Produtos cadastrados](/guia/produtos)** com SKUs válidos.
- **Preços** configurados (base e/ou atacado).
- **Estoque** disponível nos locais de venda.
- **Imagens** já enviadas via [Mídia](/guia/midia) (`POST /api/media/upload`).
---
## Conceitos
Os três pilares de um anúncio têm guias próprios — vale ler antes da primeira publicação:
- **[Categorias](/guia/anuncios-categorias)** — todo anúncio nasce numa categoria folha da árvore (departamento → níveis → folha). É a categoria que define quais atributos se aplicam.
- **[Atributos e variações](/guia/anuncios-atributos)** — características do produto (marca, material) e o que diferencia cada variação (cor, tamanho, voltagem), incluindo os tipos de valor e a cor customizada.
- **[Imagens](/guia/anuncios-imagens)** — como vincular as imagens enviadas pela Mídia ao anúncio e a cada variação.
O objeto do anúncio, de relance (resposta de `POST /items/create` — recorte; a resposta completa traz mais campos, como `slug`, `price_range` e `short_description`):
```jsonc
{
"item_id": "SAM-0000000000007", // ID do anúncio (prefixo da plataforma + 13 dígitos)
"title": "Caixa de Som JBL Go!",
"type": {…}, // { value, label } — "default" = anúncio próprio
"status": {…}, // { value, label } — status do anúncio
"gtin": {…}, // código de barras (type, value)
"store": {…}, // loja (store_id, store_name, store_code, …)
"department": {…}, // departamento (id, name, icon_svg)
"category": {…}, // categoria (id, parent_id, name, hierarchy)
"score": {…}, // qualidade do anúncio
"rejection_reason": null // preenchido quando a revisão devolve pra draft
}
```
## Status do anúncio
`draft`, `pending_review`, `active`, `paused`, `inactive`.
```mermaid
stateDiagram-v2
direction LR
[*] --> draft: POST /items/create
draft --> pending_review: PUT /publish
pending_review --> active: aprovado (operador)
pending_review --> draft: rejeitado (volta pra ajuste com rejection_reason)
```
> Você não vai achar um endpoint pra mandar o anúncio para `paused` ou `inactive` — e isso é proposital. Esses dois são estados administrativos: quem os aciona é a plataforma (por exemplo, ao suspender um anúncio que violou alguma regra). Do seu lado, você conduz o anúncio pelo caminho que controla — `draft` → `pending_review` → `active` — e deixa os estados administrativos por conta de quem cuida da moderação.
---
## Fluxo 1 — Publicar um anúncio próprio
A etapa 3 só roda se o produto tiver variações; sem variações, da 2 você vai direto pra 4.
```mermaid
flowchart TD
E1[1. Buscar categoria GET /categories/search] --> E2[2. Consultar atributos GET /categories/id/attributes]
E2 --> Q{Tem atributos com is_combinable?}
Q -->|não| E4
Q -->|sim| E3[3. Gerar combinações POST /categories/id/combine-attributes]
E3 --> E4[4. Validar POST /items/simulate]
E4 --> E5[5. Criar rascunho POST /items/create]
E5 --> E6[6. Checklist + publicar PUT /items/id/publish]
```
| Etapa | Ação | Endpoint |
|-------|------|----------|
| 1 | Buscar categoria | `GET /api/categories/search` |
| 2 | Consultar atributos | `GET /api/categories/{id}/attributes` |
| 3 | Gerar combinações (se houver variações) | `POST /api/categories/{id}/combine-attributes` |
| 4 | Validar anúncio | `POST /api/items/simulate` |
| 5 | Criar rascunho | `POST /api/items/create` |
| 6 | Conferir checklist e publicar | `PUT /api/items/{id}/publish` |
### Etapa 1. Buscar categoria
GET/api/categories/search
Retorna apenas categorias folhas (último nível). Parâmetros:
| Parâmetro | Para quê |
|-----------|----------|
| `search` | Termo de busca (obrigatório). |
| `departament_id` | Filtrar por departamento. |
Para navegar pela hierarquia nível a nível, use `GET /api/categories/browse`; para listar departamentos, `GET /api/categories/departaments`. Os três endpoints — e a receita de quando usar cada um — estão no guia de [Categorias](/guia/anuncios-categorias).
### Etapa 2. Consultar atributos
GET/api/categories/{id}/attributes
| Parâmetro | Para quê |
|-----------|----------|
| `matrix=true` | Agrupa atributos por seção (Identificação, Técnico, ...). Sem ele, a resposta é uma lista plana. |
| `only_features=1` | Apenas características do produto. |
| `only_variations=1` | Apenas atributos de variação. |
> `only_features` e `only_variations` juntos retornam **422** — escolha um.
Cada atributo vem com flags (`is_required`, `is_variant`, `is_combinable`, ...) e uma definição de valor autodescritiva. Como interpretar tudo isso — e como montar o payload de cada tipo — está no guia de [Atributos e variações](/guia/anuncios-atributos).
### Etapa 3. Gerar combinações
POST/api/categories/{id}/combine-attributes
Se a etapa 2 retornou atributos com `is_combinable: true`, o produto pode ter variações (cor + tamanho, voltagem + cor...). Esse endpoint recebe os valores escolhidos e devolve o produto cartesiano pronto para virar o array `variations[]` do create:
```json
{
"attributes": {
"1": ["110 V", "220 V"],
"3": ["Azul", "Verde"]
},
"primary_attribute_id": 1
}
```
Resultado: 4 combinações (110 V/Azul, 110 V/Verde, 220 V/Azul, 220 V/Verde), em `data.combinations`, acompanhadas de `data.primary_attribute`. O `primary_attribute_id` define o atributo principal usado para **organizar as fotos por variação** (default: o primeiro atributo).
> Produto sem variações? Pule direto para a etapa 4.
### Etapa 4. Validar anúncio
POST/api/items/simulate
Mesmo body da criação. Verifica:
- Atributos obrigatórios da categoria preenchidos.
- GTIN válido, quando informado (8, 12, 13 ou 14 dígitos).
- Formato dos dados (`description.layout`, `description.raw_content`, `technical_sheets[]`).
- Estrutura das variações.
- SKUs existentes na loja, com preço e estoque ativos.
- IDs de imagem existentes na plataforma.
Resposta **200** = pronto para criar. Erros de negócio (ex.: SKU sem estoque) voltam **422** com o motivo em `data.reason` e detalhes em `data.details`.
### Etapa 5. Criar anúncio
POST/api/items/create
```json
{
"title": "Caixa de Som JBL Go!",
"category_id": 121,
"gtin": { "type": 13, "value": "6925281995583" },
"attributes": [
{ "id": 1, "value_id": 3, "value": "HyperX" }
],
"variations": [
{
"seller_sku": "FK-JBL-2000-AZUL",
"attributes": [
{ "attribute_id": 1, "attribute_name": "Voltagem", "value": "110 V" }
],
"images": [{ "id": "{{image_id}}" }]
}
],
"images": [{ "id": "{{image_id}}" }],
"description": {
"layout": "markdown",
"raw_content": "## Caixa de Som JBL Go!\n\nSom potente, bateria de longa duração e resistência à água."
},
"technical_sheets": [
{
"type": "technical_specification",
"title": "Especificações Técnicas",
"items": [
{ "item_key": "Potência", "item_value": "4,2W RMS", "display_order": 0 },
{ "item_key": "Bateria", "item_value": "5 horas", "display_order": 1 }
]
}
]
}
```
#### Campos
| Campo | Obrigatório | Descrição |
|-------|-------------|-----------|
| `title` | Sim | Título do anúncio (máx. 100 caracteres). |
| `category_id` | Sim | ID da categoria (etapa 1). |
| `seller_sku` | Quando não há `variations` | SKU do produto vendido no anúncio sem variações. |
| `variations` | Quando não há `seller_sku` | Variações com SKU, atributos e imagens próprias (etapa 3). |
| `images` | Não* | IDs das imagens enviadas via [Mídia](/guia/anuncios-imagens). *Opcional no create, mas o checklist exige pelo menos 1 pra publicar. |
| `gtin` | Não | Código de barras (`type`: 8, 12, 13 ou 14). |
| `attributes` | Não | Características do produto (etapa 2). |
| `description` | Não | `{ layout, raw_content }`. Aceita também string (legado, equivale a `layout=markdown`). |
| `technical_sheets` | Não | Lista de fichas (`type`, `title`, `items[]`). |
> Os SKUs informados precisam **existir previamente** na loja, com preço e estoque configurados — o create não cria SKU.
> Preenchendo `description` e `technical_sheets` no `create` você dispensa chamadas a `PUT /description` e `POST /technical-sheets`. Esses endpoints continuam disponíveis para edição posterior.
O anúncio nasce como **`draft`**. Nesse status você pode editar tudo livremente.
### Etapa 6. Checklist e publicação
GET/api/items/{id}/review-checklist
Antes de publicar, confira o que falta:
```json
{
"data": {
"ready": false,
"issues": [
{ "code": "required_attributes", "severity": "error", "message": "Preencha os atributos obrigatórios da categoria.", "field": "attributes" },
{ "code": "short_description", "severity": "warning", "message": "Uma descrição curta melhora a conversão.", "field": "short_description" }
],
"checks": [
{ "code": "image", "label": "Pelo menos 1 imagem", "ok": true },
{ "code": "title", "label": "Título preenchido", "ok": true },
{ "code": "short_description", "label": "Descrição curta", "ok": false },
{ "code": "description", "label": "Descrição preenchida", "ok": true },
{ "code": "required_attributes", "label": "Atributos obrigatórios", "ok": false },
{ "code": "sku", "label": "SKU vinculado", "ok": true }
]
}
}
```
Issues com `severity: "error"` bloqueiam a publicação; `warning` (como a descrição curta) é só recomendação.
PUT/api/items/{id}/publish
O `publication_id` é o `item_id` retornado no `create` (ex.: `SAM-0000000000007`). Status muda para `pending_review`.
> Se chamar `publish` com pendências bloqueantes, vem **422** com o campo `checklist` no topo da resposta indicando o que falta.
Após a aprovação pela plataforma, o anúncio fica **`active`** e disponível para compra. Se for rejeitado, ele volta para `draft` com o motivo em `rejection_reason` — ajuste e publique de novo.
---
## Fluxo 2 — Ofertar em anúncio de catálogo
Quando o produto que você vende já tem um anúncio de catálogo na plataforma (criado pela operação), você não cria outra página: **anexa o seu SKU como oferta** em uma variação do anúncio existente. As ofertas das lojas competem pela melhor posição (buybox).
O caminho em três passos — localize o anúncio de catálogo, escolha a variação e anexe o SKU:
GET/api/items/catalog/publicationsGET/api/items/catalog/{itemId}/optionsPOST/api/items/catalog/options/{optionId}/attach-sku
```json
{
"seller_sku": "FK-JBL-2000-AZUL",
"status": "active"
}
```
- O anúncio de catálogo precisa estar **ativo**; o SKU precisa existir na sua loja com preço e estoque.
- `status` é opcional (default `active`).
- A resposta traz a oferta criada (`offer_id`) com snapshot de preço e estoque. A posição na buybox é recalculada em segundo plano — logo após o attach, `is_buybox_winner` costuma vir `false`.
- Mesma variação + mesmo SKU duplicado → **409**.
Depois, acompanhe e gerencie suas ofertas em `GET /api/items/catalog/my-offers` e `PUT`/`DELETE /api/items/catalog/offers/{offerId}` — tudo na seção **Catálogo e Ofertas** da [referência](/reference/anuncios).
---
## Depois de publicado
A gestão do anúncio também é toda por API — listagem com filtros (`GET /api/items`), detalhe, edição de título/descrição/atributos, variações, fichas técnicas e o score de qualidade. Está documentada na seção **Gestão de Anúncios** da [referência](/reference/anuncios).
> Upload de imagens é compartilhado entre produtos e anúncios — está documentado em [Mídia](/guia/midia) e no guia de [Imagens](/guia/anuncios-imagens).
=================================================================
SOURCE: https://api-docs.samdevel.com.br/guides/anuncios-categorias.md
=================================================================
# Anúncios · Categorias
Todo anúncio nasce em uma **categoria folha** — o último nível da árvore. É a categoria que define quais [atributos](/guia/anuncios-atributos) o anúncio precisa (e pode) ter, então acertar a categoria é o primeiro passo de qualquer publicação.
---
## Como a árvore é organizada
Dois níveis de organização:
- **Departamento** — o agrupamento de topo (Eletrônicos, Casa, Moda...). Tem `id`, `name` e `icon_svg`.
- **Categoria** — uma árvore de profundidade variável dentro do departamento. Cada categoria tem `parent_id` (null nas raízes), `level` e um `hierarchy`: o breadcrumb completo até ela, pronto pra exibir.
Uma categoria é **folha** quando não tem filhos. Só folhas recebem anúncios — e os endpoints já cuidam disso pra você: a busca retorna apenas folhas, e a navegação indica `has_children: false` quando você chegou ao fim do caminho.
> **Exceção à convenção de IDs:** categoria e departamento usam **ID inteiro** (ex.: `121`), diferente do resto da API, que usa IDs de 26 caracteres.
---
## Listar departamentos
GET/api/categories/departaments
Retorna os departamentos ativos, ordenados por nome:
```jsonc
{
"data": [
{ "id": 3, "name": "Eletrônicos", "icon_svg": "