- Partner Notifications
- Table of Contents
- 1. Overview
- 2. Implementation Guide
- Choosing an Approach
- Option 1 — PV2 Endpoint Without verify-only (Not Recommended)
- Option 2 — PV2 Endpoint with verify-only Flag
- Option 3 — HMAC Verification Without PV2 Endpoint (Recommended)
- Prerequisites — Shared Secret Setup
- Step 1 — Verify the Notification (HMAC Signature)
- Step 2 — Check if Already Handled (Deduplication)
- Step 3 — Confirm Handling
- Option 4 — Asynchronous Processing
- 3. API Reference — Notification Types
Payment Notifications
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
Notification Format
Every notification is an HTTP request to the partner's callback URL containing these parameters:
| Param Name | Type | Description |
|---|---|---|
| command | string | Notification type (e.g. transaction.success, subscription.rebill) |
| hash | string | Unique identifier of this notification |
| data | string | JSON-encoded string with the event data |
| verify | string | *(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:
| Attempt | Delay after previous |
|---|---|
| 1st retry | 1 minute |
| 2nd retry | 5 minutes |
| 3rd retry | 15 minutes |
| 4th retry | 30 minutes |
| 5th retry | 30 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:
| Step | What | Why |
|---|---|---|
| 1 | Verify authenticity | Confirm the notification actually came from PV2, not a third party |
| 2 | Deduplicate | Check if this hash was already processed (retries can cause duplicates) |
| 3 | Confirm handling | Tell 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 1 | Option 2 | Option 3 | Option 4 | |
|---|---|---|---|---|
| Method | PV2 endpoint | PV2 endpoint + verify-only | HMAC (no PV2 calls) | Async + PV2 endpoint |
| Step 1 — Verify | PV2 endpoint | PV2 endpoint | HMAC signature | Deferred to consumer |
| Step 2 — Dedup | PV2 endpoint | PV2 endpoint | Partner's DB | Partner's DB |
| Step 3 — Confirm | Automatic (side effect) | Return *NOTIFIED* | Return *NOTIFIED* | Consumer calls PV2 endpoint |
| Requires shared secret | No | No | Yes | No |
| Requires PV2 API call | Yes | Yes | No | Yes (deferred) |
| Recommended | No | Acceptable | Yes | For 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):
- 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.
- JSON-encode the array and compute HMAC-SHA256 using the shared secret:
$computedSignature = hash_hmac('sha256', json_encode($notification), $secretKey);
- 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):
- 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*).
- 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 Name | Type | Description |
|---|---|---|
| tran_id | int | Transaction ID |
| ptnr_id | int | Partner ID |
| ppac_id | int | Payment Processor Account ID |
| order_id | int | Order ID |
| ccdt_id | int | Credit Card Details ID |
| transaction_type | string | Transaction Type. Values: s (sale), a (auth), r (refund), c (chargeback), f (fake) |
| amount | string | Amount |
| currency | string | Currency (3 Letter ISO Code) |
| description | string | Description |
| status | string | Transaction status. Values: failed, successful |
| refunded_tran_id | int | Refunded Transaction ID |
| origin | string | Origin. Values: direct, system |
| ts | int | Timestamp |
| status_code | int | Status code |
| payment_code | int | Payment Code (700 on success) |
| order_type | string | Order type. Values: basic, xsale |
| pp_id | int | Payment Processor ID |
| processor | string | Payment Processor abbreviation |
| user_id | int | User ID in Payment Application |
| tracking_user | int | User Identifier from Partner |
| tracking_tag | int | Tag ID from Partner |
| items | array | Array 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 Name | Type | Description | Processors |
|---|---|---|---|
| merchant | array | Merchant account identifier. RocketGate: [merchant_id, merchant_account]. Payon: [channel]. | RocketGate, Payon |
| selected_cc_data | array | Card 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 Name | Type | Description |
|---|---|---|
| sub_id | int | Subscription ID |
| item_id | int | Item ID |
| status | string | Subscription status (s/b initial on create, rebill after 1st rebill) |
| start_ts | int | Start timestamp |
| change_ts | int | Change (edit) timestamp |
| end_ts | int | End timestamp |
| rebill_count | int | Number of recurring payments |
| last_rebill_ts | int | Last rebill timestamp |
| next_rebill_ts | int | Next rebill timestamp |
| next_rebill_amount | int | Next rebill amount |
| step_down | int | Step Down (charge amount reductions) |
| order_id | int | Order ID |
| description | int | Order Description |
| rebill_amount | int | Rebill amount (not including next one) |
| trial_unit | string | Trial Unit (day, week, month, year) |
| rebill_unit | string | Rebill Unit (day, week, month, year) |
| rebill_period | int | Rebill period |
| max_rebill_count | int | Max Rebill Count |
| trial_period | int | Trial period |
| tracking_item | int | Tracking item ID |
| pp_id | int | Payment Processor ID |
| user_id | int | PV2 user ID |
| hash | string | Hash |
| first_name | int | Customer's First Name |
| last_name | int | Customer's Last Name |
| ppac_id | int | Payment Processor Account ID |
| int | Customer's Email Address | |
| ptnr_id | int | Partner ID |
| currency | string | Currency |
| tracking_tag | int | Tracking Tag param |
| ip | int | Customer's IP Address |
| order_type | int | Order type. Values: basic, xsale |
| tracking_user | int | User ID on partners' site |
| processor | int | Payment 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 Name | Type | Description |
|---|---|---|
| is_cancel | boolean | Should be true |
| cancel_reason | string | Optional, 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).
- Last Author
- pmiroslawski
- Last Edited
- Thu, Mar 26, 07:52