Page MenuHomeDatingVIP

Payment Notifications
Updated YesterdayPublic

Partner Notifications

PV2 sends notifications to partners regarding various actions (transactions, subscriptions). Notifications are always sent to the same callback URL defined for each partner. If a partner doesn't confirm the notification, PV2 will retry delivery automatically.

Table of Contents

  1. Overview
  2. Implementation Guide
  3. API Reference — Notification Types

1. Overview

Notification Format

Every notification is an HTTP request to the partner's callback URL containing these parameters:

Param NameTypeDescription
commandstringNotification type (e.g. transaction.success, subscription.rebill)
hashstringUnique identifier of this notification
datastringJSON-encoded string with the event data
verifystring*(Optional)* HMAC-SHA256 signature — present only when a shared secret is configured

Delivery & Retry Schedule

If PV2 does not receive confirmation, it retries the same notification on this schedule:

AttemptDelay after previous
1st retry1 minute
2nd retry5 minutes
3rd retry15 minutes
4th retry30 minutes
5th retry30 minutes

After the 5th retry, PV2 stops trying.

Three Steps of Handling a Notification

Regardless of which implementation approach you choose, every notification must go through these three steps:

StepWhatWhy
1Verify authenticityConfirm the notification actually came from PV2, not a third party
2DeduplicateCheck if this hash was already processed (retries can cause duplicates)
3Confirm handlingTell PV2 that the notification was received and processed

The four implementation options below differ in *how* they accomplish these steps.


2. Implementation Guide

Choosing an Approach

Option 1Option 2Option 3Option 4
MethodPV2 endpointPV2 endpoint + verify-onlyHMAC (no PV2 calls)Async + PV2 endpoint
Step 1 — VerifyPV2 endpointPV2 endpointHMAC signatureDeferred to consumer
Step 2 — DedupPV2 endpointPV2 endpointPartner's DBPartner's DB
Step 3 — ConfirmAutomatic (side effect)Return *NOTIFIED*Return *NOTIFIED*Consumer calls PV2 endpoint
Requires shared secretNoNoYesNo
Requires PV2 API callYesYesNoYes (deferred)
RecommendedNoAcceptableYesFor specific use cases

Option 1 — PV2 Endpoint Without verify-only (Not Recommended)

PV2                          Partner Callback                PV2 API
 |                                  |                           |
 |-- notification (command,hash) -->|                           |
 |                                  |                           |
 |                                  |-- notification.hash       |
 |                                  |   .validation(hash) ----->|
 |                                  |                           |
 |                                  |   (verify + dedup +       |
 |                                  |    mark as handled)       |
 |                                  |                           |
 |                                  |<-- success/fail ----------|
 |                                  |                           |
 |                                  | process notification      |
 |                                  | (if validation passed)    |
 |                                  |                           |
 |<-------- (any response) ---------|                           |

Call the notification.hash.validation endpoint (without verify-only flag), passing the received hash as an argument.

The endpoint returns success only if:

  • The given hash exists on the PV2 side, and
  • The notification has not been handled yet.

At the same time, the endpoint immediately marks the notification as "handled" (Step 3).

Why is this approach not recommended?

The processes of verification (Step 1) and handling confirmation (Step 3) cannot be separated. If you simply want to verify the notification, it will still be marked as "handled." This can cause issues because, in the event of an exception during processing, the PV2 system will never reattempt the notification (since handled = 1).


Option 2 — PV2 Endpoint with verify-only Flag

PV2                          Partner Callback                PV2 API
 |                                  |                           |
 |-- notification (command,hash) -->|                           |
 |                                  |                           |
 |                                  |-- notification.hash       |
 |                                  |   .validation(hash,       |
 |                                  |   verify-only=1) -------->|
 |                                  |                           |
 |                                  |   (verify + dedup,        |
 |                                  |    NOT marked as handled) |
 |                                  |                           |
 |                                  |<-- success/fail ----------|
 |                                  |                           |
 |                                  | process notification      |
 |                                  | (if validation passed)    |
 |                                  |                           |
 |<------ "*NOTIFIED*" (HTTP 200) --|                           |
Steps 1 & 2 — Verify + Dedup

Call the notification.hash.validation endpoint with the additional parameter verify-only=1, passing the received hash as an argument.

The endpoint returns success only if:

  • The given hash exists on the PV2 side, and
  • The notification has not been handled yet.

With verify-only=1, the notification is not marked as handled — this is the key difference from Option 1.

This single call covers both verification (Step 1) and deduplication (Step 2).

Step 3 — Confirm Handling

After processing the notification, the callback script must return an HTTP 200 response with the plain text body: *NOTIFIED*.


Option 3 — HMAC Verification Without PV2 Endpoint (Recommended)

PV2                          Partner Callback
 |                                  |
 |-- notification (command,hash, -->|
 |   data, verify)                  |
 |                                  |
 |                                  | 1. compute HMAC-SHA256
 |                                  |    from command+hash+data
 |                                  |    using shared secret
 |                                  |
 |                                  | 2. compare with "verify"
 |                                  |    field from notification
 |                                  |    (reject if mismatch)
 |                                  |
 |                                  | 3. check hash in local DB
 |                                  |    (dedup — skip if exists)
 |                                  |
 |                                  | 4. process notification
 |                                  |
 |                                  | 5. save hash to local DB
 |                                  |
 |<------ "*NOTIFIED*" (HTTP 200) --|

All three steps are handled on the partner's side — no callbacks to PV2 API are needed. Authenticity is verified cryptographically using a shared secret.

Prerequisites — Shared Secret Setup

Before receiving notifications, the partner and PV2 must agree on a secret key (a string, e.g. an MD5 hash or any random string). This secret must be:

  • Configured on the PV2 side — in the notification URL settings for the given partner.
  • Stored on the partner's side — in their application config, securely, never exposed publicly.

Both sides must have exactly the same secret. It is never transmitted in the notification itself.

Step 1 — Verify the Notification (HMAC Signature)

When a secret key is configured, PV2 appends an extra field verify to the notification. The notification will look like this:

json
{
    "command": "transaction.success",
    "hash": "abc123def456...",
    "data": {
        "tran_id": 12345,
        "amount": "29.99",
        "currency": "USD"
    },
    "verify": "f75e721c6ff19ffcfd060a7d0ff02424407eb6a832d29196969d6bf844562571"
}

The verify value is an HMAC-SHA256 signature that PV2 computed from the notification content using the shared secret. To verify that the notification truly came from PV2, the partner must recompute the same signature and compare it.

How to compute the signature (step by step):

  1. Build an array from the received notification using only these three fields, in this exact order:
$notification = [
    'command' => $command,   // e.g. "transaction.success"
    'hash'    => $hash,      // e.g. "abc123def456..."
    'data'    => $data       // the data object/array as received
];

Important: Use the raw values from the received notification. The array must contain exactly these 3 keys in this order: command, hash, data. Do not include the verify field itself.

  1. JSON-encode the array and compute HMAC-SHA256 using the shared secret:
$computedSignature = hash_hmac('sha256', json_encode($notification), $secretKey);
  1. Compare the result with the verify field from the notification:
if ($computedSignature === $receivedVerify) {
    // Notification is authentic — it came from PV2
} else {
    // Signature mismatch — reject this notification (possible tampering or wrong secret)
}

Why this works: This is a standard HMAC signature verification pattern — the message is signed with a shared secret. Only PV2 and the partner know the secret key. Without it, an attacker cannot produce a valid verify signature. If even a single character of command, hash, or data was altered in transit, the computed signature will not match.

Common pitfalls:

  • The json_encode output must be identical on both sides. Make sure you're passing the same data types (e.g. if data arrived as a JSON string, decode it to an array/object first before building $notification).
  • If the verify key is absent in the notification, it means no secret was configured for this partner on the PV2 side — HMAC verification is not available in that case.
Step 2 — Check if Already Handled (Deduplication)

PV2 may resend the same notification if it didn't receive confirmation (see Retry Schedule). To avoid processing the same event twice, the partner must implement deduplication.

Recommended approach: Store each successfully processed notification hash in a database table. Before processing a new notification, check if its hash already exists in that table:

// Pseudocode
if ($db->hashAlreadyProcessed($receivedHash)) {
    // Already handled — skip processing, but still return *NOTIFIED* to stop retries
} else {
    // New notification — process it
    processNotification($data);
    $db->markHashAsProcessed($receivedHash);
}
Step 3 — Confirm Handling

After successfully processing (or recognizing a duplicate), the partner's callback endpoint must return an HTTP 200 response with the plain text body:

*NOTIFIED*

This tells PV2 to stop retrying. If PV2 does not receive this response, it will keep resending the notification according to the retry schedule.


Option 4 — Asynchronous Processing

PV2                   Partner Callback         Queue          Consumer           PV2 API
 |                           |                   |                |                  |
 |-- notification ---------->|                   |                |                  |
 |                           |                   |                |                  |
 |                           |-- add to queue -->|                |                  |
 |                           |                   |                |                  |
 |<-- (blank response) ------|                   |                |                  |
 |                           |                   |                |                  |
 | (PV2 may retry since      |                   |                |                  |
 |  no *NOTIFIED* received)  |                   |                |                  |
 |                           |                   |                |                  |
 |                           |                   |-- pick up ---->|                  |
 |                           |                   |                |                  |
 |                           |                   |                | process          |
 |                           |                   |                | notification     |
 |                           |                   |                |                  |
 |                           |                   |                |-- notification   |
 |                           |                   |                |   .hash          |
 |                           |                   |                |   .validation -->|
 |                           |                   |                |   (hash)         |
 |                           |                   |                |                  |
 |                           |                   |                |   (verify +      |
 |                           |                   |                |    confirm)      |
 |                           |                   |                |                  |
 |                           |                   |                |<-- success/fail -|

For partners who need to decouple receiving notifications from processing them (e.g. heavy processing, external dependencies):

  1. Callback URL receives the notification, adds it to an internal async queue (e.g. RabbitMQ, database job table), and returns a blank response (not *NOTIFIED*).
  2. Consumer process picks up the queued notification, performs the actual processing, and calls notification.hash.validation without the verify-only flag to both validate and confirm handling.

Note: This approach introduces a potential risk due to the time gap between receiving the notification and running the processing script. During this window, PV2 may retry delivery. To mitigate this, consider using an additional table to track which notifications have been received but not yet processed.


3. API Reference — Notification Types

Transaction Notifications

All transaction notifications (transaction.success, transaction.failed, transaction.change) share the same base data structure.

Common Transaction Data Fields
Param NameTypeDescription
tran_idintTransaction ID
ptnr_idintPartner ID
ppac_idintPayment Processor Account ID
order_idintOrder ID
ccdt_idintCredit Card Details ID
transaction_typestringTransaction Type. Values: s (sale), a (auth), r (refund), c (chargeback), f (fake)
amountstringAmount
currencystringCurrency (3 Letter ISO Code)
descriptionstringDescription
statusstringTransaction status. Values: failed, successful
refunded_tran_idintRefunded Transaction ID
originstringOrigin. Values: direct, system
tsintTimestamp
status_codeintStatus code
payment_codeintPayment Code (700 on success)
order_typestringOrder type. Values: basic, xsale
pp_idintPayment Processor ID
processorstringPayment Processor abbreviation
user_idintUser ID in Payment Application
tracking_userintUser Identifier from Partner
tracking_tagintTag ID from Partner
itemsarrayArray of items
Processor-Dependent Fields

The following fields are only present for certain processors (currently RocketGate and Payon). They are included in transaction.success and transaction.failed, but not in transaction.change.

Param NameTypeDescriptionProcessors
merchantarrayMerchant account identifier. RocketGate: [merchant_id, merchant_account]. Payon: [channel].RocketGate, Payon
selected_cc_dataarrayCard data fields: ccdt_id, bin, ccnum_last4, exp_date, cc_type, cc_status (with optional flag: marked_as_fraud). Only when using already existing card (rebills/rejoins).RocketGate

transaction.success

Sent in case of a successful transaction.

Data: Common Transaction Data Fields + Processor-Dependent Fields


transaction.failed

Sent in case of a failed transaction.

Data: Common Transaction Data Fields + Processor-Dependent Fields


transaction.change

Sent when a transaction has been changed.

Possible cases:

  • transaction.delete_chargeback
  • transaction.chargeback
  • Refund for the same transaction already exists, so we update Refund -> Chargeback

Data: Common Transaction Data Fields only (no processor-dependent fields)


Subscription Notifications

All subscription notifications share the same base data structure, with extra fields noted per notification type.

Common Subscription Data Fields
Param NameTypeDescription
sub_idintSubscription ID
item_idintItem ID
statusstringSubscription status (s/b initial on create, rebill after 1st rebill)
start_tsintStart timestamp
change_tsintChange (edit) timestamp
end_tsintEnd timestamp
rebill_countintNumber of recurring payments
last_rebill_tsintLast rebill timestamp
next_rebill_tsintNext rebill timestamp
next_rebill_amountintNext rebill amount
step_downintStep Down (charge amount reductions)
order_idintOrder ID
descriptionintOrder Description
rebill_amountintRebill amount (not including next one)
trial_unitstringTrial Unit (day, week, month, year)
rebill_unitstringRebill Unit (day, week, month, year)
rebill_periodintRebill period
max_rebill_countintMax Rebill Count
trial_periodintTrial period
tracking_itemintTracking item ID
pp_idintPayment Processor ID
user_idintPV2 user ID
hashstringHash
first_nameintCustomer's First Name
last_nameintCustomer's Last Name
ppac_idintPayment Processor Account ID
emailintCustomer's Email Address
ptnr_idintPartner ID
currencystringCurrency
tracking_tagintTracking Tag param
ipintCustomer's IP Address
order_typeintOrder type. Values: basic, xsale
tracking_userintUser ID on partners' site
processorintPayment Processor's abbreviation

subscription.created

Sent when subscription is successfully created and subscription does not have trial.

Subscription does not have trial if the appropriate item passed in transaction.init call has an empty value for parameter trial_period.

Minor use case: Also sent for manually rebilled subscriptions — in this case value of rebill_period for appropriate item in transaction.init call should be -1.

Data: Common Subscription Data Fields + tran_id*

  • tran_id (int) — Last Transaction's ID. Not present for all payment processors.

subscription.trial

Sent when subscription is successfully created and subscription does have trial.

Subscription does have trial if the appropriate item passed in transaction.init call has a non-empty value for parameter trial_period.

Data: Common Subscription Data Fields + tran_id*

  • tran_id (int) — Last Transaction's ID. Not present for all payment processors.

subscription.stopped

Sent in case of stopped subscription (action initiated via API call).

Data: Common Subscription Data Fields + extra fields:

Param NameTypeDescription
is_cancelbooleanShould be true
cancel_reasonstringOptional, not always sent. If value is cancel_reason_rebill_bin_filter it means subscription was stopped because BIN used for purchase is not allowed to rebill

subscription.suspended

Sent in case of suspended subscription (usually happens when we can't rebill user after defined number of attempts).

Data: Common Subscription Data Fields


subscription.rebill

Sent after a successful rebill. It will be sent right after transaction.success.

Data: Common Subscription Data Fields + tran_id*

  • tran_id (int) — Last Transaction's ID. Not present for all payment processors.

subscription.completed

Sent after completed subscription (happens only on subscriptions with limited number of rebills).

Data: Common Subscription Data Fields


subscription.change

Sent when subscription is changed (action initiated via subscription.change API call).

Data: Common Subscription Data Fields

Last Author
pmiroslawski
Last Edited
Thu, Mar 26, 07:52