MUPLI-WHITELABEL / API

Como enviar seus dados.

O seu sistema manda um objeto JSON para o mupli.io e o Mupli-WhiteLabel desenha: um número grande, um gráfico, linhas e um recado. Todo campo é opcional. O formato é o mesmo para qualquer dado: vendas, visitas, serviços, energia, temperatura.

Em 1 minuto

Crie a chave do aparelho em Meus Mupli (uma chave por aparelho, mostrada uma vez) e mande:

curl -X POST https://mupli.io/api/apps/whitelabel/v1/screen -H "Authorization: Bearer mupli_wl1_SUA_CHAVE_AQUI" -H "Content-Type: application/json" \
  -d '{"title":"Visitas","value":1280}'

A resposta diz o que a tela mostra:

{"ok":true,"seq":4822,"changed":true,"accepted_at":"…","fresh_until":"…","expires_at":"…",
 "pages":[{"layout":"number","shown":["title","value"],"not_shown":[]}],
 "warnings":[],"device_bytes":100,"alert":{"fired":false,"seq":null,"replaced":false},
 "device":{"online":true,"rom":"whitelabel","delivered_seq":4821,"next_fetch_in_s":42},"next_in_s":5}

O que você manda é seu e aparece numa tela que fica na mesa, à vista de quem passa. Não mande dados sensíveis (saúde, religião, opinião política, biometria, origem racial), dados de crianças e de adolescentes, senhas, documentos nem dados de cartão.

Tudo é opcional

Até o vazio vale. {} responde 200 e a tela volta à mesa, com o relógio e o bichinho:

curl -X POST https://mupli.io/api/apps/whitelabel/v1/screen -H "Authorization: Bearer mupli_wl1_SUA_CHAVE_AQUI" -H "Content-Type: application/json" \
  -d '{}'
  • Campo que não veio não aparece. null vale o mesmo que não mandar.
  • Cada envio troca a tela inteira: o que não veio sai.
  • Sem pages, os campos de página escritos direto no objeto são a página 1 (o atalho do exemplo acima).
  • O servidor recusa pouco e avisa sempre: o que não cabe sai da tela e a resposta diz o quê.

A página e os blocos

Até 4 páginas, que passam em rotação. Cada uma tem um título com ícone e estado, e até quatro blocos: o número grande (value), o gráfico (chart), as linhas (rows) e um recado (text). Uma página aparece se tiver ao menos um número, um gráfico, uma linha, um recado ou um estado.

Um painel de loja com 3 páginas e um aviso:

curl -X POST https://mupli.io/api/apps/whitelabel/v1/screen -H "Authorization: Bearer mupli_wl1_SUA_CHAVE_AQUI" -H "Content-Type: application/json" \
  --data @- <<'JSON'
{
  "name": "Loja Centro", "ttl": 900,
  "pages": [
    {"title": "Vendas hoje", "icon": "cart", "value": 12840.50, "prefix": "R$", "label": "38 pedidos",
     "delta": 3.4, "delta_unit": "%",
     "chart": {"type": "line", "points": [8, 9, 9, 12, 11, 14, 13, 15], "labels": ["8 h", "agora"]}},
    {"title": "Serviços", "icon": "server", "status": "warn",
     "rows": [{"label": "API", "value": "120 ms", "status": "ok"},
              {"label": "Fila", "value": 1204, "status": "warn"},
              {"label": "Pagamento", "value": "fora", "status": "bad"}]},
    {"title": "Recado", "text": "Reunião às 15h na sala 2"}
  ],
  "alert": {"id": "pedido-4822", "level": "info", "text": "Novo pedido!"}
}
JSON

Os gráficos

Um objeto chart só, seis tipos. O servidor acerta a escala e o aparelho desenha com as cores da casa.

typePrecisaDesenha
line2 pontos ou mais (até 48; passou, ficam os últimos)linha com o último ponto marcado, máximo e mínimo
area2 pontos ou mais (até 48)a linha com a área cheia até a base
sparkline2 pontos ou mais (até 48)só a linha, sem números: cabe ao lado de linhas
bar1 ponto ou mais (até 24); negativos descem do zerobarras, um rótulo por barra e a meta tracejada
gaugeum valor: chart.value, o value da página ou o último pontomeio círculo de min a max (padrão 0 a 100), o valor no centro
donut1 a 4 partes de valor 0 ou mais (mais: "Outros"); com 1, o maxanel com a legenda e a porcentagem de cada parte

Linha: número e gráfico

curl -X POST https://mupli.io/api/apps/whitelabel/v1/screen -H "Authorization: Bearer mupli_wl1_SUA_CHAVE_AQUI" -H "Content-Type: application/json" \
  -d '{"title":"Visitas","icon":"users","value":1280,"delta":12,"delta_unit":"%","chart":{"type":"line","points":[820,940,910,1100,1180,1280],"labels":["há 6 h","agora"]}}'

Área: só o gráfico, em verde, com unidade

curl -X POST https://mupli.io/api/apps/whitelabel/v1/screen -H "Authorization: Bearer mupli_wl1_SUA_CHAVE_AQUI" -H "Content-Type: application/json" \
  -d '{"title":"Energia solar","icon":"bolt","chart":{"type":"area","points":[0,0.4,1.8,3.2,3.6,2.9,1.1,0.2],"unit":"kW","color":"green"}}'

Barras: um rótulo por barra e a meta tracejada

curl -X POST https://mupli.io/api/apps/whitelabel/v1/screen -H "Authorization: Bearer mupli_wl1_SUA_CHAVE_AQUI" -H "Content-Type: application/json" \
  -d '{"title":"Pedidos","icon":"cart","chart":{"type":"bar","points":[12,18,9,22,30,26,14],"labels":["S","T","Q","Q","S","S","D"],"target":20}}'

Sparkline: linhas e um gráfico sem legenda

curl -X POST https://mupli.io/api/apps/whitelabel/v1/screen -H "Authorization: Bearer mupli_wl1_SUA_CHAVE_AQUI" -H "Content-Type: application/json" \
  -d '{"title":"Fila","icon":"server","status":"warn","rows":[{"label":"Agora","value":42,"status":"warn"},{"label":"Média","value":31}],"chart":{"type":"sparkline","points":[30,28,35,41,39,42]}}'

Medidor: a meta do mês, com o valor no centro

curl -X POST https://mupli.io/api/apps/whitelabel/v1/screen -H "Authorization: Bearer mupli_wl1_SUA_CHAVE_AQUI" -H "Content-Type: application/json" \
  -d '{"title":"Meta do mês","icon":"money","prefix":"R$","chart":{"type":"gauge","value":72300,"max":100000,"target":90000}}'

Rosca: partes de um todo

curl -X POST https://mupli.io/api/apps/whitelabel/v1/screen -H "Authorization: Bearer mupli_wl1_SUA_CHAVE_AQUI" -H "Content-Type: application/json" \
  -d '{"title":"De onde vêm","icon":"chart","chart":{"type":"donut","points":[540,230,120],"labels":["Busca","Direto","Redes"]}}'

Os layouts e a escolha automática

O layout sai do que veio preenchido; a primeira linha que casa vence. A dica layout pede outro, se a página tiver o bloco principal dele.

TemLayoutSai, com aviso
número e gráficonumber_chartlinhas e texto
númeronumbero que não couber embaixo (até 4 linhas, ou 2 linhas e 2 de texto)
gráfico e linhasrows_charta 4ª linha e o texto
gráficochartnada
linhasrowsnada
texto ou estadomessagenada
nada dissonenhuma página não aparece (empty_page)

Os campos

O objeto

vinteirosó 1 (padrão 1)
nametextoaté 16 letras: o nome do painel, na mesa
ttlinteiro (s)150 a 604800, padrão 900: por quanto tempo o dado é fresco
stale_alertbooleanopadrão false: dado velho dispara uma vez o aviso "Sem notícias"
pageslistaaté 4 páginas, na ordem da rotação
alertobjeto{ text, level, id }: o aviso do aparelho

A página

titletextoaté 18 letras e a largura da linha do título
iconnomecart, money, user, users, server, cloud, bolt, temp, drop, home, code, clock, star, heart, bell, chart
statusnomeok, warn, bad, info, off
layoutnomeauto (padrão), number, number_chart, chart, rows, rows_chart, message
valuenúmeroo número grande; com as casas decimais, até 9 dígitos
decimalsinteiro0 a 3; padrão: as casas do número como veio
prefixtextoaté 4 letras ("R$")
unittextoaté 10 letras ("pedidos", "kWh")
labeltextoaté 22 letras, embaixo do número
deltanúmeroa variação, com seta
delta_unittexto"%" ou nada
goodnomeup (padrão) ou down: qual direção é boa
chartobjetoo gráfico (abaixo)
rowslistaaté 4 linhas { label, value, status }
texttextoaté 60 letras, até 2 linhas

O gráfico

typenomeline, area, sparkline, bar, gauge, donut
pointslista de númerosnull marca uma falta
valuenúmerosó no gauge
labelslista de textosrótulos do gráfico
min, maxnúmeroampliam a faixa, nunca a cortam
targetnúmeroa meta, tracejada
colornomeauto, violet, blue, green, red, pink, gray, white
unittextoaté 6 letras
decimalsinteiro0 a 3

Texto: as letras do português e do espanhol entram (ç, ã, é); emoji e símbolos que a tela não tem tiram o campo da tela, com aviso. Nada é cortado com reticências: o texto que não cabe sai.

Limites e em quanto tempo a tela muda

  • 1 envio aceito a cada 5 segundos por aparelho; acima disso, 429 com Retry-After. O último vence: nunca há fila.
  • Corpo de até 8 KiB e 6 níveis.
  • A tela mostra o envio mais novo em até 70 segundos.
  • O dado fica fresco pelo ttl (padrão 15 minutos); depois, a tela o mostra apagado, com a idade, e ele some depois de 4 vezes o ttl (no mínimo 24 horas, nunca mais de 7 dias).
  • Até 2 chaves por aparelho, para trocar sem buraco. A chave vai só no cabeçalho Authorization: escrita nos parâmetros do endereço (depois do ?), ela é recusada e revogada na hora, salvo se o limite de chaves inválidas já tiver barrado o endereço de origem (então revogue-a você em Meus Mupli).
  • Um aviso (alert) por minuto por aparelho; o mesmo id não volta em 24 horas. Acima disso, o envio é aceito sem o aviso. O id nunca aparece na tela: qualquer texto serve nele.
  • O aviso que tocou chega ao aparelho mesmo que você mande outro número logo depois: ele vai junto nos envios seguintes até o aparelho confirmar que o recebeu (na busca seguinte). Enquanto nenhum número muda, ele espera o aparelho voltar, o tempo que for; com um número novo, vai junto por até 3 minutos depois de tocar. Um aviso novo antes disso troca o anterior, e a resposta diz ("replaced":true).
  • Na resposta, alert.fired diz se o aviso deste envio tocou e alert.seq qual seq o leva: quando device.delivered_seq for igual ou maior, o servidor já o entregou ao aparelho (se essa entrega se perder no caminho, o aviso segue nos envios seguintes até o aparelho confirmar). device.next_fetch_in_s é quando o aparelho busca de novo, mais ou menos (ele olha a cada minuto: mandar mais rápido não muda a tela); null com o aparelho desligado.
  • Contra abuso: até 600 pedidos por minuto de um mesmo endereço de origem (no IPv6, a mesma rede /64; no ritmo máximo, cerca de 50 aparelhos por servidor), 10 tentativas com chave inválida por minuto pelo mesmo endereço (ou rede /64), 60 leituras por minuto por chave e 3.000 pedidos por minuto no serviço inteiro, somados todos os clientes; acima deste, 503 com Retry-After. São os limites da cláusula 3.3 dos Termos da API.

Mandar o mesmo de novo responde "changed":false e renova a validade do que já está na tela. Repita um envio com segurança com o cabeçalho Idempotency-Key.

Recusas e avisos

Recusa (422): nada muda na tela, e a resposta diz cada campo.

{"error":"validation_failed","errors":[{"path":"pages[0].chart.type","code":"enum","allowed":["line","area","sparkline","bar","gauge","donut"]}]}
invalid_jsonJSON malformado (400, com "at": a posição)
unsupported_versionv diferente de 1
unknown_fieldum campo que o formato não tem, em qualquer nível
duplicate_keya mesma chave duas vezes no mesmo objeto
too_deepmais de 6 níveis
typetipo errado ("value":"12", pages que não é lista, 1e400)
enumum nome fora da lista (vem com "allowed")
too_manymais de 4 páginas, ou mais de 4 linhas numa página
pages_and_page_fieldso atalho de uma página e pages juntos
control_charcaractere de controle num texto
invisible_charcaractere invisível num texto
link_not_allowedum endereço num texto (:// ou www.)
system_phraseo nome (name) ou o texto do aviso (alert.text) igual a uma frase do sistema do aparelho ("Sem dados", "Atualizando"…): em outro campo é só um aviso

Aviso (200): a tela muda sem aquele campo.

curl -X POST https://mupli.io/api/apps/whitelabel/v1/screen -H "Authorization: Bearer mupli_wl1_SUA_CHAVE_AQUI" -H "Content-Type: application/json" \
  -d '{"title":"Pedidos de hoje no balcão","value":1280,"rows":[{"label":"Ontem","value":1100}],"chart":{"type":"line","points":[1100,1280]},"text":"Aberto até as 22h"}'
too_longtexto acima do limite de letras: o campo sai
too_widetexto ou número que não cabe na tela (vem com "px" e "max_px"): o campo sai
char_unsupporteduma letra que a tela não tem (vem com "char"): o campo sai
transliterateduma letra trocada pela sem acento (ł vira l)
decimals_reducedo número só coube com menos casas
rangenúmero grande demais para a escala, ou min maior que max
clampedttl ou valor do gauge fora da faixa: encosta na ponta
not_meaningfulgráfico sem o que precisa: o gráfico sai
trimmedpontos demais: ficam os últimos
mergedpartes demais na rosca: o resto vira "Outros"
ignoredum campo que aquele gráfico não usa
layout_ignoreda dica layout pede um bloco que a página não tem
not_shownum bloco que não coube no layout (vem com "layout")
empty_pagepágina sem nada para mostrar: sai da rotação
alert_emptyaviso sem text
alert_repeatedo mesmo aviso de novo (o mesmo id em 24 horas, ou o mesmo texto sem id)
alert_rate_limitedmais de um aviso por minuto
device_budgeto conjunto passou do que o aparelho guarda: sai o texto, depois as linhas (toda tela guarda lugar para o maior aviso, então uma tela no limite pode perder um texto ou uma linha mesmo sem aviso)
system_phraseum rótulo, uma linha ou um texto igual a uma frase do sistema ("Offline", "Erro", "Link"…): só aquele campo sai, o resto do envio vale

As respostas

200aceito: seq, changed, as horas, o layout de cada página, os avisos, o aviso (alert: se tocou e o seq que o leva) e o estado do aparelho (com next_fetch_in_s)
400JSON malformado, ou a chave escrita no endereço (token_in_query: a chave é revogada na hora)
401chave ausente, errada ou revogada, ou o Mupli foi desvinculado (sempre {"error":"invalid_token"})
404outro caminho, ou um endereço com "?"
409a mesma Idempotency-Key com outro corpo
413corpo acima de 8 KiB
415sem Content-Type: application/json
422recusa: o corpo diz cada campo e o motivo
429mais de 1 envio a cada 5 segundos (Retry-After diz quanto esperar)
503manutenção ou teto do serviço (Retry-After)

Exemplos recusados

  • JSON quebrado: {"title":"Visitas", → 400 invalid_json
  • Campo que o formato não tem: {"titel":"Visitas","value":1280} → 422 unknown_field
  • Número escrito como texto: {"title":"Visitas","value":"1280"} → 422 type
  • Tipo de gráfico fora da lista: {"title":"Visitas","chart":{"type":"pizza","points":[1,2]}} → 422 enum
  • Endereço num texto: {"title":"Visitas","text":"Veja em www.exemplo.com"} → 422 link_not_allowed
  • O atalho de uma página junto com pages: {"title":"Visitas","pages":[{"title":"Vendas","value":3}]} → 422 pages_and_page_fields

Ler, conferir e apagar

O que está na tela agora, como o aparelho mostra:

curl https://mupli.io/api/apps/whitelabel/v1/screen -H "Authorization: Bearer mupli_wl1_SUA_CHAVE_AQUI"

Conferir um corpo sem guardar nem mudar a tela (1 por segundo):

curl -X POST https://mupli.io/api/apps/whitelabel/v1/screen/validate -H "Authorization: Bearer mupli_wl1_SUA_CHAVE_AQUI" -H "Content-Type: application/json" -d @painel.json

Apagar a tela (o aparelho volta à mesa):

curl -X DELETE https://mupli.io/api/apps/whitelabel/v1/screen -H "Authorization: Bearer mupli_wl1_SUA_CHAVE_AQUI"

Receitas

Mande quando o dado mudar, e um envio a cada poucos minutos para manter a tela fresca. Não repita um 4xx sem mudar nada. Guarde a chave como segredo, no servidor: nunca num navegador.

Shell e cron

# a cada minuto (cron), o mesmo corpo do primeiro exemplo
* * * * * curl -fsS -X POST https://mupli.io/api/apps/whitelabel/v1/screen -H "Authorization: Bearer $MUPLI_WL_KEY" -H "Content-Type: application/json" -d '{"title":"Visitas","value":1280}'

Python

import os, requests
s = requests.Session()  # uma conexão só, reaproveitada
s.headers.update({"Authorization": "Bearer " + os.environ["MUPLI_WL_KEY"]})
r = s.post("https://mupli.io/api/apps/whitelabel/v1/screen", json={"title":"Visitas","value":1280})
print(r.status_code, r.json().get("warnings"))

Node.js

const r = await fetch("https://mupli.io/api/apps/whitelabel/v1/screen", {
  method: "POST",
  headers: { "Authorization": `Bearer ${process.env.MUPLI_WL_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({"title":"Visitas","value":1280}),
})
console.log(r.status, (await r.json()).warnings)

Versões

Este é o formato v 1. Um campo novo vem numa versão nova, e a v1 continua valendo.