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