# 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<br/>{ 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

<a class="api-route" href="/reference/primeiros-passos#tag/autenticação/POST/api/integration/auth"><span class="api-method api-method-post">POST</span><span class="api-path">/api/integration/auth</span></a>

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

<a class="api-route" href="/reference/primeiros-passos#tag/sessão/GET/api/stores/auth/session"><span class="api-method api-method-get">GET</span><span class="api-path">/api/stores/auth/session</span></a>

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

<a class="api-route" href="/reference/primeiros-passos#tag/media/POST/api/media/upload"><span class="api-method api-method-post">POST</span><span class="api-path">/api/media/upload</span></a>

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

<a class="api-route" href="/reference/primeiros-passos#tag/media/GET/api/media"><span class="api-method api-method-get">GET</span><span class="api-path">/api/media</span></a>

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

<a class="api-route" href="/reference/primeiros-passos#tag/media/GET/api/media/{file_hash}"><span class="api-method api-method-get">GET</span><span class="api-path">/api/media/{file_hash}</span></a>

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

<a class="api-route" href="/reference/primeiros-passos#tag/media/DELETE/api/media/{file_hash}"><span class="api-method api-method-delete">DELETE</span><span class="api-path">/api/media/{file_hash}</span></a>

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

<a class="api-route" href="/reference/loja#tag/página-da-loja/GET/api/stores/page"><span class="api-method api-method-get">GET</span><span class="api-path">/api/stores/page</span></a>

Retorna tudo: dados da loja, da `company`, textos descritivos e URLs públicas do logo/background.

### Textos descritivos

<a class="api-route" href="/reference/loja#tag/página-da-loja/PUT/api/stores/page"><span class="api-method api-method-put">PUT</span><span class="api-path">/api/stores/page</span></a>

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

<a class="api-route" href="/reference/loja#tag/página-da-loja/POST/api/stores/page/logo"><span class="api-method api-method-post">POST</span><span class="api-path">/api/stores/page/logo</span></a>

<a class="api-route" href="/reference/loja#tag/página-da-loja/DELETE/api/stores/page/logo"><span class="api-method api-method-delete">DELETE</span><span class="api-path">/api/stores/page/logo</span></a>

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

<a class="api-route" href="/reference/loja#tag/página-da-loja/POST/api/stores/page/background"><span class="api-method api-method-post">POST</span><span class="api-path">/api/stores/page/background</span></a>

<a class="api-route" href="/reference/loja#tag/página-da-loja/DELETE/api/stores/page/background"><span class="api-method api-method-delete">DELETE</span><span class="api-path">/api/stores/page/background</span></a>

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

<a class="api-route" href="/reference/loja#tag/avaliação/GET/api/stores/rating"><span class="api-method api-method-get">GET</span><span class="api-path">/api/stores/rating</span></a>

### Histórico

<a class="api-route" href="/reference/loja#tag/avaliação/GET/api/stores/rating/history"><span class="api-method api-method-get">GET</span><span class="api-path">/api/stores/rating/history</span></a>

=================================================================
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

<div class="api-routes">
  <a class="api-route" href="/reference/produtos#tag/produtos/GET/api/products"><span class="api-method api-method-get">GET</span><span class="api-path">/api/products</span></a>
  <a class="api-route" href="/reference/produtos#tag/produtos/POST/api/products"><span class="api-method api-method-post">POST</span><span class="api-path">/api/products</span></a>
  <a class="api-route" href="/reference/produtos#tag/produtos/GET/api/products/{id}"><span class="api-method api-method-get">GET</span><span class="api-path">/api/products/{id}</span></a>
  <a class="api-route" href="/reference/produtos#tag/produtos/PUT/api/products/{id}"><span class="api-method api-method-put">PUT</span><span class="api-path">/api/products/{id}</span></a>
</div>

| 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

<div class="api-routes">
  <a class="api-route" href="/reference/produtos#tag/galeria-de-imagens/GET/api/products/{id}/gallery"><span class="api-method api-method-get">GET</span><span class="api-path">/api/products/{id}/gallery</span></a>
  <a class="api-route" href="/reference/produtos#tag/galeria-de-imagens/POST/api/products/{id}/gallery"><span class="api-method api-method-post">POST</span><span class="api-path">/api/products/{id}/gallery</span></a>
  <a class="api-route" href="/reference/produtos#tag/galeria-de-imagens/DELETE/api/products/{id}/gallery/{image_id}"><span class="api-method api-method-delete">DELETE</span><span class="api-path">/api/products/{id}/gallery/{image_id}</span></a>
  <a class="api-route" href="/reference/produtos#tag/galeria-de-imagens/PUT/api/products/{id}/gallery/thumbnail"><span class="api-method api-method-put">PUT</span><span class="api-path">/api/products/{id}/gallery/thumbnail</span></a>
  <a class="api-route" href="/reference/produtos#tag/galeria-de-imagens/PUT/api/products/{id}/gallery/reorder"><span class="api-method api-method-put">PUT</span><span class="api-path">/api/products/{id}/gallery/reorder</span></a>
</div>

---

## Dados auxiliares

Rotas públicas (sem autenticação). Use pra popular dropdowns no seu sistema.

<div class="api-routes">
  <a class="api-route" href="/reference/produtos#tag/dados-auxiliares/GET/api/global/enums/product/units"><span class="api-method api-method-get">GET</span><span class="api-path">/api/global/enums/product/units</span></a>
  <a class="api-route" href="/reference/produtos#tag/dados-auxiliares/GET/api/global/enums/product/price-types"><span class="api-method api-method-get">GET</span><span class="api-path">/api/global/enums/product/price-types</span></a>
</div>

---

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

<a class="api-route" href="/reference/produtos#tag/preços-de-produtos/POST/api/products/{id}/prices"><span class="api-method api-method-post">POST</span><span class="api-path">/api/products/{id}/prices</span></a>

```json
{
  "min_quantity": 1,
  "price": 89.90
}
```

### Atualizar

<a class="api-route" href="/reference/produtos#tag/preços-de-produtos/PUT/api/products/{id}/prices/{min_quantity}"><span class="api-method api-method-put">PUT</span><span class="api-path">/api/products/{id}/prices/1</span></a>

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

<a class="api-route" href="/reference/produtos#tag/preços-de-produtos/POST/api/products/{id}/prices"><span class="api-method api-method-post">POST</span><span class="api-path">/api/products/{id}/prices</span></a>

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

<a class="api-route" href="/reference/produtos#tag/preços-de-produtos/PUT/api/products/{id}/prices/{min_quantity}"><span class="api-method api-method-put">PUT</span><span class="api-path">/api/products/{id}/prices/10</span></a>

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

<a class="api-route" href="/reference/produtos#tag/preços-de-produtos/DELETE/api/products/{id}/prices/{min_quantity}"><span class="api-method api-method-delete">DELETE</span><span class="api-path">/api/products/{id}/prices/10</span></a>

---

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

<a class="api-route" href="/reference/produtos#tag/preços-de-produtos/GET/api/products/{id}/prices"><span class="api-method api-method-get">GET</span><span class="api-path">/api/products/{id}/prices</span></a>

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

<a class="api-route" href="/reference/produtos#tag/estoque-de-produtos/GET/api/products/{product_id}/stock"><span class="api-method api-method-get">GET</span><span class="api-path">/api/products/{id}/stock</span></a>

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

<a class="api-route" href="/reference/produtos#tag/estoque-de-produtos/POST/api/products/{product_id}/stock"><span class="api-method api-method-post">POST</span><span class="api-path">/api/products/{id}/stock</span></a>

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<br/>(operador)
    pending_review --> draft: rejeitado<br/>(volta pra ajuste<br/>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<br/>GET /categories/search] --> E2[2. Consultar atributos<br/>GET /categories/id/attributes]
    E2 --> Q{Tem atributos<br/>com is_combinable?}
    Q -->|não| E4
    Q -->|sim| E3[3. Gerar combinações<br/>POST /categories/id/combine-attributes]
    E3 --> E4[4. Validar<br/>POST /items/simulate]
    E4 --> E5[5. Criar rascunho<br/>POST /items/create]
    E5 --> E6[6. Checklist + publicar<br/>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

<a class="api-route" href="/reference/anuncios#tag/categorias/GET/api/categories/search"><span class="api-method api-method-get">GET</span><span class="api-path">/api/categories/search</span></a>

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

<a class="api-route" href="/reference/anuncios#tag/publicacao/GET/api/categories/{category_id}/attributes"><span class="api-method api-method-get">GET</span><span class="api-path">/api/categories/{id}/attributes</span></a>

| 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

<a class="api-route" href="/reference/anuncios#tag/publicacao/POST/api/categories/{category_id}/combine-attributes"><span class="api-method api-method-post">POST</span><span class="api-path">/api/categories/{id}/combine-attributes</span></a>

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

<a class="api-route" href="/reference/anuncios#tag/publicacao/POST/api/items/simulate"><span class="api-method api-method-post">POST</span><span class="api-path">/api/items/simulate</span></a>

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

<a class="api-route" href="/reference/anuncios#tag/publicacao/POST/api/items/create"><span class="api-method api-method-post">POST</span><span class="api-path">/api/items/create</span></a>

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

<a class="api-route" href="/reference/anuncios#tag/publicacao/GET/api/items/{publication_id}/review-checklist"><span class="api-method api-method-get">GET</span><span class="api-path">/api/items/{id}/review-checklist</span></a>

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.

<a class="api-route" href="/reference/anuncios#tag/publicacao/PUT/api/items/{publication_id}/publish"><span class="api-method api-method-put">PUT</span><span class="api-path">/api/items/{id}/publish</span></a>

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:

<a class="api-route" href="/reference/anuncios#tag/catálogo-e-ofertas/GET/api/items/catalog/publications"><span class="api-method api-method-get">GET</span><span class="api-path">/api/items/catalog/publications</span></a>

<a class="api-route" href="/reference/anuncios#tag/catálogo-e-ofertas/GET/api/items/catalog/{itemId}/options"><span class="api-method api-method-get">GET</span><span class="api-path">/api/items/catalog/{itemId}/options</span></a>

<a class="api-route" href="/reference/anuncios#tag/catálogo-e-ofertas/POST/api/items/catalog/options/{optionId}/attach-sku"><span class="api-method api-method-post">POST</span><span class="api-path">/api/items/catalog/options/{optionId}/attach-sku</span></a>

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

<a class="api-route" href="/reference/anuncios#tag/categorias/GET/api/categories/departaments"><span class="api-method api-method-get">GET</span><span class="api-path">/api/categories/departaments</span></a>

Retorna os departamentos ativos, ordenados por nome:

```jsonc
{
  "data": [
    { "id": 3, "name": "Eletrônicos", "icon_svg": "<svg …>" },
    { "id": 7, "name": "Informática", "icon_svg": "<svg …>" }
  ]
}
```

---

## Navegar nível a nível

<a class="api-route" href="/reference/anuncios#tag/categorias/GET/api/categories/browse"><span class="api-method api-method-get">GET</span><span class="api-path">/api/categories/browse</span></a>

Um nível por vez — ideal pra montar uma interface de navegação em cascata:

| Parâmetro | Para quê |
|-----------|----------|
| `departament_id` | Limita ao departamento. |
| `parent_id` | Filhos dessa categoria. Sem ele, retorna as raízes. |

Cada item vem com `has_children`. Quando vier `false`, você chegou numa **folha** — pode usar o `id` no anúncio.

```jsonc
{
  "data": [
    {
      "id": 121,
      "name": "Caixas de Som",
      "has_children": false,            // folha: pronta pra receber anúncio
      "departament": { "id": 3, "name": "Eletrônicos" },
      "parent": { "id": 45, "name": "Áudio" }
    }
  ]
}
```

---

## Buscar direto

<a class="api-route" href="/reference/anuncios#tag/categorias/GET/api/categories/search"><span class="api-method api-method-get">GET</span><span class="api-path">/api/categories/search</span></a>

O caminho rápido: busca por termo e retorna **apenas folhas**, já com o breadcrumb completo.

| Parâmetro | Para quê |
|-----------|----------|
| `search` | Termo de busca (obrigatório — vazio retorna 422). |
| `departament_id` | Restringe ao departamento. |

```jsonc
{
  "data": [
    {
      "id": 121,
      "name": "Caixas de Som",
      "departament": { "id": 3, "name": "Eletrônicos", "icon_svg": "<svg …>" },
      "parent": { "id": 45, "name": "Áudio", "hierarchy": [ /* breadcrumb do pai */ ] },
      "hierarchy": [                     // breadcrumb completo, pronto pra exibir
        { "id": 12, "name": "Áudio e Vídeo" },
        { "id": 45, "name": "Áudio" },
        { "id": 121, "name": "Caixas de Som" }
      ],
      "level": 3
    }
  ]
}
```

---

## Qual usar?

- **Integração de ERP/hub** que já sabe o que vende: use o `search` — uma chamada e você tem a folha com breadcrumb.
- **Interface de cadastro** com navegação guiada: `departaments` → `browse` em cascata até `has_children: false`.
- **Breadcrumb na sua UI**: o campo `hierarchy` já vem montado — não precisa de chamadas extras pra subir a árvore.

## Da categoria pros atributos

Com a folha em mãos, o próximo passo é consultar o que ela exige:

<a class="api-route" href="/reference/anuncios#tag/publicacao/GET/api/categories/{category_id}/attributes"><span class="api-method api-method-get">GET</span><span class="api-path">/api/categories/{id}/attributes</span></a>

Os atributos retornados — obrigatórios, de variação, combináveis — estão explicados no guia de [Atributos e variações](/guia/anuncios-atributos).

=================================================================
SOURCE: https://api-docs.samdevel.com.br/guides/anuncios-atributos.md
=================================================================

# Anúncios · Atributos e variações

Os atributos são o vocabulário do anúncio: dizem **o que o produto é** (marca, material, potência) e **no que cada variação difere** (cor, tamanho, voltagem). Quem dita as regras é a [categoria](/guia/anuncios-categorias) — cada uma define seus próprios atributos, obrigatórios ou não.

---

## O vínculo categoria–atributo

<a class="api-route" href="/reference/anuncios#tag/publicacao/GET/api/categories/{category_id}/attributes"><span class="api-method api-method-get">GET</span><span class="api-path">/api/categories/{id}/attributes</span></a>

Cada atributo retornado carrega flags **do vínculo com aquela categoria**:

| Flag | Significado |
|------|-------------|
| `is_required` | Obrigatório — sem ele o anúncio não passa no checklist de publicação. |
| `is_feature` | Característica descritiva do produto (marca, material, peso). |
| `is_variant` | Atributo de variação (cor, tamanho, voltagem). |
| `is_combinable` | Pode entrar na geração de combinações de variações. |
| `is_filter` | Vira filtro de busca na vitrine. |

| Parâmetro | Para quê |
|-----------|----------|
| `matrix=true` | Agrupa por seção (Identificação, Técnico, ...). Sem ele, lista plana. |
| `only_features=1` | Apenas características. |
| `only_variations=1` | Apenas atributos de variação. |

> `only_features` e `only_variations` juntos retornam **422**.

---

## Anatomia de um atributo

A definição é autodescritiva — o backend te diz como montar o formulário e o payload:

```jsonc
{
  "id": 14,
  "name": "Cor",
  "type": "color",                  // estrutura do valor (tabela abaixo)
  "value_type": "select",           // tipo primitivo, relevante nos types default*
  "is_feature": false,
  "is_variant": true,
  "ui": {
    "public_name": "Cor",
    "description": "Cor predominante do produto",
    "select_type": "select"         // select/radio/checkbox = opção pré-cadastrada; text = digitação livre
  },
  "value_definition": {
    "fields": [ /* schema dinâmico do formulário (ex.: value, main_color, hex) */ ],
    "options": [                    // opções pré-cadastradas, quando houver
      {
        "id": 87,
        "value": "Azul",
        "component": { "value": "Azul", "main_color": "blue", "hex": "#1565C0", "brightness": "dark" }
      }
    ]
  }
}
```

**`type`** define a estrutura do valor:

| `type` | O que é | Payload típico |
|--------|---------|----------------|
| `default` | Valor simples | `{ "id": 1, "value": "Algodão" }` |
| `default_unit` | Número + unidade (entre `available_units`) | `{ "id": 5, "value": 350, "unit": "ml" }` |
| `default_list` | Lista de itens | `{ "id": 9, "items": ["Cabo USB", "Manual"] }` |
| `dimension_2d` | Largura × altura + unidade | `{ "id": 11, "width": 20, "height": 30, "unit": "cm" }` |
| `dimension_3d` | Largura × altura × profundidade + unidade | `{ "id": 12, "width": 20, "height": 30, "depth": 10, "unit": "cm" }` |
| `color` | Cor (pré-cadastrada ou customizada) | ver abaixo |
| `brand` | Marca | `{ "id": 2, "value_id": 3 }` ou `{ "id": 2, "value": "HyperX" }` |
| `image`, `packaging` | Tipos especializados | conforme `value_definition.fields` |

**`value_type`** (`text`, `float`, `number`, `date`, `boolean`, `select`, `radio`, `checkbox`) é o tipo primitivo do valor — relevante nos types `default*`; os tipos especializados o ignoram.

**Opção pré-cadastrada vs. valor livre:** quando `ui.select_type` é `select`/`radio`/`checkbox`, envie o `value_id` de uma das `options` (ou um `value` que case exatamente com uma opção). Quando é `text`, digite livre. A exceção é a cor:

### Cor customizada

Atributos `type=color` aceitam, além das opções pré-cadastradas, uma **cor livre**:

```jsonc
{ "id": 14, "value": "Azul Petróleo", "hex": "#0F4C5C" }
```

- `value` (nome) + `hex` (`#RGB` ou `#RRGGBB`) são suficientes.
- `main_color` e `brightness` são opcionais — `brightness` (light/dark) é calculada a partir do hex quando omitida.
- Vale tanto em `attributes[]` quanto em `variations[].attributes[]`; a cor volta completa nas respostas (campo `component`).

---

## Onde os atributos entram no anúncio

No [create/simulate](/guia/anuncios#etapa-5-criar-anuncio), há dois lugares:

- **`attributes[]`** — características do anúncio como um todo (marca, material...).
- **`variations[].attributes[]`** — o que diferencia **cada variação** (a cor azul desta, a voltagem 110 V daquela).

A API aceita os aliases `attribute_id`/`id`, `attribute_value`/`value` e `value_unit`/`unit` — use o par que preferir, mas seja consistente.

---

## Variações: da combinação ao SKU

### 1. Gerar combinações

<a class="api-route" href="/reference/anuncios#tag/publicacao/POST/api/categories/{category_id}/combine-attributes"><span class="api-method api-method-post">POST</span><span class="api-path">/api/categories/{id}/combine-attributes</span></a>

Envie os valores escolhidos de cada atributo `is_combinable` — texto, `option_id` ou objeto (cor customizada, dimensões):

```json
{
  "attributes": {
    "1": ["110 V", "220 V"],
    "14": [{ "value": "Azul Petróleo", "hex": "#0F4C5C", "custom": true }, "Verde"]
  },
  "primary_attribute_id": 1
}
```

A resposta traz `combinations[]` (o produto cartesiano, cada uma com `attributes`, `attributes_details` e `description`) e `primary_attribute`. O atributo principal serve pra **agrupar as fotos por variação** — escolha aquele que muda a aparência (cor, em geral).

> Valores customizados só passam em atributos com `select_type: "text"` ou `type: "color"`; nos demais, use opções pré-cadastradas. Atributo fora da lista de combináveis da categoria → **400**.

### 2. Vincular um SKU em cada variação

Cada combinação vira um item de `variations[]` no create, com seu `seller_sku`:

```json
{
  "seller_sku": "FK-JBL-2000-AZUL",
  "attributes": [{ "attribute_id": 14, "value": "Azul Petróleo", "hex": "#0F4C5C" }],
  "images": [{ "id": "{{image_id}}" }]
}
```

Use **SKUs diferentes** quando cada variação tem estoque/preço próprio, ou o **mesmo SKU** quando as variações são apenas visuais. O SKU precisa existir na loja com preço e estoque configurados.

### Status da variação

`active`, `pending` (default na criação) ou `hidden`. Não confunda com o status da **oferta** de catálogo (`active`, `inactive`, `paused`, `pending`), usado no [attach-sku](/guia/anuncios#fluxo-2-ofertar-em-anuncio-de-catalogo).

---

## Uma pegadinha: atributos calculáveis

Alguns atributos (ex.: Quantidade) são `is_calculable: true`. Em ofertas de catálogo, a **unidade** desse atributo precisa casar com a `base_unit` do seu SKU — divergência retorna **422** (`UNIT_MISMATCH`). Se o anúncio vende "caixa com 12", seu SKU precisa estar cadastrado na unidade compatível.

=================================================================
SOURCE: https://api-docs.samdevel.com.br/guides/anuncios-imagens.md
=================================================================

# Anúncios · Imagens

As imagens do anúncio seguem o fluxo padrão da plataforma: **suba uma vez pela [Mídia](/guia/midia), receba o ID, vincule onde precisar**. Esta página cobre só o lado do anúncio — upload, formatos, limites e ciclo de vida da imagem estão no guia de [Mídia](/guia/midia).

---

## O fluxo em duas chamadas

1. **Upload** — <a class="api-route" href="/reference/primeiros-passos#tag/media/POST/api/media/upload"><span class="api-method api-method-post">POST</span><span class="api-path">/api/media/upload</span></a> com o arquivo em `multipart/form-data`. A resposta traz o `id` da imagem, já processada em todos os tamanhos.
2. **Vínculo** — no [create/simulate](/guia/anuncios#etapa-5-criar-anuncio) do anúncio, referencie esse `id`.

```jsonc
{
  "title": "Caixa de Som JBL Go!",
  "images": [                          // galeria do anúncio
    { "id": "01K8PBIMG00001VWXYZ12345678" },
    { "id": "01K8PBIMG00002VWXYZ12345678" }
  ],
  "variations": [
    {
      "seller_sku": "FK-JBL-2000-AZUL",
      "images": [                      // fotos específicas desta variação
        { "id": "01K8PBIMG00003VWXYZ12345678" }
      ]
    }
  ]
}
```

- **`images[]`** (nível do anúncio) — a galeria principal. A primeira imagem tende a virar a thumbnail.
- **`variations[].images[]`** — fotos da variação (a foto azul na variação azul). É aqui que o `primary_attribute` da [geração de combinações](/guia/anuncios-atributos#1-gerar-combinacoes) ajuda: ele agrupa as fotos pelo atributo que muda a aparência.

---

## O que volta nas respostas

Imagens do anúncio retornam como `{ id, resources[] }`, onde `resources` são as variações de tamanho geradas no upload:

```jsonc
{
  "images": [
    {
      "id": "01K8PBIMG00001VWXYZ12345678",
      "resources": [
        { "name": "sm", "size": "200x200",   "url": "https://cdn…/sm_…webp" },
        { "name": "lg", "size": "800x800",   "url": "https://cdn…/lg_…webp" }
      ]
    }
  ],
  "thumbnail": { "id": "01K8PBIMG00001VWXYZ12345678", "resource": { "name": "sm", "url": "…" } }
}
```

Nas variações, os `resources` vêm filtrados para os tamanhos `sm` e `lg` — suficientes pra listagem e zoom.

---

## Regras práticas

- **Publicação exige imagem.** O [checklist](/guia/anuncios#etapa-6-checklist-e-publicacao) tem o check `image`: pelo menos 1 imagem na galeria (ou thumbnail definida) pra sair de `draft`.
- **O ID precisa existir.** O create valida a existência do ID da imagem na plataforma — ID errado derruba a chamada. Suba a imagem **antes** de criar o anúncio.
- **Imagem em uso fica protegida.** Vinculada ao anúncio, a imagem passa a `in_use: true` na Mídia — excluir retorna **409** até você remover o vínculo. Detalhes no [ciclo de vida da Mídia](/guia/midia#ciclo-de-vida).
- **Mesma imagem, vários lugares.** O mesmo ID pode servir à galeria do produto e ao anúncio — é o ponto do upload compartilhado: não suba o arquivo duas vezes.
- **Imagem solta expira.** Imagem sem vínculo é limpa pela rotina da plataforma (~72h). Não acumule uploads "pra usar depois".

=================================================================
SOURCE: https://api-docs.samdevel.com.br/guides/pedidos.md
=================================================================

# Visão geral

Como sua loja recebe pedidos, avança pelos estados do ciclo de vida, e onde estão as outras peças do quebra-cabeça (fiscal, etiquetas, devoluções, industrialização).

> Este guia é o **mapa**. Cada tema tem um guide próprio com o passo a passo aprofundado:
>
> - [Industrialização](/guia/pedidos-industrializacao), para o workflow `production`.
> - [Fiscal](/guia/pedidos-fiscal): NF-e, Declaração de Conteúdo, XML.
> - [Logística](/guia/pedidos-logistica): shipments e etiquetas.
> - [Devoluções](/guia/devolucoes): RMA encaminhada ao seller.

---

## Conceitos

O objeto que `GET /orders/{id}` devolve, de relance — campos principais (vários trazem um par `*_label` em pt-BR para exibir):

```jsonc
{
  "order_number": "ORD-000123",      // ← identificador público do pedido
  "checkout_group_id": "01K8PB1QA…", // agrupa pedidos do mesmo checkout
  "store": {…},                      // loja responsável
  "customer": {…},                   // comprador
  "type": "standard",                // tipo do pedido (+ type_label)
  "channel": "marketplace",          // canal de venda (+ channel_label)
  "workflow_type": "standard",       // ← workflow: standard | production (industrialização)
  "status": "paid",                  // ← estado atual (+ status_label, status_color)
  "totals": {…},                     // produtos, frete, descontos, total
  "items": [{…}],                    // linhas do pedido
  "applied_benefits": [{…}],         // cupons/descontos aplicados
  "invoices": [{…}],                 // documentos fiscais (ver Fiscal)
  "shipping": {…},                   // transportadora e frete (ver Logística)
  "delivery_method": "shipping",     // shipping | pickup (+ delivery_method_label)
  "is_production_order": false,      // true quando workflow_type=production
  "payment_method": "pix",           // forma de pagamento (+ payment_method_label)
  "payment_url": "https://…",        // link de pagamento (canais direct/store_seller; null fora de waiting_payment)
  "payment_link_target": "store",    // onde o link abre: platform | store
  "created_at": "2026-04-26T14:32:11Z",
  "updated_at": "2026-04-26T14:35:42Z"
}
```

> Campos que não se aplicam ao pedido vêm `null` (ex.: `picked_up_at` sem retirada, `payment_expires_at` após o pagamento). Trate a ausência como "não se aplica", não como erro.

---

## Fluxos de pedido

Cada pedido segue um **workflow** — o trilho de estados entre o pagamento e a conclusão, definido pelo `workflow_type`. O dia a dia da loja muda conforme o trilho, e cada um tem um guia próprio com o passo a passo:

- [Entrega](/guia/pedidos-entrega): o fluxo padrão, com envio por transportadora.
- [Retirada](/guia/pedidos-retirada): cliente paga online e busca no balcão.
- [Sob encomenda](/guia/pedidos-sob-encomenda): pré-venda e espera por reposição de estoque.
- [Industrialização](/guia/pedidos-industrializacao): produto que passa pela fábrica antes de embalar.

---

## Como pensar num pedido

Um pedido é o **estado** de uma compra, desde o pagamento confirmado até a entrega (ou cancelamento). Quem dirige o estado é o seller, chamando a API conforme o pedido caminha.

Três ideias fixam tudo:

**1. `order_number` é o identificador.** Em toda rota onde aparece `{id}`, é o `order_number` (ex.: `ORD-000123`). É público, estável e visível ao cliente.

**2. Toda transição passa por uma única rota.** `POST /orders/{id}/status` muda o estado, e o que muda de uma transição para outra é só o valor de `status` (mais um `data`, quando o estado pedir). Você não vai encontrar um endpoint para cada ação — nada de `/cancel`, `/ship` ou `/deliver` por conta própria. Isso é de propósito: em vez de te obrigar a conhecer dezenas de rotas, juntamos tudo numa só. Você aprende uma rota e conduz o pedido do início ao fim com ela.

**3. Você não precisa decorar o que pode chamar.** A rota `GET /orders/{id}/possible-statuses` já devolve, para o estado atual do pedido, **quais transições são válidas** e **qual o payload de cada uma** (`payload_schema`). Se você só lê essa rota e age sobre o que ela devolve, não tem como mandar um `status` inválido.

### O ciclo padrão (`workflow_type=standard`)

```mermaid
stateDiagram-v2
    direction LR
    [*] --> paid
    paid --> preparing
    paid --> ready_to_ship: pular<br/>preparação
    preparing --> ready_to_ship
    preparing --> shipped: envio<br/>direto
    ready_to_ship --> shipped
    shipped --> delivered
    delivered --> [*]
```

Esse é o caminho da maioria dos pedidos. Outros `workflow_type` (production, in_store_pickup, pre_order, backorder, collective) **inserem etapas** antes ou dentro desse fluxo, mas no fim caem no mesmo `delivered`.

### Conceitos que aparecem em vários lugares

**`workflow_type`** define o "perfil" do pedido e o conjunto de transições aceitas. Os valores possíveis são `standard`, `production`, `in_store_pickup`, `pre_order`, `backorder` e `collective`. Você não escolhe o workflow: ele é definido no momento da criação do pedido, pela natureza do produto (sob encomenda, retirada na loja, pré-venda etc.).

**Operações assíncronas.** Algumas ações disparam jobs em background e retornam **202 Accepted** imediatamente. Isso acontece em três lugares: emissão de Declaração de Conteúdo, registro de NF-e com XML (gera DANFE em background) e geração de etiquetas. Em todos, o padrão é: dispara, faz polling, consome o resultado.

**Downloads via S3 assinado.** PDFs (DANFE, declaração, etiqueta) **não são servidos pela API**. Ela devolve uma URL S3 com assinatura, válida por **15 minutos**. Se expirar, você chama a rota de download de novo, que gera uma nova URL.

---

## Anatomia de um pedido

Quando você busca um pedido com `GET /orders/{id}`, recebe um objeto grande. Não precisa entender campo a campo de uma vez — agrupando por função, ele fica simples. A **listagem** (`GET /orders`) traz um subconjunto enxuto desses campos; o **detalhe** traz tudo.

| Bloco | Campos principais | Para que serve |
|---|---|---|
| **Identificação** | `order_number`, `checkout_group_id` | `order_number` é o ID público (o `{id}` das rotas). `checkout_group_id` agrupa pedidos nascidos do mesmo checkout — um carrinho com itens de várias lojas vira **um pedido por loja**, todos com o mesmo `checkout_group_id`. |
| **Quem** | `customer`, `store`, `sales_agent`, `physical_store` | Cliente, loja, e — quando a venda passou por um agente ou balcão — o agente de vendas e a loja física. Cada um vem como objeto (`{ id, name, ... }`) e também como `*_id`/`*_uid` solto. |
| **Classificação** | `type`, `channel`, `workflow_type`, `status` | O "perfil" do pedido: o tipo, o canal de origem, o [workflow](#conceitos-que-aparecem-em-varios-lugares) e o estado atual. Todos vêm com `_label` em pt-BR; `status` traz também `status_color`. |
| **Itens** | `items[]`, `items_count`, `lines_count`, `items_preview` | `items[]` é o detalhe linha a linha (produto, variação, `quantity`, `prices`, `net_value`). `items_count` soma as quantidades, `lines_count` conta as linhas, e `items_preview` traz os 3 primeiros com thumbnail — pronto para um card de listagem. |
| **Dinheiro** | `totals`, `payment_method`, `payment_status`, `payment_expires_at`, `applied_benefits` | `totals` resume `{ products, shipping, discounts, order }`. Forma e situação do pagamento vêm com `_label`. `applied_benefits` lista cupons/descontos. O extrato completo (splits, comissões) está na rota [`/financial`](#financeiro). |
| **Entrega** | `delivery_method`, `shipping`, `picked_up_at` | Método de entrega e a opção de frete escolhida (`carrier_name`, `display_name`, prazo). Rastreio consolidado fica em [`/tracking`](#tracking-de-entrega); etiquetas, no guia de [logística](/guia/pedidos-logistica). |
| **Fiscal** | `invoices[]` | Resumo dos documentos fiscais (`type`, `status`, `invoice_number`, `pdf_url`). O fluxo completo está no guia [fiscal](/guia/pedidos-fiscal). |
| **Operacional** | `notes`, `metadata`, `occurrences`, `created_at`, `updated_at` | Anotação interna da loja, metadados livres, ocorrências/SLA ligadas ao pedido, e os timestamps. |

Duas convenções valem para o objeto inteiro:

**Campos `*_label` e `*_color` já vêm prontos.** Todo enum do pedido (status, canal, tipo, workflow, pagamento) chega com o valor cru **e** o rótulo em pt-BR — e cor, onde faz sentido. Você nunca precisa traduzir esses valores no front: use o `_label` direto. Os valores possíveis (para popular filtros e badges) vêm da rota [`/enums`](/reference/pedidos#tag/enums-de-pedido/GET/api/v1/sellers/orders/enums).

**Campos são condicionais.** Vários campos só aparecem no contexto em que fazem sentido — `payment_expires_at` só quando o pedido aguarda pagamento, `pickup_address` só em retirada na loja, e assim por diante. Trate a **ausência** de um campo como "não se aplica a este pedido", não como erro.

---

## Encontrando pedidos

Tem duas formas: **listar com filtros** (visão operacional) ou **olhar números agregados** (visão gerencial).

### Listagem com filtros

<a class="api-route" href="/reference/pedidos#tag/pedidos/GET/api/v1/sellers/orders"><span class="api-method api-method-get">GET</span><span class="api-path">/api/v1/sellers/orders</span></a>

Esta é a rota que o painel do seller chama no dia a dia. Sem filtros, devolve todos os pedidos da loja (paginados, mais novo primeiro).

Quando você está montando uma fila de trabalho, normalmente combina um filtro de **status** (`paid`, `preparing`, `shipped`...) com um filtro de **data** (`date_from`, `date_to`). Os filtros mais usados:

| Para… | Use |
|---|---|
| Pedidos pendentes de preparação | `?status=paid,preparing` (CSV aceito) |
| Pedidos enviados aguardando entrega | `?status=shipped` |
| Busca por nº de pedido ou nome de cliente | `?search=ORD-0001` ou `?search=João` |
| Pedidos de um cliente específico | `?customer_id=01K8PB2RCUST00001A2B3C4D5E` |
| Pedidos de um canal | `?channel=marketplace` (CSV aceito) |
| Pedidos sob encomenda | `?workflow=production` |
| Pedidos com um item específico | `?item_id=ITM-0551` |
| Janela de datas | `?date_from=2026-04-01&date_to=2026-04-30` |
| Ordenação | `?sort=-created_at` (`-` = desc) |

`limit` controla a paginação (default 20). A resposta vem em `data[]` com `meta` para paginação. **Os campos completos do pedido na listagem são um subconjunto enxuto**; para o detalhe completo use a rota individual abaixo.

> **Valores aceitos nos filtros (`status`, `channel`, `type`, `workflow`, métodos de pagamento…)** não precisam ser hardcodados. A rota [`GET /api/v1/sellers/orders/enums`](/reference/pedidos#tag/enums-de-pedido/GET/api/v1/sellers/orders/enums) devolve **todos** os enums do pedido (`value` + `label` pt-BR, com `color` onde faz sentido) num só request — use para popular dropdowns e badges. Para os status **agrupados por workflow**, há também a rota pública [`GET /api/global/enums/order/status`](/reference/pedidos#tag/enums-de-pedido/GET/api/global/enums/order/status).

### Visão agregada

<a class="api-route" href="/reference/pedidos#tag/pedidos/GET/api/v1/sellers/orders/summary"><span class="api-method api-method-get">GET</span><span class="api-path">/api/v1/sellers/orders/summary</span></a>

KPIs fixos **do dia atual**: quantos pedidos chegaram, receita, ticket médio, contadores por status (preparing, ready_to_ship, shipped, delivered_today) e por canal. Independe dos filtros da listagem: é sempre o panorama global da loja. Use para cabeçalho de dashboard.

<a class="api-route" href="/reference/pedidos#tag/pedidos/GET/api/v1/sellers/orders/dashboard"><span class="api-method api-method-get">GET</span><span class="api-path">/api/v1/sellers/orders/dashboard</span></a>

Estatísticas consolidadas **num período configurável** (`days`, default 30): gráfico de evolução diária (vendidos, pendentes, cancelados), totais do período, comparativo com o período anterior de mesma duração, e lista dos pedidos mais recentes. Use para tela de "Visão geral" do seller.

### Pedido individual

<a class="api-route" href="/reference/pedidos#tag/pedidos/GET/api/v1/sellers/orders/{id}"><span class="api-method api-method-get">GET</span><span class="api-path">/api/v1/sellers/orders/{id}</span></a>

Payload completo: itens, cliente, snapshot de endereço, totais, splits financeiros (resumo), fiscais (`invoices[]`), anotações, metadata de envio (carrier, opção escolhida). É o que a tela de detalhe do pedido consome.

---

## Avançando o pedido

Esta é a parte central. O fluxo correto, **sempre**, é:

```mermaid
sequenceDiagram
    autonumber
    participant I as Integrador
    participant A as API
    I->>A: GET /orders/{id}/possible-statuses
    A-->>I: { next_actions: [ { target_status, payload_schema }, ... ] }
    Note over I: escolhe a próxima ação
    I->>A: POST /orders/{id}/status<br/>{ status, data? }
    A-->>I: 200 + pedido atualizado
```

### Próximos passos válidos

<a class="api-route" href="/reference/pedidos#tag/pedidos/GET/api/v1/sellers/orders/{id}/possible-statuses"><span class="api-method api-method-get">GET</span><span class="api-path">/api/v1/sellers/orders/{id}/possible-statuses</span></a>

Devolve, para o status atual do pedido, as transições válidas. Cada item de `next_actions` traz:

- `action`: slug semântico (ex.: `mark-preparing`, `cancel`, `start-production`).
- `target_status`: o valor a enviar em `status` na rota única.
- `label`: texto pronto para botão/UI.
- `payload_schema`: JSONSchema do `data` aceito. `null` quando a transição não exige payload.

Exemplo (pedido em `paid`, workflow `standard`):

```json
{
  "data": {
    "current_status": "paid",
    "workflow_type": "standard",
    "next_actions": [
      {
        "action": "mark-preparing",
        "target_status": "preparing",
        "label": "Iniciar preparação",
        "payload_schema": null
      },
      {
        "action": "cancel",
        "target_status": "cancelled",
        "label": "Cancelar pedido",
        "payload_schema": {
          "type": "object",
          "properties": {
            "reason": {
              "type": "string",
              "maxLength": 500
            }
          }
        }
      }
    ]
  }
}
```

> **Padrão de UI recomendado:** renderize um botão por `next_action`, com label = `next_action.label`. Quando clicado, monte `data` a partir do `payload_schema` (se houver) e chame a rota única.

### A rota única de status

<a class="api-route" href="/reference/pedidos#tag/pedidos/POST/api/v1/sellers/orders/{id}/status"><span class="api-method api-method-post">POST</span><span class="api-path">/api/v1/sellers/orders/{id}/status</span></a>

```http
POST /api/v1/sellers/orders/ORD-000123/status
Content-Type: application/json

{ "status": "preparing" }
```

Cenários comuns:

```jsonc
// iniciar preparação (workflow standard)
{
  "status": "preparing"
}

// marcar enviado com rastreio
{
  "status": "shipped",
  "data": {
    "tracking_code": "BR123456789BR"
  }
}

// cancelar com motivo
{
  "status": "cancelled",
  "data": {
    "reason": "Cliente solicitou cancelamento."
  }
}
```

**O que pode dar errado:**

| Erro | Quando acontece | Como tratar |
|---|---|---|
| `422 VALIDATION_ERROR` (transição) | `status` enviado não está em `next_actions` do estado atual. `errors.status` vem com `"Transição inválida: não é possível ir de X para Y"`. | Refaça `GET /possible-statuses`. O pedido provavelmente já avançou (concorrência) ou seu cache está velho. |
| `422 VALIDATION_ERROR` (payload) | Payload em `data` não bate com `payload_schema` (campo faltando, tipo errado, max ultrapassado). | Olhe o `errors` da resposta, que vem por campo. |
| `404` | `order_number` não existe **ou** não pertence à sua loja. | Cheque o ID. Pedido de outra loja aparece como 404 (não 403) por motivos de privacidade. |

### Referência rápida: payloads por status

Você normalmente **não precisa** desta tabela, porque `possible-statuses` já entrega o `payload_schema` por transição. Está aqui só para debug rápido.

| `status` | `data` | obrigatório? |
|---|---|---|
| `preparing` | (nenhum) | não se aplica |
| `ready_to_ship` | (nenhum) | não se aplica |
| `shipped` | `{ tracking_code? }` (string, máx 100) | opcional |
| `delivered` | (nenhum) | não se aplica |
| `cancelled` | `{ reason? }` (string, máx 500) | opcional |
| `production_started` | `{ estimated_days? }` (integer) | opcional |
| `awaiting_materials` | `{ materials? }` (array de strings) | opcional |
| `production_in_progress` | (nenhum) | não se aplica |
| `production_finished` | (nenhum) | não se aplica |
| `ready_for_pickup` | (nenhum) | não se aplica |
| `picked_up` | `{ confirmation_code }` (string, máx 50) | **obrigatório** |
| `pickup_expired` | (nenhum) | não se aplica |
| `available` | (nenhum) | não se aplica |
| `restocked` | (nenhum) | não se aplica |

---

## Cancelamento

Cancelar costuma ser a primeira coisa que as pessoas procuram como rota própria — e ela não existe de propósito. Não há `DELETE /orders/{id}` nem `/cancel`: cancelar é uma transição de status como qualquer outra, igual a marcar enviado ou entregue. Você usa a mesma rota de sempre, mandando `status: cancelled` — mas só **enquanto o pedido não entrou em preparação/produção**. A partir daí, o cancelamento sai das suas mãos: é a operação da plataforma que cancela. E há estados sem saída nenhuma: `delivered`, `cancelled`, `completed` e `payment_expired` (já `pickup_expired` não é terminal — ele ainda transiciona para `cancelled`).

```json
{
  "status": "cancelled",
  "data": {
    "reason": "Sem estoque para entregar no prazo."
  }
}
```

`reason` é opcional, fica no histórico (`GET /orders/{id}/events`) e pode aparecer para o cliente. O `possible-statuses` indica quando essa ação está disponível para o estado atual.

---

## Acompanhando o pedido

Três rotas auxiliares que **não mudam estado**, só leem dados para telas/relatórios.

### Tracking de entrega

<a class="api-route" href="/reference/pedidos#tag/pedidos/GET/api/v1/sellers/orders/{id}/tracking"><span class="api-method api-method-get">GET</span><span class="api-path">/api/v1/sellers/orders/{id}/tracking</span></a>

Visão **consolidada** de rastreio: status atual, `tracking_code`, `tracking_url` pública (se disponível), `estimated_delivery_at`, `delivered_at`, `picked_up_at`. É o "onde está meu pedido", perfeito para uma tela simples de acompanhamento.

### Histórico granular (eventos)

<a class="api-route" href="/reference/pedidos#tag/pedidos/GET/api/v1/sellers/orders/{id}/events"><span class="api-method api-method-get">GET</span><span class="api-path">/api/v1/sellers/orders/{id}/events</span></a>

Linha do tempo completa do pedido: cada mudança de status, anotação ou ação fiscal vira um evento. Cada evento traz quem fez (`author_type`, `author_name`) e contexto (`metadata`). É o que você quer para uma timeline detalhada ou para auditoria.

### Financeiro

<a class="api-route" href="/reference/pedidos#tag/pedidos/GET/api/v1/sellers/orders/{id}/financial"><span class="api-method api-method-get">GET</span><span class="api-path">/api/v1/sellers/orders/{id}/financial</span></a>

Splits de receita (quem recebe quanto: seller, marketplace, sales agent), audit de comissão por item, e benefícios aplicados (cupons, descontos, regras absorvidas pelo marketplace). Use para tela de "extrato do pedido" ou para conferir o repasse esperado.

### Anotações internas

<a class="api-route" href="/reference/pedidos#tag/pedidos/PUT/api/v1/sellers/orders/{id}/notes"><span class="api-method api-method-put">PUT</span><span class="api-path">/api/v1/sellers/orders/{id}/notes</span></a>

`notes` é texto livre da loja sobre o pedido, **não aparece para o cliente**. Útil para deixar lembretes ("ligar antes de despachar", "embalagem para presente", "cliente vai retirar quarta"). Envia o texto inteiro a cada chamada (não é append) ou `null` para limpar.

---

## Outros workflows resumidos

A maior parte dos pedidos é `standard`, mas a loja pode receber outros tipos. Aqui um resumo; cada um tem peculiaridades que merecem leitura aprofundada.

### `production`: sob encomenda

Pedido que **passa pela fábrica antes de embalar**: tecido, móveis sob medida, brindes personalizados. Em vez de pular para `preparing`, o seller informa início, eventual espera por insumos, fabricação e conclusão. Quando a produção termina, o pedido entra no trilho padrão (`preparing` → `ready_to_ship` → ...).

→ **Detalhe completo em [Pedidos: industrialização](/guia/pedidos-industrializacao).**

### `in_store_pickup`: retirada na loja

Cliente paga online e vai buscar no balcão. O pedido segue o trilho normal até `preparing` — é ali que o caminho bifurca, porque `delivery_method=pickup`: em vez de `ready_to_ship`, o próximo passo é `ready_for_pickup`. Quando o cliente aparece, o seller confirma com um código de retirada. Ao confirmar `picked_up`, o pedido vira `delivered` automaticamente; não chame `delivered` em seguida.

```mermaid
stateDiagram-v2
    direction LR
    [*] --> paid
    paid --> preparing
    preparing --> ready_for_pickup
    ready_for_pickup --> picked_up: código<br/>confirmado
    ready_for_pickup --> pickup_expired
    picked_up --> delivered: automático
```

| Ação | Body |
|---|---|
| Iniciar preparação | `{ "status": "preparing" }` |
| Pronto para retirada | `{ "status": "ready_for_pickup" }` |
| Confirmar retirada | `{ "status": "picked_up", "data": { "confirmation_code": "A1B2C3" } }` |
| Retirada expirada | `{ "status": "pickup_expired" }` |

### `pre_order` e `backorder`

Espelhados. Um aguarda o produto chegar ao estoque pela primeira vez (`pre_order` → `available`), o outro aguarda reposição (`backorder` → `restocked`). Quando o seller libera, o pedido entra no fluxo padrão a partir de `paid`.

```mermaid
flowchart LR
    A(["pre_order<br/>awaiting_availability"]) -- "status: available" --> P["paid"]
    B(["backorder<br/>awaiting_restock"]) -- "status: restocked" --> P
    P --> S["Workflow standard"]
```

### `collective`: compra coletiva

Pedido vinculado a uma campanha de compra coletiva. Fica em `awaiting_campaign_conclusion` até a campanha encerrar; o sistema decide automaticamente se o pedido prossegue ou é cancelado. O seller só atua a partir de `preparing` em diante; não há transições manuais antes disso.

---

## Para onde ir agora

| Você precisa… | Vá para |
|---|---|
| Emitir NF-e, Declaração ou DANFE | [Pedidos: fiscal](/guia/pedidos-fiscal) |
| Gerar e baixar etiquetas | [Pedidos: logística](/guia/pedidos-logistica) |
| Tratar uma devolução | [Devoluções](/guia/devolucoes) |
| Pedidos sob encomenda | [Pedidos: industrialização](/guia/pedidos-industrializacao) |
| Schemas e códigos de erro | [Referência da API](/reference/pedidos) |

=================================================================
SOURCE: https://api-docs.samdevel.com.br/guides/pedidos-entrega.md
=================================================================

# Entrega

Workflow `standard` com `delivery_method=shipping`: a venda comum, que termina com um pacote saindo da loja e chegando na casa do cliente. É o caminho da grande maioria dos pedidos — e a base sobre a qual os outros workflows se encaixam.

> Pré-requisitos: leia primeiro [Pedidos: visão geral](/guia/pedidos). Os conceitos de `possible-statuses`, rota única `/status` e `workflow_type` valem aqui também.

---

## Quando um pedido cai aqui

Todo pedido nasce com dois eixos definidos na criação — você não escolhe nenhum deles depois:

- **`workflow_type`** é o perfil comercial. Sem natureza especial (fabricação, espera de estoque, campanha), o pedido é `standard`.
- **`delivery_method`** é o eixo de fulfillment: `shipping` (envio) ou `pickup` (retirada presencial).

Este guia cobre a combinação mais comum: `standard` + `shipping`. Se o cliente escolheu retirar na loja, o começo é igual, mas o caminho bifurca depois de `preparing` — veja [Pedidos: retirada](/guia/pedidos-retirada).

```jsonc
{
  "order_number": "ORD-000123",   // identificador público do pedido
  "workflow_type": "standard",    // ← perfil padrão
  "delivery_method": "shipping",  // ← termina com envio
  "status": "paid",               // pagamento confirmado, sua vez
  "shipping": {…}                 // frete escolhido no checkout
}
```

Os valores possíveis dos dois eixos (e de todos os outros enums do pedido) vêm de <a class="api-route" href="/reference/pedidos#tag/enums-de-pedido/GET/api/v1/sellers/orders/enums"><span class="api-method api-method-get">GET</span><span class="api-path">/api/v1/sellers/orders/enums</span></a>. Para os status já agrupados por workflow, use a rota pública <a class="api-route" href="/reference/pedidos#tag/enums-de-pedido/GET/api/global/enums/order/status"><span class="api-method api-method-get">GET</span><span class="api-path">/api/global/enums/order/status</span></a>.

---

## O caminho

```mermaid
stateDiagram-v2
    direction LR
    [*] --> paid
    paid --> preparing
    preparing --> ready_to_ship
    preparing --> shipped: envio<br/>direto
    ready_to_ship --> shipped
    shipped --> delivered
    delivered --> [*]
```

Sua fila de entrada é a listagem filtrada por pedidos pagos:

```http
GET /api/v1/sellers/orders?status=paid,preparing&sort=-created_at
```

E o avanço, como sempre, é o par `possible-statuses` + rota única:

<a class="api-route" href="/reference/pedidos#tag/pedidos/GET/api/v1/sellers/orders/{id}/possible-statuses"><span class="api-method api-method-get">GET</span><span class="api-path">/api/v1/sellers/orders/{id}/possible-statuses</span></a>

<a class="api-route" href="/reference/pedidos#tag/pedidos/POST/api/v1/sellers/orders/{id}/status"><span class="api-method api-method-post">POST</span><span class="api-path">/api/v1/sellers/orders/{id}/status</span></a>

## Etapa a etapa

| Status | O que significa | Sua ação | Próximo |
|---|---|---|---|
| `paid` | Pagamento confirmado. Dados do comprador liberados (`customer_data_released=true`). | `{ "status": "preparing" }` quando começar a separar/embalar. | `preparing` |
| `preparing` | Pedido sendo embalado. **Aqui abre a janela fiscal**: emita a NF-e ou Declaração ([fiscal](/guia/pedidos-fiscal)) e gere as etiquetas ([logística](/guia/pedidos-logistica)). | `{ "status": "ready_to_ship" }` com o pacote fechado e documento emitido. | `ready_to_ship` |
| `ready_to_ship` | Aguardando coleta ou postagem. | `{ "status": "shipped", "data": { "tracking_code": "BR…" } }` quando o pacote sair. | `shipped` |
| `shipped` | Em trânsito com a transportadora. | `{ "status": "delivered" }` quando o cliente receber — **se** a entrega for operada por você (ver abaixo). | `delivered` |
| `delivered` | Entregue. Estado terminal. | Nada. | — |

O `tracking_code` em `shipped` é opcional (string, máx 100), mas alimenta a tela de acompanhamento do cliente e a rota <a class="api-route" href="/reference/pedidos#tag/pedidos/GET/api/v1/sellers/orders/{id}/tracking"><span class="api-method api-method-get">GET</span><span class="api-path">/api/v1/sellers/orders/{id}/tracking</span></a> — informe sempre que tiver.

---

## Pontos de atenção

### O gate fiscal trava o despacho

Mercadoria não sai sem documento fiscal. As transições para `ready_to_ship` e `shipped` **recusam com 422** (campo `fiscal` no erro) se o pedido não tiver uma NF-e ou Declaração de Conteúdo emitida:

```json
{
  "errors": {
    "fiscal": ["É necessário emitir uma NF-e ou Declaração de Conteúdo antes de despachar o pedido."]
  }
}
```

Por isso a ordem prática dentro de `preparing` é: emitir documento → gerar etiqueta → marcar `ready_to_ship`. O passo a passo da emissão está em [Pedidos: fiscal](/guia/pedidos-fiscal).

> A máquina de estados aceita pular de `paid` direto para `ready_to_ship`, mas na prática o atalho raramente serve: a emissão fiscal só abre a partir de `preparing`, e sem documento o gate barra a transição. Passe por `preparing`.

### Partes do fluxo podem andar sozinhas

Não assuma que você é o único a mover o pedido — releia `possible-statuses` antes de cada ação em vez de confiar num estado em cache:

- **`paid` → `preparing` automático**: lojas com preparação automática habilitada pela plataforma têm esse passo dado pelo sistema assim que o pagamento confirma.
- **`ready_to_ship` → `shipped` automático**: quando o shipment é rastreado pela plataforma e entra em trânsito, o pedido é marcado como enviado sem você chamar nada.
- **`shipped` → `delivered` automático**: idem quando a transportadora confirma a entrega.

Cada mudança fica registrada em `GET /orders/{id}/events`, com `author_type` dizendo quem fez (você, o sistema, o admin).

### Quem pode confirmar `delivered`

Depende de quem opera a entrega:

| Situação | Confirmação |
|---|---|
| Frete com transportadora própria do seller, ou entrega manual sem transportadora | **Você**, via `{ "status": "delivered" }`. |
| Frete com transportadora da plataforma | **Automática/da operação.** Sua chamada manual recusa com 422. |
| Entrega com motorista da plataforma em andamento | Confirmação vem do app do motorista; manual só depois que ele concluir ou registrar falha. |

### Prazos viram ocorrências

Pedido pago tem SLA: documento fiscal e preparação têm prazos configurados pela plataforma, e estourar gera [ocorrência](/guia/ocorrencias) ligada ao pedido. Não deixe pedidos parados em `paid`.

---

## Cancelamento

A janela do seller é **curta**: só enquanto o pedido não entrou em preparação. Em `paid` você ainda pode:

```json
{
  "status": "cancelled",
  "data": { "reason": "Sem estoque para entregar no prazo." }
}
```

A partir de `preparing` (inclusive), o cancelamento é exclusivo da operação da plataforma — a API recusa com 422 e o `possible-statuses` **já nem lista** a opção `cancel` para você. Se precisar cancelar um pedido em preparação ou despachado, abra o caso com o suporte da plataforma.

`delivered` é terminal: depois dele, o caminho para desfazer a venda é a [devolução](/guia/devolucoes), não o cancelamento.

---

## Emissão fiscal

A elegibilidade abre em `preparing` e segue valendo em `ready_to_ship`, `shipped` e `delivered`. Em `paid` a API fiscal recusa com 422 ("o pedido precisa estar em preparação"). Ou seja:

```mermaid
flowchart LR
    A["paid<br/>(fiscal bloqueado)"] --> B["preparing<br/>(fiscal liberado ✓)"]
    B --> C["ready_to_ship → shipped → delivered<br/>(fiscal segue liberado)"]
```

Emitiu tarde demais? Sem problema — dá para registrar a NF-e mesmo com o pedido já `delivered` (caso clássico: corrigir documento depois da entrega). O fluxo completo de emissão, polling e download está em [Pedidos: fiscal](/guia/pedidos-fiscal).

---

## Para onde ir agora

| Você precisa… | Vá para |
|---|---|
| Emitir NF-e ou Declaração | [Pedidos: fiscal](/guia/pedidos-fiscal) |
| Gerar e baixar etiquetas | [Pedidos: logística](/guia/pedidos-logistica) |
| Pedido com retirada na loja | [Pedidos: retirada](/guia/pedidos-retirada) |
| Pedido aguardando reposição de estoque | [Pedidos: sob encomenda](/guia/pedidos-sob-encomenda) |
| Produto fabricado sob demanda | [Pedidos: industrialização](/guia/pedidos-industrializacao) |

=================================================================
SOURCE: https://api-docs.samdevel.com.br/guides/pedidos-retirada.md
=================================================================

# Retirada na loja

Pedidos com `delivery_method=pickup`: o cliente paga online (ou no balcão) e **vai buscar pessoalmente**. Sem transportadora, sem rastreio — no lugar disso, um código de confirmação que o cliente apresenta na hora de retirar.

> Pré-requisitos: leia primeiro [Pedidos: visão geral](/guia/pedidos). Os conceitos de `possible-statuses`, rota única `/status` e `workflow_type` valem aqui também.

---

## Quando um pedido cai aqui

Retirada é um **eixo de fulfillment**, não um perfil comercial: o que manda é o campo `delivery_method=pickup`, definido na criação do pedido. Qualquer `workflow_type` pode terminar em retirada — um pedido de industrialização, por exemplo, passa pela fábrica e depois fica pronto para o cliente buscar. Em pedidos antigos você ainda encontra `workflow_type=in_store_pickup`; trate igual.

```jsonc
{
  "order_number": "ORD-000456",      // identificador público do pedido
  "delivery_method": "pickup",       // ← é por aqui que você reconhece
  "workflow_type": "in_store_pickup",
  "status": "ready_for_pickup",
  "pickup_address": {…},             // onde o cliente retira (quando definido)
  "picked_up_at": null               // preenche quando o cliente buscar
}
```

O que muda na prática: o pedido **nunca passa** por `ready_to_ship`/`shipped`. O `possible-statuses` já cuida disso — em pedidos de retirada, os status de envio nem aparecem como opção (e vice-versa: pedidos de envio nunca veem os status de retirada).

---

## O caminho

```mermaid
stateDiagram-v2
    direction LR
    [*] --> paid
    paid --> preparing
    preparing --> ready_for_pickup
    ready_for_pickup --> picked_up: código<br/>confirmado
    ready_for_pickup --> pickup_expired: prazo<br/>estourou
    picked_up --> delivered: automático
    pickup_expired --> cancelled: automático
    delivered --> [*]
    cancelled --> [*]
```

A bifurcação em relação ao fluxo de [entrega](/guia/pedidos-entrega) acontece **em `preparing`**: onde o pedido de envio iria para `ready_to_ship`, o de retirada vai para `ready_for_pickup`. Antes disso, o começo é idêntico.

> Você pode encontrar um status `reserved` no catálogo de enums. Ignore-o ao integrar: nenhuma transição parte de `paid` para ele — o caminho real é `paid → preparing → ready_for_pickup`.

## Etapa a etapa

| Status | O que significa | Sua ação | Próximo |
|---|---|---|---|
| `paid` | Pagamento confirmado. | `{ "status": "preparing" }` quando começar a separar. | `preparing` |
| `preparing` | Separando o pedido. **Emita aqui a NF-e ou Declaração** ([fiscal](/guia/pedidos-fiscal)) — sem documento, a próxima transição é recusada. | `{ "status": "ready_for_pickup" }` com o pedido separado e documento emitido. | `ready_for_pickup` |
| `ready_for_pickup` | Cliente avisado de que pode buscar. O relógio de expiração está correndo. | Quando o cliente aparecer: `{ "status": "picked_up", "data": { "confirmation_code": "4821" } }` com o código que **ele** apresentar. | `picked_up` → `delivered` |
| `picked_up` | Cliente retirou. | Nada — o sistema encadeia `delivered` **no mesmo request**. | `delivered` (automático) |
| `pickup_expired` | O prazo de retirada venceu sem o cliente aparecer. | Nada — o sistema cascateia o cancelamento com estorno. | `cancelled` (automático) |

Tudo pela dupla de sempre:

<a class="api-route" href="/reference/pedidos#tag/pedidos/GET/api/v1/sellers/orders/{id}/possible-statuses"><span class="api-method api-method-get">GET</span><span class="api-path">/api/v1/sellers/orders/{id}/possible-statuses</span></a>

<a class="api-route" href="/reference/pedidos#tag/pedidos/POST/api/v1/sellers/orders/{id}/status"><span class="api-method api-method-post">POST</span><span class="api-path">/api/v1/sellers/orders/{id}/status</span></a>

---

## O código de confirmação

É a peça central da retirada — e a parte mais incompreendida.

**Quem tem o código é o cliente, não você.** Um código numérico de 4 dígitos é gerado na criação do pedido e entregue só ao comprador (ele vê no acompanhamento do pedido dele). A API **nunca devolve esse código para a loja** — nem no detalhe do pedido, nem no `metadata`. É de propósito: o código prova que quem está no balcão é o comprador. Sua integração não consulta o código; ela **digita o que o cliente apresentar**.

```http
POST /api/v1/sellers/orders/ORD-000456/status
Content-Type: application/json

{ "status": "picked_up", "data": { "confirmation_code": "4821" } }
```

O `confirmation_code` é obrigatório nessa transição (o `payload_schema` do `possible-statuses` marca `required`). O que pode dar errado:

| Resposta | Quando | Como tratar |
|---|---|---|
| `422` em `confirmation_code` — "Código de confirmação incorreto. N tentativa(s) restante(s)." | Cliente ditou errado. | Peça para conferir e tente de novo. São **10 tentativas no total**. |
| `422` — "Pedido bloqueado por excesso de tentativas." | Estourou as 10 tentativas. | Só o suporte da plataforma destrava. |
| `422` em `status` — "ainda não está pronto para retirada" | Pedido não está em `ready_for_pickup`. | Confira o estado atual antes de confirmar. |
| `200`, mas o pedido virou `pickup_expired` | O prazo venceu antes da confirmação. | A expiração tem precedência — ver abaixo. |

**Confirmou, acabou.** O `picked_up` encadeia `delivered` automaticamente no mesmo request (o ato de digitar o código *é* a entrega física). Não chame `delivered` depois — a resposta já volta com o pedido entregue, e `picked_up_at` preenchido.

---

## Expiração da retirada

O pedido tem um prazo para ser retirado (por padrão, **3 dias** a partir da criação). Vencido o prazo:

- Uma tentativa de confirmação tardia move o pedido para `pickup_expired` em vez de aceitar a retirada — mesmo com o código certo.
- Você também pode marcar manualmente: `{ "status": "pickup_expired" }`.
- Em seguida o **sistema cancela sozinho** (`pickup_expired → cancelled`), disparando o pipeline completo: estorno ao cliente, liberação do estoque reservado, e-mail. Você não precisa (nem deve) chamar `cancelled` por conta própria.

Na timeline (`GET /orders/{id}/events`) o cancelamento aparece com motivo "Retirada expirada" e autoria do sistema. Cliente desistiu mas o prazo ainda não venceu? Aí é um cancelamento comum — ver abaixo.

---

## Cancelamento

Mesma regra dos outros fluxos: o seller só cancela **antes da preparação**. Em `paid`, mande `{ "status": "cancelled" }` com um `reason` opcional. De `preparing` em diante, o `possible-statuses` esconde a opção e a API recusa — cancelamento vira assunto da operação da plataforma.

A exceção prática é a expiração: `pickup_expired → cancelled` acontece sozinho, sem depender de ninguém.

---

## Emissão fiscal

A janela abre em `preparing` e segue aberta em `ready_for_pickup` e `picked_up` (e `delivered`). Retirada presencial **não dispensa documento**: a transição `preparing → ready_for_pickup` tem o mesmo gate fiscal do envio e recusa com 422 (campo `fiscal`) se não houver NF-e ou Declaração de Conteúdo emitida.

Ordem prática: separar → emitir documento ([fiscal](/guia/pedidos-fiscal)) → `ready_for_pickup` → aguardar o cliente. Etiqueta de transporte não se aplica — não há shipment de transportadora para gerar.

---

## Para onde ir agora

| Você precisa… | Vá para |
|---|---|
| Fluxo com envio/transportadora | [Pedidos: entrega](/guia/pedidos-entrega) |
| Emitir NF-e ou Declaração | [Pedidos: fiscal](/guia/pedidos-fiscal) |
| Pedido fabricado sob demanda (pode terminar em retirada) | [Pedidos: industrialização](/guia/pedidos-industrializacao) |
| Schemas e códigos de erro | [Referência da API](/reference/pedidos) |

=================================================================
SOURCE: https://api-docs.samdevel.com.br/guides/pedidos-sob-encomenda.md
=================================================================

# Sob encomenda

Workflow `backorder`: venda **sem estoque disponível**. O cliente paga, e o pedido fica estacionado aguardando a mercadoria chegar — quando você **abastece o estoque do SKU**, a plataforma detecta e retoma o pedido sozinha. Se a sua integração só conhece o fluxo padrão, esses pedidos parecem "travados depois do pago"; este guia explica essa espera e como ela se destrava.

> Pré-requisitos: leia primeiro [Pedidos: visão geral](/guia/pedidos). Os conceitos de `possible-statuses`, rota única `/status` e `workflow_type` valem aqui também.

---

## Quando um pedido cai aqui

Pedido sob encomenda nasce de venda B2B (pedido direto, proposta de representante) sobre produto que **permite venda sem estoque** — e só quando a plataforma tem o recurso habilitado. Você reconhece pelo `workflow_type`:

```jsonc
{
  "order_number": "ORD-000789",       // identificador público do pedido
  "workflow_type": "backorder",       // ← sob encomenda (label "Sob Encomenda")
  "status": "awaiting_restock",       // ← a espera: "Aguardando Reposição"
  "channel": "direct",                // canal de origem da venda
  "delivery_method": "shipping"       // a entrega segue o eixo normal depois
}
```

Existe um workflow espelho, `pre_order` (pré-venda): em vez de aguardar *reposição* (`awaiting_restock → restocked`), aguarda a *primeira disponibilidade* (`awaiting_availability → available`). A mecânica é idêntica; este guia usa o backorder como referência.

---

## O caminho

```mermaid
stateDiagram-v2
    direction LR
    [*] --> paid
    paid --> awaiting_restock: automático
    awaiting_restock --> restocked: estoque abastecido<br/>(automático)
    restocked --> paid: retomada<br/>(automático)
    state "preparing → … → delivered" as trilho
    paid --> trilho
```

Em palavras: o pagamento confirma (`paid`), o **sistema move sozinho** o pedido para `awaiting_restock`, e ali ele fica — fora das filas de preparação — até a mercadoria chegar. Quando você **abastece o estoque do SKU** (o mesmo `POST /api/products/{id}/stocks` de sempre — veja [Produtos · Estoque](/guia/produtos-estoque)), a plataforma detecta a reposição, marca o pedido como `restocked`, **debita a quantidade vendida do saldo** e o devolve ao trilho padrão a partir de `paid` — dali ele segue como qualquer [entrega](/guia/pedidos-entrega) (ou [retirada](/guia/pedidos-retirada), se for o caso).

> Pedidos esperando o mesmo SKU retomam **do mais antigo para o mais novo**, e cada um só sai da espera quando **todos** os seus itens têm saldo disponível.

## Etapa a etapa

| Status | O que significa | Sua ação | Próximo |
|---|---|---|---|
| `paid` (relance) | Pagamento confirmou. Estado de passagem: um job em background move o pedido em seguida. | Nada — não tente `preparing` aqui. | `awaiting_restock` (automático) |
| `awaiting_restock` | **A espera.** Pedido aguardando a mercadoria chegar ao seu estoque. | Provisione a mercadoria e, quando ela chegar, **dê entrada no estoque** (`POST /api/products/{id}/stocks` com `increment`). | `restocked` (automático) |
| `restocked` | Estoque reposto; marco registrado na timeline. | Nada — a quantidade vendida é debitada e o pedido retoma sozinho. | `paid`, e daí `preparing → …` |
| `preparing` em diante | Pedido voltou ao fluxo comum. | Igual ao guia de [entrega](/guia/pedidos-entrega): fiscal, etiqueta, envio. | — |

Existe também o destravamento **manual**, para quando você quer forçar a retomada sem dar entrada no estoque (ex.: a mercadoria chegou mas o ajuste de saldo vem depois). É a dupla de sempre:

<a class="api-route" href="/reference/pedidos#tag/pedidos/GET/api/v1/sellers/orders/{id}/possible-statuses"><span class="api-method api-method-get">GET</span><span class="api-path">/api/v1/sellers/orders/{id}/possible-statuses</span></a>

<a class="api-route" href="/reference/pedidos#tag/pedidos/POST/api/v1/sellers/orders/{id}/status"><span class="api-method api-method-post">POST</span><span class="api-path">/api/v1/sellers/orders/{id}/status</span></a>

```http
POST /api/v1/sellers/orders/ORD-000789/status
Content-Type: application/json

{ "status": "restocked" }
```

Mesmo no manual, a retomada completa acontece: o pedido passa por `restocked`, o saldo disponível é debitado (sem nunca ficar negativo) e ele volta pra `paid`.

---

## Como a integração percebe a espera

Quatro sinais, do mais direto ao mais sutil:

**1. O campo `status` é `awaiting_restock`.** Com `status_label` "Aguardando Reposição". O pedido não vai aparecer nos seus filtros de fila de preparação (`?status=paid,preparing`) — e isso é correto, não é um pedido perdido.

**2. A listagem filtra por workflow.** Sua fila de provisionamento é:

```http
GET /api/v1/sellers/orders?workflow=backorder&status=awaiting_restock&sort=created_at
```

(Ordenado do mais antigo para o mais novo: quem espera há mais tempo provisiona primeiro.)

**3. O `possible-statuses` só oferece duas saídas.** Em `awaiting_restock`, as `next_actions` são `mark-restocked` (→ `restocked`) e `cancel` (→ `cancelled`). Nada de `preparing`: se sua integração renderiza um botão por ação, ela já mostra o fluxo certo sem código especial.

```json
{
  "data": {
    "current_status": "awaiting_restock",
    "workflow_type": "backorder",
    "next_actions": [
      { "action": "mark-restocked", "target_status": "restocked", "label": "Reposto", "payload_schema": null },
      { "action": "cancel", "target_status": "cancelled", "label": "Cancelado", "payload_schema": { "type": "object", "properties": { "reason": { "type": "string", "maxLength": 500 } } } }
    ]
  }
}
```

**4. A timeline conta a história.** `GET /orders/{id}/events` registra o `paid → awaiting_restock` com autoria do sistema e, na retomada, o `awaiting_restock → restocked → paid` — com autoria "Sistema (reposição de estoque)" quando veio do abastecimento, ou a sua quando foi forçada manualmente.

---

## Pontos de atenção

**O pulo para `awaiting_restock` é assíncrono.** Logo após o webhook de pagamento, o pedido pode aparecer por alguns instantes como `paid` — e nessa janela curta o `possible-statuses` ainda oferece `preparing`, porque o sistema não estacionou o pedido. Não morda a isca: num pedido `workflow_type=backorder`, não dispare a esteira de preparação no primeiro `paid`. Espere o estado assentar em `awaiting_restock`; o produto, por definição, ainda não está no estoque.

**Os dados do comprador ficam visíveis durante a espera.** O pedido sob encomenda só estaciona **depois** do pagamento confirmado, então `customer_data_released` vem `true` e o PII do comprador (nome, endereço, documento) aparece normalmente em `awaiting_restock` e `restocked` — você pode planejar a entrega enquanto provisiona. O que continua bloqueado nesse período é a emissão fiscal (abaixo).

**O débito de estoque acontece na retomada.** A venda sob encomenda não reserva saldo (não havia saldo). Quando o pedido retoma (`restocked → paid`), a quantidade vendida é debitada dos seus locais — por isso a entrada de estoque vem primeiro. Na retomada manual sem saldo suficiente, o sistema debita o que houver (sem negativar) e registra a diferença em log.

**A reposição deve ser verdade.** O cliente já pagou e vê a linha do tempo do pedido ("Reposto" é um marco visível). Forçar a retomada sem ter a mercadoria só transfere o atraso para a etapa de preparação — onde os prazos de SLA da plataforma passam a contar.

---

## Cancelamento

Aqui o seller tem **mais** janela que nos outros fluxos: a espera inteira é pré-operacional, então `cancelled` está disponível tanto em `awaiting_restock` quanto em `restocked` — útil quando o fornecedor falha de vez:

```json
{
  "status": "cancelled",
  "data": { "reason": "Produto descontinuado pelo fornecedor, sem previsão de reposição." }
}
```

O estorno ao cliente segue o pipeline normal de cancelamento. A janela fecha no lugar de sempre: quando o pedido retoma o trilho e entra em `preparing`, cancelar vira assunto da operação da plataforma.

---

## Emissão fiscal

**Nada durante a espera.** `awaiting_restock` e `restocked` (e o `paid` de passagem) estão fora da janela fiscal — a API de emissão recusa com 422. A elegibilidade abre quando o pedido retoma o trilho e chega em `preparing`, exatamente como no fluxo de [entrega](/guia/pedidos-entrega). A partir daí: emitir documento ([fiscal](/guia/pedidos-fiscal)), gerar etiqueta ([logística](/guia/pedidos-logistica)), despachar.

---

## Para onde ir agora

| Você precisa… | Vá para |
|---|---|
| O trilho padrão pós-reposição | [Pedidos: entrega](/guia/pedidos-entrega) |
| Pedido que será retirado na loja | [Pedidos: retirada](/guia/pedidos-retirada) |
| Produto fabricado sob demanda (outro tipo de espera) | [Pedidos: industrialização](/guia/pedidos-industrializacao) |
| Emitir NF-e ou Declaração | [Pedidos: fiscal](/guia/pedidos-fiscal) |

=================================================================
SOURCE: https://api-docs.samdevel.com.br/guides/pedidos-industrializacao.md
=================================================================

# Industrialização

Workflow `production`. Para produtos que **passam pela fábrica antes de embalar**: sob encomenda, personalização, móveis sob medida, brindes corporativos, tecido.

> Pré-requisitos: leia primeiro [Pedidos: visão geral](/guia/pedidos). Os conceitos de `possible-statuses`, rota única `/status` e `workflow_type` valem aqui também.

---

## Para que serve

No workflow padrão, o pedido vai de `paid` direto para `preparing` (embalar) e depois `ready_to_ship` (despachar). Não dá conta de produto que **precisa ser feito**.

Industrialização insere quatro estados antes do trilho de envio:

```mermaid
stateDiagram-v2
    direction LR
    [*] --> paid
    paid --> production_started
    production_started --> awaiting_materials: faltam<br/>insumos
    production_started --> production_in_progress
    awaiting_materials --> production_in_progress: insumos<br/>chegaram
    production_in_progress --> production_finished
    production_finished --> preparing
    production_finished --> ready_to_ship: pular<br/>preparing
```

| Estado | Significa |
|---|---|
| `production_started` | A fábrica recebeu o pedido. Equipe foi avisada, ordem de produção foi criada. |
| `awaiting_materials` | A produção pausou porque falta insumo. Comum quando o seller compra material sob demanda. |
| `production_in_progress` | Está sendo fabricado agora. |
| `production_finished` | Saiu da fábrica. A partir daqui o pedido entra no trilho padrão de envio (`preparing` ou direto `ready_to_ship`). |

Quando a produção termina, o resto do ciclo é igual ao `standard`: emite documento fiscal, gera etiqueta, marca enviado, entregue.

---

> Mesmo com os estados novos de produção, você não vai encontrar um endpoint para cada etapa — nada de `/start-production`, `/finish-production` e afins. É de propósito: assim como no fluxo padrão, tudo passa pela **rota única de status**. Você consulta os estados válidos em `possible-statuses` e envia a mudança em `/status`; o que muda de uma etapa para outra é só o `status` que você manda. Uma rota só, do começo ao fim da produção.

## Fluxo passo a passo

### 1. Pedido entra como `paid`, workflow `production`

Você reconhece um pedido de industrialização lendo `workflow_type` no detalhe (ou filtrando a listagem por `?workflow=production`):

```http
GET /api/v1/sellers/orders?workflow=production&status=paid
```

A fábrica precisa saber que tem trabalho novo; esta é a fila de entrada.

### 2. Iniciar produção

Quando a equipe da fábrica vai pegar o pedido, marque início. **Use `production_started` no lugar de `preparing`**, pois `preparing` é para embalar produto pronto, não fabricar.

```http
POST /api/v1/sellers/orders/ORD-000123/status
Content-Type: application/json

{
  "status": "production_started",
  "data": { "estimated_days": 7 }
}
```

`estimated_days` é opcional, mas **fortemente recomendado**: a plataforma usa esse número para calcular uma estimativa de entrega que o cliente vê na tela de acompanhamento. Sem ele, o cliente fica sem previsão para algo que demora.

### 3. (Opcional) Pausar por falta de materiais

Se a produção precisar parar porque faltou insumo, registre. Isso fica no histórico do pedido e ajuda no SAC quando o cliente pergunta "por que está demorando?".

```json
{
  "status": "awaiting_materials",
  "data": { "materials": ["Tecido azul indigo 1.6m", "Zíper 20cm cor 4"] }
}
```

`materials[]` é opcional; você pode mandar apenas `{ "status": "awaiting_materials" }` para registrar a pausa sem detalhar a lista.

Quando o insumo chegar, volte para `production_in_progress`:

```json
{
  "status": "production_in_progress"
}
```

> Pode ir direto de `production_started` para `production_in_progress` se não houver pausa por insumos.

### 4. Concluir produção

Quando o produto sai da fábrica e está pronto para embalar:

```json
{
  "status": "production_finished"
}
```

A partir daqui, o pedido **deixa o trilho de produção** e entra no fluxo padrão. As próximas transições válidas (`GET /possible-statuses`) vão ser `preparing` (embalar) ou `ready_to_ship` (pular embalagem se já saiu pronto da fábrica).

### 5. Encaixar com o resto do ciclo

Depois de `production_finished`, o pedido se comporta exatamente como um `standard` em `paid`:

- Emite documento fiscal: ver [Pedidos: fiscal](/guia/pedidos-fiscal).
- Gera e baixa etiquetas: ver [Pedidos: logística](/guia/pedidos-logistica).
- Marca `shipped` com `tracking_code`, depois `delivered`.

---

## Cenários comuns

### "Recebi 3 pedidos sob encomenda hoje"

```http
GET /api/v1/sellers/orders?workflow=production&status=paid&sort=-created_at
```

Para cada pedido, dispare `production_started` com a estimativa real (não use um valor genérico, pois o cliente vê):

```json
{
  "status": "production_started",
  "data": {
    "estimated_days": 5
  }
}
```

### "A fábrica avisou que ficou sem o tecido X"

Pausa o pedido afetado registrando o que falta:

```json
{
  "status": "awaiting_materials",
  "data": { "materials": ["Tecido X cor 7"] }
}
```

Quando o insumo chegar, retoma:

```json
{
  "status": "production_in_progress"
}
```

### "Terminei de produzir, vou embalar"

```json
{
  "status": "production_finished"
}
```

E em seguida o trilho normal:

```json
{
  "status": "preparing"
}
```

### "Terminei de produzir e já vai direto pra transportadora (sem caixa interna)"

`production_finished` aceita pular `preparing` e ir direto para `ready_to_ship`. O `possible-statuses` mostra ambas as opções:

```json
{
  "status": "ready_to_ship"
}
```

### "Esse pedido vai dar muito trabalho, preciso cancelar"

Pela API, o cancelamento só está disponível **antes de a produção começar** (pedido ainda em `paid`). A partir de `production_started`, cancelar passa a ser com a operação da plataforma — acione o suporte. Se ainda está em `paid`:

```json
{
  "status": "cancelled",
  "data": {
    "reason": "Insumo descontinuado pelo fornecedor."
  }
}
```

O `reason` vai para o histórico e pode aparecer no SAC do cliente.

---

## Cuidados

**Não use `preparing` no início.** É comum a equipe estar acostumada com o fluxo padrão e marcar `preparing` quando o pedido chega — e um POST direto até passa, mas fura o fluxo de produção: o pedido pula os estados da fábrica e o cliente perde o acompanhamento. Tanto é desvio que `preparing` nem aparece em `possible-statuses` para pedidos de produção. A partir de `paid`, use `production_started`.

**`estimated_days` muda a UX do cliente.** Esse número alimenta a estimativa de prazo na tela do cliente. Ser otimista demais gera reclamação; prefira margem de segurança.

**Dá para emitir documento fiscal já em `production_finished`?** Sim — a elegibilidade fiscal abre em qualquer estado de produção (`production_started`, `awaiting_materials`, `production_in_progress`, `production_finished`), sem precisar avançar pra `preparing` (ver [Pedidos: fiscal](/guia/pedidos-fiscal)). Útil quando a nota precisa sair antes de o produto ficar pronto.

**O cliente acompanha a produção?** Sim. As mudanças de status de produção ficam visíveis para o cliente como linha do tempo do pedido, incluindo o `awaiting_materials` (sem revelar o `materials[]`, que é interno). Por isso `estimated_days` e a passagem para `production_in_progress` são importantes: dão sinal de vida.

=================================================================
SOURCE: https://api-docs.samdevel.com.br/guides/pedidos-fiscal.md
=================================================================

# Fiscal

NF-e (modelo 55), Declaração de Conteúdo, registro de XML e download de DANFE/PDF. **Um documento fiscal por pedido.**

> Pré-requisitos: leia primeiro [Pedidos: visão geral](/guia/pedidos). O pedido precisa estar em estado fiscal-elegível (`preparing` em diante — ou, em pedidos de produção, qualquer estado da fábrica) antes de qualquer operação aqui.

---

## Conceitos

O objeto que `GET /orders/{id}/fiscal` devolve — o(s) documento(s) já registrado(s) e a disponibilidade da Declaração:

```jsonc
{
  "invoices": [                  // 0 ou 1 documento por pedido
    {
      "id": "5b4f…",
      "type": "nfe",             // ← nfe | declaration
      "status": "completed",     // ← pending | validating | valid | invalid | processing | completed | error
      "invoice_key": "3526…",    // chave de acesso (44 dígitos, NF-e)
      "invoice_number": "1234",  // só display
      "invoice_value": 249.9,    // só display
      "has_pdf": true,           // ← DANFE/PDF pronto p/ download (olhe aqui, não no status)
      "error_message": null,     // motivo quando a validação fiscal falha (status error/invalid)
      "created_at": "2026-04-26T15:10:00Z"
    }
  ],
  "declaration_available": true  // loja pode emitir Declaração de Conteúdo
}
```

---

## Quem emite o quê

Quem emite a NF-e é a sua loja, não a plataforma — e isso faz sentido: a nota nasce dos seus dados fiscais, do seu CNPJ e do seu regime tributário, então ela sai do seu ERP e é autorizada pela SEFAZ no seu nome. O papel da plataforma vem depois que a nota existe: **guardar a chave** (e, idealmente, o XML), **gerar o DANFE** a partir do XML, e **disponibilizar para download** quando o cliente ou o transportador precisar. Ou seja, você cuida da emissão; a gente cuida de hospedar e entregar.

A Declaração de Conteúdo é diferente: a plataforma **gera o PDF dela** para você, em background, a partir dos dados do pedido. É uma alternativa à NF-e para casos onde a loja não emite nota fiscal (típico em PF, MEI sem inscrição estadual, ou em pedidos de baixo valor).

Você usa **um ou outro por pedido**:

```mermaid
flowchart TD
    A["Pedido em preparing+"] --> B{"Sua loja emite<br/>NF-e?"}
    B -->|sim| C["Emite no ERP da loja"]
    C --> D{"Tem o XML<br/>autorizado?"}
    D -->|"sim, em mãos"| E["POST /fiscal/nfe/key<br/>com invoice_xml"]
    D -->|"XML em arquivo separado"| G["POST /invoice/upload-xml<br/>multipart"]
    E --> H["DANFE gerado<br/>em background"]
    G --> H
    B -->|"não, loja PF"| I["POST /fiscal/declaration"]
    I --> J["Declaração gerada<br/>em background"]
    H --> K["GET /fiscal — polling até has_pdf=true"]
    J --> K
    K --> L["GET /fiscal/&lcub;invoiceId&rcub;/pdf<br/>URL S3 15 min"]
```

---

## Pré-condições

**O pedido precisa estar num estado fiscal-elegível**: qualquer estado de produção (`production_started`, `awaiting_materials`, `production_in_progress`, `production_finished`) ou `preparing` em diante (`ready_to_ship`, `shipped`, `delivered`, `ready_for_pickup`, `picked_up`). Tentar emitir em `paid` retorna **422**:

```json
{
  "success": false,
  "code": 422,
  "message_code": "VALIDATION_ERROR",
  "errors": {
    "status": ["O pedido precisa estar em preparação antes de emitir documentos fiscais. Status atual: paid"]
  }
}
```

**Declaração de Conteúdo exige feature da loja.** A primeira chamada que você deve fazer é a consulta:

<a class="api-route" href="/reference/pedidos#tag/fiscal/GET/api/v1/sellers/orders/{id}/fiscal"><span class="api-method api-method-get">GET</span><span class="api-path">/api/v1/sellers/orders/{id}/fiscal</span></a>

```json
{
  "data": {
    "invoices": [],
    "declaration_available": true
  }
}
```

- `invoices[]`: documentos já emitidos para esse pedido (vazio se for o primeiro).
- `declaration_available`: se sua loja tem a feature `fiscal_declaration` ativa. Se vier `false`, só resta NF-e.

---

## NF-e: dois caminhos

A NF-e nasce no seu ERP. O que muda é **o que você tem em mãos** quando vai registrar:

### Caminho 1: XML completo em string

Se o seu ERP devolveu o XML autorizado junto com a chave, mande tudo de uma vez:

<a class="api-route" href="/reference/pedidos#tag/fiscal/POST/api/v1/sellers/orders/{id}/fiscal/nfe/key"><span class="api-method api-method-post">POST</span><span class="api-path">/api/v1/sellers/orders/{id}/fiscal/nfe/key</span></a>

```json
{
  "type": "nfe",
  "invoice_key": "35260411222333000144550010000012341123456789",
  "invoice_number": "1234",
  "invoice_value": 249.90,
  "invoice_xml": "<?xml version=\"1.0\"?><nfeProc>...</nfeProc>"
}
```

| Campo | Obrigatório | O que é |
|---|---|---|
| `type` | sim | Sempre `nfe` por enquanto. |
| `invoice_key` | sim | Chave de acesso de 44 caracteres (aqui a validação é só de tamanho; o dígito mod-11 é conferido nos fluxos de lookup/upload de XML). |
| `invoice_xml` | **sim** | XML autorizado. A partir dele a plataforma gera o DANFE em background. |
| `invoice_number` | não | Número da nota, só para exibição. |
| `invoice_value` | não | Valor total, só para exibição. |

Retorno **201 Created**. O DANFE entra na fila e fica pronto em alguns segundos.

### Caminho 2: upload manual do XML

Quando você tem o arquivo .xml em mãos mas **não tem ainda a chave registrada na plataforma** (cenário típico: XML salvo do email do contador, exportado de outro sistema). Mande o arquivo direto:

<a class="api-route" href="/reference/pedidos#tag/fiscal/POST/api/v1/sellers/orders/{id}/invoice/upload-xml"><span class="api-method api-method-post">POST</span><span class="api-path">/api/v1/sellers/orders/{id}/invoice/upload-xml</span></a>

`multipart/form-data` com o campo `xml` contendo o arquivo (máx 1 MB, extensão `.xml`):

```http
POST /api/v1/sellers/orders/ORD-000123/invoice/upload-xml
Content-Type: multipart/form-data; boundary=----X

------X
Content-Disposition: form-data; name="xml"; filename="nfe.xml"
Content-Type: application/xml

<?xml version="1.0"?>...
------X--
```

A chave é **extraída do próprio XML** (atributo `Id="NFe..."` em `infNFe`) e validada por mod-11. Resposta igual ao caminho 1.

Erros típicos:
- **422**: XML sem chave, chave inválida, arquivo vazio, extensão errada.
- **409 `chave_already_used`**: essa NF-e já está vinculada a outro pedido.
- **409 `invoice_already_validated`**: refaça o invoice antes de reenviar.

---

## Declaração de Conteúdo

Para lojas sem NF-e. **A plataforma gera o PDF a partir dos dados do pedido**; você só dispara:

<a class="api-route" href="/reference/pedidos#tag/fiscal/POST/api/v1/sellers/orders/{id}/fiscal/declaration"><span class="api-method api-method-post">POST</span><span class="api-path">/api/v1/sellers/orders/{id}/fiscal/declaration</span></a>

Sem body. Resposta **202 Accepted**:

```json
{
  "success": true,
  "code": 202,
  "data": {
    "invoice": { "id": "8c7e...", "type": "declaration", "status": "pending", "has_pdf": false }
  }
}
```

A partir daí, polling até `has_pdf=true` no `GET /fiscal`.

Erros típicos:
- **403**: `declaration_available=false` na sua loja. Peça ativação da feature `fiscal_declaration`.
- **422**: pedido em status que não permite emissão fiscal (típico: `paid`).

---

## Polling do PDF

NF-e (com XML) e Declaração geram PDF em background. O padrão é o mesmo para os dois:

```mermaid
sequenceDiagram
    autonumber
    participant I as Integrador
    participant A as API
    participant W as Worker
    I->>A: POST /fiscal/declaration<br/>(ou /fiscal/nfe/key com XML)
    A-->>I: 202 / 201
    A->>W: dispara job
    Note over W: gera PDF
    loop polling (a cada N segundos)
      I->>A: GET /fiscal
      A-->>I: { invoices: [{ has_pdf: false }] }
    end
    W-->>A: PDF pronto
    I->>A: GET /fiscal
    A-->>I: { invoices: [{ has_pdf: true }] }
    I->>A: GET /fiscal/{invoiceId}/pdf
    A-->>I: { url, expires_in: "15 minutos" }
```

> **Cadência recomendada:** primeiros 30 segundos a cada 3s, depois a cada 10s, com timeout em 2 minutos. PDFs simples ficam prontos em ~5s; declarações com muitos itens podem levar mais.

---

## Baixar o PDF

<a class="api-route" href="/reference/pedidos#tag/fiscal/GET/api/v1/sellers/orders/{id}/fiscal/{invoiceId}/pdf"><span class="api-method api-method-get">GET</span><span class="api-path">/api/v1/sellers/orders/{id}/fiscal/{invoiceId}/pdf</span></a>

`{invoiceId}` é o `id` que veio em `invoices[]` na resposta de `/fiscal`. A rota é a mesma para DANFE e Declaração; o `filename` já vem com o prefixo certo (`danfe_` ou `declaracao_`).

Quando o PDF está pronto:

```json
{
  "success": true,
  "code": 200,
  "data": {
    "url": "https://s3.amazonaws.com/.../danfe_ORD-000123.pdf?X-Amz-Signature=...",
    "filename": "danfe_ORD-000123.pdf",
    "expires_in": "15 minutos"
  }
}
```

A `url` é S3 assinada, válida por **15 minutos**. Se expirar, chame a rota de novo, que gera uma nova URL. Não armazene a URL.

Quando ainda não está pronto, retorna **202 Accepted** com o status atual (não é um erro, é só sinal de "tente de novo").

---

## Consulta de dados (sem PDF)

<a class="api-route" href="/reference/pedidos#tag/fiscal/GET/api/v1/sellers/orders/{id}/fiscal/{invoiceId}/view"><span class="api-method api-method-get">GET</span><span class="api-path">/api/v1/sellers/orders/{id}/fiscal/{invoiceId}/view</span></a>

Devolve os dados completos do invoice (chave, número, valor, status, metadata da declaração) **sem o PDF**. Útil para telas de detalhe fiscal no painel.

---

## Cenários comuns

### "Acabei de emitir a NF-e no ERP, quero registrar agora"

ERP devolveu chave + XML:

```http
POST /api/v1/sellers/orders/ORD-000123/fiscal/nfe/key
{ "type": "nfe", "invoice_key": "...", "invoice_xml": "<?xml...?>" }
```

Depois faça polling do `/fiscal` até `has_pdf=true` e baixe.

### "Loja é MEI, vai usar Declaração"

Confirme primeiro:

```http
GET /api/v1/sellers/orders/ORD-000123/fiscal
```

Se `declaration_available=true`, dispara:

```http
POST /api/v1/sellers/orders/ORD-000123/fiscal/declaration
```

Polling → download.

### "O cliente está reclamando que não recebeu o DANFE, quero reenviar"

A URL expirou. Faça `GET /fiscal/{invoiceId}/pdf` de novo, que vai gerar uma URL nova.

### "Errei a NF-e, emiti outra no ERP, quero corrigir"

A rota `/fiscal/nfe/key` aceita ser chamada de novo no mesmo pedido **se o invoice ainda não foi validado**. Se já foi validado, vai bater **409 `invoice_already_validated`**; nesse caso é caso de operação (contate suporte para reabrir o invoice).

---

## Cuidados

**Não chame `/fiscal` em loop curto.** Para acompanhar geração de PDF, cadência de 3–10 segundos é suficiente. Loop em ms gasta quota sem benefício.

**`invoice_xml` é obrigatório.** Esquecer o XML no `/fiscal/nfe/key` retorna 422. Se você ainda não tem o XML registrado na plataforma, use `/invoice/upload-xml` para enviar o arquivo.

**Um pedido carrega um único documento fiscal — NF-e ou Declaração, nunca os dois.** Isso é proposital: o documento fiscal é o que acompanha a mercadoria, e ter dois competindo pelo mesmo pedido só geraria ambiguidade na hora de imprimir, despachar e mostrar pro cliente. Por isso você escolhe um caminho no começo. Se emitiu um e precisa trocar, não é algo que você desfaz pela API — é caso de operação (contate o suporte).

**Não armazene as URLs S3.** Elas têm assinatura curta. Sempre chame `/fiscal/{invoiceId}/pdf` na hora de servir o download, pois a URL anterior pode estar expirada.

=================================================================
SOURCE: https://api-docs.samdevel.com.br/guides/pedidos-logistica.md
=================================================================

# Logística

Shipments do pedido, geração e download de etiquetas de envio. **Operação assíncrona** com polling.

> Pré-requisitos: leia primeiro [Pedidos: visão geral](/guia/pedidos). As etiquetas só fazem sentido depois que o pedido tem um documento fiscal; veja [Pedidos: fiscal](/guia/pedidos-fiscal).

---

## O modelo mental

Um **pedido** pode ter **um ou mais shipments**. Cada shipment representa um pacote físico que sai da loja e tem um destino, transportador e (quando aplicável) código de rastreio. Para a maioria dos pedidos é 1 shipment, mas pode ter mais (ex.: produto de um SKU vem de uma origem e outro de outra; ou um pedido com retirada parcial e envio parcial).

Cada **shipment** tem zero, uma ou mais **etiquetas (`labels`)**. A etiqueta é o PDF que vai colado na caixa. Pode ter mais de uma quando o transportador exige formatos diferentes (ex: A4 + impressão térmica) ou para shipments com múltiplos volumes.

```mermaid
flowchart LR
    O[Order] --- S1[Shipment 1]
    O --- S2[Shipment 2]
    S1 --- L1[Label A4]
    S1 --- L2[Label térmica]
    S2 --- L3[Label A4]
```

Você como seller **dispara a geração** (fila um job em background), faz **polling** até as etiquetas ficarem prontas, e baixa cada uma via URL S3 assinada.

`GET /orders/{id}/shipments` devolve `{ "shipments": [ … ] }`. Cada **shipment**:

```jsonc
{
  "id": 4421,
  "uid": "shp_a1b2c3",
  "type": "sales_order",         // tipo: sales_order | return | transfer | pickup
  "status": "ready_to_ship",     // ← status do shipment (valores abaixo)
  "tracking_code": "BR123…BR",   // rastreio fica aqui, não na etiqueta
  "carrier": {…},                // transportadora (id, name)
  "labels": [{…}],               // etiquetas deste shipment
  "labels_ready": 1,             // quantas já prontas
  "labels_total": 1              // total esperado
}
```

Os valores de `status` do shipment: `pending`, `processing`, `ready_to_ship`, `in_transit`, `delivered`, `cancelled`, `return_in_progress`, `returned`, `awaiting_return`, `failed`, `error`.

E cada **label** dentro de `labels`:

```jsonc
{
  "id": "lbl_xyz",
  "format": "a4",                // formato: a4 | zebra | thermal (+ format_label em pt-BR)
  "status": "completed",         // ← pending | processing | completed | error
  "is_ready": true,              // já pode baixar? (pronto = completed)
  "completed_at": "2026-04-26T16:02:00Z"
}
```

---

## Fluxo completo

```mermaid
sequenceDiagram
    autonumber
    participant I as Integrador
    participant A as API
    participant W as Worker
    I->>A: POST /orders/{id}/shipment-labels/generate
    A-->>I: 202 { queued_shipments: [4421], total: 1 }
    A->>W: dispara job (1 por shipment)
    loop polling (a cada N segundos)
      I->>A: GET /orders/{id}/shipments
      A-->>I: shipments com labels[] (is_ready: false)
    end
    W-->>A: labels prontas
    I->>A: GET /orders/{id}/shipments
    A-->>I: labels com is_ready: true
    I->>A: GET /logistics/shipments/{id}/labels/{labelId}/download
    A-->>I: { url, expires_in: "15 minutos" }
```

### 1. Disparar geração

<a class="api-route" href="/reference/pedidos#tag/etiquetas/POST/api/v1/sellers/orders/{id}/shipment-labels/generate"><span class="api-method api-method-post">POST</span><span class="api-path">/api/v1/sellers/orders/{id}/shipment-labels/generate</span></a>

Sem body. Enfileira um job para **cada shipment do pedido**. Resposta **202**:

```json
{
  "success": true,
  "code": 202,
  "data": {
    "queued_shipments": [4421, 4422],
    "total": 2
  }
}
```

`queued_shipments` lista os IDs efetivamente enfileirados. Pode ser menor que o número total de shipments do pedido quando algum tem `label_generation_blocked=true` (ver "Cuidados" abaixo).

> **Chamar de novo regenera.** Se a primeira geração falhou ou se você corrigiu algo no shipment e quer outra rodada, basta chamar `/generate` de novo: as etiquetas anteriores são descartadas e novos jobs são disparados.

### 2. Polling

<a class="api-route" href="/reference/pedidos#tag/etiquetas/GET/api/v1/sellers/orders/{id}/shipments"><span class="api-method api-method-get">GET</span><span class="api-path">/api/v1/sellers/orders/{id}/shipments</span></a>

Consulta a lista de shipments com etiquetas embutidas. O que você procura em cada shipment é `labels_ready === labels_total`:

```json
{
  "data": {
    "shipments": [
      {
        "id": 4421,
        "uid": "shp_a1b2c3",
        "type": "sales_order",
        "status": "ready_to_ship",
        "tracking_code": "BR123456789BR",
        "carrier": { "id": 12, "name": "Correios" },
        "labels": [
          {
            "id": "lbl_xyz",
            "format": "a4",
            "format_label": "A4 (PDF)",
            "status": "completed",
            "is_ready": true,
            "completed_at": "2026-04-26T16:02:00Z"
          }
        ],
        "labels_ready": 1,
        "labels_total": 1
      }
    ]
  }
}
```

> **Cadência recomendada:** primeiros 15 segundos a cada 2s, depois a cada 5s, com timeout em 2 minutos. A geração típica leva 5–20 segundos por shipment dependendo do transportador.

### 3. Baixar a etiqueta

Quando `is_ready=true`, baixe via URL S3:

<a class="api-route" href="/reference/pedidos#tag/etiquetas/GET/api/seller/logistics/shipments/{id}/labels/{labelId}/download"><span class="api-method api-method-get">GET</span><span class="api-path">/api/seller/logistics/shipments/{id}/labels/{labelId}/download</span></a>

`{id}` é o `shipment.id` (numérico). `{labelId}` é o `label.id`.

```json
{
  "success": true,
  "code": 200,
  "data": {
    "label_id": "lbl_xyz",
    "format": "a4",
    "url": "https://s3.amazonaws.com/.../label_4421.pdf?X-Amz-Signature=...",
    "expires_in": "15 minutos"
  }
}
```

A `url` é S3 assinada, válida por **15 minutos**. Não armazene a URL; chame a rota de novo se precisar de outra cópia.

Se a etiqueta ainda não estiver pronta no momento do download, retorna **422 `UNPROCESSABLE_ENTITY`** com `Status: Em processamento`. Volte para o polling antes de tentar de novo.

---

## Rota alternativa (só labels)

Quando você **já tem o `shipment.id` em mãos** e só quer a lista de etiquetas (sem trazer o resto do shipment):

<a class="api-route" href="/reference/pedidos#tag/etiquetas/GET/api/seller/logistics/shipments/{id}/labels"><span class="api-method api-method-get">GET</span><span class="api-path">/api/seller/logistics/shipments/{id}/labels</span></a>

É a mesma informação que vem dentro do shipment em `/orders/{id}/shipments`, mas servida isoladamente. Útil quando você está numa tela "detalhe do shipment" e não quer recarregar todos os shipments do pedido.

A resposta traz também o `shipment_id` e a lista `formats_pending[]` (formatos ainda em processamento), e cada label vem com `source`/`source_label` (de onde a etiqueta veio) e `status_label` em pt-BR — bom para exibir direto na tela.

---

## Quando gerar (e quando não)

**Antes de gerar, o documento fiscal precisa existir** (NF-e registrada ou Declaração emitida). O motivo é prático: nenhum transportador aceita despachar uma encomenda sem nota acompanhando. A API não vai te impedir de gerar a etiqueta antes do fiscal — não é um erro técnico — mas o shipment vai ficar travado na hora do despacho. Por isso a ordem importa: resolva o fiscal primeiro e a etiqueta depois.

**Ordem recomendada no fluxo padrão:**

```
paid → preparing → [emite fiscal] → /shipment-labels/generate → ready_to_ship → shipped
```

Gere as etiquetas em **`preparing`** (depois do fiscal), imprima, cole na caixa, marque `ready_to_ship` quando o pacote estiver na transportadora.

---

## Cuidados

**Shipment com `label_generation_blocked=true` é pulado.** Esse flag fica em `true` quando o sistema sabe que a etiqueta tem um problema impossível de resolver automaticamente (ex.: dados de endereço inválidos, conta com o transportador suspensa, peso fora do contrato). Você vai ver o shipment em `queued_shipments` **vazio** ou com `total` menor que o número real de shipments. Resolva o problema no shipment via operação antes de tentar gerar de novo.

**Chamar `/generate` várias vezes regenera tudo.** Não é incremental: uma chamada apaga as etiquetas anteriores e dispara jobs novos para **todos** os shipments não bloqueados. Use com consciência: se 1 de 3 shipments deu errado, chamar `/generate` vai regerar os outros 2 também.

**A URL S3 expira em 15 minutos.** Se você está montando uma tela de impressão em massa, gere as URLs sob demanda (no momento em que o usuário clica em "Imprimir"), não antes.

**Tracking code aparece no shipment, não na label.** Para ver o `tracking_code`, leia `shipments[].tracking_code` em `/orders/{id}/shipments` ou use [`GET /orders/{id}/tracking`](/guia/pedidos#tracking-de-entrega) para a versão consolidada por pedido.

**Pedido com workflow `in_store_pickup` pode não ter shipment de saída.** Faz sentido: se o cliente vai retirar na loja, não há nada para despachar, então também não há etiqueta a imprimir. Se você chamar `/generate` num pedido sem nenhum shipment, a resposta é **404** com a mensagem "Nenhum shipment encontrado para este pedido.".

=================================================================
SOURCE: https://api-docs.samdevel.com.br/guides/devolucoes.md
=================================================================

# Devoluções

Tratamento de devoluções (RMA) **encaminhadas ao seller** pela plataforma — aprovação, rejeição e geração de coleta reversa — e o acompanhamento do reembolso ao cliente.

> Pré-requisitos: leia primeiro [Pedidos: visão geral](/guia/pedidos). A devolução é um recurso vinculado a um pedido; ela só existe depois que o cliente solicita devolução de um pedido entregue.
>
> **Reembolso é processado pela plataforma, não pelo seller.** Não há endpoint de estorno nesta API — o seller acompanha o reembolso pelo status da devolução (`refunded`). Veja [Reembolsos](#reembolsos) ao final.

---

## Como devoluções chegam ao seller

O fluxo de devolução começa do lado do cliente, não do seller. A plataforma faz uma triagem inicial: algumas devoluções são resolvidas automaticamente (ex.: pedidos com seguro, casos óbvios de extravio). Outras são **encaminhadas ao seller** com status `forwarded_to_seller` — "encaminhar" transfere a **tomada de decisão**, não a visibilidade: a API lista todas as devoluções da loja desde a abertura, para a operação acompanhar mesmo quando a decisão ainda é da plataforma.

```mermaid
flowchart TD
    A[Cliente solicita devolução] --> B{Triagem da<br/>plataforma}
    B -->|resolvido automaticamente| Z[closed]
    B -->|encaminhado| C[forwarded_to_seller]
    C --> D{Decisão do<br/>seller}
    D -->|approve| E[approved]
    D -->|reject| F[rejected]
    E --> G{Coleta?}
    G -->|method=carrier| H[POST /reverse/generate<br/>+ carrier_id]
    H --> I[label_generated<br/>+ shipment reverso criado]
    G -->|method=manual| J[POST /reverse/generate<br/>method=manual]
    J --> K[label_generated<br/>seller organiza coleta]
    I --> L[return_in_progress<br/>coleta em trânsito]
    K --> L
    L -->|POST /mark-received| M[received<br/>produto voltou à loja]
    K -->|POST /mark-received| M
    M --> N{Desfecho<br/>plataforma decide}
    N -->|criar solicitação de estorno| O[refunded<br/>financeiro processando]
    O -->|estorno concluído| P[closed<br/>resolution=refunded]
    N -->|resolvido fora da plataforma| Q[closed<br/>resolution=resolved_externally]
```

**Status possíveis** (`OrderReturnStatusEnum`):

| Status | Significa |
|---|---|
| `pending` | Solicitação recém-criada pelo cliente, ainda em triagem. |
| `forwarded_to_seller` | A plataforma encaminhou; sua loja precisa aprovar ou rejeitar. |
| `approved` | Seller aprovou. A coleta reversa pode ser gerada. |
| `rejected` | Seller recusou. Cliente vê o motivo. |
| `cancelled` | Cliente desistiu antes de qualquer decisão. |
| `label_generated` | Coleta reversa gerada (em qualquer modo); aguardando coleta/etiqueta. Daqui você já pode confirmar o recebimento quando o produto chegar. |
| `return_in_progress` | Coleta em andamento (em qualquer modo). |
| `received` | Produto chegou de volta à loja — confirmado via `/mark-received`. É o **ponto de decisão** do desfecho. |
| `refunded` | Solicitação de estorno criada; o financeiro da plataforma está processando o reembolso. |
| `closed` | Caso encerrado. O campo `resolution` diz como: `refunded` (estorno concluído) ou `resolved_externally` (resolvido fora da plataforma, sem estorno). |

> O desfecho — quem decide, quanto é estornado em devoluções parciais, e o caso "resolvido por fora" — tem guia próprio: **[Devoluções — Resolução](/guia/devolucoes-resolucao)**.

---

## 1. Listagem

<a class="api-route" href="/reference/devolucoes#tag/devolucoes/GET/api/v1/sellers/orders/returns"><span class="api-method api-method-get">GET</span><span class="api-path">/api/v1/sellers/orders/returns</span></a>

**Sem filtros, devolve todas as devoluções da loja** — inclusive as que ainda estão em triagem na plataforma (`pending`) e as já encerradas. Use os filtros para recortar o que interessa:

| Para… | Use |
|---|---|
| Fila aguardando **sua** decisão | `?status=forwarded_to_seller` |
| Todas as devoluções de um pedido | `?order_id=01JX8K3M9PEDIDO000000000AB` |
| Apenas as recusadas | `?status=rejected` |
| Buscar por número de pedido | `?q=ORD-0001` |
| Janela de datas | `?date_from=2026-04-01&date_to=2026-04-30` |

Resposta paginada (`?per_page=15` é o default):

```json
{
  "success": true,
  "message_code": "SUCCESS",
  "data": {
    "meta": {
      "pagination": {
        "page": 1,
        "per_page": 15,
        "last_page": 1,
        "has_prev_page": false,
        "has_next_page": false,
        "records": { "from": 1, "to": 1, "records": 1 }
      }
    },
    "data": [
      {
        "id": "01JX8K3M9DEVOLUCAO00000000",
        "order_id": "01JX8K3M9PEDIDO000000000AB",
        "order_number": "ORD-000123",
        "status": "forwarded_to_seller",
        "notes": "Produto com defeito",
        "seller_response_deadline_at": "2026-04-28T10:15:00Z",
        "sla_exceeded": false,
        "created_at": "2026-04-26T10:15:00Z"
      }
    ]
  }
}
```

> **`seller_response_deadline_at` e `sla_exceeded`** indicam o SLA configurado na plataforma (`returns.seller_response_sla_hours`, default 48h). Use para destacar visualmente devoluções com prazo estourado.

---

## 2. Detalhe da devolução

<a class="api-route" href="/reference/devolucoes#tag/devolucoes/GET/api/v1/sellers/orders/returns/{id}"><span class="api-method api-method-get">GET</span><span class="api-path">/api/v1/sellers/orders/returns/{id}</span></a>

Devolve o registro completo: motivo informado pelo cliente (`reason`), itens devolvidos (`items[]`), endereço de coleta (`pickup_address`), janela e contato (preenchidos quando vai haver coleta), histórico de timestamps (`approved_at`, `rejected_at`, `cancelled_at`, etc.).

É o que a tela "Detalhe da devolução" consome para mostrar todas as informações antes do seller decidir.

---

## 3. Decidir: aprovar ou rejeitar

### Aprovar

<a class="api-route" href="/reference/devolucoes#tag/devolucoes/POST/api/v1/sellers/orders/returns/{id}/approve"><span class="api-method api-method-post">POST</span><span class="api-path">/api/v1/sellers/orders/returns/{id}/approve</span></a>

```json
{
  "seller_notes": "Defeito confirmado nas fotos. Reembolso autorizado."
}
```

`seller_notes` é opcional (máx 1000 caracteres) e fica **só no histórico interno**; não é exibido ao cliente. Use para registrar a justificativa interna ("conferi com o setor X", "concordo com o atendimento que abriu o ticket").

Depois de aprovar, a próxima ação é **gerar a coleta reversa** (passo 4 abaixo).

### Rejeitar

<a class="api-route" href="/reference/devolucoes#tag/devolucoes/POST/api/v1/sellers/orders/returns/{id}/reject"><span class="api-method api-method-post">POST</span><span class="api-path">/api/v1/sellers/orders/returns/{id}/reject</span></a>

```json
{
  "reason": "Produto fora do prazo de devolução (entregue há mais de 60 dias)."
}
```

`reason` é **obrigatório** (máx 1000 caracteres) e **é exibido ao cliente** como justificativa. Escreva pensando que o cliente vai ler: explicação clara, sem jargão interno.

---

## 4. Coleta reversa

Aprovada a devolução, falta trazer o produto de volta. **Dois modos:**

- **`carrier`**: usa um transportador parceiro da plataforma. Você escolhe da lista de elegíveis, a plataforma cria o shipment reverso (com `tracking_code` etc.) e o cliente recebe a etiqueta.
- **`manual`**: você organiza a coleta por fora (motoboy próprio, cliente leva na loja, retirada combinada). Nenhum shipment é criado; só o estado da devolução avança e suas anotações ficam no histórico.

### 4a. Listar carriers elegíveis (modo `carrier`)

<a class="api-route" href="/reference/devolucoes#tag/devolucoes/GET/api/v1/sellers/orders/returns/{id}/reverse/eligible-carriers"><span class="api-method api-method-get">GET</span><span class="api-path">/api/v1/sellers/orders/returns/{id}/reverse/eligible-carriers</span></a>

Lista os transportadores que **cobrem o CEP do cliente** e o **frete estimado** de cada um. Use para montar a UI de escolha do transportador.

```json
{
  "data": {
    "has_coverage": true,
    "zip_code": "01310-100",
    "carriers": [
      { "id": 7, "uid": "01HM0Z6E...", "name": "Correios PAC",   "estimated_freight": 18.90 },
      { "id": 9, "uid": "01HM0X1A...", "name": "Loggi Reverso",  "estimated_freight": 22.50 }
    ]
  }
}
```

**`has_coverage=false`** significa que nenhum carrier parceiro atende o endereço do cliente. Nesse caso, o modo `carrier` não está disponível; só resta `manual` (ou negociar suporte da plataforma).

### 4b. Gerar coleta

<a class="api-route" href="/reference/devolucoes#tag/devolucoes/POST/api/v1/sellers/orders/returns/{id}/reverse/generate"><span class="api-method api-method-post">POST</span><span class="api-path">/api/v1/sellers/orders/returns/{id}/reverse/generate</span></a>

**Modo carrier:**

```json
{
  "method": "carrier",
  "carrier_id": 7,
  "freight_cost": 18.90,
  "pickup_contact_phone": "+5511999999999",
  "pickup_window_from": "2026-04-28T09:00:00Z",
  "pickup_window_to":   "2026-04-28T18:00:00Z"
}
```

| Campo | Quando preencher |
|---|---|
| `method` | **Obrigatório.** `carrier` ou `manual`. |
| `carrier_id` | Obrigatório se `method=carrier`. Use o `id` de `eligible-carriers`. |
| `freight_cost` | Valor que vai ser cobrado/reservado para o frete. Default 0. Normalmente o `estimated_freight` retornado por `eligible-carriers`. |
| `notes` | Observações livres (máx 1000 caracteres). Útil em `manual`. |
| `pickup_window_from` / `pickup_window_to` | Janela acordada com o cliente. Opcionais, mas se enviar ambos, o "to" precisa ser ≥ "from". |
| `pickup_contact_phone` | Telefone no endereço de coleta. Opcional (máx 32 caracteres). |

Resposta em modo `carrier`:

```json
{
  "data": {
    "order_return": { "id": "01JX8K3M9DEVOLUCAO00000000", "status": "label_generated" },
    "shipment": {
      "id": 9921,
      "uid": "01HM1Z3...",
      "tracking_code": null,
      "status": "pending"
    }
  }
}
```

**Modo manual:**

```json
{
  "method": "manual",
  "notes": "Cliente vai trazer pessoalmente sexta-feira à tarde."
}
```

Em `manual` não há shipment; a resposta traz só o `order_return` atualizado.

---

## 5. Confirmar recebimento

<a class="api-route" href="/reference/devolucoes#tag/devolucoes/POST/api/v1/sellers/orders/returns/{id}/mark-received"><span class="api-method api-method-post">POST</span><span class="api-path">/api/v1/sellers/orders/returns/{id}/mark-received</span></a>

O destino da coleta reversa é o endereço da loja — quem recebe o pacote fisicamente é você. Quando o produto chegar, confirme o recebimento: a devolução vira `received` e a plataforma assume a decisão do desfecho (estorno ou encerramento sem estorno).

Disponível a partir de `label_generated` ou `return_in_progress`, sem corpo na requisição. Na coleta `manual` não existe rastreio nenhum, então **esta confirmação é a única forma de a devolução avançar** — sem ela o caso fica parado e o cliente sem resposta.

```http
POST /api/v1/sellers/orders/returns/01JX8K3M9DEVOLUCAO00000000/mark-received
```

---

## 6. Ações possíveis (workflow dirigido pela API)

<a class="api-route" href="/reference/devolucoes#tag/devolucoes/GET/api/v1/sellers/orders/returns/{id}/possible-actions"><span class="api-method api-method-get">GET</span><span class="api-path">/api/v1/sellers/orders/returns/{id}/possible-actions</span></a>

Em vez de replicar a máquina de estados no seu sistema, pergunte à API o que pode ser feito agora. A resposta lista as ações válidas para o status atual (`approve`/`reject` em `forwarded_to_seller`, `generate_reverse_label` em `approved`, `mark_received` em `label_generated`/`return_in_progress`), cada uma com `endpoint`, `method` e os campos esperados em `requires_input`. `is_terminal=true` indica que a devolução encerrou e não há mais nada a fazer.

A decisão de estorno (após `received`) é exclusiva da plataforma e nunca aparece aqui — acompanhe pelos campos `status` e `resolution`.

---

## Cenários comuns

### "Recebi devolução por defeito de fabricação, vou aprovar e gerar coleta"

```http
POST /api/v1/sellers/orders/returns/01JX8K3M9DEVOLUCAO00000000/approve
{ "seller_notes": "Defeito visível nas fotos. Reembolso autorizado." }
```

```http
GET /api/v1/sellers/orders/returns/01JX8K3M9DEVOLUCAO00000000/reverse/eligible-carriers
```

Escolha um carrier baseado em `estimated_freight` e prazo, depois:

```http
POST /api/v1/sellers/orders/returns/01JX8K3M9DEVOLUCAO00000000/reverse/generate
{ "method": "carrier", "carrier_id": 7, "freight_cost": 18.90 }
```

### "Vou recusar, cliente está fora do prazo legal de 7 dias"

```http
POST /api/v1/sellers/orders/returns/01JX8K3M9DEVOLUCAO00000000/reject
{
  "reason": "O prazo legal de 7 dias para devolução sem motivo (Art. 49 CDC) já expirou. Recebimento confirmado em 12/03; solicitação aberta em 28/03."
}
```

### "Cliente mora numa cidade que nenhum carrier atende"

`GET /reverse/eligible-carriers` retorna `has_coverage=false`. Resolva manualmente:

```http
POST /api/v1/sellers/orders/returns/01JX8K3M9DEVOLUCAO00000000/reverse/generate
{
  "method": "manual",
  "notes": "Sem cobertura de carrier. Combinado com cliente: motoboy contratado pela loja vai retirar dia 29/04."
}
```

### "A coleta era manual e o produto chegou na loja"

```http
POST /api/v1/sellers/orders/returns/01JX8K3M9DEVOLUCAO00000000/mark-received
```

A devolução vira `received` e a plataforma decide o desfecho. Sem essa confirmação, a devolução em coleta manual não avança.

### "Cliente acha que vai dar tudo certo mas quero ver o histórico"

```http
GET /api/v1/sellers/orders/returns?order_id=01JX8K3M9PEDIDO000000000AB
```

Devolve todas as devoluções do pedido `01JX8K3M9PEDIDO000000000AB`, em qualquer status.

### "Quero ver minhas devoluções recusadas no último mês"

```http
GET /api/v1/sellers/orders/returns?status=rejected&date_from=2026-04-01&date_to=2026-04-30
```

---

## Reembolsos

Você não vai encontrar um endpoint de estorno nesta API — e isso é de propósito. O reembolso mexe com dinheiro do cliente, conciliação de pagamento e regras de repasse, então a plataforma centraliza esse passo para garantir que o estorno só aconteça quando o produto realmente voltou e o caso fechou. Do seu lado, em vez de "emitir" um reembolso, você acompanha o desfecho pelos campos `status` e `resolution` da devolução.

Pela ótica do seller, o ciclo final é:

| Status | O que significa para o reembolso |
|---|---|
| `received` | Produto chegou de volta à loja. A plataforma decide o desfecho. |
| `refunded` | Solicitação de estorno criada — o financeiro da plataforma está processando. O valor corresponde aos **itens devolvidos** (devolução parcial estorna só a parte devolvida). |
| `closed` + `resolution=refunded` | Estorno concluído. O valor (e eventuais descontos de frete reverso) entra no seu extrato/repasse. |
| `closed` + `resolution=resolved_externally` | Encerrada **sem estorno** — o caso foi resolvido fora da plataforma (ex.: você reenviou o produto direto ao cliente). A justificativa fica em `resolution_notes`. |

Pontos a ter em mente:

- **Você não inicia nem cancela um reembolso pela API.** Casos excepcionais (divergência de valor, acordo direto com o cliente) são tratados pela operação/suporte da plataforma — e refletem na devolução via `resolution`.
- **O frete reverso pode impactar o repasse.** O `freight_cost` informado na [geração da coleta](#4b-gerar-coleta) é o valor que entra no acerto financeiro daquela devolução.
- **Aprovar a devolução não é o mesmo que reembolsar.** Aprovar (`/approve`) autoriza a volta do produto; o desfecho financeiro só é decidido no fim do trilho, após o recebimento.
- **Devoluções parciais sucessivas são possíveis.** Encerrada uma devolução, o cliente pode abrir outra para o restante dos itens — cada uma com seu próprio desfecho. Detalhes e exemplos em **[Devoluções — Resolução](/guia/devolucoes-resolucao)**.

---

## Cuidados

**`reject.reason` é visto pelo cliente.** Escreva como se fosse uma resposta no SAC: profissional, clara, sem jargão interno. Para anotações internas use `approve.seller_notes` (que **não** é visto pelo cliente).

**Coleta reversa só depois de `approved`.** Tentar gerar coleta antes de aprovar retorna 422 com `A devolução precisa estar aprovada para gerar a coleta reversa.`. O fluxo é: `forwarded_to_seller` → `approve` (vira `approved`) → `reverse/generate`.

**`freight_cost` é o valor real (não estimativa).** Use o `estimated_freight` como base, mas se você negociou um valor diferente com o carrier, mande o valor negociado. Esse é o número que entra no extrato/repasse.

**`pickup_address` é montado a partir do snapshot do pedido.** Você não precisa enviar; ele vem do endereço que estava no pedido original. Se for diferente (cliente mudou), edite no shipment via operação depois de gerar.

**Aprovar é um caminho só de ida — não há "des-aprovar".** A razão é que, assim que você aprova, o cliente já é avisado e a coleta reversa passa a poder ser gerada; voltar atrás depois disso confundiria o cliente e a logística. Por isso, uma vez `approved`, a devolução segue o trilho da coleta reversa. Se aprovou por engano, não é algo que você reverte pela API — é caso de operação (contate o suporte).

**Datas inválidas em `date_from`/`date_to` são ignoradas silenciosamente.** Não há erro; o filtro simplesmente não é aplicado. Se a busca não está filtrando, cheque o formato (ISO 8601).

=================================================================
SOURCE: https://api-docs.samdevel.com.br/guides/comunicacao.md
=================================================================

# Visão geral

A loja conversa com o comprador por dois canais diferentes, e vale entender desde já que eles são separados de propósito: cada um vive numa rota própria, com regras próprias. Saber qual usar em cada momento evita que você procure no lugar errado — pré-venda e pós-pagamento têm fluxos bem distintos.

| Canal | Quando | Onde vive | Guia |
|---|---|---|---|
| **Chat** | Do pagamento à entrega (`pre_delivery`) e pós-venda (`post_delivery`) | Vinculado a um `order_id` | [Chat com cliente](/guia/chat) |
| **Perguntas** | Pré-venda, no detalhe do anúncio | Vinculado a uma publicação (`item_id`) | [Perguntas](/guia/perguntas) |

---

## Decidindo onde olhar

```mermaid
flowchart TD
    A{Já é um<br/>pedido pago?} -->|sim| B[Central de Atendimento<br/>conversa ancorada no pedido]
    A -->|não, ainda escolhendo| C[Q&A da publicação<br/>resposta pública]
    B --> D[/guia/chat/]
    C --> E[/guia/perguntas/]
```

- **Chat** é privado entre comprador, loja e (eventualmente) plataforma. A conversa `pre_delivery` abre **assim que o pedido é pago** e fica disponível durante todo o caminho até a entrega; quando o pedido é entregue, ela fecha sozinha. O comprador ainda pode abrir uma `post_delivery` manualmente depois (devolução, problema pós-venda etc.). Aceita anexos (até 25 MB) e tem SLA de resposta.
- **Perguntas** são públicas no detalhe do anúncio. Passam por moderação antes de ficarem visíveis. Aqui é só texto: como a resposta fica à vista de qualquer visitante da publicação, não há campo para anexar arquivos — se precisar trocar fotos ou documentos com o comprador, esse é o papel do chat do pedido.

---

## Canais

### Chat (Central de Atendimento)

Listagem, mensagens, anexos S3 em 3 etapas, SLA. Detalhe completo em [Chat com cliente](/guia/chat).

### Perguntas (Q&A das publicações)

Listagem, resposta com moderação, contador de não respondidas. Detalhe completo em [Perguntas](/guia/perguntas).

---

## Badges no menu lateral

Os dois canais expõem um contador específico para alimentar o badge do menu do seller:

| Canal | Rota | O que conta |
|---|---|---|
| Chat | `GET /v1/seller/conversations/unread-count` | Mensagens não lidas em conversas `open`. Mensagens da própria loja não somam. |
| Perguntas | `GET /api/seller/interactions/unanswered-count` | Perguntas `APPROVED` sem resposta `APPROVED`. |

Use as duas em paralelo no header do painel.

=================================================================
SOURCE: https://api-docs.samdevel.com.br/guides/chat.md
=================================================================

# Chat com cliente

Central de Atendimento. Chat ancorado em um pedido, com anexos e SLA de resposta.

> Pré-requisitos: leia primeiro [Comunicação: visão geral](/guia/comunicacao). Toda conversa nasce ancorada num pedido (ULID em `order_id`) — você não vai encontrar uma forma de abrir um chat "solto", sem pedido por trás. Isso é de propósito: amarrar a conversa ao pedido dá contexto imediato (o que foi comprado, o status, o histórico) para você e para o comprador, sem precisar perguntar "de qual pedido você está falando?".

---

## Conceitos

**Participantes:** `customer` (comprador), `store` (sua loja), `platform` (atendimento da plataforma).

**Tipo:** `pre_delivery` (aberta no pagamento do pedido, encerra na entrega) ou `post_delivery` (pós-venda aberto manualmente pelo comprador).

**Status:** `open` ou `closed`. Conversas encerradas trazem `closed_reason`, que pode ser `order_delivered`, `customer_closed`, `admin_closed` ou `auto_inactive`.

**Kind da mensagem:** `user` (humano) ou `system` (evento automático, ex.: "Pedido marcado como entregue").

---

## Listar conversas

<a class="api-route" href="/reference/comunicacao#tag/central-de-atendimento/GET/v1/seller/conversations"><span class="api-method api-method-get">GET</span><span class="api-path">/v1/seller/conversations</span></a>

Ordenada por `last_message_at` desc. Filtros:

| Parâmetro | Para quê |
|-----------|----------|
| `status` | `open` ou `closed`. |
| `type` | `pre_delivery` ou `post_delivery`. |
| `subject_id` | Slug do assunto pós-venda (ex.: `defective_product`, `wrong_item`). |
| `q` | Busca por `order_id` (ULID do pedido). |
| `page`, `per_page` | Paginação (default `per_page=20`). |

---

## Detalhes da conversa

<a class="api-route" href="/reference/comunicacao#tag/central-de-atendimento/GET/v1/seller/conversations/{conversation}"><span class="api-method api-method-get">GET</span><span class="api-path">/v1/seller/conversations/{conversation}</span></a>

---

## Não lidas

<a class="api-route" href="/reference/comunicacao#tag/central-de-atendimento/GET/v1/seller/conversations/unread-count"><span class="api-method api-method-get">GET</span><span class="api-path">/v1/seller/conversations/unread-count</span></a>

Soma das mensagens não lidas em todas as conversas abertas. Mensagens enviadas pela própria loja não contam. É a métrica usada nos badges do menu lateral.

---

## Mensagens

<a class="api-route" href="/reference/comunicacao#tag/central-de-atendimento/GET/v1/seller/conversations/{conversation}/messages"><span class="api-method api-method-get">GET</span><span class="api-path">/v1/seller/conversations/{conversation}/messages</span></a>

Vêm em ordem cronológica (mais antiga primeiro). Cada mensagem tem `kind`:

- `user`: escrita por um participante humano (`customer`, `store` ou `platform`).
- `system`: gerada pela plataforma (ex.: "Pedido marcado como entregue").

O bloco `sender` já vem com `display_name`, `avatar_url`, `avatar_color` e `badge`.

### Polling incremental

Para atualizar a tela sem refazer a paginação, envie `after_message_ulid` com o ULID da última mensagem recebida. Só voltam as posteriores:

```http
GET /v1/seller/conversations/{id}/messages?after_message_ulid=01K8PBMSG00004VWXYZ12345678
```

### Enviar mensagem

<a class="api-route" href="/reference/comunicacao#tag/central-de-atendimento/POST/v1/seller/conversations/{conversation}/messages"><span class="api-method api-method-post">POST</span><span class="api-path">/v1/seller/conversations/{conversation}/messages</span></a>

Pode conter `body` (texto, máx. 5000), `attachments` (até 5 ULIDs já reservados via intent) ou ambos:

```json
{
  "body": "Segue a nota fiscal e a foto da embalagem.",
  "attachments": [
    "01K8PBATT00001VWXYZ12345678",
    "01K8PBATT00002VWXYZ12345678"
  ]
}
```

Erros comuns:

| `message_code` | Quando |
|----------------|--------|
| `CONVERSATION_NOT_PARTICIPANT` (403) | A loja não participa da conversa. |
| `CONVERSATION_CLOSED` (422) | Conversa encerrada. |
| `CONVERSATION_ATTACHMENT_NOT_CONFIRMED` (422) | Algum ULID de anexo ainda não teve `PUT` no storage. |

---

## Marcar como lida

<a class="api-route" href="/reference/comunicacao#tag/central-de-atendimento/POST/v1/seller/conversations/{conversation}/read"><span class="api-method api-method-post">POST</span><span class="api-path">/v1/seller/conversations/{conversation}/read</span></a>

Marca como lidas todas as mensagens até (e incluindo) `last_message_ulid`. Retorna **204**.

```json
{
  "last_message_ulid": "01K8PBMSG00004VWXYZ12345678"
}
```

---

## Anexos

Envio em **3 etapas**: o cliente sobe o arquivo direto no storage (S3/MinIO) por uma URL pré-assinada, e só depois vincula o ULID a uma mensagem. O anexo nasce `RESERVED` e vira `CONFIRMED` automaticamente quando a mensagem é criada (o backend faz a checagem de que o arquivo chegou ao storage).

```mermaid
sequenceDiagram
    autonumber
    participant I as Integrador
    participant A as API
    participant S as Storage S3

    I->>A: POST /attachments/intent<br/>{ original_name, mime_type, size_bytes }
    A-->>I: { attachment_ulid, upload_url, expires_at }
    Note over A: anexo: status=RESERVED

    I->>S: PUT {upload_url}<br/>(binário do arquivo)
    S-->>I: 200 OK

    I->>A: POST /messages<br/>{ body, attachments: [ulid] }
    Note over A: confirma anexo<br/>(checa que existe no S3)<br/>status=CONFIRMED
    A-->>I: { id, ... }
```

### 1. Reservar slot

<a class="api-route" href="/reference/comunicacao#tag/central-de-atendimento/POST/v1/seller/conversations/{conversation}/attachments/intent"><span class="api-method api-method-post">POST</span><span class="api-path">/v1/seller/conversations/{conversation}/attachments/intent</span></a>

Resposta:

```json
{
  "data": {
    "attachment_ulid": "01K8PBATT00001VWXYZ12345678",
    "upload_url": "https://cdn-content.s3.amazonaws.com/.../01K8PBATT00001....jpg?X-Amz-Signature=...",
    "method": "PUT",
    "expires_at": "2026-05-19T11:47:08-03:00"
  }
}
```

### 2. Subir o arquivo

Faça um `PUT {upload_url}` com o binário do arquivo.

### 3. Vincular à mensagem

Chame `POST /messages` enviando `attachments: [attachment_ulid]`.

### Limites

| Limite | Valor |
|--------|-------|
| Tamanho máximo | 25 MB |
| Validade da URL de upload | 15 minutos |
| Anexos por mensagem | 5 |
| Tipos permitidos | `image/jpeg`, `image/png`, `image/webp`, `image/gif`, `application/pdf`, `video/mp4`, `video/webm`, `video/quicktime` |

Erros já na etapa 1:

| `message_code` | Quando |
|----------------|--------|
| `CONVERSATION_ATTACHMENT_TOO_LARGE` (422) | Tamanho acima de 25 MB. |
| `CONVERSATION_ATTACHMENT_MIME_NOT_ALLOWED` (422) | Tipo fora da lista. |

### Baixar

<a class="api-route" href="/reference/comunicacao#tag/central-de-atendimento/GET/v1/seller/conversations/{conversation}/attachments/{attachment}/access"><span class="api-method api-method-get">GET</span><span class="api-path">/v1/seller/conversations/{conversation}/attachments/{attachment}/access</span></a>

Retorna uma URL temporária assinada pelo storage, válida por **30 minutos**:

```json
{
  "data": {
    "url": "https://cdn-content.s3.amazonaws.com/.../01K8PBATT00001....jpg?X-Amz-Signature=...",
    "expires_at": "2026-05-19T12:02:08-03:00"
  }
}
```

> Faça a requisição direto no `url`. Cada chamada gera uma URL nova; as antigas continuam válidas até expirarem.

---

## SLA de resposta

<a class="api-route" href="/reference/comunicacao#tag/central-de-atendimento/GET/v1/seller/conversations/config"><span class="api-method api-method-get">GET</span><span class="api-path">/v1/seller/conversations/config</span></a>

Tempo de resposta esperado (em horas), definido pela plataforma. Use para destacar conversas perto do prazo na UI. A mesma rota devolve também `subjects`: o catálogo de assuntos de pós-venda (cada um com `id`, `label` e `applies_to`) — é dele que saem os valores aceitos no filtro `subject_id` da listagem.

```json
{
  "data": {
    "sla_response_hours": 24,
    "subjects": [
      { "id": "defective_product", "label": "Produto com defeito", "applies_to": "post_delivery" },
      { "id": "wrong_item", "label": "Item errado", "applies_to": "post_delivery" }
    ]
  }
}
```

---

## Erros

| `message_code` | HTTP | Quando |
|----------------|------|--------|
| `UNAUTHORIZED` | 401 | Token ausente, inválido ou expirado. |
| `FORBIDDEN` | 403 | Sem permissão. |
| `NOT_FOUND` | 404 | Conversa inexistente. |
| `VALIDATION_ERROR` | 422 | Campos inválidos. |
| `CONVERSATION_NOT_PARTICIPANT` | 403 | Loja não participa da conversa. |
| `CONVERSATION_CLOSED` | 422 | Tentativa de enviar em conversa encerrada. |
| `CONVERSATION_ATTACHMENT_TOO_LARGE` | 422 | Anexo acima de 25 MB. |
| `CONVERSATION_ATTACHMENT_MIME_NOT_ALLOWED` | 422 | Tipo de arquivo fora da lista. |
| `CONVERSATION_ATTACHMENT_NOT_CONFIRMED` | 422 | Anexo não teve `PUT` no storage. |

=================================================================
SOURCE: https://api-docs.samdevel.com.br/guides/perguntas.md
=================================================================

# Perguntas

Q&A das publicações da sua loja. Canal direto entre comprador e seller no detalhe do produto, com moderação antes da publicação.

> Pré-requisitos: leia primeiro [Comunicação: visão geral](/guia/comunicacao). Aqui a âncora é a publicação (o anúncio), não o pedido — faz sentido, porque a pergunta acontece antes da compra, com alguém ainda decidindo se vai comprar. Quem já comprou conversa pelo chat do pedido; quem ainda está olhando a vitrine pergunta por aqui.

---

## Conceitos

**Status do Q&A:** `PENDING` (em moderação), `APPROVED` (público) ou `REJECTED`.

**Moderação:** toda pergunta nasce `PENDING` e só fica visível publicamente quando vira `APPROVED`. O mesmo vale para as respostas da loja: padrão é `PENDING`, exceto se a loja tem a feature `trusted_answers` (publica direto como `APPROVED`).

---

## Listar perguntas

<a class="api-route" href="/reference/comunicacao#tag/perguntas-e-respostas/GET/api/seller/interactions/questions"><span class="api-method api-method-get">GET</span><span class="api-path">/api/seller/interactions/questions</span></a>

Ordenadas da mais recente para a mais antiga. Retorna perguntas em qualquer status; filtre na UI pelo campo `status`.

Cada item traz:

- `customer`: nome e avatar de quem perguntou.
- `publication`: `item_id`, título, thumbnail e `frontend_url`.
- `answers[]`: respostas já cadastradas com o status de moderação.

Filtros:

| Parâmetro | Para quê |
|-----------|----------|
| `item_id` | Perguntas de uma publicação específica (ex.: `US123456`). |
| `page`, `per_page` | Paginação (default `per_page=15`). |

---

## Responder

<a class="api-route" href="/reference/comunicacao#tag/perguntas-e-respostas/POST/api/seller/interactions/questions/{uid}/answer"><span class="api-method api-method-post">POST</span><span class="api-path">/api/seller/interactions/questions/{uid}/answer</span></a>

```json
{
  "content": "Sim, este produto é compatível com a versão 2024 e acompanha cabo USB-C."
}
```

`content` é obrigatório, mínimo 2 caracteres.

**Moderação:**

| Cenário | Status inicial |
|---------|----------------|
| Loja padrão | `PENDING`, aguarda aprovação. |
| Loja com feature `trusted_answers` | `APPROVED`, publicada imediatamente. |

---

## Contador de não respondidas

<a class="api-route" href="/reference/comunicacao#tag/perguntas-e-respostas/GET/api/seller/interactions/unanswered-count"><span class="api-method api-method-get">GET</span><span class="api-path">/api/seller/interactions/unanswered-count</span></a>

```json
{
  "data": {
    "count": 2,
    "oldest_age_hours": 14,
    "deep_link": "/portal/interacoes/perguntas?status=unanswered"
  }
}
```

Conta apenas perguntas `APPROVED` que não têm nenhuma resposta `APPROVED`. Repare que responder não é o suficiente para sair da conta: enquanto sua resposta estiver `PENDING` (em moderação) ou tiver sido `REJECTED`, a pergunta segue contando como pendente — afinal, do ponto de vista do comprador ela ainda está sem resposta visível. O contador zera quando a resposta de fato fica pública.

---

## Erros

| `message_code` | HTTP | Quando |
|----------------|------|--------|
| `UNAUTHORIZED` | 401 | Token ausente, inválido ou expirado. |
| `FORBIDDEN` | 403 | Usuário sem loja associada ou token sem o escopo necessário. |
| `NOT_FOUND` | 404 | Pergunta inexistente. |
| `VALIDATION_ERROR` | 422 | `content` vazio, muito curto ou tipo errado. |

=================================================================
SOURCE: https://api-docs.samdevel.com.br/guides/ocorrencias.md
=================================================================

# Guia de Ocorrências

**Ocorrências** são casos abertos pela plataforma que envolvem a sua loja: uma devolução em análise, um problema de coleta, uma pendência cadastral. Aqui você lista, consulta detalhes e troca comentários com o time interno.

---

## Conceitos

**Matriz** (categoria da ocorrência):

| Slug | Nome | Quando aparece |
|------|------|----------------|
| `rma` | Devoluções | Cliente pediu devolução ou troca |
| `logistics` | Logística | Problemas com coleta, transportadora, atraso |
| `orders` | Pedidos | Divergências ou pendências em pedidos |
| `payments` | Pagamentos | Estornos e divergências de repasse |
| `stock` | Estoque | Divergências de inventário |
| `stores` | Lojas | Pendências cadastrais |
| `general` | Geral | Outros casos |

**Etapa:** sequência típica abaixo. Quem move a ocorrência de uma etapa para outra é sempre o operador da plataforma — você não vai encontrar uma rota para avançar ou voltar etapa, e isso é intencional: a ocorrência é um caso conduzido pelo time interno. Seu papel aqui é acompanhar e responder pelos comentários; a etapa anda conforme o operador trata o caso.

```mermaid
stateDiagram-v2
    direction LR
    [*] --> new
    new --> analyzing
    analyzing --> waiting: precisa<br/>de retorno
    analyzing --> resolving
    waiting --> resolving: resposta<br/>recebida
    resolving --> resolved
    new --> cancelled
    analyzing --> cancelled
    waiting --> cancelled
    resolving --> cancelled
    resolved --> [*]
    cancelled --> [*]
```

**Severidade:** `low`, `medium`, `high`, `critical`.

**Status:** `open`, `resolved`, `cancelled`, `auto_closed` (escondido por padrão nas listagens).

**Origem:** `manual`, `system`, `scheduled`.

O objeto que `GET /{id}` devolve, de relance — cada campo apontando para o conceito acima:

```jsonc
{
  "id": "01K8PB0KMM…",       // ID da ocorrência
  "title": "Devolução…",      // resumo do caso
  "severity": "high",         // ← Severidade
  "severity_label": "Alta",   // versão pt-BR p/ exibir (vale p/ todo *_label)
  "status": "open",           // ← Status
  "origin": "system",         // ← Origem
  "matrix": {…},              // ← Matriz (categoria)
  "step": {…},                // ← Etapa — quem move é a plataforma
  "assigned_to": {…},         // operador responsável (ou null)
  "is_overdue": false,        // passou do due_at
  "relations": [{…}],         // entidades vinculadas (pedido, loja, item)
  "comments": [{…}]           // só os shared (ver seção Comentários)
}
```

> Para não hardcodar essas listas, a rota [`GET /api/seller/occurrences/enums`](/reference/ocorrencias#tag/enums-de-ocorrências/GET/api/seller/occurrences/enums) devolve origem, severidade (com `color`) e status, cada um com `value` + `label` pt-BR.

---

## Listar

<a class="api-route" href="/reference/ocorrencias#tag/ocorrências/GET/api/seller/occurrences"><span class="api-method api-method-get">GET</span><span class="api-path">/api/seller/occurrences</span></a>

Retorna apenas ocorrências visíveis à sua loja. Filtros suportados:

| Parâmetro | Para quê |
|-----------|----------|
| `matrix` | Slug da matriz (`rma`, `logistics`, ...). |
| `status` | Lista separada por vírgula. |
| `date_from`, `date_to` | Intervalo por data de criação. |
| `page`, `per_page` | Paginação (default `per_page=20`). |

---

## Dashboard

<a class="api-route" href="/reference/ocorrencias#tag/ocorrências/GET/api/seller/occurrences/dashboard"><span class="api-method api-method-get">GET</span><span class="api-path">/api/seller/occurrences/dashboard</span></a>

Resumo pronto para a home:

- `open_count`: total em aberto.
- `requires_action`: ocorrências esperando resposta sua.
- `last_updated_at`: data da última atualização.
- `items`: as 10 mais relevantes (ordenadas por severidade), cada uma com o flag `has_unread_message`.

---

## Cards de atenção

<a class="api-route" href="/reference/ocorrencias#tag/ocorrências/GET/api/seller/occurrences/attention"><span class="api-method api-method-get">GET</span><span class="api-path">/api/seller/occurrences/attention</span></a>

Agrupa as ocorrências abertas por matriz. Cada card traz `title`, `count`, `accent` (cor) e `href` (link da listagem filtrada). Aceita `limit` (1..10, default `4`).

---

## Detalhes

<a class="api-route" href="/reference/ocorrencias#tag/ocorrências/GET/api/seller/occurrences/{id}"><span class="api-method api-method-get">GET</span><span class="api-path">/api/seller/occurrences/{id}</span></a>

Retorna a ocorrência completa: dados, entidades vinculadas (pedido, loja, item) e comentários.

> **Abrir uma ocorrência marca ela como lida** e zera o aviso de "não lidas". Não chame em loop só para checar mudanças; use a listagem ou o dashboard.

---

## Comentários

- Você só vê comentários `shared`. O time interno também troca anotações privadas durante a análise, mas essas ficam fora da sua visão de propósito — você recebe apenas o que foi marcado para compartilhar com a loja, então não precisa filtrar nada: tudo que chega até você já é para ser lido.
- `is_system=true` indica evento automático (mudança de etapa, fechamento). Exiba como timeline, não como mensagem.
- `author_type`: `store` (sua loja), `operator` (time interno) ou `system`.

### Responder

<a class="api-route" href="/reference/ocorrencias#tag/comentários/POST/api/seller/occurrences/{id}/comments"><span class="api-method api-method-post">POST</span><span class="api-path">/api/seller/occurrences/{id}/comments</span></a>

```json
{
  "content": "Segue foto do produto recebido com avaria na lateral."
}
```

`content` é obrigatório, até 2000 caracteres. Sempre criado como `shared`.

### Flag `has_unread_message`

Fica `true` quando existe comentário `shared` do operador mais recente que a sua última leitura (`last_store_read_at`). O flag aparece nos itens do dashboard (`GET /occurrences/dashboard`) — a listagem comum não o devolve. Para zerar, abra a ocorrência.

```mermaid
sequenceDiagram
    autonumber
    participant O as Operador
    participant A as API
    participant I as Integrador
    O->>A: comenta na ocorrência (shared)
    Note over A: comentário do operador criado<br/>(created_at > last_store_read_at)
    I->>A: GET /occurrences/dashboard
    A-->>I: items[...] com has_unread_message=true
    I->>A: GET /occurrences/{id} (abre detalhes)
    Note over A: ocorrência.last_store_read_at = agora
    I->>A: GET /occurrences/dashboard
    A-->>I: items[...] com has_unread_message=false
```

---

## Visibilidade

Você só vê uma ocorrência se ela estiver marcada como visível à loja **e** sua loja estiver vinculada a ela. Qualquer outro caso retorna **403**.

A listagem já aplica esse filtro automaticamente, então tudo que aparece na lista pode ser aberto em detalhes.

---

## Erros

| Status | `message_code` | Quando |
|--------|----------------|--------|
| 401 | `UNAUTHORIZED` | Token ausente, inválido ou expirado. |
| 403 | `FORBIDDEN` | Ocorrência não vinculada à sua loja. |
| 404 | `NOT_FOUND` | ID inexistente. |
| 422 | `VALIDATION_ERROR` | `content` vazio ou maior que 2000 caracteres. |
