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 Stripe | Signification | Statut Salesforce (fictif) |
|---|---|---|
incomplete | Premier paiement pas encore confirmé | En attente |
incomplete_expired | Premier paiement jamais abouti | Abandonné |
trialing | Période d’essai | Essai |
active | À jour | Actif |
past_due | Échéance impayée, nouvelles tentatives en cours | Impayé |
unpaid | Tentatives épuisées, abonnement maintenu sans facturation | Suspendu |
paused | Suspendu volontairement | En pause |
canceled | Terminé | 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__cdonne 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.