Paynow Callback Notifications (IPN) Not Reaching resulturl - PHP Integration Issue


#1

I’m encountering a persistent issue with receiving callback notifications (IPN/Status Updates) from Paynow to my designated resulturl using PHP SDK. Paynow callback POST notifications are not reaching my resulturl at /paynow_update.php after transactions are completed or cancelled. My server’s PHP error logs and web server access logs show no indication that paynow_update.php is being triggered by Paynow’s server. My server is running HTTPS, and the paynow_update.php endpoint responds with a 200 OK status when directly accessed via POST (verified by curl or similar tools, although this is not the expected flow from Paynow)

The Integration ID IS : 20903

A test payment is initiated from my website, successfully redirecting the user to Paynow’s payment page

As per Paynow documentation, a POST notification is expected to be sent to my paynow callback url which is paynow_update.php`. i have added logs at the beggining of this file but its not popping in the logs meaning the script is not running. I am pasting the script here, please help me out

<?php error_log("--- Paynow Update Script Triggered ---"); ini_set('display_errors', 1); ini_set('display_startup_errors', 1); error_reporting(E_ALL); require_once __DIR__ . '/../vendor/autoload.php'; require_once __DIR__ . '/db_connection.php'; try { $dotenv = Dotenv\Dotenv::createImmutable(__DIR__ . '/..'); $dotenv->load(); } catch (\Dotenv\Exception\InvalidPathException $e) { error_log("Paynow Update Script Dotenv Error: " . $e->getMessage()); echo "Error loading environment configuration."; exit(); } $integrationId = $_ENV['PAYNOW_INTEGRATION_ID'] ?? null; $integrationKey = $_ENV['PAYNOW_INTEGRATION_KEY'] ?? null; if (!$integrationId || !$integrationKey) { $error_message = 'Paynow integration credentials are not set in the .env file or environment.'; error_log("Paynow Update Script Credentials Error: " . $error_message); echo "Error processing payment update (credentials missing)."; exit(); } $paynowData = $_POST; error_log("Paynow Update Script Received POST Data: " . print_r($paynowData, true)); $pollUrl = $paynowData['pollurl'] ?? null; if (!$pollUrl) { error_log("Paynow Update Script Error: Missing 'pollurl' in received data. This script expects Paynow to send it."); echo "Error processing payment update (missing poll URL)."; exit(); } try { $placeholderResultUrl = 'http://example.com/result'; $placeholderReturnUrl = 'http://example.com/return'; $paynow = new Paynow\Payments\Paynow( $integrationId, $integrationKey, $placeholderResultUrl, $placeholderReturnUrl ); $statusResponse = $paynow->pollTransaction($pollUrl); error_log("Paynow Update Script Status Response for Poll URL " . $pollUrl . ": " . print_r($statusResponse, true)); if ($statusResponse && method_exists($statusResponse, 'status')) { $transactionStatus = $statusResponse->status(); $paynowReference = $statusResponse->paynowReference(); $orderReference = $statusResponse->reference(); error_log("Paynow Update Script SUCCESS Status Response Object Structure for Poll URL " . $pollUrl . ": " . print_r($statusResponse, true)); $paymentMethod = "N/A"; if (method_exists($statusResponse, 'getPaymentChannel')) { $paymentMethod = $statusResponse->getPaymentChannel(); } else if (isset($statusResponse->paymentChannel)) { $paymentMethod = $statusResponse->paymentChannel; } else { error_log("Paynow Update Script Warning: 'paymentchannel' information not available in status response. Ensure your Paynow account is configured for 'return payment instrument details' if needed."); } $update_stmt = $db->prepare("UPDATE orders SET status = ?, paynow_reference = ?, payment_method = ? WHERE order_reference = ?"); if ($update_stmt) { $update_stmt->bind_param("ssss", $transactionStatus, $paynowReference, $paymentMethod, $orderReference); if ($update_stmt->execute()) { error_log("Paynow Update Script: Order " . $orderReference . " status updated to " . $transactionStatus . ", Payment Method: " . ($paymentMethod ?? 'N/A') . " (Paynow Ref: " . $paynowReference . ") in DB."); echo "OK"; exit(); } else { error_log("Paynow Update Script: Database Error updating order " . $orderReference . ": " . $update_stmt->error); echo "Error updating database."; exit(); } $update_stmt->close(); } else { error_log("Paynow Update Script: Database Error preparing update statement: " . $db->error); echo "Error preparing database update."; exit(); } } else { $error_message = 'Paynow Update Script Error: Polling failed or returned invalid response for Poll URL ' . $pollUrl; if ($statusResponse && method_exists($statusResponse, 'error') && !empty($statusResponse->error())) { $error_message .= ' Details: ' . $statusResponse->error(); } else if (!$statusResponse) { $error_message .= ' Status response object is NULL.'; } else { $error_message .= ' Status response object is not NULL but lacks expected methods (e.g., status()).'; } error_log("Paynow Update Script FAILURE Status Response for Poll URL " . $pollUrl . ": " . print_r($statusResponse, true)); error_log($error_message); echo "Error checking transaction status."; exit(); } } catch (\Exception $e) { $error_message = 'Paynow Update Script Exception: ' . $e->getMessage(); error_log($error_message); echo "An unexpected error occurred during payment update processing."; exit(); } error_log("Paynow Update Script Error: Script reached end unexpectedly."); echo "Unexpected script termination."; ?>

#3

Good morning @Colleen_Marasha, Please note that your resultUrl is returning a 404 error. Kindly ensure that you are not mixing up the returnUrl and resultUrl .

your current resultUrl : http://xxxx.co.zw/jobsim/order_complete.php?order=ORD-xxxxx
returnUrl: https://xxxx.co.zw/jobsim/public/paynow_update.php


#4

Actually the intention is to swap the two. I tried testing ecocash with 0771111111 and the logs are as follows. my database is not being populated with the pollUrl although l can see it in the logs. Also, after the 20s countdown on test payment l am being redirected to paynow login page and not the order_complete.php. Also, my paynow_update.php` (Result URL) is not receiving a callback for any transaction… My question then is am l evenreaching a final state for Paynow to send the IPN to my resultUrl?


#5

Good morning @elphas my resultUrl is jobsim.co.zw/jobsim/public/paynow_update.php . It is reachable when accessed directly. I tried checking paynow_update_raw_post.log and paynow_update_processed_post.log after a payment test, and they are both empty, indicating that my resultUrl script is not receiving any post data from the callback. My script is responding with a 200 OK ans is expecting a POST data. Can you please help me with this if possible today please.


#6

Hi @Colleen_Marasha kindly share your code for initiating a payment to Paynow (Please do not include your secret key)


#7

It’s all working. Thank you so much for the help l just needed to interchange the resultUrl and returnUrl position. the code is here <?php
// initiate_transaction.php - Displays order summary and initiates Paynow transaction

ini_set(‘display_errors’, 1);
ini_set(‘display_startup_errors’, 1);
error_reporting(E_ALL);

require_once DIR . ‘/…/vendor/autoload.php’;
require_once DIR . ‘/db_connection.php’; // Your DB connection

// — Check if DB connection is valid —
if (!$db) {
error_log(“Initiate Transaction: Database connection failed. Check db_connection.php.”);
echo “Database connection error. Please try again later.”;
exit();
}

// Load environment variables
try {
$dotenv = Dotenv\Dotenv::createImmutable(DIR . ‘/…’);
$dotenv->load();
} catch (\Dotenv\Exception\InvalidPathException $e) {
error_log("Initiate Transaction: Dotenv Error: " . $e->getMessage());
echo “Error loading environment configuration. Please try again later.”;
exit();
}

$integrationId = $_ENV[‘PAYNOW_INTEGRATION_ID’] ?? null;
$integrationKey = $_ENV[‘PAYNOW_INTEGRATION_KEY’] ?? null;

if (!$integrationId || !$integrationKey) {
error_log(‘Initiate Transaction: Paynow credentials missing.’);
echo ‘Paynow credentials not configured.’;
exit();
}

// 1. Get order_reference from GET parameter
$order_reference = $_GET[‘order_reference’] ?? null;

if (!$order_reference) {
error_log(“Initiate Transaction: Error: Order reference is missing from URL.”);
echo “Error: Order reference is missing.”;
exit();
}

// 2. Fetch order details from your database using the order_reference
$stmt = $db->prepare(“SELECT customer_email, total_amount, status FROM orders WHERE order_reference = ?”);

// — ERROR CHECKING FOR PREPARE STATEMENT —
if ($stmt === false) {
error_log("Initiate Transaction: Database prepare error for SELECT order details: " . $db->error);
echo “Database error fetching order details. Please try again later.”;
exit();
}
// — END ERROR CHECKING —

$stmt->bind_param(“s”, $order_reference);
$stmt->execute();
$result = $stmt->get_result();
$order = $result->fetch_assoc();
$stmt->close();

if (!$order) {
error_log(“Initiate Transaction: Error: Order not found for reference: " . $order_reference . " in DB.”);
echo "Error: Order not found for reference: " . htmlspecialchars($order_reference);
exit();
}

// Ensure the order status is still pending before initiating payment
if ($order[‘status’] !== ‘PENDING’) {
error_log("Initiate Transaction: Order " . $order_reference . " is not in PENDING status. Current status: " . $order[‘status’]);
echo “This order has already been processed or is not in a pending state.”;
exit();
}

$customer_email = $order[‘customer_email’];
$cart_total = $order[‘total_amount’]; // Use total_amount from DB

// Set your resultUrl and returnUrl (browserUrl)
// IMPORTANT: Adjust these to your live domain
$resultUrl = ‘https://xxx.co.zw/xxx/public/paynow_update.php?order_reference=’ . urlencode($order_reference);
$returnUrl = ‘https://xxx.co.zw/xxx/public/order_complete.php?order_reference=’ . urlencode($order_reference); // Optional: add order_reference for display

// Check if the form was submitted to initiate payment
if (isset($_POST[‘initiate_payment’])) {

try {
    $paynow = new Paynow\Payments\Paynow(
        $integrationId,
        $integrationKey,
        $returnUrl,// Interchange these 2 with resultUrl ontop then followed by resultUrl
        $resultUrl 
          
    );

    $payment = $paynow->createPayment($order_reference, $customer_email);
    $payment->add($order_reference, $cart_total); // Add item (you might want to add more specific items here)

    $response = $paynow->send($payment);

    if ($response->success()) {
        $pollUrl = $response->pollUrl(); // Get the pollUrl from Paynow's response
        $paynowReference = null;

        // --- NEW LOGIC: Extract guid from pollUrl ---
        $parsedUrl = parse_url($pollUrl);
        if (isset($parsedUrl['query'])) {
            parse_str($parsedUrl['query'], $query_params);
            if (isset($query_params['guid'])) {
                $paynowReference = $query_params['guid'];
                error_log("Initiate Transaction: DEBUG: Extracted Paynow Reference (GUID) from pollUrl: " . $paynowReference);
            }
        }
        // --- END NEW LOGIC ---

        if (empty($paynowReference)) {
            error_log("Initiate Transaction: Critical Error: Could not obtain Paynow Reference for order " . $order_reference . ". Full response object: " . print_r($response, true));
            echo "Critical error: Could not obtain Paynow Reference.";
            exit();
        }

        // Crucially: Store the pollUrl and PaynowReference in your database
        // so paynow_update.php can retrieve it later using order_reference
        // CHANGED: paynow_pollurl to paynow_poll_url to match DB column name
        $update_stmt = $db->prepare("UPDATE orders SET paynow_poll_url = ?, paynow_reference = ?, status = 'INITIATED' WHERE order_reference = ?");

        // --- ERROR CHECKING FOR PREPARE STATEMENT ---
        if ($update_stmt === false) {
            error_log("Initiate Transaction: Database prepare error for UPDATE pollUrl: " . $db->error);
            echo "Database error preparing order for payment.";
            exit();
        }
        // --- END ERROR CHECKING ---

        if ($update_stmt) {
            $update_stmt->bind_param("sss", $pollUrl, $paynowReference, $order_reference);
            $update_stmt->execute();
            $update_stmt->close();
            error_log("Initiate Transaction: Paynow transaction initiated for order " . $order_reference . ". Poll URL stored: " . $pollUrl);
        } else {
            error_log("Initiate Transaction: Error preparing DB statement to save pollUrl for order " . $order_reference . ": " . $db->error);
            echo "Error preparing order for payment.";
            exit();
        }

        // Redirect the user to Paynow's payment page
        header("Location: " . $response->redirectUrl());
        exit();

    } else {
        $error_message = "Initiate Transaction: Paynow initiation failed: " . print_r($response->errors(), true);
        error_log($error_message);
        echo "Error initiating payment. Please try again. " . implode(", ", $response->errors());
        exit();
    }

} catch (\Exception $e) {
    error_log("Initiate Transaction: Exception during Paynow initiation for order " . $order_reference . ": " . $e->getMessage());
    echo "An unexpected error occurred during payment initiation.";
    exit();
}

}

// — Now include header.php after all potential redirects —
include ‘header.php’;

?>

<style>
    /* Styles copied directly from cart.php for consistency */
    body {
        font-family: 'Georgia', serif;
        line-height: 1.6;
        color: #333;
    }

    .page-section {
        padding: 60px 0;
    }

    .classic-container {
        background-color: #f8f8f8;
        border: 1px solid #ddd;
        padding: 40px;
        margin-top: 30px;
        margin-bottom: 30px;
        box-shadow: 5px 5px 15px rgba(0,0,0,0.1);
        max-width: 900px;
        margin-left: auto;
        margin-right: auto;
    }

    .classic-container h1,
    .classic-container h2,
    .classic-container h3 {
        font-family: 'Times New Roman', serif;
        color: #555;
        margin-bottom: 20px;
    }

    .classic-container p {
        margin-bottom: 15px;
    }

    .classic-container ul {
        list-style: disc;
        margin-left: 20px;
        margin-bottom: 15px;
    }

    .classic-container ul li {
        margin-bottom: 5px;
    }

    .classic-container .table {
        margin-top: 20px;
        border-collapse: collapse;
        width: 100%;
    }

    .classic-container .table th,
    .classic-container .table td {
        border: 1px solid #ddd;
        padding: 10px;
        text-align: left;
    }

    .classic-container .table th {
        background-color: #e9e9e9;
        font-weight: bold;
    }

    .classic-container .table tfoot td {
        font-weight: bold;
    }

    .classic-container .btn-paynow {
        background-color: #1a73e8;
        border-color: #1a73e8;
        color: white;
        font-size: 1.2rem;
        padding: 10px 20px;
        border-radius: 5px;
        transition: background-color 0.3s ease;
    }

    .classic-container .btn-paynow:hover {
        background-color: #0f5ac9;
        border-color: #0f5ac9;
    }

    /* Style for the SVG image button (if used) */
    .classic-container .paynow-image-button {
        display: inline-block; /* Allows margin/padding if needed */
        border: none; /* No border for the image */
        padding: 0; /* No padding for the image */
        cursor: pointer; /* Indicate it's clickable */
        max-width: 250px; /* Adjust max width as needed for your SVG */
        height: auto; /* Maintain aspect ratio */
        margin-top: 20px; /* Example margin */
    }


    .page-section.bg-light.text-dark h2,
    .page-section.bg-light.text-dark h3,
    .page-section.bg-light.text-dark p,
    .page-section.bg-light.text-dark a,
    .page-section.bg-light.text-dark ul,
    .page-section.bg-light.text-dark li {
        color: #212529 !important;
    }

    .page-section.bg-light.text-dark .text-muted {
        color: #6c757d !important;
    }
</style>

<section class="page-section py-5 bg-light text-dark">
    <div class="container classic-container">
        <h1 class="text-center">Confirm Your Order</h1>
        <p>Order Reference: <strong><?php echo htmlspecialchars($order_reference); ?></strong></p>
        <p>Total Amount: <strong>ZWL$<?php echo htmlspecialchars(number_format($cart_total, 2)); ?></strong></p>
        <p>Customer Email: <strong><?php echo htmlspecialchars($customer_email); ?></strong></p>

        <form action="" method="POST">
            <button type="submit" name="initiate_payment" class="btn btn-primary btn-xl">Proceed to Pay with Paynow</button>
        </form>

        <p class="text-center mt-3"><a href="cart.php" class="btn btn-outline-primary">Back to Cart</a></p>
    </div>
</section>

<section class="page-section py-5 text-white text-center" style="background: rgba(0, 0, 0, 0.7);">
    <div class="container">
        <h2 class="mt-0">Need Assistance with Your Order?</h2>
        <hr class="divider bg-light my-4">
        <p class="mb-5">
            If you have any questions about your order or need help with the payment process, please contact us.
        </p>
        <button class="btn btn-primary btn-xl" title="Get Help with Order" aria-label="Get Help with Order" data-toggle="modal" data-target="#quoteModal">Get Help</button>
    </div>
</section>
<?php include 'quote_modal.php'; include 'footer.php'; ?>

#8

Thanks @Colleen_Marasha