Push

Envoi et repli

Un appel envoie à tous les appareils d'un utilisateur et peut envoyer un SMS quand aucun appareil n'accepte la notification.

Envoyer une notification

POST /v1/push prend app_id, to (user_id pour tous les appareils actifs d'un utilisateur, ou device_id pour un seul), ainsi que title, body, data, badge, sound, thread_id, ttl (en secondes, jusqu'à 28 jours), priority (high par défaut, ou normal) et collapse_key (de 1 à 64 caractères ASCII visibles : une notification plus récente avec la même clé remplace une plus ancienne pas encore affichée). Il faut un titre, un corps ou des données. Les valeurs de data sont des chaînes, avec au plus 64 clés et 4096 octets en JSON. Les notifications de données seules (sans titre ni corps, vers un appareil sans clé de chiffrement) sont livrées en silence comme notifications d'arrière-plan : rien ne s'affiche sur l'écran verrouillé et votre application traite les données. Une notification d'arrière-plan ne porte ni pastille ni son, même si vous les envoyez.

Envoyez un en-tête Idempotency-Key. La même clé renvoie la notification d'origine, et la même clé avec un corps différent donne un 409 IDEMPOTENCY_KEY_REUSED. Les modes test et production ne se rejouent jamais l'un l'autre.

const { data: push, error } = await honkio.push.send({
  appId: 'APP_ID',
  to: { userId: 'user_1042' },
  title: 'Your code',
  body: '123456',
}, { idempotencyKey: 'login-code-1042-2026-11-03' })
if (error) throw new Error(error.message)
console.log(push.id, push.status, push.device_count) // queued 2

L'appel répond 202 avec la notification en file et son device_count. GET /v1/push/:id la renvoie avec le résultat de chaque appareil, et GET /v1/push liste les notifications.

La charge finale envoyée au fournisseur, après chiffrement pour un appareil qui a une clé, est limitée à 4096 octets. Une charge plus grande est refusée avec un 422 PUSH_PAYLOAD_TOO_LARGE et details.bytes_over, et rien n'est facturé.

Sans appareil actif et sans repli, la notification est acceptée et se termine en failed avec NO_DEVICES, sans aucune facturation.

const { data: push, error } = await honkio.push.get('PUSH_ID')
if (error) throw new Error(error.message)
console.log(push.status) // scheduled, queued, sending, sent, failed, fell_back or cancelled
for (const delivery of push.deliveries ?? []) console.log(delivery.device_id, delivery.status, delivery.error_code)

Programmer une notification

Ajoutez scheduled_at pour envoyer plus tard, par exemple un rappel une heure avant un rendez-vous : une date et heure ISO 8601 avec un décalage, de 1 minute à 30 jours à l'avance. L'appel répond 202 avec le statut scheduled, est facturé tout de suite pour les appareils visés à ce moment, et compte dans la limite quotidienne de notifications du jour où elle part. Chaque notification relue porte scheduled_at, null pour une notification envoyée immédiatement, et GET /v1/push accepte status=scheduled ou status=cancelled.

const { data: push, error } = await honkio.push.send({
  appId: 'APP_ID',
  to: { userId: 'user_1042' },
  title: 'Appointment in one hour',
  body: 'Acme Dental, 2 pm today.',
  scheduledAt: '2026-10-01T13:00:00-04:00',
  expiresAt: '2026-10-01T13:45:00-04:00', // optional: not after 1:45 pm (default: 24 hours after scheduledAt)
}, { idempotencyKey: 'appt-5521-push-reminder' })
if (error) throw new Error(error.message)
console.log(push.id, push.status, push.scheduled_at, push.expires_at) // scheduled

Tant qu'elle est programmée, PATCH /v1/push/:id avec un nouveau scheduled_at la déplace (la clé doit avoir push:m ; 429 PUSH_DAILY_CAP si le nouveau jour est plein) et DELETE /v1/push/:id l'annule, rembourse les frais et rend au mois ses envois gratuits (push:d). Une annulation est irréversible. L'un ou l'autre sur une notification qui n'est plus programmée donne un 422 PUSH_NOT_SCHEDULED. Ils déclenchent push.rescheduled et push.cancelled.

// Move it, or cancel it (refunded), while it is still scheduled:
const moved = await honkio.push.update('PUSH_ID', { scheduledAt: '2026-10-02T09:00:00-04:00' })
if (moved.error) throw new Error(moved.error.message)

const cancelled = await honkio.push.cancel('PUSH_ID')
if (cancelled.error) throw new Error(cancelled.error.message)

Une notification programmée doit partir dans les 24 heures qui suivent son scheduled_at. Pour une limite plus serrée, ajoutez expires_at : une date et heure ISO 8601 avec un décalage, de 1 minute à 24 heures après scheduled_at, acceptée seulement avec lui (clés de test comprises ; toute autre valeur donne un 422 VALIDATION_ERROR). Une notification qui ne peut pas partir avant sa limite échoue avec le failure_code SCHEDULED_SEND_EXPIRED : rien n'est envoyé, elle est remboursée comme pour une annulation, et push.failed est déclenché. PATCH /v1/push/:id accepte aussi expires_at, vérifié par rapport au nouveau scheduled_at, ou null pour le délai par défaut de 24 heures ; s'il est omis, celui qui est enregistré est gardé tant qu'il convient encore à la nouvelle heure et abandonné sinon, ce qui ramène le délai par défaut de 24 heures. Chaque notification relue porte expires_at, null pour le délai par défaut de 24 heures.

À l'heure prévue, elle part comme toute autre notification, évaluée à ce moment : si le compte a été suspendu ou si les notifications ont été désactivées pour lui depuis (PUSH_DISABLED), la notification échoue et est remboursée ; un appareil désactivé depuis est un envoi refusé et remboursé ; et un SMS de repli applique toutes les règles d'un SMS envoyé à ce moment, donc un destinataire qui s'est désabonné ne reçoit pas de texto. Une notification de test passe par la livraison simulée comme toute notification de test.

Se replier sur un SMS

Ajoutez fallback.sms avec to, from et body, et honkio envoie ce SMS quand aucun appareil n'accepte la notification. from est obligatoire et doit être un numéro qui vous appartient, comme pour POST /v1/messages. La clé doit pouvoir envoyer des messages en plus des notifications (sinon 403 FORBIDDEN), et le doit encore au moment de l'envoi du SMS : si elle a été révoquée ou a perdu ce droit entre-temps, la notification échoue avec FORBIDDEN et aucun SMS ne part.

const { data: push, error } = await honkio.push.send({
  appId: 'APP_ID',
  to: { userId: 'user_1042' },
  title: 'Your code',
  body: '123456',
  fallback: {
    sms: { to: '+14165550123', from: '+14165550100', body: 'Your Acme code is 123456' },
  },
}, { idempotencyKey: 'login-code-1042-2026-11-03' })
if (error) throw new Error(error.message)

Le SMS est envoyé quand chaque envoi à un appareil a échoué : aucun appareil actif, un jeton refusé, un identifiant refusé ou absent, ou Apple ou Google qui échouent encore après la fenêtre de reprise. Il n'est pas envoyé si un appareil a été accepté.

Accepté ne veut pas dire vu. Une fois qu'Apple ou Google accepte une notification, le travail de honkio est terminé. honkio ne peut pas savoir si le téléphone était éteint, sans connexion ou avec les notifications désactivées : une notification acceptée ne se replie donc jamais sur un SMS.

Le SMS passe par toutes les règles qu'applique POST /v1/messages : consentement, désabonnements, limites d'envoi, numéro de mobile vérifié du compte et solde. Ses vérifications statiques (le numéro vous appartient et peut envoyer, la destination, le contenu et sa longueur) s'exécutent à l'appel de POST /v1/push, de sorte qu'un repli défectueux échoue tout de suite. Les vérifications liées au destinataire ne s'exécutent qu'à l'envoi du SMS : une notification qui n'a pas besoin de SMS n'est donc jamais refusée pour une raison de SMS.

La notification passe à fell_back, et fallback_message_id désigne le message, facturé au prix normal d'un SMS et qui déclenche les événements de message habituels. Si le SMS est refusé, la notification passe à failed et fallback_reason porte le code d'erreur du SMS. Un SMS que l'opérateur ne confirme pas à temps compte tout de même comme fell_back : le statut du message et ses webhooks donnent le résultat.

Sans appareil actif et avec un repli, honkio saute la notification et envoie le SMS tout de suite. La notification elle-même n'est pas facturée.

Reprises

Un échec récupérable d'Apple ou de Google, comme un service occupé ou indisponible, est repris avec un délai croissant. Avec un repli, honkio reprend pendant au plus 60 secondes environ, puis se replie. Sans repli, il fait au plus trois tentatives en environ 60 secondes, puis l'envoi échoue.

Un échec permanent (jeton invalide, identifiant refusé ou absent) n'est jamais repris. Un SMS de repli refusé pour une raison récupérable, comme une limite de débit ou un opérateur indisponible, est repris jusqu'à environ deux minutes avant que la notification échoue.

Prix et allocation gratuite

5,00 $ par 1 000 après 2 500 gratuits chaque mois.

Vous payez par appareil qu'Apple ou Google accepte. Un appareil qui refuse la notification n'est pas facturé. Les 2 500 premiers envois acceptés en production de chaque mois civil (heure de l'Est) sont gratuits, sans report. Les notifications de test sont gratuites et ne comptent pas. GET /v1/push/usage indique free_used et free_monthly. Un SMS de repli est facturé comme un SMS, sur son propre message.

Tant qu'un loyer est dû pour un numéro libéré, les notifications en production sont refusées, même celles que l'allocation gratuite couvrirait. Une limite quotidienne de notifications s'applique à chaque compte, et le mode test est limité à 1 000 par jour : au-delà, l'appel donne un 429 PUSH_DAILY_CAP.

Erreurs

Erreurs propres au push, en plus des erreurs habituelles de validation et d'authentification :

CodeStatutSignification
PUSH_PAYLOAD_TOO_LARGE422La charge finale dépasse 4096 octets. details.bytes_over indique de combien. Rien n'est facturé.
PUSH_NO_CREDENTIAL422L'application n'a aucun identifiant pour la plateforme des appareils visés, et il n'y a pas de repli.
PUSH_NOT_SCHEDULED422Seule une notification programmée peut être déplacée ou annulée.
PUSH_DAILY_CAP429Le compte a atteint sa limite quotidienne de notifications.
PUSH_DISABLED403Les notifications sont désactivées pour ce compte. Rien n’a été envoyé ni facturé; contactez le soutien.
INSUFFICIENT_BALANCE402Le solde ne couvre pas la notification. Rechargez et renvoyez.
RENT_OWED402Un loyer est dû pour un numéro libéré : les notifications en production sont refusées, même celles que l'allocation gratuite couvrirait.
IDEMPOTENCY_KEY_REUSED409L'Idempotency-Key a déjà servi avec un corps différent.
PUSH_IDEMPOTENCY_IN_PROGRESS409La même Idempotency-Key a été rejouée alors que le premier appel était encore en cours d'acceptation. Réessayez sous peu avec la même clé.
PUSH_PROVIDER_UNAVAILABLE503Renvoyé quand vous téléversez ou remplacez un identifiant et qu'Apple ou Google n'a pas pu être joint pour le vérifier. Réessayez sous peu.
PUSH_DEVICE_LIMIT409À l'enregistrement d'un appareil : l'application a déjà son maximum d'appareils actifs dans ce mode (1 000 000 en production, 1 000 en test). Désinscrivez les appareils auxquels vous n'envoyez plus.
DPA_PUSH_ACCEPTANCE_REQUIRED409À la création d’une application push : le compte a accepté l’entente sur le traitement des renseignements personnels de HonkIO avant qu’elle couvre les notifications push. Acceptez la version en vigueur dans Paramètres, Entente sur le traitement des renseignements personnels. Un compte qui n’a jamais accepté l’entente n’est pas visé.

Webhooks

Huit événements couvrent les notifications, les appareils et les identifiants. Les charges contiennent des identifiants, un statut et des codes d'erreur, jamais le contenu, ainsi que livemode. Les événements de message d'un SMS de repli se déclenchent comme d'habitude.

ÉvénementQuand il se déclencheChamps de data
push.queuedLa notification a été acceptée et mise en file.id, app_id, status, user_id, device_count, accepted_count, fallback_message_id, failure_code, livemode
push.sentAu moins un appareil a été accepté.id, app_id, status, user_id, device_count, accepted_count, fallback_message_id, failure_code, livemode
push.failedAucun appareil ne l'a acceptée et il n'y avait pas de repli utilisable, ou une notification programmée n'a pas pu partir avant sa limite (failure_code SCHEDULED_SEND_EXPIRED).id, app_id, status, user_id, device_count, accepted_count, fallback_message_id, failure_code, livemode
push.fell_backAucun appareil ne l'a acceptée et le SMS de repli a été soumis.id, app_id, status, user_id, device_count, accepted_count, fallback_message_id, failure_code, livemode
push.cancelledUne notification programmée a été annulée avant son départ, et ce qu'elle avait réservé a été rendu.id, app_id, status, user_id, device_count, accepted_count, fallback_message_id, failure_code, livemode, details.cancelled_at
push.rescheduledUne notification programmée a été déplacée à une nouvelle heure.id, app_id, status, user_id, device_count, accepted_count, fallback_message_id, failure_code, livemode, details.previous_scheduled_at, details.scheduled_at
push_device.deactivatedUn appareil a été retiré par Apple, Google ou le service push d'un navigateur (UNREGISTERED ou INVALID_TOKEN, aussi pour un abonnement de navigateur que honkio ne peut plus lire), comme jeton d'une autre application avec des informations d'identification qui fonctionnent (APP_MISMATCH), par la limite d'appareils par utilisateur (DEVICE_LIMIT), comme navigateur dont le service push refusait sans cesse la clé de l'application (VAPID_KEY_MISMATCH), ou comme second exemplaire d'un abonnement de navigateur (DUPLICATE). Pas déclenché quand vous supprimez vous-même un appareil, ni pour chaque navigateur retiré par un remplacement de clés (<code>push_app.vapid_key_replaced</code> les couvre).id, app_id, user_id, reason, livemode
push_app.credentials_invalidUn fournisseur a refusé l'identifiant. Une fois par passage à invalide.id (l'application), provider, environment, reason, livemode
push_app.vapid_key_replacedLa paire de clés web push de l'application a été remplacée. Chaque navigateur de l'application a été retiré avec VAPID_KEY_REPLACED, en une seule étape : un seul événement, avec le nombre par mode.id (l'application), vapid_public_key (la nouvelle), devices_retired.live, devices_retired.test, livemode