Tutoriel · Stripe · Salesforce

Synchroniser les abonnements Stripe dans Salesforce

Du checkout à la résiliation : traduire statuts d’abonnement et factures Stripe en enregistrements Salesforce, suivre les impayés et donner aux équipes une vue à jour.

Le cycle de vie d’un abonnement

Dans le partage des rôles de la série, Stripe est maître des abonnements ; Salesforce en reçoit une copie fidèle pour que le conseiller réponde au client sans ouvrir un second outil. Il faut donc traduire chaque statut Stripe en une valeur compréhensible par les équipes :

Statut StripeSignificationStatut Salesforce (fictif)
incompletePremier paiement pas encore confirméEn attente
incomplete_expiredPremier paiement jamais aboutiAbandonné
trialingPériode d’essaiEssai
activeÀ jourActif
past_dueÉchéance impayée, nouvelles tentatives en coursImpayé
unpaidTentatives épuisées, abonnement maintenu sans facturationSuspendu
pausedSuspendu volontairementEn pause
canceledTerminéRésilié

Le passage de past_due à unpaid ou canceled après les tentatives dépend des réglages de relance du compte Stripe : c’est une décision métier à arrêter avant de coder.

Les objets Salesforce

Deux objets personnalisés fictifs, rattachés au compte : un abonnement, et un paiement par facture Stripe. Les identifiants Stripe sont des External ID uniques, clés de chaque upsert.

Abonnement__c
  Stripe_Subscription_Id__c   Texte(255), External ID, unique   sub_...
  Compte__c                   Référence Account
  Statut__c                   Liste : En attente, Essai, Actif, Impayé, Suspendu, En pause, Résilié, Abandonné
  Formule__c                  Texte            (nom du prix Stripe)
  Montant_Mensuel__c          Devise
  Fin_Periode__c              Date/heure       fin de la période payée
  Resiliation_Prevue__c       Case à cocher    résiliation en fin de période demandée

Paiement__c
  Stripe_Invoice_Id__c        Texte(255), External ID, unique   in_...
  Abonnement__c               Référence Abonnement__c
  Statut__c                   Liste : Brouillon, À payer, Payée, Irrécouvrable, Annulée
  Date_Facture__c             Date/heure       création de la facture Stripe
  Montant_Du__c / Montant_Paye__c   Devise
  Tentatives__c               Nombre
  Prochaine_Tentative__c      Date/heure
  Lien_Facture__c             URL              facture hébergée par Stripe

Ces champs sont en lecture seule pour les utilisateurs : seul l’utilisateur d’intégration du middleware les modifie.

Synchroniser l’abonnement

Le service est appelé par le handler de webhooks pour tout événement customer.subscription.*. Il relit l’abonnement chez Stripe, puis écrit l’état complet dans Salesforce :

<?php

namespace App\Sync;

use App\Salesforce\SalesforceClient;
use Stripe\StripeClient;
use Stripe\Subscription;

final class SubscriptionSync
{
    private const STATUTS = [
        'incomplete' => 'En attente',
        'incomplete_expired' => 'Abandonné',
        'trialing' => 'Essai',
        'active' => 'Actif',
        'past_due' => 'Impayé',
        'unpaid' => 'Suspendu',
        'paused' => 'En pause',
        'canceled' => 'Résilié',
    ];

    public function __construct(
        private readonly StripeClient $stripe,
        private readonly SalesforceClient $salesforce,
    ) {
    }

    public function sync(string $subscriptionId): void
    {
        $sub = $this->stripe->subscriptions->retrieve($subscriptionId, ['expand' => ['items.data.price.product']]);
        $item = $sub->items->data[0];

        $this->salesforce->request('PATCH', '/sobjects/Abonnement__c/Stripe_Subscription_Id__c/' . $sub->id, [
            'json' => [
                // Rattachement par External ID du parent : pas besoin de connaître l'Id Salesforce du compte.
                'Compte__r' => ['Stripe_Customer_Id__c' => $sub->customer],
                'Statut__c' => self::STATUTS[$sub->status] ?? throw new \UnexpectedValueException($sub->status),
                'Formule__c' => $item->price->product->name,
                'Montant_Mensuel__c' => $item->price->unit_amount * $item->quantity / 100,
                // Depuis la version 2025-03-31, la période est portée par chaque ligne d'abonnement.
                'Fin_Periode__c' => gmdate('c', $item->current_period_end),
                'Resiliation_Prevue__c' => $sub->cancel_at_period_end,
            ],
        ]);
    }
}

Un statut inconnu lève une exception plutôt que d’écrire une valeur vide : si Stripe introduit un nouveau statut, le message part au transport d’échec et l’équipe le voit, au lieu d’afficher un abonnement sans statut.

Un upsert qui référence un compte encore absent de Salesforce échoue : le client doit exister avant l’abonnement. C’est le rôle de l’étape de création décrite dans l’article Déclencher un paiement Stripe depuis Salesforce, qui part toujours d’un compte Salesforce existant.

Synchroniser les factures

Chaque échéance produit une facture Stripe. Les événements invoice.paid, invoice.payment_failed et invoice.finalized déclenchent le même service :

public function sync(string $invoiceId): void
{
    $invoice = $this->stripe->invoices->retrieve($invoiceId);
    $subscriptionId = $invoice->parent?->subscription_details?->subscription;
    if ($subscriptionId === null) {
        return; // facture ponctuelle, hors abonnement : traitée par un autre flux
    }

    $this->salesforce->request('PATCH', '/sobjects/Paiement__c/Stripe_Invoice_Id__c/' . $invoice->id, [
        'json' => [
            'Abonnement__r' => ['Stripe_Subscription_Id__c' => $subscriptionId],
            'Statut__c' => self::STATUTS[$invoice->status],   // draft, open, paid, uncollectible, void
            'Date_Facture__c' => gmdate('c', $invoice->created),
            'Montant_Du__c' => $invoice->amount_due / 100,
            'Montant_Paye__c' => $invoice->amount_paid / 100,
            'Tentatives__c' => $invoice->attempt_count,
            'Prochaine_Tentative__c' => $invoice->next_payment_attempt ? gmdate('c', $invoice->next_payment_attempt) : null,
            'Lien_Facture__c' => $invoice->hosted_invoice_url,
        ],
    ]);
}

Les montants Stripe sont en centimes ; la conversion se fait ici et nulle part ailleurs. Le lien vers la facture hébergée permet au conseiller de la renvoyer au client sans la reconstituer.

Impayés et relances

En cas d’échec de prélèvement, Stripe réessaie selon son propre calendrier et peut écrire au client. Salesforce n’a pas à refaire ce travail ; il doit en revanche prévenir le conseiller au bon moment. La logique d’alerte reste dans Salesforce, où les administrateurs peuvent l’ajuster sans redéployer le middleware :

Flow déclenché par enregistrement — Paiement__c, après mise à jour
  Condition : Statut__c = "À payer" ET Tentatives__c a changé ET Tentatives__c >= 1
  Action    : créer une tâche pour le propriétaire du compte
              « Échec de prélèvement (tentative {Tentatives__c}) — prochaine tentative le {Prochaine_Tentative__c} »

Le middleware se contente d’écrire des faits (statut, nombre de tentatives, prochaine tentative) ; les processus métier se branchent dessus.

Résilier ou changer de formule

Une action du conseiller ne modifie jamais directement l’abonnement dans Salesforce. Elle appelle le middleware, qui agit chez Stripe ; le nouvel état revient par webhook, comme pour tout autre changement :

// POST /api/abonnements/{id}/resiliation — appelé depuis Salesforce par le conseiller
public function cancel(string $subscriptionId): void
{
    // Fin de période : le client garde la couverture payée, Stripe n'émet plus de facture.
    $this->stripe->subscriptions->update($subscriptionId, ['cancel_at_period_end' => true]);
    // Rien à écrire dans Salesforce ici : customer.subscription.updated arrivera et le fera.
}

public function changePlan(string $subscriptionId, string $newPriceId): void
{
    $sub = $this->stripe->subscriptions->retrieve($subscriptionId);
    $this->stripe->subscriptions->update($subscriptionId, [
        'items' => [['id' => $sub->items->data[0]->id, 'price' => $newPriceId]],
        'proration_behavior' => 'create_prorations',   // le prorata est calculé par Stripe
    ]);
}

Ce détour garantit qu’il n’existe qu’un chemin d’écriture vers Salesforce. Une résiliation faite par le client dans le portail Stripe et une résiliation faite par le conseiller produisent exactement le même enregistrement.

Bonnes pratiques

  • Écrire l’état, pas l’événement : chaque synchronisation écrit l’objet complet, ce qui la rend rejouable et insensible à l’ordre.
  • Champs en lecture seule : profils et ensembles d’autorisations empêchent toute modification manuelle des données Stripe dans Salesforce.
  • Version d’API Stripe figée : l’emplacement de la période ou du lien facture–abonnement a changé selon les versions ; le mapping se teste à chaque montée.
  • Historique : activer le suivi d’historique sur Statut__c donne gratuitement la chronologie d’un abonnement.
  • Volume : pour une reprise initiale de milliers d’abonnements, passer par la Bulk API plutôt que par des upserts unitaires.
🍪 Gestion des cookies
Ce site utilise des cookies pour mesurer l'audience (Google Analytics) et lire des vidéos (YouTube). Vous pouvez personnaliser votre choix.
Mentions légales
Réglages des cookies
Essentiels Nécessaires au fonctionnement du site — ne peuvent pas être désactivés.
Google Analytics Analyse d'audience (pages vues, sources de trafic). Aucune donnée personnelle identifiante.
YouTube Permet de lire les vidéos YouTube intégrées sur cette page.