Atualizado: 15 de setembro de 2026
O expo-notifications é a biblioteca oficial do Expo para enviar e receber push notifications no React Native. Ela usa o Expo Push Service, que encaminha as mensagens para FCM v1 no Android e APNs no iOS. Em 2026, com o Expo SDK 54, o fluxo padrão exige um development build (o Expo Go deixou de receber pushes remotas desde o SDK 53), permissões runtime no Android 13+ e canais de notificação no Android 8+. Este guia mostra a configuração completa: credenciais, obtenção do token, envio pelo backend com expo-server-sdk, tratamento em foreground/background e as principais armadilhas. Honestamente, boa parte do que quebra em produção não é o código, e sim a configuração de credenciais no EAS.
Pré-requisitos e limites do Expo Go em 2026
Antes de tocar em uma linha de código, entenda o que mudou. Desde o Expo SDK 53 o aplicativo Expo Go deixou de ser um cliente válido para push notifications remotas. Se você executar expo start e abrir no Expo Go, o getExpoPushTokenAsync devolve o erro "Must use physical device and development build to receive push notifications". A razão é simples: para autenticar contra APNs/FCM v1 o binário precisa carregar credenciais do seu projeto, e o Expo Go compartilha um bundle genérico.
Na prática, você vai precisar de três coisas para testar pushes em 2026:
- Um development build gerado com
eas build --profile development(ou um build de produção). Simulador iOS não recebe remotas, precisa de dispositivo físico. O emulador Android com Google Play Services até recebe, mas o dispositivo real elimina qualquer variável. - Credenciais configuradas no EAS Credentials: chave de APNs
.p8para iOS e arquivogoogle-services.jsoncom FCM v1 habilitado para Android. - Um projectId vinculado à sua conta Expo. O token push v2 tem o formato
ExponentPushToken[xxxxxxxx]e só é emitido segetExpoPushTokenAsync({ projectId })conseguir resolver o project ID a partir deapp.json(extra.eas.projectId).
Você também precisa do Node 20+ se for enviar mensagens do backend com expo-server-sdk. Para uma visão geral de builds e distribuição, veja nosso guia de EAS Update e OTA no React Native e Expo. O mesmo pipeline EAS gera os builds que você usará para testar as notificações.
Instalação e configuração do expo-notifications
Instale o pacote com npx expo. Assim ele já resolve a versão compatível com o seu SDK:
npx expo install expo-notifications expo-device expo-constants
No Expo SDK 54 (setembro de 2026) a versão instalada é expo-notifications ~0.32.x. O pacote expo-device permite verificar se estamos em um dispositivo físico, e expo-constants expõe o projectId a partir de app.json.
Em seguida, declare o plugin em app.json. É ele quem injeta o entitlement de Push no iOS, o serviço FCM no AndroidManifest.xml e o ícone monocromático do Android:
{
"expo": {
"name": "MinhaApp",
"slug": "minha-app",
"plugins": [
[
"expo-notifications",
{
"icon": "./assets/notification-icon.png",
"color": "#0F172A",
"defaultChannel": "default",
"sounds": ["./assets/notification.wav"]
}
]
],
"ios": {
"bundleIdentifier": "com.suaempresa.minhaapp",
"infoPlist": {
"UIBackgroundModes": ["remote-notification"]
}
},
"android": {
"package": "com.suaempresa.minhaapp",
"googleServicesFile": "./google-services.json"
}
}
}
O ícone monocromático (icon) é obrigatório no Android 8+. Sem ele, a notificação aparece como um quadrado branco (aprendi isso da pior forma, num TestFlight de sexta à noite). Recomenda-se um PNG 96x96 transparente com forma preenchida em branco.
Configurando FCM v1 no Android e APNs no iOS
Esse é o passo em que mais projetos travam. O Firebase Cloud Messaging Legacy HTTP API foi descontinuado em 20 de junho de 2024. Qualquer token enviado hoje sem FCM v1 recebe MismatchSenderId ou InvalidCredentials. O Expo Push Service já migrou para FCM v1 desde o SDK 51, mas você continua responsável por subir as credenciais corretas.
Passo a passo Android (FCM v1)
- No Firebase Console, crie ou selecione o projeto. Adicione um app Android usando o mesmo
packagedeclarado emapp.json. - Faça o download do
google-services.jsone salve na raiz do projeto (mesmo diretório deapp.json). Adicione o caminho emexpo.android.googleServicesFile. - Em Configurações do projeto → Contas de serviço → Firebase Admin SDK, gere uma nova chave privada. Isso baixa um JSON. Não commite no repositório.
- Suba a chave para o EAS:
eas credentials→ Android → "Manage your Google Service Account Key for Push Notifications (FCM V1)" → "Upload a new service account key".
Passo a passo iOS (APNs)
- No Apple Developer Portal, crie uma chave APNs
.p8com o serviço Apple Push Notifications service (APNs) habilitado. - Anote o Key ID e o Team ID. A chave
.p8só pode ser baixada uma vez. - Ative o capability Push Notifications no App ID (o EAS faz isso automaticamente quando o plugin está declarado).
- Rode
eas credentials→ iOS → "Manage push notifications" → "Set up your project to use Push Notifications" para fazer upload da chave.
Solicitando permissões e obtendo o Expo push token
O código de registro precisa fazer três coisas em ordem: verificar se estamos em dispositivo físico, pedir permissão ao usuário e chamar getExpoPushTokenAsync. O snippet abaixo é o padrão que uso em produção. Copie e cole:
import * as Device from 'expo-device';
import * as Notifications from 'expo-notifications';
import Constants from 'expo-constants';
import { Platform } from 'react-native';
export async function registerForPushNotificationsAsync(): Promise<string | null> {
if (!Device.isDevice) {
console.warn('Push notifications requerem dispositivo físico.');
return null;
}
// Android 8+ exige um canal padrão antes do primeiro token.
if (Platform.OS === 'android') {
await Notifications.setNotificationChannelAsync('default', {
name: 'Padrão',
importance: Notifications.AndroidImportance.DEFAULT,
vibrationPattern: [0, 250, 250, 250],
lightColor: '#0F172A',
});
}
const { status: existing } = await Notifications.getPermissionsAsync();
let finalStatus = existing;
if (existing !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync({
ios: {
allowAlert: true,
allowBadge: true,
allowSound: true,
allowProvisional: false,
},
});
finalStatus = status;
}
if (finalStatus !== 'granted') {
console.warn('Usuário negou permissão de notificações.');
return null;
}
const projectId =
Constants.expoConfig?.extra?.eas?.projectId ??
Constants.easConfig?.projectId;
if (!projectId) {
throw new Error('projectId ausente em app.json (extra.eas.projectId).');
}
const { data: token } = await Notifications.getExpoPushTokenAsync({
projectId,
});
return token; // ExponentPushToken[xxxxxxxxxxxx]
}
No Android 13+ (API 33) a chamada requestPermissionsAsync gera o prompt runtime da permissão POST_NOTIFICATIONS. Se o usuário negar, o token continua sendo emitido, porém nenhuma notificação será exibida. Sempre trate finalStatus !== 'granted' como "desabilitado". No iOS, o Provisional Authorization (allowProvisional: true) permite entregar notificações silenciosamente ao Notification Center sem alerta. É útil para o primeiro contato, mas atrapalha engagement se abusado.
Canais de notificação no Android 8+
Canais são obrigatórios no Android 8.0 (API 26) e superiores. Cada canal representa uma categoria de notificação (por exemplo, "Mensagens", "Promoções", "Alertas críticos") e o usuário pode desabilitar canais individualmente nas configurações do sistema. As propriedades de um canal (som, vibração, importância, badge) não podem ser modificadas depois de criado. Para alterar, você precisa deletar o canal e criar outro com nome novo.
await Notifications.setNotificationChannelAsync('messages', {
name: 'Mensagens',
description: 'Mensagens diretas de outros usuários.',
importance: Notifications.AndroidImportance.HIGH,
sound: 'notification.wav',
vibrationPattern: [0, 250, 250, 250],
lightColor: '#3B82F6',
lockscreenVisibility: Notifications.AndroidNotificationVisibility.PUBLIC,
bypassDnd: false,
});
await Notifications.setNotificationChannelAsync('promotions', {
name: 'Promoções',
importance: Notifications.AndroidImportance.LOW,
showBadge: false,
});
Ao enviar do backend, especifique o canal com o campo channelId no payload. Assim o usuário consegue silenciar promoções sem perder mensagens críticas. Se você usa canais dinâmicos por conversa (comum em apps de mensageria), crie o canal on-demand e reutilize o mesmo identifier em pushes subsequentes. O Android agrupa automaticamente.
Enviando push do backend com expo-server-sdk
O envio pelo Expo Push Service é feito via HTTP POST para https://exp.host/--/api/v2/push/send. Você pode chamar direto com fetch, mas o expo-server-sdk-node cuida de chunking (limite de 100 mensagens por request), backoff em 429 e leitura de tickets/receipts.
// server/pushSender.ts
import { Expo, ExpoPushMessage, ExpoPushTicket } from 'expo-server-sdk';
const expo = new Expo({
accessToken: process.env.EXPO_ACCESS_TOKEN, // opcional, mas recomendado
useFcmV1: true, // default em 2026
});
export async function sendPush(
tokens: string[],
title: string,
body: string,
data: Record<string, unknown> = {}
): Promise<ExpoPushTicket[]> {
const messages: ExpoPushMessage[] = tokens
.filter((t) => Expo.isExpoPushToken(t))
.map((to) => ({
to,
sound: 'default',
title,
body,
data,
channelId: 'messages',
priority: 'high',
ttl: 3600,
}));
const chunks = expo.chunkPushNotifications(messages);
const tickets: ExpoPushTicket[] = [];
for (const chunk of chunks) {
try {
const receipts = await expo.sendPushNotificationsAsync(chunk);
tickets.push(...receipts);
} catch (err) {
console.error('Falha ao enviar chunk:', err);
}
}
return tickets;
}
Depois de 15 minutos, consulte os receipts pelo ticketId. É lá que erros como DeviceNotRegistered (desinstalado), MessageTooBig (payload > 4KB) e MessageRateExceeded aparecem:
const ticketIds = tickets
.filter((t): t is Extract<ExpoPushTicket, { status: 'ok' }> => t.status === 'ok')
.map((t) => t.id);
const receiptChunks = expo.chunkPushNotificationReceiptIds(ticketIds);
for (const chunk of receiptChunks) {
const receipts = await expo.getPushNotificationReceiptsAsync(chunk);
for (const [id, receipt] of Object.entries(receipts)) {
if (receipt.status === 'error') {
if (receipt.details?.error === 'DeviceNotRegistered') {
await disableTokenInDb(id); // remova do seu DB
}
}
}
}
Tratando notificações em foreground e background
Diferente do que muitos esperam, notificações não são exibidas automaticamente quando o app está em foreground. Você precisa registrar um handler que decide o que fazer com o payload recebido. Essa chamada normalmente vai em App.tsx ou em um _layout.tsx raiz do Expo Router:
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowBanner: true,
shouldShowList: true,
shouldPlaySound: true,
shouldSetBadge: false,
}),
});
Os campos shouldShowBanner e shouldShowList substituíram o antigo shouldShowAlert a partir do expo-notifications ~0.30 (SDK 53). O banner é o pop-up transitório, e a list é o Notification Center persistente. No Android, essas flags viram um heads-up se a importância do canal for HIGH.
Para reagir a taps do usuário (abrir uma tela específica quando ele toca na notificação), combine addNotificationResponseReceivedListener com o Expo Router:
import { useEffect } from 'react';
import { router } from 'expo-router';
import * as Notifications from 'expo-notifications';
export function useNotificationTap() {
useEffect(() => {
const sub = Notifications.addNotificationResponseReceivedListener((response) => {
const data = response.notification.request.content.data as { screen?: string; id?: string };
if (data?.screen === 'chat' && data.id) {
router.push(`/chat/${data.id}`);
}
});
return () => sub.remove();
}, []);
}
Para background handling puro no Android (executar código quando chega uma data-only message sem UI), registre uma task com TaskManager, a mesma abordagem descrita no nosso guia de tarefas em segundo plano com expo-background-task. No iOS, background remote notifications exigem content-available: 1 no payload APNs e a flag UIBackgroundModes: ["remote-notification"]. Vale lembrar que o iOS ainda pode adiar ou descartar a entrega se o dispositivo estiver no Low Power Mode.
Notificações locais e agendadas
Nem toda notificação precisa passar por um servidor. Notifications.scheduleNotificationAsync agenda notificações locais (funciona em Expo Go também) para lembretes, timers e reengajamento offline:
await Notifications.scheduleNotificationAsync({
content: {
title: 'Hora de treinar',
body: 'Você marcou 30 minutos de exercício para agora.',
sound: 'default',
data: { screen: 'workout' },
},
trigger: {
type: Notifications.SchedulableTriggerInputTypes.CALENDAR,
hour: 18,
minute: 30,
repeats: true,
},
});
No SDK 54, o formato do trigger mudou. Em vez de passar objetos crus com hour/minute no root, você especifica um type discriminado (CALENDAR, TIME_INTERVAL, DATE, DAILY, WEEKLY, MONTHLY, YEARLY). Migrações do SDK 51/52 costumam quebrar aqui, então confira a coluna "Trigger" do changelog de expo-notifications.
Para categorias com ações (botões inline como "Responder" ou "Arquivar"), registre categorias com setNotificationCategoryAsync e referencie o categoryIdentifier no payload. É a mesma API usada pelo Expo Router quando você combina push com deep links protegidos, combinação natural com a arquitetura descrita em nossa migração para a Nova Arquitetura do React Native, onde os TurboModules do expo-notifications reduzem o overhead da bridge legada em cerca de 30%.
Por que minhas notificações não chegam?
Sete causas cobrem 90% dos casos de push que "não chegam". Percorra a lista antes de abrir issue no repositório. Eu mesmo já perdi uma tarde inteira num app de produção por causa da linha 3 dessa tabela (chave APNs de sandbox subida como produção).
| Sintoma | Causa provável | Como corrigir |
|---|---|---|
Erro Must use physical device... | Rodando em Expo Go ou simulador iOS | Gerar development build com eas build --profile development |
| Token gerado, mas nenhum push chega no Android | Permissão POST_NOTIFICATIONS negada ou canal desabilitado | Verificar Settings do sistema; recriar canal com nome novo |
Receipt retorna MismatchSenderId | google-services.json antigo ou de outro projeto Firebase | Baixar novo google-services.json e rodar eas build novamente |
| Push chega no dev build mas some em produção | Chave APNs de sandbox usada em Release | Reenviar .p8 no EAS Credentials, marcar "Production" |
| Notificação silenciosa (sem som/banner) | Handler não chamado em foreground | Registrar setNotificationHandler antes do primeiro render |
Erro DeviceNotRegistered | App desinstalado ou token expirado | Remover token do banco e reemitir no próximo login |
| Notificação chega no debug mas não em release | ProGuard removendo classes do FCM | Manter regras oficiais do expo-notifications no proguard-rules.pro |
Um debug rápido: envie um push de teste pela Expo Push Tool, cole o token, envie e observe o response. Se retornar ticket.status === 'ok' mas nenhuma notificação aparece, o problema está no cliente (canal/permissão). Se retornar erro, o problema está nas credenciais.
Perguntas frequentes
Push notifications funcionam no Expo Go em 2026?
Não. Desde o Expo SDK 53 (2025) o Expo Go deixou de emitir tokens Expo Push válidos para mensagens remotas. Notificações locais e agendadas (scheduleNotificationAsync) continuam funcionando no Expo Go, mas para push remoto você precisa de um development build gerado via eas build --profile development.
Preciso migrar para FCM v1 se uso o Expo Push Service?
O Expo já usa FCM v1 automaticamente desde o SDK 51. Você só precisa garantir que o google-services.json foi gerado a partir de um projeto Firebase com a API HTTP v1 habilitada, e que a Service Account key foi feita upload no EAS Credentials. O FCM Legacy foi desligado em 20 de junho de 2024.
Qual a diferença entre notificações locais e remotas?
Locais são agendadas pelo próprio dispositivo (sem servidor) via scheduleNotificationAsync, ótimas para lembretes offline. Remotas partem do seu backend, passam pelo Expo Push Service e chegam via APNs/FCM v1. São usadas para chats, alertas e conteúdo dinâmico.
Como pedir permissão de notificação no Android 13?
A partir do Android 13 (API 33) você precisa chamar Notifications.requestPermissionsAsync() em runtime. A instalação sozinha não concede permissão. O prompt do sistema aparece apenas uma vez; se o usuário negar, direcione-o para as configurações do sistema com Linking.openSettings().
Qual o limite de tamanho do payload no expo-notifications?
O payload total (incluindo title, body e data) não pode exceder 4KB por mensagem, limite compartilhado por APNs e FCM v1. Você pode enviar até 100 mensagens por request ao Expo Push Service; use expo.chunkPushNotifications para dividir automaticamente.
Como testar push notifications sem publicar o app?
Use a Expo Push Tool no navegador: cole o ExponentPushToken[...] gerado pelo dispositivo, escreva título/corpo e envie. Alternativamente, execute curl -X POST https://exp.host/--/api/v2/push/send com o mesmo payload JSON.


