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.
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.
| Header | Tipo | Descrição |
|---|---|---|
| Authorization | string | HMAC-SHA256 {parceiro_uuid}:{assinatura}. O UUID identifica a integração; a assinatura autentica o conteúdo da requisição. |
| X-Request-Timestamp | integer | Timestamp Unix UTC da requisição. A diferença máxima permitida em relação ao servidor é de 300 segundos. |
| X-Request-Nonce | string | Valor 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-Type | string | application/json quando a requisição possuir corpo JSON. |
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
| Parte | Regra |
|---|---|
| HTTP_METHOD | Método HTTP em letras maiúsculas: GET, POST ou DELETE. |
| PATH | Caminho da URL, iniciado por /, sem domínio e sem query string. |
| CANONICAL_QUERY_STRING | Query parameters ordenados pelo nome da chave, codificados como chave=valor e unidos por &. Deve ser uma string vazia quando não houver parâmetros. |
| TIMESTAMP | Mesmo valor enviado em X-Request-Timestamp. |
| NONCE | Mesmo valor enviado em X-Request-Nonce. |
| SHA256_BODY | Hash SHA-256 hexadecimal dos bytes exatos do corpo. Para requisições sem corpo, usar o hash SHA-256 da sequência vazia: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855. |
Authorization: HMAC-SHA256 550e8400-e29b-41d4-a716-446655440000:4b7c...e91a X-Request-Timestamp: 1790352000 X-Request-Nonce: 01K5ZW6KQ1S5N0X6QXGJ9QZC2A Content-Type: application/json
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”.
Sugestão de rota: nome e caminho reais ficam a critério do parceiro, desde que a resposta siga o formato descrito abaixo.
| Nome | Localização | Obrigatoriedade | Tipo | Descriçã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". |
| Campo | Obrigatoriedade | Descriçã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. |
application/json.
{ "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" } ] }
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.
| Campo | Obrigatoriedade | Descriçã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. |
application/json.
{ "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" } ] } ] }
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.
| Campo | Obrigatoriedade | Descrição |
|---|---|---|
| idstring | Obrigatório | Identificador único do local no parceiro. |
| nomestring | Obrigatório | Nome exibido do local. |
application/json.
{ "locais": [ { "id": "central", "nome": "Central (Padrão)" }, { "id": "camara-fria-1", "nome": "Câmara Fria 1" }, { "id": "estoque-seco", "nome": "Estoque Seco" } ] }
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.
| Evento | Quando ocorre |
|---|---|
| CRIADA | Etiqueta foi criada a partir de um produto. |
| USADA | Etiqueta deu baixa (uso total ou parcial da quantidade armazenada). |
| DESCARTADA | Etiqueta foi descartada (total ou parcial), com motivo. |
| TRANSFERENCIA_ENVIADA | Etiqueta foi enviada para outro estabelecimento. |
| TRANSFERENCIA_RECEBIDA | Etiqueta foi recebida em outro estabelecimento (some do estoque de origem). |
| TRANSFERENCIA_CANCELADA | Transferência de etiqueta foi cancelada (volta ao estoque de origem). |
| Campo | Obrigatoriedade | Descriçã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. |
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.