Payment Gateway IPN and Webhooks Explained: Verify Payments the Safe Way

Payment Gateway IPN and Webhooks Explained: Verify Payments the Safe Way

An IPN (instant payment notification), also called a webhook, is a message the payment gateway sends to your server when a payment is confirmed. Getting it right is the difference between a store that delivers every paid order and one that gives goods away or loses sales. This guide explains how the ZurvaPay IPN works and how to handle it safely.

What is an IPN?

When a customer pays, your server is not in the room. The gateway tells your server, with a signed request, that the payment happened. You never have to guess from a browser redirect, because the redirect can be faked or never arrive.

Why the IPN is the only trusted confirmation

The customer is sent back to your success page after paying, but that page is just a URL. Anyone can open it by hand. The signed IPN, sent server to server and verified with your secret key, is proof the payment is real. Show a message on the success page, and let the verified IPN update the order.

What the notification contains

  • status: for a paid payment this is success.
  • identifier: the unique order reference you sent.
  • signature and status_signature: two signatures you must verify.
  • data: payment transaction ID, amount, payer name, payment timestamp, the charge, and the currency.

How to verify it

  1. Compute HMAC-SHA256(amount + identifier) with your secret key, in upper case, and compare it with signature.
  2. Compute HMAC-SHA256(status + "|" + identifier + "|" + amount) the same way and compare it with status_signature.
  3. Use the values exactly as received, as raw strings. Do not reformat the amount.
  4. Use a constant-time comparison function.
  5. Look up your order by identifier, confirm it is still pending and that the amount is what you expected.

PHP verification example

<?php
$secret     = 'YOUR_SECRET_KEY';
$status     = $_POST['status'] ?? '';
$identifier = $_POST['identifier'] ?? '';
$signature  = $_POST['signature'] ?? '';
$statusSig  = $_POST['status_signature'] ?? '';
$amount     = $_POST['data']['amount'] ?? '';

$expected       = strtoupper(hash_hmac('sha256', $amount . $identifier, $secret));
$expectedStatus = strtoupper(hash_hmac('sha256', $status . '|' . $identifier . '|' . $amount, $secret));

if (!hash_equals($expected, $signature) || !hash_equals($expectedStatus, $statusSig) || $status !== 'success') {
    http_response_code(200);
    echo json_encode(['status' => 'failed']);
    exit;
}

$order = find_order_by_identifier($identifier);   // your function
if ($order && $order['status'] === 'pending' && (float) $amount >= (float) $order['amount']) {
    mark_order_paid($order['id'], $_POST['data']['payment_trx']); // once only
}
http_response_code(200);
echo json_encode(['status' => 'ok']);

Mark the order paid only once

The same notification can arrive more than once, for example when a retry crosses with your cron job. Protect yourself with a single atomic update such as UPDATE orders SET status = 'paid' WHERE id = ? AND status = 'pending' and only deliver the goods when exactly one row changed. This also stops two requests from crediting the same order at the same moment.

Retries and what your server should answer

Answer with HTTP 200 as soon as you have saved the result, then do slower work such as sending emails afterwards. If your server does not answer with a success code, ZurvaPay retries after about 2, 5, 15, 30, 60, 180 and 360 minutes, eight attempts in total. Payments that are confirmed later by our automatic verification are notified the same way.

The safety net: status check and cron

Even with retries, a server that is down for hours could miss every notification. For that reason there is an optional payment status API that returns the current state of up to 50 payments in one call, and a small cron job that runs every few minutes, checks your pending orders and completes any that were paid. Together they mean no paid order stays stuck. See how to reduce pending and failed payments.

Frequently asked questions

Can I trust the success page instead?

No. Anyone can open a URL. Only a verified IPN, or a verified status response, should mark an order paid.

Why do I get the same notification twice?

Retries and the automatic verification can overlap. Make your handler idempotent so a second notification changes nothing.

What should I return if the signature is wrong?

Answer HTTP 200 with a failed message and do nothing to the order, so the sender does not retry a request that will never be valid.

Is my secret key ever sent to my server?

No. It is used only to calculate and check signatures. Keep it on your server and never put it in browser or app code.

Build it right the first time

Open a ZurvaPay merchant account or contact us and we will help you get started.

Related guides

Start accepting payments with ZurvaPay

Open a merchant account, connect your website with one API and get notified the moment a customer pays.