NeuroAgent

Why is YooKassa not updating the database?

YooKassa issue: Webhooks are delivered successfully (200 OK), but MySQL database is not updating. Learn the causes and solutions for two projects simultaneously.

Question

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
<?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
<?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?

NeuroAgent

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

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:

php
// 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:

php
// 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:

sql
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:

php
// 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:

php
$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:

php
echo 'PHP Version: ' . phpversion();

4. Database Connection Optimization

Add connection parameters to improve stability:

php
// 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

php
// 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

php
// Amount check and normalization
$amount = floatval($data['object']['amount']['value']);
if ($amount <= 0) {
    http_response_code(400);
    exit('Invalid amount');
}

3. Improved Subscription Handling

php
// 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:

php
// 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:

php
// 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:

sql
-- 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:

  1. Hosting configuration - check MySQL limits and PHP parameters
  2. Database connection file - add logging for diagnosis
  3. Access rights - ensure the MySQL user has all necessary privileges
  4. Error handling - modify the code with complete exception handling
  5. Temporary failures - implement a retry mechanism

Recommended actions:

  1. Add detailed logging at all points of database interaction
  2. Check MySQL user access permissions
  3. Implement error handling and transactions
  4. Set up monitoring and failure notifications
  5. 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.

Sources

  1. YooKassa PHP SDK Documentation
  2. PHP MySQL Error Handling Best Practices
  3. Webhook to MySQL Integration Guide
  4. Database Transaction Management
  5. PHP Connection Timeout Settings