# API de Parceiros SimplesFika — Catálogo e Estoque (v1)

> Documento de referência completo e autossuficiente. Pode ser entregue inteiro a um
> assistente de IA para gerar o código de integração. Especificação OpenAPI 3.1:
> `https://simplesfika.com/docs/openapi-parceiros-v1.json`. Índice para IAs:
> `https://simplesfika.com/llms.txt`.

## Resumo em uma tela

| Item | Valor |
|---|---|
| URL base | `https://<dominio-da-loja>/api/partner/v1/` (cada loja tem o próprio domínio) |
| Autenticação | Header `X-Partner-Key: <chave>` em toda requisição |
| Formato | JSON UTF-8, `Content-Type: application/json` |
| Identificador do produto | `sku` (texto, até 50 caracteres) |
| Limite | 120 requisições por minuto por chave (padrão; pode variar) |
| Escopo da v1 | Produtos simples: 1 SKU = 1 produto, com estoque próprio |
| Ambiente de testes | Não há sandbox; use uma loja de teste com chave própria |

Endpoints:

| Método | Caminho | Escopo | Para quê |
|---|---|---|---|
| `GET` | `/products/` | `catalog:read` | Listar os produtos desta integração |
| `POST` | `/products/` | `catalog:write` | Criar produto (falha se o SKU existir) |
| `PUT` | `/products/{sku}/` | `catalog:write` | Criar ou atualizar por SKU (idempotente) |
| `PATCH` | `/products/{sku}/stock/` | `stock:write` | Definir o estoque |
| `DELETE` | `/products/{sku}/` | `catalog:write` | Desativar produto |

## Conceitos

### Loja e domínio

A API roda no domínio de cada loja (ex.: `https://loja-exemplo.com.br/api/partner/v1/`).
A chave vale somente para a loja em que foi emitida: a mesma chave em outro domínio retorna
`INVALID_PARTNER_KEY`. Para integrar várias lojas, guarde um par (domínio, chave) por loja.

### Chave e escopos

O próprio lojista gera a chave no painel da loja, em **Integrações → API para outros
sistemas → Gerenciar chaves**, escolhendo as permissões. A tela mostra a chave uma única vez e
também o endereço da API da loja; o lojista envia os dois ao responsável pela integração.
Se o lojista preferir, a equipe SimplesFika emite a chave.

Guarde a chave como segredo (variável de ambiente ou cofre), nunca em código-fonte ou log.
O lojista pode revogá-la a qualquer momento; ela para de funcionar na hora. Para trocar a
chave sem perder os produtos da integração, o lojista cria a nova com o mesmo identificador
e só depois revoga a antiga.

| Escopo | Permite |
|---|---|
| `catalog:read` | `GET /products/` |
| `catalog:write` | `POST`, `PUT` e `DELETE` de produto |
| `stock:write` | `PATCH /products/{sku}/stock/` |
| `catalog:link` | No `PUT`, assumir produto já cadastrado pelo lojista com o mesmo SKU |

No painel, os escopos aparecem como: Consultar produtos (`catalog:read`), Cadastrar e alterar
produtos (`catalog:write`), Atualizar estoque (`stock:write`) e Assumir produtos já cadastrados
(`catalog:link`).

### Posse do produto

Cada produto tem um dono: o lojista ou uma integração (campo `gerenciado_por`).

- A integração altera somente produtos dela: os que ela criou ou os vinculados a ela.
- Produto do lojista com o mesmo SKU retorna `409 PRODUCT_NOT_LINKED`. Para assumir esses
  produtos, o lojista precisa autorizar e a chave recebe o escopo `catalog:link`; então o
  `PUT` vincula o produto à integração.
- Produto de outra integração retorna `409 EXTERNAL_SOURCE_CONFLICT`.

Em produto da integração, ela controla **SKU, preço e estoque**. Nome, descrição, fotos,
categorias, SEO, variações e publicação na vitrine continuam com o lojista, no painel.

### Ciclo de vida

1. Produto criado pela API nasce ativo para estoque e venda direta, mas **fora da vitrine**
   (`needs_enrichment: true`). O lojista completa foto e descrição e publica pelo painel.
2. Quando o lojista publica, `needs_enrichment` passa a `false`.
3. `DELETE` desativa o produto: sai da vitrine e da venda, e o histórico de pedidos é mantido.
   A API não reativa produto desativado; peça a reativação ao lojista.

### Produtos fora do escopo da v1

A API recusa, sem alterar nada:

- produto com grade/variação (tamanho, cor) e estoque por variação: `VARIANT_STOCK_NOT_SUPPORTED`;
- kit, link externo, encomenda ou produto sem controle de estoque: `PRODUCT_MODE_NOT_SUPPORTED`.

## Formato das respostas

Sucesso:

```json
{ "ok": true, "data": { } }
```

Erro:

```json
{ "ok": false, "error": "Mensagem legível para humanos", "code": "CODIGO_ESTAVEL" }
```

Trate os erros pelo `code`, que é estável. O texto de `error` pode mudar.

### Objeto `Produto`

Retornado por `GET`, `POST`, `PUT` e `PATCH`.

| Campo | Tipo | Descrição |
|---|---|---|
| `id` | inteiro | ID interno na loja |
| `sku` | texto | SKU do produto |
| `tipo` | texto | Sempre `"product"` na v1 |
| `estoque` | inteiro ≥ 0 | Estoque atual |
| `preco` | texto decimal | Preço com 2 casas, ex.: `"79.90"` |
| `gerenciado_por` | texto | Integração dona do produto; vazio = lojista |
| `needs_enrichment` | booleano | `true` enquanto o produto não aparece na vitrine |
| `updated_at` | texto ISO 8601 ou `null` | Última alteração |

```json
{
  "id": 4210,
  "sku": "CAMISETA-AZUL-M",
  "tipo": "product",
  "estoque": 12,
  "preco": "79.90",
  "gerenciado_por": "erp-exemplo",
  "needs_enrichment": true,
  "updated_at": "2026-09-23T12:00:00+00:00"
}
```

### Regras de validação

| Campo | Regra |
|---|---|
| `sku` | Obrigatório, até 50 caracteres. Na URL, use URL-encoding. SKU com `/` não é suportado. |
| `nome` | Texto; usado só na criação (até 200 caracteres; o excedente é cortado). Sem `nome`, o SKU vira o nome. |
| `preco` | Decimal maior que zero, no máximo 2 casas e até `99999999.99`. Envie como texto (`"79.90"`) para evitar arredondamento. Obrigatório na criação. |
| `estoque` | Inteiro ≥ 0. Aceita número (`7`) ou texto numérico (`"7"`); recusa fração (`1.5`). |

Cada requisição é atômica: se qualquer campo for recusado, nada da requisição é gravado.

## Endpoints

### `GET /products/` — listar produtos da integração

Escopo `catalog:read`. Retorna até 100 produtos gerenciados por esta integração (inclusive
os que ainda aguardam enriquecimento), em ordem de `id`. Não há paginação na v1.

```http
GET /api/partner/v1/products/ HTTP/1.1
Host: loja-exemplo.com.br
X-Partner-Key: SUA_CHAVE
```

`200 OK`:

```json
{ "ok": true, "data": { "products": [ { "id": 4210, "sku": "CAMISETA-AZUL-M", "tipo": "product", "estoque": 12, "preco": "79.90", "gerenciado_por": "erp-exemplo", "needs_enrichment": true, "updated_at": "2026-09-23T12:00:00+00:00" } ] } }
```

### `POST /products/` — criar produto

Escopo `catalog:write`. Campos: `sku` (obrigatório), `preco` (obrigatório), `nome`, `estoque`.

```http
POST /api/partner/v1/products/ HTTP/1.1
Host: loja-exemplo.com.br
X-Partner-Key: SUA_CHAVE
Content-Type: application/json

{ "sku": "CAMISETA-AZUL-M", "nome": "Camiseta Azul M", "preco": "79.90", "estoque": 12 }
```

`201 Created` com `data` = objeto `Produto`. Se o SKU já existir (de qualquer dono):
`409 SKU_ALREADY_EXISTS`. Para "criar ou atualizar", use o `PUT`.

### `PUT /products/{sku}/` — criar ou atualizar por SKU

Escopo `catalog:write`. Idempotente: repetir a mesma requisição dá o mesmo resultado.
Campos opcionais: `nome`, `preco`, `estoque`.

- SKU não existe: cria (exige `preco`).
- SKU existe e é da integração: atualiza só o que foi enviado (`preco` e/ou `estoque`).
  `nome` é ignorado na atualização.
- SKU existe e é do lojista: `409 PRODUCT_NOT_LINKED`, salvo com escopo `catalog:link`.
- Produto com preço promocional ativo e `preco` no corpo: `409 PROMOTIONAL_PRICE_ACTIVE`.
  Para mudar só o estoque desse produto, envie sem `preco` ou use o `PATCH`.

```http
PUT /api/partner/v1/products/CAMISETA-AZUL-M/ HTTP/1.1
Host: loja-exemplo.com.br
X-Partner-Key: SUA_CHAVE
Content-Type: application/json

{ "nome": "Camiseta Azul M", "preco": "79.90", "estoque": 12 }
```

`200 OK` com `data` = objeto `Produto`.

### `PATCH /products/{sku}/stock/` — definir estoque

Escopo `stock:write`. Define o estoque **absoluto** (não é incremento). A loja registra
a diferença no histórico de movimentação.

```http
PATCH /api/partner/v1/products/CAMISETA-AZUL-M/stock/ HTTP/1.1
Host: loja-exemplo.com.br
X-Partner-Key: SUA_CHAVE
Content-Type: application/json

{ "estoque": 7 }
```

`200 OK` com `data` = objeto `Produto`. SKU inexistente: `404 PRODUCT_NOT_FOUND`.

### `DELETE /products/{sku}/` — desativar produto

Escopo `catalog:write`. Não apaga: desativa e preserva o histórico. Repetir é seguro.

```http
DELETE /api/partner/v1/products/CAMISETA-AZUL-M/ HTTP/1.1
Host: loja-exemplo.com.br
X-Partner-Key: SUA_CHAVE
```

`200 OK`:

```json
{ "ok": true, "data": { "sku": "CAMISETA-AZUL-M", "ativo": false } }
```

## Erros

| HTTP | `code` | Quando | O que fazer |
|---|---|---|---|
| 401 | `MISSING_PARTNER_KEY` | Header `X-Partner-Key` ausente | Enviar o header |
| 403 | `INVALID_PARTNER_KEY` | Chave errada, revogada ou de outra loja | Não repetir; conferir chave e domínio |
| 403 | `INSUFFICIENT_SCOPE` | Chave sem o escopo da operação | Pedir ao lojista uma chave com a permissão |
| 404 | `PRODUCT_NOT_FOUND` | SKU não existe (`PATCH`/`DELETE`) | Criar com `PUT` |
| 409 | `SKU_ALREADY_EXISTS` | `POST` com SKU existente | Usar `PUT` |
| 409 | `PRODUCT_NOT_LINKED` | Produto é do lojista | Pedir vínculo ao lojista (`catalog:link`) |
| 409 | `EXTERNAL_SOURCE_CONFLICT` | Produto é de outra integração | Não repetir; avisar o lojista |
| 409 | `PROMOTIONAL_PRICE_ACTIVE` | Preço enviado com promoção ativa | Enviar sem `preco` ou pedir ao lojista que encerre a promoção |
| 409 | `VARIANT_STOCK_NOT_SUPPORTED` | Produto com grade/variação | Fora da v1; ignorar o SKU |
| 409 | `PRODUCT_MODE_NOT_SUPPORTED` | Kit, link, encomenda ou sem estoque | Fora da v1; ignorar o SKU |
| 422 | `VALIDATION_ERROR` | JSON inválido ou campo fora das regras | Corrigir o dado; não repetir igual |
| 429 | `RATE_LIMITED` | Limite por minuto atingido | Esperar os segundos do header `Retry-After` |
| 405 | — | Método não suportado na rota (corpo não é JSON) | Conferir método e caminho |
| 5xx | — | Falha temporária da loja | Repetir com espera crescente |

Domínio que não pertence a nenhuma loja responde 404 antes de chegar à API.

## Como implementar uma integração (recomendado)

1. **Guarde, por loja:** domínio, chave, e o mapa SKU → último estoque e preço enviados.
2. **Carga inicial:** para cada produto simples do seu sistema, `PUT /products/{sku}/` com
   `nome`, `preco` e `estoque`. Registre os SKUs que retornarem `PRODUCT_NOT_LINKED`,
   `EXTERNAL_SOURCE_CONFLICT` ou `*_NOT_SUPPORTED` e mostre-os ao lojista; não repita.
3. **Mudou o estoque:** `PATCH /products/{sku}/stock/` com o valor absoluto atual.
4. **Mudou o preço:** `PUT /products/{sku}/` só com `preco`.
5. **Produto saiu de linha:** `DELETE /products/{sku}/`.
6. **Conferência periódica:** `GET /products/` e compare com o seu sistema.
7. **Limite:** mantenha no máximo ~2 requisições por segundo por loja. Em `429`, espere o
   `Retry-After`. Em `5xx` ou erro de rede, repita com espera crescente (1 s, 2 s, 4 s…,
   no máximo 5 tentativas). Não repita automaticamente respostas `4xx` além de `429`.
8. **Segurança:** HTTPS sempre, chave fora do código e dos logs, uma chave por loja.

Exemplo mínimo em Python:

```python
import os
import time

import requests

BASE = "https://loja-exemplo.com.br/api/partner/v1"
HEADERS = {"X-Partner-Key": os.environ["SIMPLESFIKA_KEY"]}


def chamar(metodo, caminho, corpo=None, tentativas=5):
    """Faz a chamada respeitando 429 (Retry-After) e repetindo falhas temporarias."""
    for tentativa in range(tentativas):
        resposta = requests.request(metodo, f"{BASE}{caminho}", json=corpo, headers=HEADERS, timeout=20)
        if resposta.status_code == 429:
            time.sleep(int(resposta.headers.get("Retry-After", "5")))
            continue
        if resposta.status_code >= 500:
            time.sleep(2 ** tentativa)
            continue
        if "application/json" not in resposta.headers.get("Content-Type", ""):
            # Ex.: 405 (metodo errado) ou 404 de dominio desconhecido: corpo nao e JSON.
            raise RuntimeError(f"Resposta inesperada {resposta.status_code}: {metodo} {caminho}")
        return resposta.status_code, resposta.json()
    raise RuntimeError(f"Falhou apos {tentativas} tentativas: {metodo} {caminho}")


status, corpo = chamar("PUT", "/products/CAMISETA-AZUL-M/", {"nome": "Camiseta Azul M", "preco": "79.90", "estoque": 12})
if not corpo["ok"]:
    print("Erro:", corpo["code"], corpo["error"])
```

## Limitações conhecidas da v1

- Sem pedidos, clientes ou webhooks: a API cobre catálogo e estoque.
- Sem grade/variação, sem imagens, categorias ou descrição.
- Listagem limitada a 100 produtos, sem paginação.
- Sem atualização em lote: uma requisição por SKU.
- A API não reativa produto desativado.

## Versionamento

A versão está na rota (`/api/partner/v1/`). Campos novos podem ser adicionados às respostas
sem aviso; ignore os que não conhecer. Mudanças que quebrem o contrato saem numa nova versão.

## Suporte

Chaves, escopos e dúvidas: `contato@simplesfika.com` ou WhatsApp (83) 99676-0205.
Portal: `https://simplesfika.com/docs/`.
