Guia de integração com parceiros v0.2 (rascunho)

Granus ↔ parceiro, contrato de integração, atualizado em 28/09/2026

Este guia complementa a referência da API. Os endpoints que a Granus expõe ao parceiro (usuários, sessões, acesso e estado da integração) ficam somente na referência, gerados a partir da implementação. Aqui ficam o conceito de autenticação, o fluxo de acesso do usuário e os contratos que o parceiro expõe para a Granus: catálogo e webhook.


AUTH

AUTH HMAC-SHA256 Headers e assinatura obrigatórios Chamadas entre backends

As chamadas entre os backends do parceiro e da Granus, nas duas direções, devem usar HTTPS e autenticação HMAC-SHA256. A abertura da redirect_url pelo navegador do usuário é uma etapa separada: ela usa o ticket temporário recebido na chamada de login, sem headers HMAC. Ao cadastrar a integração, a Granus gera um parceiro_uuid público, único e estável e o entrega ao parceiro junto do segredo compartilhado, por um canal seguro fora desta API. O UUID identifica a integração; o segredo nunca é enviado na requisição.

Headers obrigatórios

HeaderTipoDescrição
AuthorizationstringHMAC-SHA256 {parceiro_uuid}:{assinatura}. O UUID identifica a integração; a assinatura autentica o conteúdo da requisição.
X-Request-TimestampintegerTimestamp Unix UTC da requisição. A diferença máxima permitida em relação ao servidor é de 300 segundos.
X-Request-NoncestringValor aleatório e único por requisição. Um nonce já aceito para a mesma credencial não pode ser reutilizado dentro da janela de 300 segundos.
Content-Typestringapplication/json quando a requisição possuir corpo JSON.

Conteúdo assinado

A assinatura é o HMAC-SHA256, em hexadecimal minúsculo, da seguinte string canônica codificada em UTF-8:

HTTP_METHOD\n
PATH\n
CANONICAL_QUERY_STRING\n
TIMESTAMP\n
NONCE\n
SHA256_BODY
ParteRegra
HTTP_METHODMétodo HTTP em letras maiúsculas: GET, POST ou DELETE.
PATHCaminho da URL, iniciado por /, sem domínio e sem query string.
CANONICAL_QUERY_STRINGQuery parameters ordenados pelo nome da chave, codificados como chave=valor e unidos por &. Deve ser uma string vazia quando não houver parâmetros.
TIMESTAMPMesmo valor enviado em X-Request-Timestamp.
NONCEMesmo valor enviado em X-Request-Nonce.
SHA256_BODYHash SHA-256 hexadecimal dos bytes exatos do corpo. Para requisições sem corpo, usar o hash SHA-256 da sequência vazia: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.

Exemplo de headers

Authorization: HMAC-SHA256 550e8400-e29b-41d4-a716-446655440000:4b7c...e91a
X-Request-Timestamp: 1790352000
X-Request-Nonce: 01K5ZW6KQ1S5N0X6QXGJ9QZC2A
Content-Type: application/json

Falhas de autenticação

401Credencial desconhecida, parceiro inativo, assinatura inválida, timestamp fora da janela aceita ou nonce reutilizado.
403Credencial válida, mas sem autorização para acessar o recurso solicitado.

Acesso do usuário à Granus

O usuário já está autenticado no sistema do parceiro e clica em “Ver etiquetas”. O backend do parceiro identifica esse usuário pelo mesmo user_id usado no provisionamento e pede uma URL de acesso à Granus. O parceiro entrega essa URL ao navegador por redirecionamento HTTP. O navegador visita a Granus, que valida o ticket, cria o cookie de autenticação normal da aplicação e abre o painel de etiquetas.

O parceiro não precisa conhecer a senha Granus do usuário, receber o cookie authToken ou implementar uma tela de login Granus. O ticket na URL serve apenas para iniciar uma autenticação e não é aceito como credencial das demais APIs.

Usuário clica em “Ver etiquetas” no sistema do parceiro
  → backend do parceiro: POST /parceiros/sessoes (HMAC + user_id)
  ← Granus: 201 com redirect_url e expires_in
  → backend do parceiro: redireciona o navegador para redirect_url
  → navegador: GET /parceiros/acesso?token=...
  ← Granus: cookie authToken + redirecionamento para /etiquetas/painel

As chamadas POST /parceiros/sessoes e GET /parceiros/acesso estão descritas, com exemplos e respostas, na referência da API, seção “Acesso do usuário”.


Catálogo e eventos

GET /produtos Lista os produtos de um cliente Granus consome

Sugestão de rota: nome e caminho reais ficam a critério do parceiro, desde que a resposta siga o formato descrito abaixo.

Parameters

NomeLocalizaçãoObrigatoriedadeTipoDescrição
cliente_id query Obrigatório string Identifica o cliente do parceiro cujos produtos queremos listar. O mecanismo de autenticação/identificação real da chamada será definido depois; por ora, trate este parâmetro como um placeholder de "quem estamos consultando".

Atributos de cada produto

CampoObrigatoriedadeDescrição
idint Obrigatório Identificador numérico inteiro, positivo, único e estável do produto no parceiro. Usado pelo Granus para referenciar a origem do insumo depois de criada a etiqueta.
nomestring Obrigatório Nome exibido na lista de seleção de insumos ao criar a etiqueta.
categoriastring Obrigatório Id ou nome de uma categoria retornada por GET /categorias. Usada para agrupar e filtrar produtos na tela de criação.
subcategoriastring Opcional Id ou nome de uma subcategoria dentro da categoria informada, retornada por GET /categorias.
unidade_compraenum Obrigatório Define as unidades disponíveis para a etiqueta.
UngmLLKg
quantidade_por_embalagemdecimal Obrigatório Quantidade contida em uma embalagem/lote, na unidade de unidade_compra. Usada no cálculo do valor unitário.
preco_embalagemdecimal Obrigatório Preço da embalagem inteira. Usado no cálculo do valor unitário e registrado na etiqueta gerada.
peso_mediodecimal Condicional Obrigatório somente quando unidade_compra = Un. Nos demais casos pode ser omitido.
peso_medio_unidadeenum Condicional Obrigatório sempre que peso_medio for informado; caso contrário, pode ser omitido.
gmL
fornecedorstring Opcional Nome do fornecedor do produto, se existir no parceiro.
localstring Opcional Id ou nome de um local de armazenamento retornado por GET /locais, indicando onde esse produto fica fisicamente armazenado no parceiro. Se omitido, o local é escolhido manualmente na criação da etiqueta.

Responses

200 Lista de produtos do cliente. Media type: application/json.

Example Value

{
  "produtos": [
    {
      "id": 8841,
      "nome": "Filé de Frango Resfriado",
      "categoria": "Proteínas Animais",
      "subcategoria": "Aves",
      "unidade_compra": "Kg",
      "quantidade_por_embalagem": 5.000,
      "preco_embalagem": 42.90,
      "fornecedor": "Granja Bela Vista",
      "local": "Câmara Fria 1"
    },
    {
      "id": 9210,
      "nome": "Ovo Caipira",
      "categoria": "Ovos e Laticínios",
      "subcategoria": null,
      "unidade_compra": "Un",
      "quantidade_por_embalagem": 30,
      "preco_embalagem": 24.50,
      "peso_medio": 58.00,
      "peso_medio_unidade": "g",
      "fornecedor": null,
      "local": "Central (Padrão)"
    },
    {
      "id": 7733,
      "nome": "Molho Shoyu Tradicional",
      "categoria": "Molhos e Temperos",
      "subcategoria": "Molhos prontos",
      "unidade_compra": "L",
      "quantidade_por_embalagem": 1.000,
      "preco_embalagem": 18.90,
      "fornecedor": "Sakura Alimentos",
      "local": "Estoque Seco"
    }
  ]
}
GET /categorias Lista as categorias e subcategorias do parceiro Granus consome

Sugestão de rota: nome e caminho reais ficam a critério do parceiro. O Granus não impõe uma taxonomia própria: o parceiro expõe a sua, e cada produto (GET /produtos) referencia uma categoria/subcategoria desta lista. Esta lista é usada para popular os dropdowns de categoria e subcategoria na tela de criação de etiqueta do Granus, não é armazenada como uma taxonomia própria.

Atributos de cada categoria

CampoObrigatoriedadeDescrição
idstring Obrigatório Identificador único da categoria no parceiro.
nomestring Obrigatório Nome exibido da categoria.
subcategoriasarray Opcional Lista de objetos { id, nome }, se o parceiro trabalhar com subcategorias.

Responses

200 Lista de categorias do parceiro, cada uma com suas subcategorias. Media type: application/json.

Example Value

{
  "categorias": [
    {
      "id": "proteinas-animais",
      "nome": "Proteínas Animais",
      "subcategorias": [
        { "id": "aves", "nome": "Aves" },
        { "id": "bovinos", "nome": "Bovinos" }
      ]
    },
    {
      "id": "ovos-laticinios",
      "nome": "Ovos e Laticínios",
      "subcategorias": [
        { "id": "ovos", "nome": "Ovos" },
        { "id": "queijos", "nome": "Queijos" }
      ]
    },
    {
      "id": "molhos-temperos",
      "nome": "Molhos e Temperos",
      "subcategorias": [
        { "id": "molhos-prontos", "nome": "Molhos prontos" }
      ]
    }
  ]
}
GET /locais Lista os locais de armazenamento do parceiro Granus consome
Ainda não implementado. A integração atual não faz essa chamada: o campo local devolvido em GET /produtos é hoje só um texto livre, exibido como veio, sem validação contra uma lista de locais do parceiro. Este endpoint descreve o desenho pretendido, não um contrato já consumido pela Granus.

Sugestão de rota: nome e caminho reais ficam a critério do parceiro. Um parceiro pode ter vários locais de armazenamento (câmaras frias, estoques, filiais); cada produto (GET /produtos) pode referenciar um destes. Esta lista é usada para popular o dropdown de local de armazenamento na tela de criação de etiqueta do Granus.

Atributos de cada local

CampoObrigatoriedadeDescrição
idstring Obrigatório Identificador único do local no parceiro.
nomestring Obrigatório Nome exibido do local.

Responses

200 Lista de locais de armazenamento do parceiro. Media type: application/json.

Example Value

{
  "locais": [
    { "id": "central", "nome": "Central (Padrão)" },
    { "id": "camara-fria-1", "nome": "Câmara Fria 1" },
    { "id": "estoque-seco", "nome": "Estoque Seco" }
  ]
}
POST {url_definida_pelo_parceiro} Recebe um evento de consumo de produto Granus envia
Ainda não implementado. O parceiro já pode informar seu webhook_url no cadastro da integração, mas a Granus ainda não envia nenhum evento para ele. Este endpoint descreve o contrato planejado, para o parceiro já deixar o recebimento pronto; o disparo real entra em uma etapa posterior.

A URL é definida pelo parceiro (o Granus só precisa que ela seja informada). O critério para disparar esse webhook é simples: só avisamos o parceiro quando isso afeta o estoque ou a localização do produto. Ações que não mudam nem quantidade nem local (revalidar a validade, reimprimir) não geram webhook.

Eventos

EventoQuando ocorre
CRIADAEtiqueta foi criada a partir de um produto.
USADAEtiqueta deu baixa (uso total ou parcial da quantidade armazenada).
DESCARTADAEtiqueta foi descartada (total ou parcial), com motivo.
TRANSFERENCIA_ENVIADAEtiqueta foi enviada para outro estabelecimento.
TRANSFERENCIA_RECEBIDAEtiqueta foi recebida em outro estabelecimento (some do estoque de origem).
TRANSFERENCIA_CANCELADATransferência de etiqueta foi cancelada (volta ao estoque de origem).

Atributos do payload

CampoObrigatoriedadeDescrição
eventoenum Obrigatório Um dos valores da tabela de eventos acima.
ocorrido_emdatetime Obrigatório Data/hora da alteração, com timezone.
estabelecimento_idint Obrigatório Estabelecimento do Granus onde a etiqueta foi alterada.
etiqueta.idint Obrigatório Identificador da etiqueta no Granus.
etiqueta.lotestring Obrigatório Código de lote impresso na etiqueta.
etiqueta.insumo.tipoenum Obrigatório Sempre p para etiquetas originadas de um produto externo.
etiqueta.insumo.id_externoint Obrigatório O mesmo id devolvido pelo parceiro em GET /produtos. Chave de correlação entre o evento e o produto de origem.
etiqueta.insumo.nomestring Obrigatório Nome do insumo/produto no momento da criação da etiqueta.
etiqueta.status_anteriorenum Condicional Status da etiqueta antes do evento: VALIDA, VENCIDA, DESCARTADA, USADO, EM_TRANSITO ou TRANSFERIDA. Ausente no evento CRIADA, que não tem estado anterior.
etiqueta.status_atualenum Obrigatório Status da etiqueta depois do evento. Mesmos valores possíveis de status_anterior.
etiqueta.quantidade_anteriordecimal Condicional Quantidade armazenada antes do evento. Ausente no evento CRIADA.
etiqueta.quantidade_atualdecimal Obrigatório Quantidade armazenada depois do evento. É o dado central para o parceiro contabilizar o próprio estoque.
etiqueta.unidadeenum Obrigatório Unidade das quantidades acima: Un, g, Kg, mL ou L.

Responses

200 Confirma o recebimento do evento. O Granus não depende de um corpo específico na resposta, apenas do status HTTP de sucesso.

Example Value

CRIADA

{
  "evento": "CRIADA",
  "ocorrido_em": "2026-08-25T08:15:00-03:00",
  "estabelecimento_id": 12,
  "etiqueta": {
    "id": 4821,
    "lote": "12-p8841-00037-01-0",
    "insumo": {
      "tipo": "p",
      "id_externo": 8841,
      "nome": "Filé de Frango Resfriado"
    },
    "status_atual": "VALIDA",
    "quantidade_atual": "5.000",
    "unidade": "Kg"
  }
}

Não existe estado anterior na criação, então status_anterior e quantidade_anterior são omitidos.

USADA (baixa parcial)

{
  "evento": "USADA",
  "ocorrido_em": "2026-08-30T14:32:07-03:00",
  "estabelecimento_id": 12,
  "etiqueta": {
    "id": 4821,
    "lote": "12-p8841-00037-01-0",
    "insumo": {
      "tipo": "p",
      "id_externo": 8841,
      "nome": "Filé de Frango Resfriado"
    },
    "status_anterior": "VALIDA",
    "status_atual": "VALIDA",
    "quantidade_anterior": "5.000",
    "quantidade_atual": "2.500",
    "unidade": "Kg"
  }
}

Baixa parcial: só uma parte da quantidade armazenada foi usada, então o status_atual permanece VALIDA e a etiqueta continua com saldo (quantidade_atual maior que zero). Numa baixa total, status_atual viraria USADO e quantidade_atual ficaria 0.000.

DESCARTADA

{
  "evento": "DESCARTADA",
  "ocorrido_em": "2026-08-31T09:10:42-03:00",
  "estabelecimento_id": 12,
  "etiqueta": {
    "id": 4821,
    "lote": "12-p8841-00037-01-0",
    "insumo": {
      "tipo": "p",
      "id_externo": 8841,
      "nome": "Filé de Frango Resfriado"
    },
    "status_anterior": "VALIDA",
    "status_atual": "DESCARTADA",
    "quantidade_anterior": "2.500",
    "quantidade_atual": "0.000",
    "unidade": "Kg"
  }
}

TRANSFERENCIA_ENVIADA

{
  "evento": "TRANSFERENCIA_ENVIADA",
  "ocorrido_em": "2026-09-02T11:00:00-03:00",
  "estabelecimento_id": 12,
  "etiqueta": {
    "id": 4821,
    "lote": "12-p8841-00037-01-0",
    "insumo": {
      "tipo": "p",
      "id_externo": 8841,
      "nome": "Filé de Frango Resfriado"
    },
    "status_anterior": "VALIDA",
    "status_atual": "EM_TRANSITO",
    "quantidade_anterior": "5.000",
    "quantidade_atual": "5.000",
    "unidade": "Kg"
  }
}

estabelecimento_id é o estabelecimento de origem, de onde a etiqueta está saindo. A quantidade não muda ainda; só o status, que passa a EM_TRANSITO.

TRANSFERENCIA_RECEBIDA

{
  "evento": "TRANSFERENCIA_RECEBIDA",
  "ocorrido_em": "2026-09-02T15:40:00-03:00",
  "estabelecimento_id": 12,
  "etiqueta": {
    "id": 4821,
    "lote": "12-p8841-00037-01-0",
    "insumo": {
      "tipo": "p",
      "id_externo": 8841,
      "nome": "Filé de Frango Resfriado"
    },
    "status_anterior": "EM_TRANSITO",
    "status_atual": "TRANSFERIDA",
    "quantidade_anterior": "5.000",
    "quantidade_atual": "0.000",
    "unidade": "Kg"
  }
}

Reportado no estabelecimento de origem: a quantidade sai do estoque de lá (vai a zero) porque o recebimento foi confirmado no destino.

TRANSFERENCIA_CANCELADA

{
  "evento": "TRANSFERENCIA_CANCELADA",
  "ocorrido_em": "2026-09-02T12:30:00-03:00",
  "estabelecimento_id": 12,
  "etiqueta": {
    "id": 4821,
    "lote": "12-p8841-00037-01-0",
    "insumo": {
      "tipo": "p",
      "id_externo": 8841,
      "nome": "Filé de Frango Resfriado"
    },
    "status_anterior": "EM_TRANSITO",
    "status_atual": "VALIDA",
    "quantidade_anterior": "5.000",
    "quantidade_atual": "5.000",
    "unidade": "Kg"
  }
}

A transferência foi cancelada antes de ser recebida: a etiqueta volta a VALIDA no estabelecimento de origem, com a quantidade intacta.