Abbiamo implementato un’integrazione diretta e performante per gestire il ciclo di vita delle sottoscrizioni SaaS, collegando i webhook di Stripe al database MySQL di SuiteCRM.
A differenza delle soluzioni low-code standard, abbiamo optato per uno script PHP custom per avere controllo totale sulla logica dei dati e sulle performance.
⚙️ Workflow Tecnico:
Il webhook ascolta l’evento invoice.payment_succeeded ed esegue le seguenti operazioni atomiche:
- Payload Parsing: Gestione nativa del JSON di Stripe con normalizzazione dei dati (conversione timestamp Unix -> Date, cents -> decimali).
- Logica Condizionale Smart:
- Check esistenza: Lookup su tabelle
accountseemail_addressesper evitare duplicati. - Relazioni: Creazione automatica delle relazioni Many-to-Many tra Account e Contacts.
- Ghost Record Prevention: Popolamento diretto delle tabelle
_cstmper garantire la visibilità immediata dei record nelle liste del CRM.
- Check esistenza: Lookup su tabelle
- Gestione Contratti (AOS): Insert diretto nel modulo
AOS_Contractscon calcolo automatico della Renewal Reminder Date (-30 giorni dalla scadenza).
🛠 Stack: PHP (PDO), MySQL, SuiteCRM v7/v8 Schema.
Una soluzione robusta che bypassa la latenza delle API per un inserimento dati immediato e sicuro.

<?php
/**
* Webhook per Stripe -> SuiteCRM
* Versione: 3.1
*/
// --- CONFIGURAZIONE AGGIORNATA ---
$config = [
'db_host' => 'localhost',
'db_name' => 'XXXXX', // Nome DB Corretto
'db_user' => 'XXXX', // Nome utente db
'db_password' => 'XXXX',
'api_key' => 'XXXXXXXXXX',
'default_user_id' => '1',
'currency_id' => '-99'
];
// --- HELPER FUNCTIONS ---
function writeLog($message) {
$date = date('Y-m-d H:i:s');
$logFile = __DIR__ . '/stripe_receiver_log.txt';
file_put_contents($logFile, "[$date] $message" . PHP_EOL, FILE_APPEND);
}
function generateUuid() {
if (function_exists('random_bytes')) { $data = random_bytes(16); }
else { $data = openssl_random_pseudo_bytes(16); }
$data[6] = chr(ord($data[6]) & 0x0f | 0x40);
$data[8] = chr(ord($data[8]) & 0x3f | 0x80);
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
}
function sendJsonResponse($data, $statusCode = 200) {
http_response_code($statusCode);
header('Content-Type: application/json');
echo json_encode($data);
exit;
}
// --- INIZIO ESECUZIONE ---
$headers = getallheaders();
$apiKeyInput = $_GET['api_key'] ?? $headers['X-Api-Key'] ?? null;
if ($apiKeyInput !== $config['api_key']) {
writeLog("Tentativo non autorizzato. IP: " . $_SERVER['REMOTE_ADDR']);
sendJsonResponse(['error' => 'Unauthorized'], 401);
}
try {
$input = file_get_contents('php://input');
$event = json_decode($input, true);
if (!$event) {
if ($_SERVER['REQUEST_METHOD'] === 'GET') { echo "Webhook attivo su iabasuite2."; exit; }
throw new Exception("Payload JSON vuoto.");
}
if (isset($event['type']) && $event['type'] !== 'invoice.payment_succeeded') {
writeLog("Evento ignorato: " . $event['type']);
sendJsonResponse(['message' => 'Event ignored'], 200);
}
// --- ESTRAZIONE DATI ---
$dataObject = $event['data']['object'];
$stripeEmail = $dataObject['customer_email'] ?? null;
$stripeName = $dataObject['customer_name'] ?? $stripeEmail;
$amountPaid = $dataObject['amount_paid'] ?? 0;
// Logic Date: se scadenza <= inizio, forza +1 anno
$periodStartUnix = $dataObject['lines']['data'][0]['period']['start'] ?? time();
$periodEndUnix = $dataObject['lines']['data'][0]['period']['end'] ?? 0;
if ($periodEndUnix <= $periodStartUnix) {
$periodEndUnix = strtotime('+1 year', $periodStartUnix);
}
$startDate = date('Y-m-d', $periodStartUnix);
$endDate = date('Y-m-d', $periodEndUnix);
$contractValue = $amountPaid / 100;
writeLog("Processing: $stripeEmail | DB: {$config['db_name']} | User: {$config['db_user']}");
$db = new PDO(
"mysql:host={$config['db_host']};dbname={$config['db_name']};charset=utf8mb4",
$config['db_user'],
$config['db_password'],
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
$now = date('Y-m-d H:i:s');
$userId = $config['default_user_id'];
$accountId = null;
$contactId = null;
// --- 1. ACCOUNT ---
$stmt = $db->prepare("SELECT id FROM accounts WHERE name = ? AND deleted = 0");
$stmt->execute([$stripeName]);
$accountRow = $stmt->fetch(PDO::FETCH_ASSOC);
if ($accountRow) {
$accountId = $accountRow['id'];
writeLog("Account esistente: $accountId");
} else {
$accountId = generateUuid();
writeLog("Nuovo Account: $stripeName");
$stmt = $db->prepare("INSERT INTO accounts (id, name, date_entered, date_modified, created_by, assigned_user_id, deleted) VALUES (?, ?, ?, ?, ?, ?, 0)");
$stmt->execute([$accountId, $stripeName, $now, $now, $userId, $userId]);
// Fix Custom Fields Table
try { $db->prepare("INSERT INTO accounts_cstm (id_c) VALUES (?)")->execute([$accountId]); } catch(Exception $e){}
}
// --- 2. CONTACT ---
if ($stripeEmail) {
$stmt = $db->prepare("SELECT id FROM email_addresses WHERE email_address = ? AND deleted = 0");
$stmt->execute([$stripeEmail]);
$emailRow = $stmt->fetch(PDO::FETCH_ASSOC);
if ($emailRow) {
$emailId = $emailRow['id'];
$stmt = $db->prepare("SELECT bean_id FROM email_addr_bean_rel WHERE email_address_id = ? AND bean_module = 'Contacts' AND deleted = 0");
$stmt->execute([$emailId]);
$relRow = $stmt->fetch(PDO::FETCH_ASSOC);
if ($relRow) $contactId = $relRow['bean_id'];
}
if (!$contactId) {
$contactId = generateUuid();
writeLog("Nuovo Contatto: $stripeEmail");
$parts = explode(' ', $stripeName, 2);
$fName = $parts[0];
$lName = $parts[1] ?? '-';
$stmt = $db->prepare("INSERT INTO contacts (id, first_name, last_name, date_entered, date_modified, created_by, assigned_user_id, deleted) VALUES (?, ?, ?, ?, ?, ?, ?, 0)");
$stmt->execute([$contactId, $fName, $lName, $now, $now, $userId, $userId]);
// Fix Custom Fields Table
try { $db->prepare("INSERT INTO contacts_cstm (id_c) VALUES (?)")->execute([$contactId]); } catch(Exception $e){}
if (!$emailRow) {
$emailId = generateUuid();
$stmt = $db->prepare("INSERT INTO email_addresses (id, email_address, email_address_caps, date_created, deleted) VALUES (?, ?, ?, ?, 0)");
$stmt->execute([$emailId, $stripeEmail, strtoupper($stripeEmail), $now]);
} else {
$emailId = $emailRow['id'];
}
$relId = generateUuid();
$stmt = $db->prepare("INSERT INTO email_addr_bean_rel (id, email_address_id, bean_id, bean_module, primary_address, date_created, deleted) VALUES (?, ?, ?, 'Contacts', 1, ?, 0)");
$stmt->execute([$relId, $emailId, $contactId, $now]);
}
}
// --- 3. LINK ACCOUNT-CONTACT ---
if ($accountId && $contactId) {
$stmt = $db->prepare("SELECT id FROM accounts_contacts WHERE account_id = ? AND contact_id = ? AND deleted = 0");
$stmt->execute([$accountId, $contactId]);
if (!$stmt->fetch()) {
$acRelId = generateUuid();
$stmt = $db->prepare("INSERT INTO accounts_contacts (id, account_id, contact_id, date_modified, deleted) VALUES (?, ?, ?, ?, 0)");
$stmt->execute([$acRelId, $accountId, $contactId, $now]);
}
}
// --- 4. CONTRACT ---
$contractId = generateUuid();
$contractName = "SaaS Subscription - " . date('Y') . " - " . $stripeName;
$contractType = 'Service';
$status = 'Signed';
$renewalDate = date('Y-m-d', strtotime($endDate . ' -30 days'));
$sqlContract = "INSERT INTO aos_contracts
(id, name, date_entered, date_modified, modified_user_id, created_by, assigned_user_id, deleted,
total_contract_value, start_date, end_date, status, renewal_reminder_date,
currency_id, contract_account_id, contact_id, contract_type)
VALUES
(?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
$stmt = $db->prepare($sqlContract);
$stmt->execute([
$contractId, $contractName, $now, $now, $userId, $userId, $userId,
$contractValue, $startDate, $endDate, $status, $renewalDate,
$config['currency_id'], $accountId, $contactId, $contractType
]);
// Fix Custom Fields Table
try {
$db->prepare("INSERT INTO aos_contracts_cstm (id_c) VALUES (?)")->execute([$contractId]);
} catch (Exception $e) {
// Ignora se la tabella non esiste
}
writeLog("Contratto creato su $config[db_name]: $contractId");
sendJsonResponse(['success' => true, 'contract_id' => $contractId]);
} catch (Exception $e) {
writeLog("CRITICAL ERROR: " . $e->getMessage());
sendJsonResponse(['error' => $e->getMessage()], 500);
}
?>