J'ai perdu la majeure partie d'une journée à cause de l'erreur Undelivered Consumption Percentage Non Zero Error. Non pas parce que c'est une erreur complexe. Mais parce que j'avais défini delivery Status sur une valeur autre que DELIVERED tout en laissant consumption Percentage à 60000, et Apple rejette cette combinaison sans grande explication.
C'est une façon détournée d'en arriver à la vraie question.
Si vous cherchez comment répondre automatiquement aux demandes de remboursement Apple, c'est un travail plus simple que ne l'imaginent la plupart des équipes. La difficulté n'a jamais été l'appel API. C'est d'avoir les données à y mettre. Le détail champ par champ se trouve dans CONSUMPTION_REQUEST d'Apple expliqué. Celui-ci porte sur la mise en place du système pour que personne n'ait besoin de rester éveillé.
Points clés
L'automatisation des remboursements Apple se résume à un webhook plus un PUT. Apple envoie un CONSUMPTION_REQUEST, vous répondez dans les 12 heures.
Vous avez besoin d'une clé In-App Purchase, pas de votre clé API App Store Connect. Elles ne sont pas interchangeables.
Trois des cinq champs sont obligatoires, et deux d'entre eux ont des contraintes qui n'apparaissent que sous forme d'erreurs.
Sans consentement du client, vous ne devez pas répondre du tout, et vous ne pouvez pas recueillir ce consentement rétroactivement.
L'endpoint est passé à la v2. La production se trouve sur api.storekit.apple.com, et le sandbox vous donne 5 minutes au lieu de 12 heures.
Comment fonctionnent réellement les demandes de remboursement Apple pour les développeurs
Un client se rend sur reportaproblem.apple.com, retrouve l'achat effectué au cours des 90 derniers jours, choisit un motif, puis valide. Si vous avez configuré les App Store Server Notifications V2, votre endpoint reçoit un CONSUMPTION_REQUEST sous forme de payload JWS signé, contenant la transaction, le produit et le motif choisi.
Ce webhook est le déclencheur unique d'un remboursement d'achat intégré sur l'App Store. Rien n'apparaît dans votre dashboard, et il n'y a aucune file d'attente à vérifier. C'est pour cela que les équipes découvrent l'existence de ce système des semaines après avoir commencé à perdre de l'argent à cause de lui.
Ensuite, vous avez 12 heures.
Une précision à apporter, car elle est répétée partout. Ce n'est pas limité aux consommables. La documentation actuelle d'Apple indique qu'un CONSUMPTION_REQUEST arrive pour tout type de produit : consommable, non consommable, abonnement non renouvelable, abonnement renouvelable automatiquement. D'anciens guides disent encore que cela ne concerne que les consommables et les abonnements. Ils sont obsolètes.
Ce que « répondre » signifie, concrètement
Vous envoyez un corps JSON en PUT vers un seul endpoint.
PUT https://api.storekit.apple.com/inApps/v2/transactions/consumption/{transactionId}
Le sandbox est sur api.storekit-sandbox.apple.com, avec le même chemin. En cas de succès, vous obtenez 202 Accepted, pas 200, ce qui piège les personnes qui testent sur les codes de statut.
L'authentification repose sur un JWT ES256 de courte durée, signé avec une clé In-App Purchase, générée dans App Store Connect sous Users and Access, dans Integrations. Ce n'est pas votre clé API App Store Connect. Le fichier .p8 ne se télécharge qu'une seule fois, au moment de sa création : si vous l'avez perdu, vous devez en générer une nouvelle et faire pivoter tout ce qui l'utilise. Demandez-moi comment je le sais.
Les cinq champs, et les deux qui posent problème
Le corps de la requête est un Consumption Request. Trois champs obligatoires, deux facultatifs.
Customer Consented (obligatoire). True uniquement si vous disposez d'un consentement réel. Plus de détails plus bas, car ce champ conditionne tout le reste.
Delivery Status (obligatoire). Indique si l'achat est arrivé et a fonctionné.
Sample Content Provided (obligatoire). Indique si un échantillon gratuit, un essai ou des informations sur les fonctionnalités existaient avant l'achat.
Consumption Percentage (facultatif). Quelle quantité a été consommée, en milliunités. De 0 à 100000, donc 45.2 % correspond à 45200.
Refund Preference (facultatif). GRANT_FULL, GRANT_PRORATED, DECLINE, ou NO_PREFERENCE.
Deux contraintes qui vous coûteront chacune un après-midi.
Si Delivery Status n'est pas DELIVERED, Consumption Percentage doit être égal à 0. Toute autre valeur échoue avec Undelivered Consumption Percentage Non Zero Error. C'est ce qui m'a coûté ma journée.
Et si vous envoyez GRANT_PRORATED pour un abonnement renouvelable automatiquement, n'incluez pas du tout consumptionPercentage. Apple le calcule à partir du temps écoulé dans la période de facturation et rejette le vôtre avec Consumption Percentage Auto Renewable Subscription Error. Les noms d'erreurs sont au moins descriptifs, ce qui est déjà plus que ce à quoi je m'attendais.
Le consentement est un verrou, pas une formalité
La documentation d'Apple est particulièrement directe sur ce point : si le client n'a pas consenti, ne répondez pas à la notification. Pas une réponse avec Customer Consented défini sur false. Ne répondez pas.
Vous ne pouvez pas non plus recueillir le consentement après la réception d'une demande. Il doit exister au préalable, ce qui signifie que la formulation doit figurer dans vos conditions d'utilisation ou votre politique de confidentialité avant le lancement, et non après votre premier remboursement contesté. Les directives d'Apple indiquent que le consentement doit être donné librement, spécifique, informé et sans ambiguïté, que l'opt-in est préférable à l'opt-out, et que l'obtention de ce consentement relève entièrement de votre responsabilité.
Concrètement : une mention indiquant que vous pouvez partager les données d'achat et d'utilisation avec Apple pour faciliter l'examen des demandes de remboursement, accompagnée d'un moyen de se rétracter. Faites valider cela par vos propres juristes. Je ne suis pas avocat et ceci ne constitue pas un conseil juridique.
Comment les développeurs automatisent les demandes de remboursement sur l'App Store
Le pipeline, dans l'ordre où je le construirais :
Configurez une Production Server URL dans App Store Connect, sous App Information, et choisissez Version 2. Les demandes de consommation ne transitent pas par la V1.
Vérifiez la chaîne de signature JWS par rapport aux certificats racine d'Apple avant de faire confiance au moindre champ.
Associez la transaction à un utilisateur. Le appAccountToken que vous avez attaché lors de l'achat est la voie rapide. Sans lui, vous devez effectuer une recherche inversée par produit et par date, ce qui devient vite compliqué.
Vérifiez le consentement. Pas de consentement, pas de réponse. Consignez-le et passez à la suite.
Récupérez l'usage réel. Pas une estimation, pas une valeur par défaut. Si vous ne pouvez pas répondre à la question « combien en a-t-il utilisé », c'est là que se trouve la vraie faille de votre automatisation.
Construisez le ConsumptionRequest, en appliquant les deux contraintes mentionnées plus haut.
Envoyez-le en PUT, confirmez le 202, et conservez le payload envoyé.
C'est à l'étape 5 que la plupart des tentatives s'enlisent. L'appel API représente un après-midi de travail. Disposer de données de consommation fiables par utilisateur pour un achat effectué il y a sept semaines est un problème de modélisation des données que vous n'avez peut-être pas encore résolu.
Ce qui le rend fiable en production
Au-delà du scénario idéal, quatre éléments comptent :
L'idempotence. Apple relance les notifications, et votre handler verra la même notification deux fois. Dédupliquez sur l'UUID de la notification, sinon vous enverrez des réponses en double.
Des alertes qui préviennent réellement quelqu'un. Un handler qui échoue silencieusement est pire que l'absence de handler, car vous croirez être protégé.
Un harnais de test en sandbox. Le sandbox vous donne 5 minutes au lieu de 12 heures, vous ne pouvez donc pas tester cela à la main de façon désinvolte. Scriptez-le.
Et un historique de ce que vous avez envoyé, par cas. Quand un remboursement est refusé, vous voudrez savoir quelle préférence vous avez indiquée et pourquoi. Quand un remboursement est accordé malgré votre DECLINE, vous voudrez le savoir aussi.
Estimation honnête : quelques jours de colle si vous avez déjà construit de la plomberie webhook, et quelques semaines pour lui faire confiance. La majeure partie de ce temps n'est pas du côté d'Apple.
Ou laissez quelque chose d'autre s'en charger
Si vous utilisez déjà RevenueCat ou Adapty, vérifiez d'abord ce qu'ils proposent. Les deux ont ajouté des fonctionnalités de réponse aux remboursements, et vos reçus ainsi que le mapping client sont déjà en place.
Un logiciel de gestion des remboursements Apple conçu spécifiquement va plus loin. Refund Sensor fonctionne avec les clés App Store Connect que vous possédez déjà, répond à chaque demande avec des preuves de transaction dans le délai imparti, et consigne le résultat pour chaque cas. Pas de SDK, pas de modification de code, environ cinq minutes pour se connecter. Nous avons comparé l'ensemble du marché dans outils de gestion des remboursements Apple, si vous préférez comparer par vous-même.
Du côté du client, puisqu'on nous le demande
Sur la manière de demander un remboursement pour un achat sur l'App Store, du point de vue de l'utilisateur : reportaproblem.apple.com, connectez-vous, retrouvez l'achat effectué au cours des 90 derniers jours, choisissez un motif. Apple indique aux clients qu'ils recevront une réponse sous 48 heures.
Vous pouvez aussi afficher ce même formulaire directement dans votre application grâce au refund request sheet de StoreKit, qui redirige les utilisateurs mécontents vers Apple plutôt que vers un avis une étoile. Il ne peut pas accorder de remboursement. Seul Apple peut le faire. Le guide d'Apple Support pour demander un remboursement explique ce que voit le client.
Il faut le dire clairement, car la gestion des remboursements sur l'App Store est souvent survendue : vous ne pouvez ni approuver ni refuser un remboursement. Vous pouvez présenter des preuves à Apple pendant sa décision, et indiquer une préférence. C'est toute l'influence dont vous disposez, et c'est déjà plus que ce qu'utilisent la plupart des développeurs.
Sources
Questions fréquentes
Oui. C'est justement le rôle de la notification CONSUMPTION_REQUEST et de l'endpoint Send Consumption Information. Vous ne pouvez pas accorder ni refuser le remboursement, mais Apple utilise les informations que vous envoyez comme élément de décision.
Recevez les App Store Server Notifications V2, vérifiez la signature JWS, associez la transaction à un utilisateur, vérifiez le consentement, rassemblez des données de consommation réelles, puis envoyez-les en PUT à l'endpoint de consommation v2 dans un délai de 12 heures. Tout le reste relève de l'idempotence, des alertes et de la tenue de registres.
Une clé In-App Purchase, disponible dans Users and Access, sous Integrations. Votre clé API App Store Connect n'authentifiera pas ces appels, et vous obtiendrez une erreur 401 qui ressemble à un problème d'horloge.
Non, c'est facultatif. Mais si deliveryStatus n'est pas DELIVERED, il doit être égal à 0, et pour un abonnement renouvelable automatiquement au prorata, vous devez l'omettre entièrement.
Ne répondez pas. Les directives d'Apple sont explicites à ce sujet, et le consentement ne peut pas être recueilli après la réception de la demande.
C'est un élément d'appréciation, pas une garantie. Apple indique qu'elle utilise divers facteurs. En pratique, un DECLINE bien étayé sur un achat largement consommé est refusé bien plus souvent qu'une demande restée sans réponse.



