**This is an advanced topic. This is only recommended for vendors with advanced programming knowledge or those that have the assistance of a programmer.
JVZIPN v2
The modern JVZoo Instant Payment Notification system — richer data, simpler verification, and a complete payout breakdown in every transaction event.
How It Works
When a transaction event occurs, JVZoo sends an HTTP POST to the IPN v2 URL you configure in your product settings. Your script verifies the request, then processes it.
V1 and V2 are independent — you can configure one, both, or neither on each product. They fire from separate URLs and use different verification algorithms.
Setup
- In JVZoo, go to your product's Advanced Settings and select JVZIPN v1 / JVZIPN v2 / KeyGen / External Program
- Enter your endpoint URL in the JVZIPN V2 URL field
- Set a Secret Key — used to verify incoming requests. Navigate to My Account → Seller Settings to create one
- Save your settings
Transaction Types
| transaction_type | Description | What to do |
|---|---|---|
| SALE | New purchase completed | Grant access, send welcome email |
| BILL | Recurring subscription rebilled | Extend access period |
| RFND | Transaction refunded or chargeback filed |
Revoke access. Note: chargebacks also arrive
as RFND — v2 does not distinguish
between the two
|
POST Fields
All fields are sent as application/x-www-form-urlencoded.
Amounts are in dollars (not pennies — this differs
from v1).
Transaction
| Field | Type | Description | Example |
|---|---|---|---|
| paykey | string | Unique JVZoo payment key | AP-ABC123XYZ |
| prekey | string | Pre-approval key (recurring only) | PRE-XYZ789 |
| transaction_id | string | Receipt number. Rebills append -B001, -B002, etc. | ABC123XYZ |
| transaction_type | string | SALE, BILL, or RFND | SALE |
| total | string | Transaction amount in dollars | 47.00 |
| status | string | Transaction status | Completed |
| date | string | Transaction date (used in verification) | 2024-04-06 14:30:00 |
| payment_method | string | Payment processor code | PYPL |
| cverify | string | Verification hash — always validate this | A1B2C3D4 |
Product
| Field | Type | Description | Example |
|---|---|---|---|
| product_id | string | JVZoo product ID | 12345 |
| product_name | string | Product name | Premium Course |
| product_type | string | STANDARD or RECURRING | STANDARD |
Customer
| Field | Type | Description | Example |
|---|---|---|---|
| customer_email | string | Customer email address | buyer@example.com |
| customer_first_name | string | First name | John |
| customer_last_name | string | Last name | Doe |
| customer_ip | string | IP address at time of purchase | 203.0.113.42 |
| customer_phone | string | Phone number if collected | +1-555-123-4567 |
Vendor
| Field | Type | Description | Example |
|---|---|---|---|
| vendor_id | string | Vendor user ID | 11111 |
| vendor_name | string | Vendor display name | ACME Corp |
| vendor_email | string | Vendor email | vendor@example.com |
| paypal_email | string | Customer's payment email (PayPal transactions) | buyer-paypal@example.com |
Affiliate
| Field | Type | Description | Example |
|---|---|---|---|
| affiliate_id | string | Affiliate user ID (0 if none) | 67890 |
| affiliate_name | string | Affiliate display name | Jane Smith |
| affiliate_email | string | Affiliate email | affiliate@example.com |
| tid | string | Affiliate tracking ID | campaign123 |
| other_params | string | Custom data from your buy link (same as cvendthru in v1) | userid=42 |
Tax & Fees
| Field | Type | Description | Example |
|---|---|---|---|
| tax_total | string | Total tax | 4.50 |
| vat_tax | string | EU VAT | 3.20 |
| national_sales_tax | string | Country sales tax | 1.30 |
| international_sales_tax | string | International tax | 0.00 |
| shipping_fee | string | Shipping fee | 5.99 |
Recurring Subscription
These fields are only populated for RECURRING products.
| Field | Type | Description | Example |
|---|---|---|---|
| start_date | string | Subscription start date | 2024-04-06 00:00:00 |
| end_date | string | Subscription end date (empty if open-ended) | 2025-04-06 00:00:00 |
| next_payment_date | string | Next rebill date | 2024-05-06 00:00:00 |
Delivery Address
Only populated when the product collects shipping information.
| Field | Description |
|---|---|
| delivery_name | Recipient name |
| delivery_phone | Phone number |
| delivery_address_line_1 | Street address |
| delivery_address_line_2 | Apartment/suite |
| delivery_city | City |
| delivery_region | State or region |
| delivery_country | Country |
| delivery_postal_code | Postal code |
Transaction Payouts
transactionPayouts is a
JSON-encoded string within the form body. It
is only present when payout data exists. Always check before
parsing.
JSON[
{
"payee_amount": 33.60,
"payee_user_id": 11111,
"payee_name": "ACME Corp",
"payout_type": "VENDOR",
"payment_processor": "PayPal",
"payout_status": "Settled"
},
{
"payee_amount": 9.40,
"payee_user_id": 67890,
"payee_name": "Jane Smith",
"payout_type": "AFFILIATES",
"payment_processor": "PayPal",
"payout_status": "Settled"
},
{
"payee_amount": 4.00,
"payee_user_id": 1,
"payee_name": "JVZoo",
"payout_type": "JVZOO",
"payment_processor": "JVZoo",
"payout_status": "Settled"
}
]
Sample Payload
A typical JVZIPN v2 POST (values sanitized):
POST bodycustomer_first_name: Jamie
customer_last_name: Rivers
customer_email: jamie.rivers@example.com
delivery_name: Jamie Rivers
delivery_phone: +1-843-555-0199
delivery_address_line_1: 742 Evergreen Lane
delivery_address_line_2:
delivery_city: Seaside
delivery_region: FL
delivery_postal_code: 33004
delivery_country: UNITED_STATES
product_id: 20455
product_name: Premium Webinar Toolkit
product_type: STANDARD
transaction_type: SALE
transaction_id: 9TX000111Z999000A
paykey: PT-9TX000111Z999000A
affiliate_id: 17
affiliate_name: Growth Labs LLC
affiliate_email: partners@growthlabs.example
vendor_id: 42
vendor_name: Acme Digital Corp
vendor_email: billing@acmedigital.example
total: 97.00
payment_method: PYPL
status: COMPLETED
date: 2024-09-11 12:16:42
tax_total: 0.00
shipping_fee: 0.00
cverify: 8F9C3A12
other_params:
next_payment_date:
start_date:
end_date:
transactionPayouts: [...]
Verifying Requests
cverify before processing. V2 uses a simpler algorithm than v1 — only 5 fields are hashed.
Algorithm
-
Concatenate these 5 fields in this exact order, separated
by pipes, with a trailing pipe after the
last field:
paykey|customer_email|product_name|transaction_type|date| - Append your Secret Key (no pipe after the secret)
- Convert to UTF-8
- Compute SHA-1
- Take the first 8 characters, uppercase
PHP Example
PHP<?php
$secretKey = 'your_secret_key_here';
$paykey = $_POST['paykey'] ?? '';
$customer_email = $_POST['customer_email'] ?? '';
$product_name = $_POST['product_name'] ?? '';
$transaction_type = $_POST['transaction_type'] ?? '';
$date = $_POST['date'] ?? '';
$incoming_verify = $_POST['cverify'] ?? '';
$string = $paykey . '|'
. $customer_email . '|'
. $product_name . '|'
. $transaction_type . '|'
. $date . '|'
. $secretKey;
$calculated = strtoupper(substr(sha1(mb_convert_encoding($string, 'UTF-8')), 0, 8));
if ($calculated !== $incoming_verify) {
http_response_code(403);
die('Verification failed');
}
// Verified — process the transaction
$type = $_POST['transaction_type'];
$email = $_POST['customer_email'];
$amount = $_POST['total']; // Already in dollars
// Parse payouts if present
$payouts = [];
if (!empty($_POST['transactionPayouts'])) {
$payouts = json_decode($_POST['transactionPayouts'], true);
}
switch ($type) {
case 'SALE':
grantAccess($email);
break;
case 'RFND':
revokeAccess($email);
break;
case 'BILL':
extendAccess($email, $_POST['next_payment_date']);
break;
}
http_response_code(200);
echo 'OK';
Python Example
Pythonimport hashlib
import json
from flask import Flask, request
app = Flask(__name__)
SECRET_KEY = 'your_secret_key_here'
@app.route('/ipn/v2', methods=['POST'])
def handle_ipn_v2():
data = request.form
string = '|'.join([
data.get('paykey', ''),
data.get('customer_email', ''),
data.get('product_name', ''),
data.get('transaction_type', ''),
data.get('date', ''),
]) + '|' + SECRET_KEY
calculated = hashlib.sha1(string.encode('utf-8')).hexdigest()[:8].upper()
if calculated != data.get('cverify', ''):
return 'Verification failed', 403
txn_type = data.get('transaction_type')
email = data.get('customer_email')
amount = data.get('total') # Already in dollars
# Parse payouts if present
payouts = []
if data.get('transactionPayouts'):
payouts = json.loads(data['transactionPayouts'])
if txn_type == 'SALE':
grant_access(email)
elif txn_type == 'RFND':
revoke_access(email)
elif txn_type == 'BILL':
extend_access(email)
return 'OK', 200
Working with transactionPayouts
Loop through the payouts array to record who received what on each transaction — useful for accounting, reporting, and affiliate reconciliation.
PHP
PHP$payouts = isset($_POST['transactionPayouts'])
? json_decode($_POST['transactionPayouts'], true)
: [];
foreach ($payouts as $payout) {
$amount = $payout['payee_amount'] ?? 0;
$payeeId = $payout['payee_user_id'] ?? null;
$payeeName = $payout['payee_name'] ?? '';
$type = $payout['payout_type'] ?? '';
$processor = $payout['payment_processor'] ?? '';
$status = $payout['payout_status'] ?? '';
// store or process each payout row
}
Python
Pythonpayouts_raw = data.get('transactionPayouts', '')
payouts = json.loads(payouts_raw) if payouts_raw else []
for payout in payouts:
amount = payout.get('payee_amount')
payee_id = payout.get('payee_user_id')
payee = payout.get('payee_name')
ptype = payout.get('payout_type')
processor = payout.get('payment_processor')
status = payout.get('payout_status')
Migrating from v1
If you already have a v1 handler, this table shows how v1 fields map to their v2 equivalents:
| v1 Field | v2 Equivalent | Notes | |
|---|---|---|---|
| cproditem | → | product_id | |
| cprodtitle | → | product_name | |
| cprodtype | → | product_type | |
| ctransaction | → | transaction_type | |
| ctransreceipt | → | transaction_id | |
| ctransamount | → | total | v1 is in pennies, v2 is in dollars |
| ctranspaymentmethod | → | payment_method | |
| ctranstime | → | date | v1 is Unix epoch, v2 is formatted date string |
| ctransvendor | → | vendor_id | |
| ctransaffiliate | → | affiliate_id | |
| caffitid | → | tid | |
| cvendthru | → | other_params | |
| ccustemail | → | customer_email | |
| ccustname | → | customer_first_name + customer_last_name | v2 splits into separate fields |
| No equivalent | → | transactionPayouts | New in v2 — structured payout breakdown |
Keep your v1 handler running for existing products. Detect v2 payloads by checking for
transactionPayouts
or customer_first_name fields. Run v1 and v2
handlers in parallel during rollout and compare results.
FAQ
transactionPayouts
array?
transactionPayouts?
cverify matches the incoming
value. Without verification, anyone could POST fake transaction
data to your endpoint.
Key Points
total is in dollars, not
pennies — this is different from v1's ctransamount
date format matters for verification
— use the exact string from the POST, do not reformat
it
transactionPayouts is a JSON string
inside form data — call json_decode() /
json.loads() on it separately
transactionPayouts may be absent
— always check before parsing
other_params is the v2 equivalent of cvendthru
in v1 — custom passthrough data from your buy link
funnel_id
in the payload