TUTORIEL BOUTON PAYPAL
Public: Développeurs web - HTML et PHPDate publication: 15 avril 2024
Vous ouvrez une boutique en ligne?
À un moment donné le client devra payer.
Voici comment créer un bouton Paypal pour payer un produit ou une commande, avec l'API NVP/SOAP IPN.
Le client peut payer avec son compte Paypal, ou sa carte bleue s'il n'a pas de compte Paypal.
J'ai créé cette page parce que la doc Paypal est un vrai bordel.
Attention il faut être rigoureux et précis, à la moindre erreur ça plante.
Cependant ce n'est pas compliqué, il n'y a que deux fichiers HTML, un fichier PHP et une image.
CRÉER UN COMPTE PAYPAL BUSINESS
Paypal gère deux types de comptes utilisateur: Personnel et Business.Il faut créer un compte Business pour recevoir les paiements (le nom doit correspondre à la carte d'identité pour éviter la fraude), et pour créer des comptes de test.
PHASE DE DÉVELOPPEMENT
Dans le Dashboard du compte Business, créer deux comptes Sandbox: un vendeur et un acheteur et leur donner $5000 à chacun| mot de passe | Nom | |
|---|---|---|
| client@machin.fr | azertyui | Jean Bon |
| vendeur@truc.fr | azertyui | Jacques Attali (vendeur de pantalons) |
Ensuite activer les messages IPN
- Se connecter à Paypal avec le compte Business
- Passer la souris sur l'icone du compte (en haut à droite) → Paramètres du compte
- (à gauche) Notifications → Notifications Instantanées de Paiement (Cliquer sur Mettre à jour)
Exemple https://www.monsite.fr/boutique/notify.php
On va créer et uploader ce script, le code est ci-dessous.
FORMULAIRE HTML AVEC LE BOUTON
form.html<meta charset="utf-8"> Blouson en daim<br> 74.35€ TTC<br> <form name="_xclick" action="https://ipnpb.sandbox.paypal.com/cgi-bin/webscr" method="post"> <!--<form name="_xclick" action="https://www.paypal.com/cgi-bin/webscr" method="post">--> <input type="hidden" name="cmd" value="_xclick"> <input type="hidden" name="business" value="vendeur@truc.fr"> <!--<input type="hidden" name="business" value="votre-vraie-adresse@paypal-business.fr">--> <!--Référence commande générée par la boutique, ce qui permet à un tiers de payer la commande avec un autre email--> <input type="hidden" name="custom" value="AF05D5BA48E8877DAAF"> <input type="hidden" name="item_name" value="Blouson en daim"> <input type="hidden" name="amount" value="74.35"> <input type="hidden" name="currency_code" value="EUR"> <input type="hidden" name="lc" value="fr-FR"> <!--The URL of the 150x50-pixel image displayed as your logo in the upper left corner of the PayPal checkout pages--> <input type="hidden" name="image_url" value="https://www.monsite.fr/boutique/paypal/harry.png"> <!-- Instant Payment Notification messages script --> <input type="hidden" name="notify_url" value="https://www.monsite.fr/boutique/notify.php"> <input type="hidden" name="rm" value="2"><!-- send POST to IPN--> <!--Page "Annuler le paiement et revenir au site"--> <input type="hidden" name="cancel_return" value="https://www.monsite.fr/boutique/"> <!-- Après avoir effectué le paiement, quand le client clique sur "Retourner sur le site du marchand" dirige vers une page "Merci pour votre achat", ou "Mes commandes". --> <input type="hidden" name="return" value="https://www.monsite.fr/boutique/paypal/payment-ok.html"> <input type="image" src="http://www.paypalobjects.com/fr_FR/i/btn/btn_buynow_LG.gif" border="0" name="submit" alt="Payer avec Paypal!"> </form>
Le formulaire contient l'email vendeur du sandbox dans le champ "business",
et les données de la commande "item_name" "custom" "amount" "currency_code".
Le champ "custom" est un identifiant unique de la commande généré par la boutique.
On pourrait faire sans ce champ optionnel, mais il permet à quelqu'un de payer avec une adresse paypal différente de celle de la boutique (ou permettre à un tiers de payer sa commande).
Ce bouton est du type _xclick <input type="hidden" name="cmd" value="_xclick">, il existe d'autres types de boutons Paypal
form.html ressemble à ceci:

Plus tard dans le développement, form.html deviendra commande.php et PHP au moment d'envoyer le formulaire au client renseignera les champs
- nom de la commande "item_name",
- "custom" en générant un identifiant unique pour cette commande,
- montant TTC "amount"
SCRIPT PHP DE NOTIFICATION
Lorsque le client valide le paiement sur le site Paypal, Paypal invoque le script de notification sur votre serveur précisé dans "notify_url".Ce script de notification,
Dans un 1er temps reçoit les données de la transaction en POST du serveur Paypal.
Dans un 2e temps, renvoie les données au serveur paypal pour lui dire "ok je suis bien un script IPN qui tourne, tu peux faire le paiement", puis
Dans un 3e temps reçoit de Paypal la réponse 'VERIFIED' ou 'INVALID'
et finalement envoie au serveur Paypal un en-tête HTTP 200 OK.
Si Paypal a effectué avec succès la transaction (VERIFIED) alors ce script de notification (à vous de voir ce dont vous aurez besoin):
- Regarde le statut du paiement payment_status pour s'assurer que le paiement a bien été effectué
- Regarde l'id de transaction txn_id pour s'assurer que la transaction n'a pas déjà été faite (empêcher le client de payer deux fois)
- Vérifie que l'email du vendeur receiver_email est bien le mien de vendeur
- Vérifie que le montant mc_gross et la devise mc_currency sont conformes à ce qui était prévu avant la transaction
Exemple officiel avec une classe IPN
Doc officelle: comment recevoir les données IPN
notify.php
<?php
/* Ce script écrit plusieurs fichiers log en mode append:
log-error-php.log erreurs PHP
log-paypal données IPN reçues et urldécodées
log-erreur-transaction en cas d'erreur de transaction
ATTENTION AUX LIGNES 118/119/120 qui doivent correspondre au formulaire HTML; et adapter si base de données*/
// SANDBOX
const VERIFY_URI = 'https://ipnpb.sandbox.paypal.com/cgi-bin/webscr';
const vendeur = "vendeur@truc.fr";
/* PROD
const VERIFY_URI = 'https://ipnpb.paypal.com/cgi-bin/webscr';
const vendeur = "maVraieAdresseBusiness@monsite.fr";
*/
error_reporting(E_ALL);
ini_set("log_errors", 1);
ini_set("display_errors", 0);
ini_set("error_log", realpath(dirname(__FILE__))."/log-error-php.log");
if (!count($_POST)){throw new Exception("Missing POST Data");}
$raw_post_data = file_get_contents('php://input');
$raw_post_array = explode('&', $raw_post_data);
$myPost = array();
$strlog='';
foreach ($raw_post_array as $keyval) {
$keyval = explode('=', $keyval);
if (count($keyval) == 2) {
// Since we do not want the plus in the datetime string to be encoded to a space, we manually encode it.
if ($keyval[0] === 'payment_date') {
if (substr_count($keyval[1], '+') === 1) {
$keyval[1] = str_replace('+', '%2B', $keyval[1]);
}
}
$myPost[$keyval[0]] = urldecode($keyval[1]);
$strlog .= $keyval[0].':'.$myPost[$keyval[0]]."\n"; // pour log-paypal qui stocke les transactions
}
}
// le script ne peut pas faire de echo, donc logue les données POST reçues pour voir à quoi ça ressemble
file_put_contents("log-paypal", $strlog."\n", FILE_APPEND);
// Build the body of the verification post request, adding the _notify-validate command
$req = 'cmd=_notify-validate';
$get_magic_quotes_exists = false;
if (function_exists('get_magic_quotes_gpc')) {
$get_magic_quotes_exists = true;
}
foreach ($myPost as $key => $value) {
if ($get_magic_quotes_exists == true && get_magic_quotes_gpc() == 1) {
$value = urlencode(stripslashes($value));
} else {
$value = urlencode($value);
}
$req .= "&$key=$value";
}
// Post the data back to PayPal, using curl. Throw exceptions if errors occur
$ch = curl_init(VERIFY_URI);
curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $req);
curl_setopt($ch, CURLOPT_SSLVERSION, 6);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
// This is often required if the server is missing a global cert bundle, or is using an outdated one
//if ($this->use_local_certs) {
// curl_setopt($ch, CURLOPT_CAINFO, __DIR__ . "/cert/cacert.pem");
//}
curl_setopt($ch, CURLOPT_FORBID_REUSE, 1);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'User-Agent: PHP-IPN-Verification-Script',
'Connection: Close',
));
$res = curl_exec($ch);
if (!($res)) {
$errno = curl_errno($ch);
$errstr = curl_error($ch);
curl_close($ch);
throw new Exception("cURL error: [$errno] $errstr");
}
$info = curl_getinfo($ch);
$http_code = $info['http_code'];
if ($http_code != 200) {throw new Exception("PayPal responded with http code $http_code");}
curl_close($ch);
// PayPal a validé la transaction pécunière
// récupère les données reçues pour ressortir les données de la commande
if($res === 'VERIFIED'){
// récupère les données reçues de Paypal...
$cmde = $_POST["item_name"];
$montant = $_POST["mc_gross"];
$txn_id = $_POST["txn_id"];
//$payer_email = $_POST["payer_email"]; // Attention un tiers ne peut payer pour un autre, sauf si on passe une référence commande en custom...
$custom = $_POST["custom"]; //id référence unique de la commande généré par la boutique
$probleme = '';
// Regarde le statut du paiement pour s'assurer que la transaction pécunière a été effectuée
$statut = $_POST["payment_status"];
if($statut != 'Completed') $probleme.='$_POST["payment_status"]='.$statut." != Completed\n";
// Vérifie que l'email du vendeur est bien le mien
if($_POST["receiver_email"] != vendeur) $probleme.='$_POST["receiver_email"]='.$_POST["receiver_email"].' != '.vendeur."\n";
// Vérifie que la devise est conforme à ce qui était prévu avant la transaction
if($_POST["mc_currency"]!='EUR') $probleme.='$_POST["mc_currency"]='.$_POST["mc_currency"]." != EUR\n";
// Données de simulation sans SQL ------------------------
$nomCmde = 'Blouson en daim';
$TTC = 74.35;
$myref = 'AF05D5BA48E8877DAAF';
// Vérifie que la transaction correspond à une commande du site
if($custom != $myref) $probleme.='$_POST["custom"]='.$custom." != $myref\n";
// Vérifie que le prix de la transaction correspond au montant de la commande en base de données
if($TTC != $montant) $probleme.='$_POST["mc_gross"]='.$montant." != $TTC\n";
// Regarde si le nom de la commande correspond bien à ce paiement
if($cmde != $nomCmde) $probleme.='$_POST["item_name"]='.$cmde." != $nomCmde\n";
// Données de simulation avec SQL -----------------------
// ... pour ressortir de la BDD les données de la commande, pour les comparer à ce qui a été reçu de Paypal.
// Le formulaire de départ contient : Référence de la commande/Nom de la commande/Montant, on connaît ces valeurs.
// "SELECT commande.id, commande.txid
// FROM commandes INNER JOIN client ON client.id=commandes.idclient
// WHERE commande.reference='$custom'
// AND commande.TTC=$montant
// AND commande.nom='$cmde';";
//$cmdeID = 1256; // Simulation de données sans SQL
//$cmdeTXID = ''; // TransactionID est théoriquement vide pour l'instant
//$myref = 'AF05D5BA48E8877DAAF'; //référence unique id de la commande généré par la boutique
// Si la requête SQL ne retourne aucun enregistrement, il y a un problème
//if(mysqli_num_rows($req)==0) $probleme.='SQL ne retourne aucun enregistrement pour cette transaction';// toutes les données reçues seront loguées pour enquête
// Regarde l'id de transaction pour s'assurer que la transaction n'a pas déjà été faite
//if($_POST["txn_id"]==$cmdeTXID) $probleme.='Paiement déjà effectué pour cette commande';
// Si problème → logue et prévient l'administrateur de la boutique par email
if($probleme){
file_put_contents("log-erreur-transaction", $probleme."\n$strlog\n\n",FILE_APPEND);
//mail(admin, );
}
//Sinon tout est nickel,
else{
// Marque sur le serveur la commande comme payée et en attente d'expédition, stocke l'id de transaction,
// $sql->execute("UPDATE commandes SET txid='$txn_id', statut=1 WHERE reference='$myref';");
// (donc dans la table si une commande a un id de transaction, ça signifie qu'elle a été payée). Statut 0:en attente de paiement. 1:payée en attente d'expédition 2:envoyée 3:reçue<
// Pour chaque produit de la commande, déduit le produit du stock
// SELECT idproduit, qte FROM details_commande WHERE refCommande='$myref';
// while(){
// $stock = $sql->execute("SELECT stock FROM produits WHERE idproduit=");
// $nouveauStock = $stock - $qtecommandee;
// $sql->execute("UPDATE stock SET qte='$nouveauStock' WHERE idproduit=");
// }
// Prévient le client par email "Le paiement de votre commande n°55 a été validé, la commande est en attente d'expédition",
//mail();
// prévient l'admin par email qu'une commande a été payée (et qu'il faut l'envoyer vite).
//mail(admin, );
}
}
else if($res === 'INVALID'){ // IPN invalid, log for manual investigation
file_put_contents("log-erreur-transaction", "IPN invalide\n$strlog\n\n",FILE_APPEND);
//mail(admin, );
}
// Reply with an empty 200 response to indicate to paypal the IPN was received correctly
header("HTTP/1.1 200 OK");
?>
TEST SANDBOX
À ce stade on doit tester un paiement fictif pour voir si ça fonctionne.Uploader sur un serveur (localhost ne marchera pas):
- notify.php à l'endroit IPN précisé sur le site Paypal,
- L'image 150x50 pixels harry.png,
- La page index de la boutique (fonctionne en localhost),
- La page "return" payment-ok.html "Merci pour votre achat."
TEST DE PAIEMENT FICTIF
Afficher le formulaire form.html en local ou par une URL de votre site
et cliquer sur ce fameux bouton "Acheter", Paypal propose de se connecter ou payer par carte bancaire:

Si le client clique sur "Annuler et retourner sur Test Store" il revient sur la page "cancel_return".
Se connecter avec le compte client Sandbox: client@machin.fr mot de passe azertyui
Le client arrive sur l'image perso et le joli bouton bleu du paiement:

Cliquer sur le montant bleu 74,35 EUR montre les détails.
Cliquer sur Terminer l'achat. Paypal appelle alors en arrière-plan le script de notification (communication serveur/serveur).
Cliquer sur détails indique le nom de la commande:
Cependant il est probable que le client ne clique pas dessus et ferme la page directement donc peu importe. La transaction se passe en arrière-plan.
Quelques secondes plus tard vous devriez avoir sur le serveur un fichier log-paypal signifiant que le serveur Paypal a invoqué votre script IPN.
Si vous ne recevez pas de messages de notification regarder ici, regarder si le log d'erreur existe et ce qu'il dit, regardez le log d'erreurs du serveur web.
notify.php ne peut être appelé qu'en POST.
Si ça marche, voici un exemple de données IPN reçues du serveur Paypal
pour une commande "Blouson en cuir" de 14.35€,
pour Jean Bon client@machin.fr,
paiement confirmé au vendeur vendeur@truc.fr
Paypal indique 0.74€ de frais de transaction, et l'id de transaction.
log-paypal
mc_gross:14.35 protection_eligibility:Eligible address_status:confirmed payer_id:L5VQFDLQ2ZUL8 address_street:Av. de la Pelouse, 87648672 Mayet payment_date:02:08:24 Apr 15, 2024 PDT payment_status:Completed charset:UTF-8 address_zip:75002 first_name:Jean mc_fee:0.74 address_country_code:FR address_name:Jean Bon notify_version:3.9 custom:AF05D5BA48E8877DAAF payer_status:verified business:vendeur@truc.fr address_country:France address_city:Paris quantity:1 verify_sign:AFbtoae17G1snrl8ZQGTOr-6vaYpAaLuXOmd.ihM9CpdMXpJ-FaYSzbh payer_email:client@machin.fr txn_id:8427561662008594T payment_type:instant last_name:Bon address_state:Alsace receiver_email:vendeur@truc.fr payment_fee:0.74 shipping_discount:0.00 insurance_amount:0.00 receiver_id:PAP8U9ASPTCEW txn_type:web_accept item_name:Blouson en cuir discount:0.00 mc_currency:EUR item_number: residence_country:FR test_ipn:1 shipping_method:Default transaction_subject: payment_gross: ipn_track_id:f825995a7ef92Liste des variables IPN
Par contre ce n'est pas franchement instantané, il faut parfois attendre quelques longues secondes.
Donc ne pas dire trop tôt au client sur la page "return" que le paiement est validé.
Plutôt lui signifier que le paiement est en cours de validation par Paypal, qu'il repasse plus tard dans "Mes commandes" pour voir le statut de la commande, et qu'il sera informé par email, par Paypal et par nous-mêmes, lorsque Paypal aura effectivement procédé la transaction.
PASSAGE EN PRODUCTION
1. Modifier quelques lignes du formulaire en passant les valeurs de prod:<form action="https://www.paypal.com/cgi-bin/webscr">
<input type="hidden" name="business" value="votre-vraie-adresse@paypal-business.fr">
2. Modifier notify.php en commentant les données SANDBOX et en décommentant les données PROD.
3. Faire un test sur une commande à 0.99€, pour être sûr que tout fonctionne en prod. Normalement oui mais la confiance n'exclut pas le contrôle.
POUR ALLER PLUS LOIN
- Ajoutez dans le formulaire le champ shipping (frais de port) pour qu'il apparaisse dans "Détails". Du coup amount représente le prix des produits sans frais de port, Paypal les additionnera pour que mc_gross représente le montant TTC.
- Le client peut ajouter des articles au panier Paypal avec le Shopping Cart
"En espérant que vous avez apprécié le tutoriel (sans pub, ni cookies, ni appel aux dons), en vous souhaitant plein de ventes, un très gros chiffre d'affaire avec une marge mirobolante, un bénéfice gargantuesque et quelques miettes après impôt." - B.M.