Why has YooKassa stopped working? On two projects simultaneously, data in the MySQL database is no longer updating when payments are made through YooKassa, even though the handler returns a 200 OK response. YooKassa support claims that notifications are being received 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 duplicate 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
/**
* YOOKASSA CALLBACK TEST EMULATOR
*/
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", // any ID (unique for test)
"status" => "succeeded",
"paid" => true,
"amount" => [
"value" => "39.00",
"currency" => "RUB"
],
"metadata" => [
"nickname" => "legolas"
],
"description" => "Test payment",
]
];
// JSON in the same format as YooKassa
$jsonData = json_encode($fakeCallbackData, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
// Initiate 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(), // as in YooKassa
"User-Agent: AHC/2.1",
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonData);
curl_setopt($ch, CURLOPT_HEADER, true); // to get response headers
// Execute 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. Both projects are hosted on Jino hosting. YooKassa support confirms receiving notifications with a 200 code, but the problem persists. What could be the cause?
Problem with YooKassa on Two Projects Simultaneously on Jino Hosting, with a Handler that Returns 200 OK but Doesn’t Update the Database, Likely Related to Webhook Features on Shared Hosting
Table of Contents
- Main Issues on Shared Hosting
- Timeout and Connection Settings
- Webhook Handler Optimization
- Diagnosis and Logging
- Jino Hosting Solutions
Main Issues on Shared Hosting
On shared hosting services like Jino, webhooks face several typical problems:
1. MySQL Connection Timeouts
Based on research, the most likely cause is MySQL connection timeouts. On shared hosting servers, connections are often closed when inactive to conserve resources.
Problem: Your handler performs multiple database operations, and if more time passes than allowed by the MySQL server between operations, the connection is broken. Subsequent queries execute “blindly,” and the script completes with a 200 code but without actually performing the operations.
Solution: Add a connection check before each query:
// Connection check function
function check_connection($db) {
try {
if (!$db->ping()) {
$db->close();
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/connect.php';
}
} catch (Exception $e) {
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/connect.php';
}
}
// Usage in the handler
check_connection($db);
2. Script Execution Limits
Shared hosting often has restrictions on execution time and memory. Webhooks from YooKassa may be called with delays, increasing execution time.
Timeout and Connection Settings
1. Increasing Timeouts in PHP
Add the following to the beginning of your handler script:
// Setting timeouts
set_time_limit(60); // 60 seconds instead of standard 30
ini_set('max_execution_time', 60);
ini_set('memory_limit', '256M');
2. Using Persistent Connections
Modify your database connection file:
// In /inc/connect.php file
$db = new mysqli('p:' . $db_host, $db_user, $db_password, $db_name, $db_port);
Note: The
p:parameter before the hostname enables persistent connections, which are not closed after script completion.
3. Configuring wait_timeout in MySQL
If you have access to phpMyAdmin or SSH, increase wait_timeout:
-- Temporary solution during debugging
SET GLOBAL wait_timeout = 300;
Webhook Handler Optimization
1. Simplifying Logic
Divide the logic into smaller operations with connection checks:
// After field validation and before any database operations
check_connection($db);
// Check for duplicates
$stmt = $db->prepare("SELECT id FROM payments WHERE payment_id = ?");
if (!$stmt) {
log_error("Prepare failed: " . $db->error);
http_response_code(500);
exit('Database error');
}
// ... rest of the code with checks
2. Error Handling
Add detailed error logging:
// Error logging function
function log_error($message) {
$log_file = $_SERVER['DOCUMENT_ROOT'] . '/logs/webhook_errors.log';
$timestamp = date('Y-m-d H:i:s');
$log_message = "[$timestamp] ERROR: $message\n";
file_put_contents($log_file, $log_message, FILE_APPEND);
}
// Example usage
try {
$stmt = $db->prepare("UPDATE users SET subscribe_date = ? WHERE nickname = ?");
$stmt->bind_param("sis", $new_subscribe_date, $nickname);
$stmt->execute();
} catch (Exception $e) {
log_error("Database update failed: " . $e->getMessage());
http_response_code(500);
exit('Database error');
}
3. Using Transactions
Wrap all database operations in a transaction:
$db->begin_transaction();
try {
// All database operations
$db->commit();
} catch (Exception $e) {
$db->rollback();
log_error("Transaction failed: " . $e->getMessage());
http_response_code(500);
exit('Transaction error');
}
Diagnosis and Logging
1. Adding Detailed Logging
Modify your handler to log all operations:
// At the beginning of the handler
$log_data = [
'timestamp' => date('Y-m-d H:i:s'),
'payment_id' => $payment_id ?? 'N/A',
'status' => $status ?? 'N/A',
'nickname' => $nickname ?? 'N/A',
'amount' => $amount ?? 'N/A',
'ip' => $_SERVER['REMOTE_ADDR'],
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'N/A'
];
file_put_contents($_SERVER['DOCUMENT_ROOT'] . '/logs/webhook.log', json_encode($log_data) . "\n", FILE_APPEND);
// After each database operation
file_put_contents($_SERVER['DOCUMENT_ROOT'] . '/logs/db_operations.log',
date('Y-m-d H:i:s') . " - Executing: " . $stmt->error . "\n", FILE_APPEND);
2. Checking Connection Status
Add connection diagnostics:
// Check database connection status
if (!$db->ping()) {
file_put_contents($_SERVER['DOCUMENT_ROOT'] . '/logs/connection_errors.log',
date('Y-m-d H:i:s') . " - Connection lost\n", FILE_APPEND);
http_response_code(500);
exit('Connection lost');
}
Jino Hosting Solutions
1. Contacting Jino Support
Contact Jino support and request:
- Check MySQL timeout settings
- Increase PHP script execution limits
- Check web server error logs
Important: Specify that the issue occurs specifically when processing webhooks from external services.
2. Alternative Approach: Asynchronous Processing
If the issue is with timeouts, consider asynchronous processing:
// Quick response to YooKassa
http_response_code(200);
echo 'OK';
exit;
// Then perform background processing
ignore_user_abort(true);
set_time_limit(0);
// Main processing logic
// ...
3. Using Queues
Implement a simple queue for payment processing:
// First, write to the queue
$stmt = $db->prepare("INSERT INTO payment_queue (payment_id, nickname, amount, status, processed) VALUES (?, ?, ?, ?, 0)");
$stmt->bind_param("ssds", $payment_id, $nickname, $amount, $status);
$stmt->execute();
// Separate cron script for queue processing
4. Switching to Different Hosting
If the issue is systemic and cannot be resolved, consider migrating to hosting with better webhook support, for example:
- VPS or dedicated server
- Hostings specializing in e-commerce
- Cloud solutions (AWS, Google Cloud)
Sources
- YooKassa - Notifications — Payment acceptance via the YooMoney API
- Connection timed out when trying to connect to MySQL database with PHP - Hosting Support
- php - mysqli_connect(): (HY000/2002): Connection timed out on hosting - Stack Overflow
- How to solve PHP database link timeout issue? (php database connection timeout) - USAVPS.COM
- Set the connection timeout when using PHP | Cloud SQL for MySQL | Google Cloud
- MySQL connection timeouts - Database Administrators Stack Exchange
Conclusion
- Most likely cause - MySQL connection timeouts on Jino’s shared hosting
- First step - add connection checks and detailed logging for diagnosis
- Second step - configure persistent connections and increase PHP timeouts
- If the problem persists - contact Jino support with detailed logs
- Long-term solution - consider migrating to VPS or cloud hosting for stable webhook operation
Start by adding connection checks and logging - this will help pinpoint exactly where the failure occurs in the payment processing chain.