
Webhooks
The Transaction Money-In Expiration Webhooks API provides optional batch notifications when unpaid money-in transactions expire. This webhook allows you to track and manage expired payment link histories, virtual account transactions, and QRIS histories in a single notification, helping you monitor unpaid invoices and improve collection efficiency.
| Method | Path | Format | Authentication |
|---|---|---|---|
| POST | https://your-webhook-url/callback | json | HMAC SHA512 Signature |
Important: This webhook is completely optional. You can choose not to configure this webhook if you don’t need transaction expiration notifications. Your transactions will still expire normally - this webhook only serves as a notification mechanism when unpaid money-in transactions expire.
This webhook sends a POST request to your configured transaction_expiration_notif_url when money-in transactions (Payment Link Histories, Virtual Account Transactions, or QRIS Histories) expire before payment is completed. Unlike other webhooks, this is a batch notification that can include multiple expired transactions in a single webhook call.

The transaction_expiration_notif_url configuration can be accessed from the settings page as shown in the screenshot above.
When money-in transactions expire without payment, Singa Payment Gateway can send a batch webhook notification to your registered callback URL. The request includes security headers for verification.
| Field | Value | Type | Mandatory | Length | Description | Example |
|---|---|---|---|---|---|---|
| Content-Type | application/json | Alphabetic | Mandatory | Specifies JSON format for the request body | application/json | |
| User-Agent | SingaPaymentGateway/1.0 | Alphabetic | Mandatory | Identifies the source of the webhook | SingaPaymentGateway/1.0 | |
| X-Signature | Alphanumeric | Optional* | 128 | HMAC SHA512 signature for request verification (included when signature security is enabled) | 5f4dcc3b5aa765d61d8327deb882cf99… | |
| X-Timestamp | Numeric | Optional* | 10 | Unix timestamp in seconds when the request was sent (included when signature security is enabled) | 1695711945 | |
| Authorization | Bearer <random_token> | Alphanumeric | Optional* | Bearer token with random value (system-generated, not user token) | Bearer a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 |
* Note: Headers marked as “Optional” are included when you enable signature-based security. See the Security Mechanisms section below to understand the different security options available.
Authorization Note: The access token in the Authorization header is a randomly generated string (not a user access token) because this webhook is triggered by the system’s scheduled expiration checker, not by a user action.
| Field | Type | Mandatory | Length | Description | Example |
|---|---|---|---|---|---|
| status | Numeric | Mandatory | 3 | HTTP Status Code | 200 |
| success | Boolean | Mandatory | 1 | Indicates if the webhook was sent successfully | true |
| event | String | Mandatory | - | Event type identifier (always “transaction_expiration”) | transaction_expiration |
| timestamp | String | Mandatory | - | Event timestamp in format “d M Y H:i:s” | 26 Dec 2025 14:00:00 |
| merchant | Object | Mandatory | - | Merchant information | - |
| > id | Numeric | Mandatory | - | Merchant ID | 123 |
| > name | String | Mandatory | - | Merchant name | PT Example Indonesia |
| data | Object | Mandatory | - | Container for expired transactions | - |
| > payment_link_histories | Array | Mandatory | - | List of expired payment link histories (transaction attempts) | [] |
| >> id | Numeric | Mandatory | - | Payment link history ID | 456 |
| >> reff_no | String | Mandatory | - | History reference number | PLH-20251226-ABC123 |
| >> payment_link_id | Numeric | Mandatory | - | Parent payment link ID | 789 |
| >> status | String | Mandatory | - | History status (should be “expired”) | expired |
| >> expired_at | String | Mandatory | - | Expiration datetime | 2025-12-26 14:00:00 |
| > virtual_account_transactions | Array | Mandatory | - | List of expired VA transactions (unpaid transactions) | [] |
| >> id | Numeric | Mandatory | - | VA transaction ID | 321 |
| >> reff_no | String | Mandatory | - | Transaction reference number | VAT-20251226-DEF456 |
| >> virtual_account_id | Numeric | Mandatory | - | Parent virtual account ID | 654 |
| >> status | String | Mandatory | - | Transaction status (should be “expired”) | expired |
| >> expired_at | String | Mandatory | - | Expiration datetime | 2025-12-26 14:00:00 |
| > qris_histories | Array | Mandatory | - | List of expired QRIS histories (transaction attempts) | [] |
| >> id | Numeric | Mandatory | - | QRIS history ID | 987 |
| >> reff_no | String | Mandatory | - | History reference number | QRH-20251226-GHI789 |
| >> qris_transaction_id | Numeric | Mandatory | - | Parent QRIS transaction ID | 246 |
| >> status | String | Mandatory | - | History status (should be “expired”) | expired |
| >> expired_at | String | Mandatory | - | Expiration datetime | 2025-12-26 14:00:00 |
| summary | Object | Mandatory | - | Summary of expiration counts | - |
| > total_expired | Numeric | Mandatory | - | Total count of all expired transactions | 15 |
| > payment_link_histories_count | Numeric | Mandatory | - | Count of expired payment link histories | 5 |
| > virtual_account_transactions_count | Numeric | Mandatory | - | Count of expired VA transactions | 7 |
| > qris_histories_count | Numeric | Mandatory | - | Count of expired QRIS histories | 3 |
Batch Expiration: Here’s an example with multiple transaction types expired.
{
"status": 200,
"success": true,
"event": "transaction_expiration",
"timestamp": "26 Dec 2025 14:00:00",
"merchant": {
"id": 123,
"name": "PT Example Indonesia"
},
"data": {
"payment_link_histories": [
{
"id": 456,
"reff_no": "PLH-20251226-ABC123",
"payment_link_id": 789,
"status": "expired",
"expired_at": "2025-12-26 14:00:00"
},
{
"id": 457,
"reff_no": "PLH-20251226-DEF456",
"payment_link_id": 790,
"status": "expired",
"expired_at": "2025-12-26 14:00:00"
}
],
"virtual_account_transactions": [
{
"id": 321,
"reff_no": "VAT-20251226-GHI789",
"virtual_account_id": 654,
"status": "expired",
"expired_at": "2025-12-26 14:00:00"
},
{
"id": 322,
"reff_no": "VAT-20251226-JKL012",
"virtual_account_id": 655,
"status": "expired",
"expired_at": "2025-12-26 14:00:00"
},
{
"id": 323,
"reff_no": "VAT-20251226-MNO345",
"virtual_account_id": 656,
"status": "expired",
"expired_at": "2025-12-26 14:00:00"
}
],
"qris_histories": [
{
"id": 987,
"reff_no": "QRH-20251226-PQR678",
"qris_transaction_id": 246,
"status": "expired",
"expired_at": "2025-12-26 14:00:00"
}
]
},
"summary": {
"total_expired": 6,
"payment_link_histories_count": 2,
"virtual_account_transactions_count": 3,
"qris_histories_count": 1
}
}
Single Transaction Type: You might receive only one type of expired transaction.
{
"status": 200,
"success": true,
"event": "transaction_expiration",
"timestamp": "26 Dec 2025 14:00:00",
"merchant": {
"id": 123,
"name": "PT Example Indonesia"
},
"data": {
"payment_link_histories": [],
"virtual_account_transactions": [
{
"id": 321,
"reff_no": "VAT-20251226-GHI789",
"virtual_account_id": 654,
"status": "expired",
"expired_at": "2025-12-26 14:00:00"
}
],
"qris_histories": []
},
"summary": {
"total_expired": 1,
"payment_link_histories_count": 0,
"virtual_account_transactions_count": 1,
"qris_histories_count": 0
}
}
To ensure the security and authenticity of webhook requests from Singa Payment Gateway, we provide two recommended security mechanisms. You can choose one or combine both for maximum protection.
Important Note: Signature validation is optional but highly recommended. You can secure your webhook endpoint using either IP whitelisting, signature validation, or both methods together.
This is the simplest security method where you restrict webhook access to only authorized IP addresses from Singa Payment Gateway.
How it works:
Pros:
Cons:
Implementation Example (Nginx):
location /webhook/transaction-expiration {
# Only allow Singa Payment Gateway IPs
allow 103.xxx.xxx.xxx; # Replace with actual IPs from Singa
allow 103.xxx.xxx.xxx; # Replace with actual IPs from Singa
deny all;
proxy_pass http://your-backend;
}
Implementation Example (PHP):
<?php
// Define allowed IPs (get these from Singa Payment Gateway)
$allowedIPs = [
'103.xxx.xxx.xxx',
'103.xxx.xxx.xxx',
];
$requestIP = $_SERVER['REMOTE_ADDR'];
if (!in_array($requestIP, $allowedIPs)) {
http_response_code(403);
echo json_encode(['status' => 'error', 'message' => 'Access denied']);
exit;
}
// Process webhook...
?>
This method uses cryptographic signatures to verify that:
How it works:
Pros:
Cons:
When to use: Production environments, handling sensitive data, or when maximum security is required.
See the detailed implementation guide in the “How to Validate Signature” section below.
For production environments, we strongly recommend using both IP whitelisting and signature validation together.
Implementation order:
Benefits:
Example Implementation:
<?php
// Layer 1: IP Whitelist
$allowedIPs = ['103.xxx.xxx.xxx', '103.xxx.xxx.xxx'];
$requestIP = $_SERVER['REMOTE_ADDR'];
if (!in_array($requestIP, $allowedIPs)) {
http_response_code(403);
exit;
}
// Layer 2: Signature Validation
$requestBody = file_get_contents('php://input');
$headers = getallheaders();
$clientSecret = 'your-client-secret';
$endpoint = '/webhook/transaction-expiration';
if (!validateWebhookSignature($requestBody, $headers, $clientSecret, $endpoint)) {
http_response_code(401);
exit;
}
// Both checks passed - process webhook
$payload = json_decode($requestBody, true);
// ... process expiration notification ...
?>
| Recommended Approach | Reason |
|---|---|
| IP Whitelist only | Simple to implement, easy to debug, suitable for testing environments |
| Signature validation only | Higher security, good for testing signature implementation |
| IP Whitelist + Signature ⭐ | Maximum security, industry best practice, recommended for production |
| IP Whitelist + Signature + Timestamp validation | Additional protection against replay attacks for high-security requirements |
To implement IP whitelisting, contact our support team or check your merchant dashboard for the official list of Singa Payment Gateway IP addresses.
Note: We will notify you in advance if our IP addresses change.
The X-Signature header is a security mechanism that ensures the webhook request is authentic and comes from Singa Payment Gateway. This signature is generated using HMAC SHA512 algorithm.
Implementation Note: Transaction Money-In Expiration webhooks use the signature generation method from the GeneratesCallbackSignature trait, which is the standard signature implementation across most webhooks.
Note: While signature validation is optional, we strongly recommend implementing it, especially for production environments, to ensure maximum security and data integrity.
The signature is created using a multi-step process that combines the request method, endpoint, access token, hashed body, and timestamp.
Reference Implementation:
App\Traits\GeneratesCallbackSignatureGeneratesCallbackSignature::generateHeadersCallback() (lines 30-81)GeneratesCallbackSignature::hashNormalizedJson() (lines 90-118)GeneratesCallbackSignature::sortRecursive() (lines 129-143)GeneratesCallbackSignature::extractEndpoint() (lines 154-167)Extract the following headers from the incoming webhook request:
Important: The access token in this webhook is a randomly generated string, not a user access token, since the webhook is triggered by the system’s scheduled job.
Retrieve your Client Secret from the merchant dashboard. This is the same secret used for API authentication and is required as the HMAC key for signature verification.
Important: The Client Secret must be kept secure and never exposed in client-side code or logs.
Extract the endpoint path from your webhook URL. For example:
https://yourdomain.com/webhook/transaction-expiration?param=value/webhook/transaction-expiration?param=valueThe endpoint includes the path and any query parameters.
The request body must be normalized before hashing to ensure consistent results:
JSON_UNESCAPED_UNICODE - Don’t escape Unicode charactersJSON_UNESCAPED_SLASHES - Don’t escape forward slashesExample:
Original JSON: {"status":200,"success":true,"event":"transaction_expiration"}
After sorting: {"event":"transaction_expiration","status":200,"success":true}
SHA-256 Hash: 5f4dcc3b5aa765d61d8327deb882cf99acd3d28e5cf0e661c02c8e8e6e8e6f9a
Concatenate the following values with colon (:) as separator:
StringToSign = METHOD + ":" + ENDPOINT + ":" + ACCESS_TOKEN + ":" + HASHED_BODY + ":" + TIMESTAMP
Example:
Method: POST
Endpoint: /webhook/transaction-expiration
Access Token: a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
Hashed Body: 5f4dcc3b5aa765d61d8327deb882cf99acd3d28e5cf0e661c02c8e8e6e8e6f9a
Timestamp: 1695711945
StringToSign = POST:/webhook/transaction-expiration:a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6:5f4dcc3b5aa765d61d8327deb882cf99acd3d28e5cf0e661c02c8e8e6e8e6f9a:1695711945
Use your Client Secret as the HMAC key and hash the string to sign:
Calculated Signature = HMAC-SHA512(StringToSign, Client Secret)
Compare the calculated signature with the signature from the X-Signature header using a constant-time comparison to prevent timing attacks.
Important: Use hash_equals() in PHP, crypto.timingSafeEqual() in Node.js, or hmac.compare_digest() in Python for secure comparison.
if (hash_equals($calculatedSignature, $receivedSignature)) {
// Signature is valid - process webhook
} else {
// Signature is invalid - reject request
}
Best Practices
hash_equals(), crypto.timingSafeEqual(), or hmac.compare_digest())client_secret as the HMAC keyCommon Mistakes to Avoid
:, not _ or -)Your webhook endpoint must return an appropriate HTTP response:
When the webhook is processed successfully:
Status Code: 200 OK
{
"status": "success"
}
Invalid Signature (401 Unauthorized):
{
"status": "error",
"message": "Invalid signature"
}
Processing Error (500 Internal Server Error):
{
"status": "error",
"message": "Failed to process webhook"
}
Important: Singa Payment Gateway will retry failed webhooks (non-200 responses) up to 3 times with exponential backoff.
This webhook is completely optional. If you don’t configure a transaction_expiration_notif_url, no webhooks will be sent for transaction expirations. Your transactions will still expire normally and change status accordingly - this webhook only serves as a notification mechanism.
When to use this webhook:
When you can skip this webhook:
IMPORTANT: This webhook is different from the Product Expiration webhook. Understanding the difference is crucial:
| Aspect | Transaction Expiration (This Webhook) | Product Expiration Webhook |
|---|---|---|
| What expires | Transaction attempts/records (unpaid) | Payment products themselves |
| Payment Link | Payment Link History (individual payment attempt) | Payment Link (the product/page) |
| Virtual Account | VA Transaction (individual transaction) | Virtual Account (the VA number) |
| QRIS | QRIS History (individual payment attempt) | QRIS Transaction (the QR code) |
| Use case | Track unpaid transactions, abandoned payments | Cleanup expired products, manage inventory |
| Example | Customer opened payment link but didn’t pay | Payment link reached max usage or expiry date |
| Reusability | Parent product can be reused after transaction expires | Product itself is expired and cannot be reused |
Example scenario to illustrate the difference:
Scenario: Payment Link with 30-minute transaction expiry
1. Customer opens payment link at 10:00 AM
→ Creates Payment Link History (transaction attempt)
→ History expires at 10:30 AM if not paid
2. At 10:30 AM - Transaction Expiration Webhook fires
→ Notifies about expired Payment Link History
→ Payment link itself is still active and can be used again
3. Customer opens same payment link again at 11:00 AM
→ Creates NEW Payment Link History
→ New history expires at 11:30 AM if not paid
4. Payment link reaches its own expiry date (e.g., end of day)
→ Product Expiration Webhook fires
→ Payment link itself is now expired
→ Can no longer be used to create new transactions
This webhook is unique - it sends batch notifications containing multiple expired transactions in a single webhook call:
Example scenarios:
// Scenario 1: Only VA transactions expired
{
"data": {
"payment_link_histories": [], // Empty
"virtual_account_transactions": [/* 10 expired VA txns */],
"qris_histories": [] // Empty
},
"summary": {
"total_expired": 10,
"virtual_account_transactions_count": 10
}
}
// Scenario 2: Mixed expired transactions
{
"data": {
"payment_link_histories": [/* 3 expired PL histories */],
"virtual_account_transactions": [/* 5 expired VA txns */],
"qris_histories": [/* 2 expired QRIS histories */]
},
"summary": {
"total_expired": 10,
"payment_link_histories_count": 3,
"virtual_account_transactions_count": 5,
"qris_histories_count": 2
}
}
When handling batch notifications, iterate through each transaction type:
$payload = json_decode($requestBody, true);
// Process expired payment link histories
foreach ($payload['data']['payment_link_histories'] as $plHistory) {
$id = $plHistory['id'];
$reffNo = $plHistory['reff_no'];
$paymentLinkId = $plHistory['payment_link_id'];
// Update your database
updatePaymentLinkHistoryStatus($id, 'expired');
// Notify customer about abandoned payment
notifyCustomerAbandonedPayment($reffNo);
// Log for analytics
logAbandonedTransaction('payment_link', $id);
// Trigger remarketing
sendRemarketingEmail($paymentLinkId);
}
// Process expired VA transactions
foreach ($payload['data']['virtual_account_transactions'] as $vaTxn) {
$id = $vaTxn['id'];
$reffNo = $vaTxn['reff_no'];
$vaId = $vaTxn['virtual_account_id'];
// Update your database
updateVATransactionStatus($id, 'expired');
// Notify customer
notifyCustomerVAExpired($reffNo, $vaId);
// Log for analytics
logAbandonedTransaction('virtual_account', $id);
}
// Process expired QRIS histories
foreach ($payload['data']['qris_histories'] as $qrisHistory) {
$id = $qrisHistory['id'];
$reffNo = $qrisHistory['reff_no'];
$qrisTxnId = $qrisHistory['qris_transaction_id'];
// Update your database
updateQrisHistoryStatus($id, 'expired');
// Log for analytics
logAbandonedTransaction('qris', $id);
}
// Use summary for quick stats
$totalExpired = $payload['summary']['total_expired'];
if ($totalExpired > 100) {
alertHighAbandonmentRate($totalExpired);
}
The webhook can contain three types of expired transactions:
Payment Link Histories (data.payment_link_histories)
Virtual Account Transactions (data.virtual_account_transactions)
QRIS Histories (data.qris_histories)
Important: Each expired transaction includes the parent product ID:
payment_link_id - ID of the parent payment linkvirtual_account_id - ID of the parent virtual accountqris_transaction_id - ID of the parent QRIS transactionUse these IDs to:
Example:
// Get parent payment link details
$plHistory = $payload['data']['payment_link_histories'][0];
$paymentLinkId = $plHistory['payment_link_id'];
// Fetch full payment link data via API
$paymentLink = fetchPaymentLinkAPI($paymentLinkId);
// Send reminder with same payment link
sendReminderEmail(
customer: getCustomerEmail($plHistory['reff_no']),
paymentLink: $paymentLink['payment_url'],
message: "Your previous payment expired. Click here to pay again!"
);
Note: The webhook sends minimal data for each expired transaction to keep the payload size manageable for batch notifications. The following fields are NOT included:
If you need complete transaction details, use the transaction’s id or reff_no to query via API, or use the parent product ID to fetch product details.
Important: This webhook uses human-readable timestamp format, not Unix timestamps:
d M Y H:i:s (e.g., “26 Dec 2025 14:00:00”)timestamp (root level)expired_at (for each transaction)Parsing timestamps:
// PHP
$timestamp = $payload['timestamp']; // "26 Dec 2025 14:00:00"
$date = DateTime::createFromFormat('d M Y H:i:s', $timestamp);
// Or use Carbon (Laravel)
$date = \Carbon\Carbon::createFromFormat('d M Y H:i:s', $timestamp);
# Python
from datetime import datetime
timestamp = payload['timestamp'] # "26 Dec 2025 14:00:00"
date = datetime.strptime(timestamp, '%d %b %Y %H:%M:%S')
// JavaScript
const timestamp = payload.timestamp; // "26 Dec 2025 14:00:00"
const date = new Date(timestamp);
The Authorization header contains a randomly generated token, not a user access token:
// This is NOT a user JWT token - it's a random string
Authorization: Bearer a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
Why random?
Str::random(32) for each webhookFor signature validation:
Use unique reference numbers to prevent duplicate processing:
// Generate unique webhook identifier
$webhookId = $payload['event'] . '_' . $payload['merchant']['id'] . '_' . strtotime($payload['timestamp']);
// Check if already processed
if (isWebhookProcessed($webhookId)) {
// Already processed, return success without re-processing
return http_response_code(200);
}
// Process webhook
processExpirationBatch($payload);
// Mark as processed
markWebhookProcessed($webhookId);
Use the summary object for quick stats and alerting:
$summary = $payload['summary'];
// Log batch statistics
logTransactionExpirationBatch([
'total' => $summary['total_expired'],
'payment_link_histories' => $summary['payment_link_histories_count'],
'va_transactions' => $summary['virtual_account_transactions_count'],
'qris_histories' => $summary['qris_histories_count'],
'timestamp' => $payload['timestamp']
]);
// Alert on high abandonment rates
if ($summary['total_expired'] > 50) {
sendAlert("High abandonment rate: {$summary['total_expired']} transactions expired");
}
// Track abandonment patterns
if ($summary['payment_link_histories_count'] > $summary['total_expired'] * 0.7) {
// More than 70% are payment link histories
alertTeam("Most expirations are payment links - review link settings or send reminders");
}
Handle cases where arrays might be empty:
// Check if any transactions expired
if ($payload['summary']['total_expired'] === 0) {
// No transactions expired (shouldn't normally happen, but be safe)
return http_response_code(200);
}
// Check each transaction type before processing
if (!empty($payload['data']['payment_link_histories'])) {
processExpiredPaymentLinkHistories($payload['data']['payment_link_histories']);
}
if (!empty($payload['data']['virtual_account_transactions'])) {
processExpiredVATransactions($payload['data']['virtual_account_transactions']);
}
if (!empty($payload['data']['qris_histories'])) {
processExpiredQrisHistories($payload['data']['qris_histories']);
}
When does this webhook fire?
Important: Don’t rely on this webhook for real-time expiration detection. If you need immediate expiration handling, use individual transaction webhooks or poll transaction status via API.
1. Abandoned Cart Recovery:
// Send follow-up for expired transactions
foreach ($payload['data']['payment_link_histories'] as $plh) {
$paymentLinkId = $plh['payment_link_id'];
$customer = getCustomerByHistory($plh['reff_no']);
if ($customer) {
// Send reminder email
sendEmail($customer->email, [
'subject' => 'Complete your payment',
'message' => 'Your payment session expired. Click here to try again!',
'payment_url' => getPaymentLinkUrl($paymentLinkId)
]);
// Track remarketing
logRemarketingCampaign($customer->id, 'abandoned_payment');
}
}
2. Conversion Tracking:
// Track conversion rates
foreach ($payload['data']['payment_link_histories'] as $plh) {
$paymentLinkId = $plh['payment_link_id'];
// Increment abandonment counter
incrementAbandonmentCount($paymentLinkId);
// Calculate conversion rate
$conversionRate = calculateConversionRate($paymentLinkId);
if ($conversionRate < 0.3) {
// Less than 30% conversion
alertMarketing("Low conversion rate for payment link #{$paymentLinkId}: {$conversionRate}%");
}
}
3. Analytics and Reporting:
// Track expiration patterns
generateTransactionExpirationReport([
'date' => $payload['timestamp'],
'total' => $payload['summary']['total_expired'],
'by_type' => [
'payment_link_histories' => $payload['summary']['payment_link_histories_count'],
'va_transactions' => $payload['summary']['virtual_account_transactions_count'],
'qris_histories' => $payload['summary']['qris_histories_count'],
],
'abandonment_rate' => calculateAbandonmentRate($payload['summary']['total_expired'])
]);
4. Collections Monitoring:
// Alert collections team
$summary = $payload['summary'];
$totalExpired = $summary['total_expired'];
// Categorize by urgency
$highPriorityCount = 0;
foreach ($payload['data']['virtual_account_transactions'] as $vaTxn) {
if (isHighValueTransaction($vaTxn['virtual_account_id'])) {
$highPriorityCount++;
}
}
if ($highPriorityCount > 10) {
alertCollectionsTeam("{$highPriorityCount} high-value transactions expired - follow up immediately");
}
// Track trends
if ($totalExpired > historicalAverage() * 1.5) {
alertOperations("Transaction expiration rate 1.5x above average - investigate payment flow");
}
5. Customer Re-engagement:
// Segment customers by expiration behavior
foreach ($payload['data']['payment_link_histories'] as $plh) {
$customer = getCustomerByHistory($plh['reff_no']);
// Count abandoned payments
$abandonmentCount = countAbandonedPayments($customer->id);
if ($abandonmentCount >= 3) {
// Frequent abandoner - special treatment
triggerSpecialOffer($customer->id);
assignAccountManager($customer->id);
} elseif ($abandonmentCount == 1) {
// First time abandoner - gentle reminder
sendFriendlyReminder($customer->email);
}
}
1. Combine with Product Expiration Webhook
Use both webhooks together for complete visibility:
2. Set Appropriate Transaction Expiry Times
Balance between user experience and operational efficiency:
3. Implement Smart Remarketing
Don’t spam customers - use intelligent follow-up:
// Track reminder frequency
$reminderCount = getReminderCount($customer->id);
if ($reminderCount < 2) {
// Send reminder
sendPaymentReminder($customer);
incrementReminderCount($customer->id);
} else {
// Too many reminders - stop
logExcessiveReminders($customer->id);
}
4. Analyze Expiration Patterns
Use the data to improve your payment flow:
5. Correlate with Success Webhooks
Compare expired transactions with successful payments:
$expirationRate = $totalExpired / ($totalExpired + $totalSuccessful);
if ($expirationRate > 0.5) {
// More than 50% of transactions expire
alertProduct("High expiration rate - review payment flow and expiry settings");
}