Why has YooKassa stopped working? On two projects simultaneously, data in the MySQL database has stopped updating after successful payments through YooKassa, although the handler returns a 200 OK response. YooKassa support confirms that notifications are being delivered successfully, but the data in the database is not being updated and payment records are not being created.
Here is the YooKassa callback handler code:
<?php
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/connect.php';
session_start();
// Set Moscow timezone (UTC+3)
date_default_timezone_set('Europe/Moscow');
$input = file_get_contents('php://input');
$data = json_decode($input, true);
// Check required fields
if (!isset($data['event'], $data['object']['id'], $data['object']['status'], $data['object']['metadata']['nickname'])) {
http_response_code(400);
exit('Invalid request');
}
$payment_id = $data['object']['id'];
$status = $data['object']['status'];
$nickname = $data['object']['metadata']['nickname'];
// Process only successful payments
if ($data['event'] !== 'payment.succeeded' || $status !== 'succeeded') {
http_response_code(200);
exit('No processing required');
}
// Protection against re-processing
$stmt = $db->prepare("SELECT id FROM payments WHERE payment_id = ?");
$stmt->bind_param("s", $payment_id);
$stmt->execute();
$stmt->store_result();
if ($stmt->num_rows > 0) {
http_response_code(200);
exit('Already processed');
}
$stmt->close();
// Get amount
$amount = $data['object']['amount']['value'];
// Map tariff by amount
$tariffs = [
'39.00' => ['subscribe_days' => 7, 'subscribe_rates' => 2],
'97.00' => ['subscribe_days' => 7, 'subscribe_rates' => 3],
'69.00' => ['subscribe_days' => 14, 'subscribe_rates' => 2],
'176.00' => ['subscribe_days' => 14, 'subscribe_rates' => 3],
'118.00' => ['subscribe_days' => 30, 'subscribe_rates' => 2],
'249.00' => ['subscribe_days' => 30, 'subscribe_rates' => 3],
'290.00' => ['subscribe_days' => 90, 'subscribe_rates' => 2],
'689.00' => ['subscribe_days' => 90, 'subscribe_rates' => 3],
];
if (!isset($tariffs[$amount])) {
http_response_code(400);
exit('Unknown amount');
}
$subscribe_days = $tariffs[$amount]['subscribe_days'];
$subscribe_rates = $tariffs[$amount]['subscribe_rates'];
// Get user's current subscription
$stmt = $db->prepare("SELECT subscribe_date FROM users WHERE nickname = ?");
$stmt->bind_param("s", $nickname);
$stmt->execute();
$stmt->bind_result($current_subscribe_date);
$stmt->fetch();
$stmt->close();
// Calculate new subscription date with time
$now = new DateTime();
$subscribe_end = new DateTime($current_subscribe_date);
if ($now > $subscribe_end) {
$new_subscribe_date = $now->modify("+$subscribe_days days")->format('Y-m-d H:i:s');
} else {
$new_subscribe_date = $subscribe_end->modify("+$subscribe_days days")->format('Y-m-d H:i:s');
}
// Update user data
$stmt = $db->prepare("UPDATE users SET subscribe_date = ?, subscribe_rate = ? WHERE nickname = ?");
$stmt->bind_param("sis", $new_subscribe_date, $subscribe_rates, $nickname);
$stmt->execute();
$stmt->close();
// Save payment information
$stmt = $db->prepare("INSERT INTO payments (payment_id, nickname, amount, status) VALUES (?, ?, ?, ?)");
$stmt->bind_param("ssds", $payment_id, $nickname, $amount, $status);
$stmt->execute();
$stmt->close();
http_response_code(200);
echo 'OK';
?>
Test file that correctly processes requests:
<?php
header('Content-Type: text/plain; charset=utf-8');
// URL where we send the "webhook" — your YooKassa handler
$callbackUrl = 'https://site.ru/pay.php';
// test JSON completely repeats YooKassa structure
$fakeCallbackData = [
"event" => "payment.succeeded",
"object" => [
"id" => "2e4f82c2-000f-5010-9000-29fca13db031",
"status" => "succeeded",
"paid" => true,
"amount" => [
"value" => "39.00",
"currency" => "RUB"
],
"metadata" => [
"nickname" => "legolas"
],
"description" => "Test payment",
]
];
// JSON in the format as expected by YooKassa
$jsonData = json_encode($fakeCallbackData, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
// Initialize CURL
$ch = curl_init($callbackUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Content-Type: application/json",
"Idempotence-Key: " . time(),
"User-Agent: AHC/2.1",
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonData);
curl_setopt($ch, CURLOPT_HEADER, true);
// Execute the request
$response = curl_exec($ch);
$errno = curl_errno($ch);
$error = curl_error($ch);
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$responseHeaders = substr($response, 0, $headerSize);
$responseBody = substr($response, $headerSize);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$totalTime = curl_getinfo($ch, CURLINFO_TOTAL_TIME);
curl_close($ch);
// Display results
echo "===== SENT JSON =====\n$jsonData\n\n";
echo "===== URL SENT TO =====\n$callbackUrl\n\n";
echo "===== HTTP CODE =====\n$httpCode\n\n";
echo "===== REQUEST TIME =====\n{$totalTime} sec.\n\n";
echo "===== RESPONSE HEADERS =====\n$responseHeaders\n\n";
echo "===== PAYMENT NOTICE RESPONSE BODY =====\n$responseBody\n\n";
if ($errno) {
echo "===== CURL ERROR =====\n$errno: $error\n\n";
}
echo "===== TEST COMPLETED =====\n";
?>
During manual testing, everything works correctly, but with real payments, the data is not being updated. Jino hosting claims there are no problems on their end. YooKassa support confirms delivery of notifications with a 200 OK response. The problem occurred simultaneously on two projects hosted on the same hosting. What could be the cause?
YooKassa Issue: Webhooks Delivered Successfully (200 OK) But MySQL Database Not Updated
The problem with YooKassa, where webhooks are delivered successfully (200 OK) but the MySQL database is not updated, especially when occurring simultaneously on two projects, indicates a systemic issue. Here are the main possible causes and solutions:
Table of Contents
- Main Problem Causes
- Diagnosis and Verification
- Solutions and Fixes
- Handler Code Optimization
- Preventive Measures
- Conclusion
Main Problem Causes
1. Database Connection Issues
The most likely cause is problems with the database connection file /inc/connect.php. In a production environment, the following issues may occur:
- Temporary connection failures with MySQL
- Connection limits on simultaneous connections
- Connection timeouts under high load
2. Lack of Error Handling
Your code lacks MySQL error handling. If an error occurs when executing a query, the script will continue running and return 200 OK, but the database will not be updated.
3. Access Rights Issues
The MySQL user may not have sufficient rights to perform UPDATE and INSERT operations on the required tables.
4. Simultaneous Processing Conflicts
If two webhooks are being processed simultaneously for the same user, a race condition may occur.
5. Timing Issues
YooKassa may send webhooks before the payment is fully processed in their system, leading to data inconsistency.
Diagnosis and Verification
1. Connection File Verification
Add logging to the /inc/connect.php file:
// At the beginning of the connection file
error_log("Database connection attempt at: " . date('Y-m-d H:i:s'));
// After establishing the connection
if (!$db) {
error_log("Database connection failed: " . mysqli_connect_error());
die("Database connection error");
}
error_log("Database connection successful");
2. Logging All Operations in Callback
Modify your handler with detailed logging:
// Enable logging at the beginning of the script
file_put_contents('yookassa_log.txt', date('Y-m-d H:i:s') . " - Received data: " . $input . "\n", FILE_APPEND);
// After each database query
file_put_contents('yookassa_log.txt', date('Y-m-d H:i:s') . " - DB query executed: $sql\n", FILE_APPEND);
// On errors
if ($stmt->error) {
file_put_contents('yookassa_log.txt', date('Y-m-d H:i:s') . " - DB Error: " . $stmt->error . "\n", FILE_APPEND);
}
3. Access Rights Verification
Check MySQL user permissions:
SHOW GRANTS FOR 'your_user'@'localhost';
Ensure the user has SELECT, INSERT, UPDATE privileges on the required tables.
Solutions and Fixes
1. Adding Error Handling
Modify your code with comprehensive error handling:
// Database connection handling
if (!$db) {
http_response_code(500);
file_put_contents('yookassa_error.log', date('Y-m-d H:i:s') . " - Database connection failed\n", FILE_APPEND);
exit('Database error');
}
// Handling each query
$stmt = $db->prepare("SELECT id FROM payments WHERE payment_id = ?");
if (!$stmt) {
http_response_code(500);
file_put_contents('yookassa_error.log', date('Y-m-d H:i:s') . " - Prepare failed: " . $db->error . "\n", FILE_APPEND);
exit('Database error');
}
if (!$stmt->execute()) {
http_response_code(500);
file_put_contents('yookassa_error.log', date('Y-m-d H:i:s') . " - Execute failed: " . $stmt->error . "\n", FILE_APPEND);
exit('Database error');
}
2. Using Transactions
Wrap database operations in a transaction:
$db->begin_transaction();
try {
// All database operations here
$db->commit();
http_response_code(200);
echo 'OK';
} catch (Exception $e) {
$db->rollback();
http_response_code(500);
file_put_contents('yookassa_error.log', date('Y-m-d H:i:s') . " - Transaction failed: " . $e->getMessage() . "\n", FILE_APPEND);
exit('Transaction error');
}
3. PHP Version Check
According to the YooKassa SDK documentation, PHP 8.0+ is required. Check the PHP version on your hosting:
echo 'PHP Version: ' . phpversion();
4. Database Connection Optimization
Add connection parameters to improve stability:
// In the connection file
$db = new mysqli($host, $user, $pass, $db_name);
// Set parameters
$db->set_charset("utf8mb4");
$db->options(MYSQLI_OPT_CONNECT_TIMEOUT, 10);
$db->options(MYSQLI_OPT_READ_TIMEOUT, 10);
Handler Code Optimization
1. Secure Data Retrieval
// JSON validity check
if (json_last_error() !== JSON_ERROR_NONE) {
http_response_code(400);
file_put_contents('yookassa_error.log', date('Y-m-d H:i:s') . " - JSON decode error\n", FILE_APPEND);
exit('Invalid JSON');
}
// Secure value retrieval
$payment_id = $data['object']['id'] ?? null;
$status = $data['object']['status'] ?? null;
$nickname = $data['object']['metadata']['nickname'] ?? null;
if (!$payment_id || !$status || !$nickname) {
http_response_code(400);
exit('Missing required fields');
}
2. Payment Amount Verification
// Amount check and normalization
$amount = floatval($data['object']['amount']['value']);
if ($amount <= 0) {
http_response_code(400);
exit('Invalid amount');
}
3. Improved Subscription Handling
// Safe date operations
try {
$now = new DateTime('Europe/Moscow');
$current_subscribe_date = new DateTime($current_subscribe_date);
if ($now > $current_subscribe_date) {
$new_subscribe_date = $now->modify("+$subscribe_days days")->format('Y-m-d H:i:s');
} else {
$new_subscribe_date = $current_subscribe_date->modify("+$subscribe_days days")->format('Y-m-d H:i:s');
}
} catch (Exception $e) {
http_response_code(500);
file_put_contents('yookassa_error.log', date('Y-m-d H:i:s') . " - Date error: " . $e->getMessage() . "\n", FILE_APPEND);
exit('Date processing error');
}
Preventive Measures
1. Monitoring and Alerts
Set up monitoring for webhook processing success:
// Send notifications on errors
if (http_response_code() !== 200) {
// Send notification to Telegram or Email
$message = "YooKassa webhook error at " . date('Y-m-d H:i:s') . "\n";
$message .= "Response code: " . http_response_code() . "\n";
$message .= "Error: " . $error;
// Use your notification method
sendNotification($message);
}
2. Retry Mechanism
For temporary failures, implement a queue with retry attempts:
// Check for existing payment with delay
$stmt = $db->prepare("SELECT id FROM payments WHERE payment_id = ?");
$stmt->bind_param("s", $payment_id);
$stmt->execute();
$stmt->store_result();
if ($stmt->num_rows > 0) {
$stmt->close();
http_response_code(200);
exit('Already processed');
}
$stmt->close();
// If error, add to retry queue
if ($db->error) {
// Save to retry_queue table
$retry_stmt = $db->prepare("INSERT INTO retry_queue (payment_id, data, attempts, created_at) VALUES (?, ?, ?, NOW())");
$retry_stmt->bind_param("ssi", $payment_id, $input, $attempts);
$retry_stmt->execute();
$retry_stmt->close();
}
3. Regular Data Verification
Set up daily data checks:
-- Script for checking inconsistencies
SELECT p.* FROM payments p
LEFT JOIN users u ON p.nickname = u.nickname
WHERE p.status = 'succeeded' AND (u.subscribe_date IS NULL OR u.subscribe_rate IS NULL);
Conclusion
The YooKassa problem occurring simultaneously on two projects indicates a system error, most likely related to:
- Hosting configuration - check MySQL limits and PHP parameters
- Database connection file - add logging for diagnosis
- Access rights - ensure the MySQL user has all necessary privileges
- Error handling - modify the code with complete exception handling
- Temporary failures - implement a retry mechanism
Recommended actions:
- Add detailed logging at all points of database interaction
- Check MySQL user access permissions
- Implement error handling and transactions
- Set up monitoring and failure notifications
- Contact hosting support for MySQL logs
A problem occurring simultaneously on two projects is most likely related to changes at the hosting level or shared database settings, rather than your code logic.