Documentation
Intégrer Pushproof
Pushproof mesure combien de notifications push arrivent réellement sur l'appareil de vos utilisateurs. Ce guide couvre l'intégration en trois étapes : l'application mobile, le serveur qui envoie les notifications (FCM/APNs), puis l'API Pushproof pour consulter les statistiques.
Vue d'ensemble
FCM et APNs confirment que le message a été pris en charge par Apple ou Google — pas qu'il est bien arrivé sur le téléphone. Pushproof mesure la réception réelle : le SDK installé dans votre app détecte l'arrivée et envoie un accusé. Votre serveur déclare les envois ; le dashboard affiche le rapport entre notifications envoyées et reçues.
- SDK (open-source) — s'installe dans votre application. Vous continuez d'envoyer les push via FCM/APNs.
- Service Pushproof — collecte les accusés, calcule les statistiques et alimente le dashboard.
Premiers pas
- Créez un compte sur app.pushproof.dev.
- Ajoutez votre application (nom + identifiant) — iOS : Bundle Identifier ; Android :
applicationIddansbuild.gradle. - Copiez vos clés API depuis le dashboard (voir ci-dessous).
- Transmettez la Partie 1 à votre équipe mobile, puis la Partie 2 à l'équipe qui gère l'envoi des notifications.
Vos clés API
| Clé | En-tête HTTP | Où l'utiliser |
|---|---|---|
ingest_key publique |
X-Ingest-Key: pk_ingest_… |
Dans l'app mobile (SDK). Envoie des accusés uniquement. |
read_key secrète |
Authorization: Bearer sk_read_… |
Sur votre backend ou scripts. Jamais dans l'app. |
read_key expose toutes vos stats : gardez-la côté serveur uniquement.
Partie 1Application mobile
Configuration du SDK dans votre application (Capacitor, iOS et Android) pour enregistrer les notifications reçues.
Installation
npm install @pushproof/capacitor@1.3.4
npx cap sync ios
npx cap sync android
Le plugin enveloppe le SDK natif iOS et Android. Vous conservez FCM/APNs pour l'envoi.
Capacitor 8 + Swift Package Manager
Sous Capacitor 8, les plugins iOS passent par Swift Package Manager. Après l'installation du package,
npx cap sync ios relie automatiquement le plugin Pushproof à votre projet Xcode.
- Installez
@pushproof/capacitor@1.3.4ou plus récent (voir Installation). - Lancez
npx cap sync ios. - Ouvrez le projet dans Xcode, faites un Clean Build et testez sur un appareil réel.
En cas d'avertissement à la synchronisation
Si tout se passe bien, vous n'avez rien d'autre à faire. En revanche, si npx cap sync ios
affiche un message du type :
[warn] @pushproof/capacitor does not have a Package.swift
le plugin n'a pas été relié automatiquement. Ajoutez-le manuellement dans
ios/App/CapApp-SPM/Package.swift :
.package(name: "PushproofCapacitor", path: "../../../node_modules/@pushproof/capacitor"),
// …
.product(name: "PushproofCapacitor", package: "PushproofCapacitor"),
Relancez npx cap sync ios, puis reconstruisez l'app dans Xcode.
Configuration
Appelez configure() au démarrage de l'app, avant tout envoi de push de test :
import { Pushproof } from '@pushproof/capacitor';
await Pushproof.configure({
ingestUrl: 'https://api.pushproof.dev/v1/receipts',
ingestKey: 'pk_ingest_…',
appGroup: 'group.com.example.app', // iOS uniquement — identique à la NSE
// displayNotification: true, // Android — défaut
});
configure() persiste la config dans l'App Group pour que la NSE (processus séparé) puisse envoyer des accusés.
Pushproof ne gère pas l'enregistrement des tokens FCM : continuez avec
@capacitor/push-notifications ou votre stack existante.
Au login / logout (plan Pro) :
await Pushproof.identify({ userId: 'usr_opaque' });
await Pushproof.clearIdentity();
Extension iOS (NSE)
Sur iOS, la capture en arrière-plan passe par une Notification Service Extension
réveillée par iOS avant l'affichage du push, si le payload contient
mutable-content: 1 (voir Partie 2).
- Xcode → File → New → Target → Notification Service Extension, nommée
PushproofNotificationExtension(recommandé). - Ajoutez le package Swift
https://github.com/csurbier/pushproofsdk(v1.3.3+) :- PushproofCore → cible App
- PushproofNSE (produit SPM, pas votre cible) → cible extension
- Remplacez
NotificationService.swiftpar :import PushproofNSE class NotificationService: PushproofNotificationService {} - App Groups : activez la même capability sur l'App et la NSE (ex.
group.com.example.app). - Dans le Info.plist de la NSE, à la racine (pas dans
NSExtension) :<key>PushproofAppGroup</key> <string>group.com.example.app</string> - Commitez le dossier
ios/— la NSE vit dans votre projet Xcode.
PushproofNSE : c'est le nom du produit SPM. Si le nom est déjà pris, définissez PRODUCT_MODULE_NAME = PushproofNotificationExt sur la cible extension.
Configuration Android
Ajoutez le service Pushproof dans android/app/src/main/AndroidManifest.xml,
à l'intérieur de la balise <application> :
<service
android:name="dev.pushproof.PushproofMessagingService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
Si vous avez déjà un service FCM
Ne déclarez pas un second service. Appelez plutôt
PushproofMessagingService.handle(this, message) depuis votre
onMessageReceived existant.
Affichage des notifications sur Android
Sur Android, votre serveur doit envoyer le titre et le texte dans le champ data
du message FCM (détails dans la Partie 2), et non dans le bloc
notification de Firebase. Ce format permet au SDK Pushproof d'enregistrer la réception
et d'afficher la notification.
Par défaut, displayNotification: true dans configure(), le SDK affiche
la notification à l'utilisateur selon l'état de l'app :
- App ouverte — Capacitor déclenche
pushNotificationReceived: affichez le message dans l'app (toast, modal, etc.) comme vous le faites déjà. - App en arrière-plan ou fermée — le SDK affiche une notification système à partir de
data.titleetdata.bodyreçus du serveur.
Pour gérer l'affichage vous-même en arrière-plan, passez displayNotification: false dans configure().
App ouverte : recordDelivery()
PushNotifications.addListener('pushNotificationReceived', (notif) => {
const notifId = notif.data?.notif_id;
if (notifId) {
Pushproof.recordDelivery({ notifId, campaign: notif.data?.campaign });
}
});
Les doublons sont ignorés pour un même couple (notif_id, appareil). Sur Android, cet appel est optionnel.
Au retour en avant-plan, vous pouvez rejouer les accusés mis en file par la NSE :
const { receipts } = await Pushproof.getPendingReceipts();
for (const r of receipts) {
await Pushproof.recordDelivery({ notifId: r.notifId, campaign: r.campaign });
}
Quand l'utilisateur ouvre la notification
L'événement pushNotificationActionPerformed se déclenche quand l'utilisateur tape sur la notification.
C'est l'endroit prévu pour ouvrir un écran, afficher un message ou lancer une action dans l'app.
- Traitez le tap même si la notification a déjà été reçue alors que l'app était ouverte — l'utilisateur choisit explicitement d'ouvrir la notification.
- Si vous avez une logique qui évite d'afficher deux fois le même contenu, réinitialisez-la au tap.
- Récupérez
notif_id,campaignet vos propres champs dansnotification.data(ignorez les clés systèmeaps,gcm.*).
Squelette complet d'un AppComponent : configuration au démarrage, identité au
login/logout, et les deux listeners routés vers un même onPush()
qui (1) accuse la réception et (2) déclenche une action métier — ici, ouvrir une popup de notation.
// app.component.ts (Ionic / Angular)
import { Capacitor } from '@capacitor/core';
import { PushNotifications } from '@capacitor/push-notifications';
import { Pushproof } from '@pushproof/capacitor';
export class AppComponent {
async ngOnInit() {
if (!Capacitor.isNativePlatform()) return;
// 1) Configurer une fois au démarrage
await Pushproof.configure({
ingestUrl: 'https://api.pushproof.dev/v1/receipts',
ingestKey: 'pk_ingest_…',
...(Capacitor.getPlatform() === 'ios'
? { appGroup: 'group.com.example.app' } : {}),
});
await this.flushPending(); // iOS : renvoyer les accusés mis en file par la NSE
// 2) Écouter les push (les deux passent par onPush)
PushNotifications.addListener('pushNotificationReceived',
(n) => this.onPush(n.data)); // app ouverte
PushNotifications.addListener('pushNotificationActionPerformed',
(a) => this.onPush(a.notification.data)); // tap utilisateur
await PushNotifications.register();
}
// 3) Depuis votre flux d'authentification
async onLogin(userId: string) { await Pushproof.identify({ userId }); }
async onLogout() { await Pushproof.clearIdentity(); }
// 4) Routeur d'action
onPush(data: any) {
const notifId = data?.notif_id ?? data?.notifId;
if (notifId) Pushproof.recordDelivery({ notifId, campaign: data?.campaign });
if (data?.notationRefCommerce) {
this.openRatingPopup(data.notationRefCommerce, data?.nomCommerce);
}
// … autres cas : data?.newProduct, data?.pushCible, etc.
}
private async flushPending() {
if (Capacitor.getPlatform() !== 'ios') return;
const { receipts } = await Pushproof.getPendingReceipts();
for (const r of receipts) {
await Pushproof.recordDelivery({ notifId: r.notifId, campaign: r.campaign });
}
}
}
recordDelivery() sur les deux listeners est volontaire (réception + tap) — le
serveur déduplique sur (notif_id, appareil). Appelez onLogin() à la
connexion, onLogout() à la déconnexion, et rappelez flushPending()
au retour en avant-plan (iOS).
Natif pur (sans Capacitor)
SDK open-source : github.com/csurbier/pushproofsdk. Même protocole, mêmes endpoints. Suivez les sections NSE et backend.
// iOS — PushproofCore
Pushproof.shared.configure(
ingestUrl: "https://api.pushproof.dev/v1/receipts",
ingestKey: "pk_ingest_…",
appGroup: "group.com.example.app"
)
// Android — JitPack com.github.csurbier:pushproofsdk:1.3.3
PushproofCore.configure(context, "https://api.pushproof.dev/v1/receipts", "pk_ingest_…")
Partie 2Serveur d'envoi (FCM / APNs)
Chaque notification envoyée doit contenir certains champs et être déclarée à Pushproof. Cette section concerne le serveur qui envoie les push — pas l'API de consultation des statistiques (Partie 3).
Flux d'envoi
- Générer un
notif_id(UUID) par notification. - Injecter
notif_id(+ optionnelcampaign) dans le payload FCM/APNs. - Envoyer via FCM (iOS et Android avec des règles différentes — voir ci-dessous).
- Déclarer l'envoi via
POST /v1/sent(mêmecampaignsi utilisé). - Les appareils envoient les accusés via le SDK →
POST /v1/receipts(automatique).
Payload commun
notif_id— UUID, obligatoire. Le SDK le relit et ne le génère jamais.campaign— libellé optionnel, identique à celui dePOST /v1/sent.titleetbody— dans le blocdataFCM (toujours).user_id— optionnel, mono-destinataire uniquement ; en batch, utilisezidentify()côté app.
Payload iOS
- Notification visible (titre + corps dans
aps.alert). mutable-content: 1dansaps— sinon la NSE ne se réveille jamais.- Un push silencieux / data-only ne déclenche pas la NSE sur iOS.
- Ajoutez
notif_idetcampaigndans FCMdataet en champs personnalisés APNs (au même niveau queaps) pour que l'extension iOS les lise dansuserInfo.
mutable_content=True (booléen), pas mutable_content=1 (entier) — sinon la clé mutable-content est absente du JSON APNs réellement envoyé.
# Firebase Admin SDK (Python)
from firebase_admin.messaging import Message, Notification, Aps, ApsAlert, APNSConfig, APNSPayload
aps = Aps(
alert=ApsAlert(title=title, body=body),
badge=1, sound='default',
mutable_content=True, # ← booléen obligatoire
)
apns_payload = APNSPayload(
aps=aps,
notif_id=str(notif_id),
campaign=str(campaign_id),
)
apns = APNSConfig(
headers={'apns-push-type': 'alert', 'apns-priority': '10'},
payload=apns_payload,
)
message = Message(
notification=Notification(title=title, body=body),
data={'notif_id': notif_id, 'campaign': campaign_id, 'title': title, 'body': body},
apns=apns,
token=device_token,
)
// FCM HTTP v1 — extrait
{
"message": {
"token": "<device_token>",
"notification": { "title": "…", "body": "…" },
"apns": {
"headers": { "apns-push-type": "alert", "apns-priority": "10" },
"payload": {
"aps": {
"alert": { "title": "…", "body": "…" },
"mutable-content": 1,
"sound": "default"
},
"notif_id": "8f14e45f-ceea-467d-9a3b-2c1d4f5e6a7b",
"campaign": "promo_2026_06"
}
},
"data": {
"notif_id": "8f14e45f-ceea-467d-9a3b-2c1d4f5e6a7b",
"campaign": "promo_2026_06",
"title": "…", "body": "…"
}
}
}
Payload Android
- Message data-only : pas de bloc
notificationau niveau FCM. titleetbodydansdata— le SDK les affiche.- Priorité haute :
android: { priority: 'high' }.
message = Message(
data={
'notif_id': notif_id,
'campaign': campaign_id,
'title': title,
'body': body,
# … vos champs métier
},
android=AndroidConfig(priority='high'),
token=device_token,
)
Envoi mixte iOS + Android : construisez des messages séparés par plateforme (data-only pour Android, notification + apns pour iOS).
Déclarer l'envoi (POST /v1/sent)
Après chaque envoi (ou lot), indiquez combien de notifications ont été envoyées. C'est ce chiffre qui sert
de base au calcul du taux de livraison. Utilisez le même libellé campaign que dans le message
si vous suivez vos campagnes séparément. Détail complet : POST /v1/sent.
Exemple bout-en-bout
- App :
configure()+ NSE + listeners (Partie 1). - Backend : pour chaque push, UUID → payload iOS/Android → FCM →
POST /v1/sent. - Dashboard : comparer « envoyés » et « accusés » sur la période.
Partie 3API Pushproof
Référence des endpoints pour déclarer les envois, consulter les statistiques ou brancher Pushproof sur un serveur existant.
URL de base : https://api.pushproof.dev, préfixe /v1, corps au format JSON.
Authentification
| Opération | En-tête |
|---|---|
| Ingestion accusés / déclaration envois | X-Ingest-Key: pk_ingest_… |
| Lecture stats | Authorization: Bearer sk_read_… |
POST /v1/receipts
Enregistrement d'un accusé de réception. Envoyé automatiquement par le SDK (extension iOS ou service Android). Les doublons sont ignorés pour un même couple (notif_id, appareil).
| Champ | Type | Requis | Description |
|---|---|---|---|
notifId | UUID | oui | Lu dans le payload reçu (jamais généré par le SDK). |
device | string | oui | Identifiant d'installation ; hashé côté serveur. |
platform | "ios" | "android" | oui | Plateforme de réception. |
campaign | string | non | Libellé de campagne. |
receivedAt | ISO 8601 | non | Horodatage (défaut : maintenant). |
userId | string | non Pro | Identifiant opaque, hashé à l'ingestion. |
curl -X POST https://api.pushproof.dev/v1/receipts \
-H "X-Ingest-Key: pk_ingest_…" \
-H "Content-Type: application/json" \
-d '{
"notifId": "8f14e45f-ceea-467d-9a3b-2c1d4f5e6a7b",
"device": "<install_id>",
"platform": "ios",
"campaign": "promo_2026_06"
}'
// 202 Accepted
{ "accepted": true, "duplicate": false }
Un doublon renvoie { "accepted": true, "duplicate": true } sans recompter.
POST /v1/sent
Déclaration d'un envoi (base du calcul du taux). Authentification : X-Ingest-Key.
| Champ | Type | Requis | Description |
|---|---|---|---|
platform | "ios" | "android" | oui | Plateforme visée. |
campaign | string | non | Libellé de campagne. |
date | YYYY-MM-DD | non | Jour d'envoi (défaut : aujourd'hui). |
count | int | non | Nombre d'envois (défaut : 1 ou nb de userIds). |
notifId | UUID | non Pro | Pour la liste « pas reçu ». |
userIds | string[] | non Pro | Destinataires visés (hashés). |
GET /v1/stats
Statistiques agrégées. Auth : Authorization: Bearer sk_read_…. Abonnement actif requis.
| Param | Description |
|---|---|
range | 7d · 30d · 90d (défaut 30d) |
from & to | Plage YYYY-MM-DD (prioritaire sur range) |
curl "https://api.pushproof.dev/v1/stats?range=30d" \
-H "Authorization: Bearer sk_read_…"
Endpoints Pro
GET /v1/receipts/lookup?notif_id=…&user_id=… — « cet utilisateur a-t-il reçu cette notif ? »
{ "received": true, "received_at": "2026-06-24T13:08:18Z", "platform": "android" }
GET /v1/notifications/{notif_id}/recipients?status=missing — liste paginée des destinataires.
DELETE /v1/users/purge?user_id=… — effacement RGPD.
Codes d'erreur
| Code | Signification |
|---|---|
202 | Accusé / envoi accepté. |
400 | Payload invalide. |
401 | Clé de lecture absente ou invalide. |
403 | Clé d'ingestion manquante, abonnement inactif, ou route Pro sur compte non-Pro. |
429 | Rate limiting par app dépassé. |
Lire le dashboard
- Push envoyées — nombre déclaré par votre serveur via
POST /v1/sent. - Accusés reçus — confirmations remontées par les appareils via le SDK.
- Taux de livraison — rapport entre accusés et envoyés. Sur iOS, considérez ce chiffre comme un minimum : le taux réel peut être légèrement plus élevé.
Plan Pro
Suivi par utilisateur via identify() dans l'app. Pour les envois groupés, le message est identique pour tous les destinataires : associez chaque appareil à un utilisateur avec identify() plutôt que d'ajouter un user_id par destinataire dans le payload.
Dépannage
| Symptôme | Cause probable | Solution |
|---|---|---|
| Push reçu, 0 accusé | mutable-content absent du JSON APNs (ex. mutable_content=1 entier en Python) | Payload iOS |
cap sync warn Package.swift | Plugin non lié en SPM Capacitor 8 | Capacitor SPM |
configure() OK, NSE silencieuse | App Group / PushproofAppGroup / entitlements | NSE |
| Accusé seulement au clic | NSE ne tourne pas ; seul recordDelivery au tap | mutable-content + rebuild |
| Contenu in-app absent au tap | Un flag empêche le code de tap de s'exécuter | Ouverture notification |
import PushproofNSE échoue | Cible Xcode nommée comme le produit SPM | Renommer cible |