Aparelhos
Registrar, identificar o usuário, atualizar, consultar e exportar aparelhos — com a regra que derruba a maioria das primeiras integrações.
Aparelhos
Um aparelho é um celular ou navegador capaz de receber um push. Cada um
tem um identificador público — o player_id, um UUID — e é por ele que você
mira um disparo.
Estas rotas fazem quatro coisas: registrar um aparelho, dizer quem está usando ele, consultar o estado dele e exportar a base inteira.
Em 30 segundos
curl -X POST https://api.pushmesh.io/api/v1/players \
-H 'Content-Type: application/json' \
-d '{
"app_id": "01926f3a-4b2c-7d8e-9f01-23456789abcd",
"device_type": 1,
"identifier": "TOKEN-DE-PUSH-DO-APARELHO",
"external_user_id": "usuario-4711"
}'
{ "success": true, "id": "01926f4c-7a1b-7c2d-8e3f-a1b2c3d4e5f6" }
O id da resposta é o player_id. Guarde-o: é ele que vai em
include_player_ids no envio e é ele que o aparelho manda de volta no recibo
de entrega.
A resposta é
200, nunca201— inclusive no primeiro registro. A rota é idempotente por(app, token): chamar cem vezes com o mesmo token devolve sempre o mesmoplayer_id.
As quatro rotas, e quem chama cada uma
| Rota | Quem chama | Credencial |
|---|---|---|
POST /api/v1/players | o app, no aparelho | nenhuma |
PUT /api/v1/players/{id} | o app, no aparelho | nenhuma |
GET /api/v1/players/{id} | o seu servidor | chave do app |
POST /api/v1/players/csv_export | o seu servidor | chave do app |
As duas primeiras são públicas de propósito: um aplicativo publicado é um segredo publicado, então a chave da API nunca precisa entrar no APK ou no IPA. O que protege essas rotas no lugar da chave:
- o
app_iddo corpo é conferido contra a base antes de qualquer escrita — inexistente responde400, aplicativo pausado responde403; - validação de forma campo a campo, antes de tocar o banco;
- corpo de no máximo 16 KB;
- teto de 120 requisições por minuto por IP de origem, com
Retry-After: 60.
As duas últimas exigem a chave do app no cabeçalho Authorization, e o
app_id da query tem de ser o mesmo app da chave — se não for, a resposta é
401, nunca 404 (a API não confirma a existência de recurso de outro
cliente).
Authorization: Basic pm_live_a1b2c3d4_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
1. Registrar um aparelho
POST https://api.pushmesh.io/api/v1/players
Chame a cada abertura do app. A rota faz um upsert: token novo cria, token
conhecido atualiza, e o player_id devolvido é sempre o mesmo.
Campos obrigatórios
| Campo | Tipo | Limite | O que faz |
|---|---|---|---|
app_id | UUID | — | O aplicativo. Precisa existir e estar ativo. |
device_type | inteiro | 0, 1, 5, 7 ou 8 | Plataforma: 0 = iOS, 1 = Android, 5 = Chrome/web, 7 = Safari, 8 = Firefox. Outro valor é recusado pelo nome. |
identifier | string | 1 a 194 bytes | O token de push do aparelho. É a chave de deduplicação. |
O teto de 194 bytes do identifier existe para gritar na porta: um token real
tem de 64 a ~170 caracteres, e um valor maior é quase sempre o payload inteiro
colado no campo errado. O erro diz quantos bytes chegaram.
Campos opcionais de identidade e conteúdo
| Campo | Tipo | Limite | O que faz |
|---|---|---|---|
external_user_id | string | 128 bytes | Seu identificador de pessoa (id de conta, e-mail hasheado…). Permite mirar por usuário em vez de por aparelho. Máximo 10 aparelhos por valor. |
app_version | string | — | Versão do seu app. Aceita também o nome legado game_version; se vierem os dois, app_version vence. |
device_model | string | — | Modelo do aparelho, como texto livre. |
tags | objeto JSON | 2 KB serializados | Pares chave/valor seus. Precisa ser objeto — lista ou string é recusada. |
language | string | 32 bytes | Código curto do idioma ("pt", "en"). |
timezone | inteiro | ±86400 | Offset do fuso em segundos (ex.: -10800). Nome de fuso aqui é erro. |
transporte | string | "fcm" ou "apns" | Por qual caminho o token deve ser entregue. Ausente: aparelho novo nasce fcm, aparelho existente mantém o que já tinha. |
Campos opcionais de perfil
São todos best-effort: valor estranho nunca derruba o registro, e campo ausente nunca apaga o que já estava gravado. Servem para você entender a base sem pedir nenhuma permissão nova ao usuário.
| Campo | Tipo | Observação |
|---|---|---|
device_os | string | Cortado em 64 bytes. |
fabricante | string | Cortado em 64 bytes. |
sdk_versao | string | Usado para saber quem já pode receber recursos novos. |
timezone_id | string | Aqui sim vai o nome do fuso. |
pais | string | Cortado em 64 bytes. |
net_type | string | Tipo de rede. |
carrier | string | Operadora. |
standby_bucket | string | Estado de economia de bateria do sistema. |
instalacao_id | string | Identificador estável da instalação. Ver abaixo — vale ouro. |
rooted | booleano | — |
bateria_irrestrita | booleano | — |
sessoes | inteiro | Contador de sessões. Nunca diminui: o servidor guarda o maior valor já visto. Negativo é tratado como ausente. |
notif_permissoes | inteiro | Máscara de bits da permissão de notificação. Negativo é tratado como ausente. |
Resposta
{ "success": true, "id": "01926f4c-7a1b-7c2d-8e3f-a1b2c3d4e5f6" }
A regra que derruba a primeira integração
No registro, campo omitido APAGA o valor gravado.
Vale para seis campos: external_user_id, app_version, device_model,
tags, language e timezone. O registro grava exatamente o retrato que você
mandou — inclusive o vazio.
O acidente clássico: o app manda external_user_id no login, e na abertura
seguinte registra sem ele. O vínculo some, e a base “perde” os usuários sem
nenhum erro aparecer.
| Grupo de campos | Campo ausente… |
|---|---|
external_user_id, app_version, device_model, tags, language, timezone | apaga o gravado |
transporte e todos os campos de perfil | preserva o gravado |
Duas formas de não cair nisso:
- no registro, mande sempre o retrato completo do aparelho (é o que o SDK faz);
- para mudar uma coisa só, use o
PUT— lá a regra é a oposta.
Registrar ressuscita token morto
Se o aparelho tinha sido marcado como inválido (o provedor rejeitou o token) e ele volta a se registrar com o mesmo token, ele volta a valer, e o motivo da invalidação é apagado. Quem espelha a nossa base do lado de lá precisa reler periodicamente — senão fica com um aparelho marcado como morto para sempre.
instalacao_id: o fim dos aparelhos fantasma
Reinstalar o app gera um token novo, e o token antigo continua parecendo vivo até algum disparo falhar nele. Resultado: uma base inflada de aparelhos inalcançáveis que você conta como se fossem gente.
Mandando instalacao_id — um identificador estável da instalação — o servidor
sabe na hora que é o mesmo aparelho e aposenta os tokens anteriores dele no
próprio registro, sem esperar disparo nenhum.
Aparelhos de teste
Um identifier que começa com test: cria um aparelho de sandbox.
Aparelho de sandbox nunca entra em público de disparo — nem quando o id dele
está explícito na lista — e não aparece na exportação. É por isso que ele volta
em invalid_player_ids no envio: não é defeito, é a proteção funcionando.
2. Login, logout e ajuste pontual
PUT https://api.pushmesh.io/api/v1/players/{player_id}
É a rota de vínculo pessoa ↔ aparelho.
# login
curl -X PUT https://api.pushmesh.io/api/v1/players/01926f4c-7a1b-7c2d-8e3f-a1b2c3d4e5f6 \
-H 'Content-Type: application/json' \
-d '{"app_id":"01926f3a-4b2c-7d8e-9f01-23456789abcd","external_user_id":"usuario-4711"}'
# logout
curl -X PUT https://api.pushmesh.io/api/v1/players/01926f4c-7a1b-7c2d-8e3f-a1b2c3d4e5f6 \
-H 'Content-Type: application/json' \
-d '{"app_id":"01926f3a-4b2c-7d8e-9f01-23456789abcd","external_user_id":null}'
{ "success": true }
| Campo | Tipo | Obrigatório | O que faz |
|---|---|---|---|
app_id | UUID | sim | O aplicativo do aparelho. |
external_user_id | string, null ou ausente | não | Três estados — ver abaixo. |
tags | objeto JSON | não | Até 2 KB serializados. |
app_version | string | não | Aceita o nome legado game_version. |
transporte | string | não | "fcm" ou "apns". |
Os três estados do external_user_id
| No corpo | Significado |
|---|---|
| campo ausente | não mexe no vínculo |
"usuario-4711" | login: define o vínculo |
null explícito | logout: limpa o vínculo |
O logout só acontece com null escrito. Omitir o campo não desloga
ninguém — e essa é a segunda causa mais comum de “a pessoa saiu da conta e
continuou recebendo”.
Aqui, campo ausente NÃO apaga
No PUT, app_version, tags e transporte preservam o valor gravado quando
não vêm no corpo. É a regra oposta à do registro: leia a do verbo que você está
usando, não a do outro.
3. Quando o mesmo aparelho troca de usuário
É o ponto onde mais integração se machuca, e vale escrever a receita inteira.
Um aparelho é um objeto físico; um external_user_id é uma pessoa. O aparelho
sobrevive à troca de pessoa — e se você não contar isso ao servidor, a pessoa
nova recebe as mensagens da pessoa antiga.
A receita:
- No login, chame
PUTcomexternal_user_idigual ao id da pessoa. - No logout, chame
PUTcomexternal_user_id: null. Explicitamente. - A cada abertura do app, ao registrar, mande de novo o
external_user_idse a pessoa continua logada. Registro sem ele apaga o vínculo (seção 1). - Não invente um
external_user_idpor aparelho. Ele existe para juntar os aparelhos de uma mesma pessoa; um valor por aparelho não junta nada e ainda consome o teto.
O teto: no máximo 10 aparelhos por external_user_id. Ele é cobrado no
registro e também no PUT — mas só quando o vínculo muda. Reenviar o mesmo
valor que o aparelho já tem passa sempre, então um SDK que reafirma o login a
cada abertura nunca esbarra no teto. Chegando ao 11º aparelho, a resposta é
400 máximo 10 devices por external_user_id.
4. Consultar um aparelho
GET https://api.pushmesh.io/api/v1/players/{player_id}?app_id=…
Do seu servidor, com a chave do app. É por aqui que a sua integração decide se vale a pena gastar um disparo com aquele aparelho.
curl 'https://api.pushmesh.io/api/v1/players/01926f4c-7a1b-7c2d-8e3f-a1b2c3d4e5f6?app_id=01926f3a-4b2c-7d8e-9f01-23456789abcd' \
-H 'Authorization: Basic pm_live_a1b2c3d4_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
{
"id": "01926f4c-7a1b-7c2d-8e3f-a1b2c3d4e5f6",
"identifier": "TOKEN-DE-PUSH-DO-APARELHO",
"device_type": 1,
"notification_types": 1,
"invalid_identifier": false,
"external_user_id": "usuario-4711",
"app_version": "3.2.0",
"device_model": "Modelo X",
"tags": { "plano": "premium" },
"last_active": 1756300000,
"created_at": 1756200000,
"playtime": 0,
"session_count": 0,
"device_os": null,
"language": "pt",
"timezone": -10800
}
| Campo | O que é |
|---|---|
identifier | O token de push, como foi gravado. |
notification_types | Estado de inscrição — ver a tabela abaixo. |
invalid_identifier | true quando o token está morto (o provedor rejeitou ou ele foi substituído). |
last_active | Último sinal de vida, em epoch de segundos. |
created_at | Primeiro registro, em epoch de segundos. |
tags | Volta {} quando não há tags, nunca null. |
playtime, session_count, device_os | Constantes de formato: sempre 0, 0 e null. Existem para compatibilidade de shape, não são medições. Não monte métrica em cima deles. |
notification_types, sem maquiagem
O contrato do campo tem quatro estados:
| Estado do aparelho | notification_types |
|---|---|
| válido e inscrito | 1 |
| desinscreveu ou bloqueou | -2 |
| nunca concedeu permissão | 0 |
| token morto | último valor conhecido, com invalid_identifier: true |
E a parte honesta: hoje nenhuma rota desta API escreve os estados 0 e
-2. Um aparelho registrado nasce inscrito e assim permanece; o que muda ao
longo da vida dele é o invalid_identifier. Se você quer uma pergunta única e
verdadeira para “consigo alcançar este aparelho?”, a resposta é
invalid_identifier === false.
Se o seu app sabe que o usuário desligou as notificações no sistema, essa
informação chega ao servidor pelo campo de perfil notif_permissoes do
registro — não pelo notification_types.
5. Exportar a base
POST https://api.pushmesh.io/api/v1/players/csv_export?app_id=…
A foto de quem está vivo, em CSV comprimido, para a sua integração conferir a base antes de gastar disparo.
curl -X POST 'https://api.pushmesh.io/api/v1/players/csv_export?app_id=01926f3a-4b2c-7d8e-9f01-23456789abcd' \
-H 'Authorization: Basic pm_live_a1b2c3d4_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
{
"csv_file_url": "https://…/exports/01926f3a-…/players-20260827T143000Z-…csv.gz",
"pm_pronto_em_s_estimado": 30,
"pm_validade_h": 72
}
A resposta é imediata e o arquivo é gerado em segundo plano. O laço do seu
lado é simples: tente baixar a URL; enquanto o arquivo não estiver pronto ela
responde 404, e responde 200 quando estiver.
O arquivo é um CSV comprimido com cinco colunas:
id,external_user_id,notification_types,invalid_identifier,last_active
last_activeé o último sinal de vida real do aparelho (sessão do app ou recibo de entrega), em epoch de segundos — é com ele que você pula aparelho inativo;- aparelhos de sandbox ficam de fora;
- vírgulas e aspas dentro de um campo são escapadas no padrão CSV, então qualquer parser de mercado lê sem tratamento especial.
Três coisas a saber antes de usar em produção:
- A URL é a credencial. O arquivo carrega
external_user_id, que na maior parte das integrações identifica uma pessoa. O que protege o arquivo é o nome ser impossível de adivinhar. URL em log, em ticket ou em chat é vazamento. pm_validade_h: 72é janela de contrato. Baixe o arquivo dentro dela, guarde do seu lado e não use a URL como armazenamento permanente.- Um export por segundo, por aplicativo. Estourou, a resposta é
429— e a orientação é reusar a URL anterior, que continua valendo, em vez de pedir outro. Este429não traz o cabeçalhoRetry-After; espere um segundo.
6. Como um aparelho sai da base
Não existe rota pública para apagar um aparelho, e a ausência é deliberada: as rotas de aparelho são públicas, e uma rota pública capaz de apagar seria uma rota pública capaz de esvaziar a sua base.
O que de fato tira um aparelho do alcance:
| Acontecimento | Efeito |
|---|---|
| O provedor rejeita o token no envio | O aparelho passa a invalid_identifier: true e sai de todos os públicos. |
O app é reinstalado (com instalacao_id) | O token anterior é aposentado no próprio registro do token novo. |
| Você apaga aparelhos pelo painel | O aparelho e o histórico de entregas dele são removidos juntos, na mesma transação. |
Vale saber: apagar não é banir. Um aparelho vivo que você apagou volta a se registrar sozinho no próximo uso do app. Para parar de falar com alguém, a ferramenta certa é o vínculo (logout) ou a sua própria regra de público, não a exclusão.
Se o que você precisa é atender a um pedido de exclusão de um titular — e saber quanto tempo cada camada guarda o quê —, a receita completa está em Dados, retenção e privacidade.
Erros
Todo erro sai no mesmo envelope:
{
"errors": ["requisição inválida: tags deve ser um objeto JSON"],
"explain": {
"causa": "tags deve ser um objeto JSON",
"como_corrigir": "corrija o payload; retry cego não resolve",
"request_id": "01926f5a-0000-7000-8000-000000000000"
}
}
Toda resposta — de sucesso ou de erro — traz o cabeçalho X-Pm-Request-Id. É o
fio que liga a sua chamada ao nosso registro de serviço; cite-o no suporte.
| Código | Quando acontece nestas rotas |
|---|---|
400 | JSON inválido; device_type ausente ou não suportado; identifier vazio ou acima de 194 bytes; tags que não é objeto ou passa de 2 KB; language acima de 32 bytes; timezone que não é inteiro ou está fora de ±86400; transporte diferente de fcm/apns; external_user_id acima de 128 bytes; app_id inexistente; 11º aparelho no mesmo external_user_id; app_id ausente na query das rotas de servidor. |
401 | Cabeçalho Authorization ausente ou em formato desconhecido; chave inexistente, revogada ou expirada; app_id da query que não é o app da chave. |
403 | Aplicativo pausado. |
404 | player_id inexistente neste aplicativo. |
413 | Corpo acima de 16 KB. |
429 | Mais de 120 requisições por minuto do mesmo IP (com Retry-After: 60); mais de 50.000 registros por hora no mesmo aplicativo; mais de um export por segundo. |
503 | Banco indisponível; armazenamento de exportação não configurado no ambiente. |
Ramifique sempre pelo código HTTP. Os textos de causa e
como_corrigir são prosa para humano, saem em português e podem mudar.
Limites, em números
| Limite | Valor | Onde vale |
|---|---|---|
| Corpo da requisição | 16 KB | rotas chamadas pelo aparelho |
identifier | 194 bytes | registro |
external_user_id | 128 bytes | registro e login |
tags (JSON serializado) | 2 KB | registro e atualização |
language | 32 bytes | registro |
timezone | ±86.400 segundos | registro |
Aparelhos por external_user_id | 10 | registro e login |
| Registros por hora, por aplicativo | 50.000 | registro |
| Requisições por minuto, por IP | 120 | rotas chamadas pelo aparelho |
| Exportações | 1 por segundo, por aplicativo | export |
O teto por IP é contado por instância do serviço e depende do IP de origem repassado pela borda. Trate-o como uma rede de proteção contra laço maluco, não como um número para calibrar retry fino.