Skip to main content
Optional webhook — This webhook is completely optional. Payment links work normally without it. Configure payment_link_inquiry_notif_url only if you need view or expiry notifications. For completed payments, use the Payment Link Transaction webhook instead.

Information

MethodPathFormatAuthentication
POSThttps://your-webhook-url/callbackjsonHMAC SHA512 Signature
SingaPay sends a POST request to your configured payment_link_inquiry_notif_url when:
  • A customer opens or views a payment link (payment_link.inquiry)
  • A payment link history record expires (payment_link.inquiry.expired)
Separate URL: Unlike money-in transaction webhooks on transaction_notif_url, inquiry events use a dedicated payment_link_inquiry_notif_url. If this URL is not configured, no inquiry webhooks are sent. For batch product expiration, see Product Expiration. For batch unpaid transaction expiration, see Transaction Money-In Expiration.

Request Details

Headers

FieldValueTypeMandatoryDescription
Content-Typeapplication/jsonAlphabeticYesSpecifies JSON format for the request body
User-AgentSingaPaymentGateway/1.0AlphabeticYesIdentifies the source of the webhook
Acceptapplication/jsonAlphabeticYesExpected response format
X-PARTNER-IDAlphanumericYesYour API Key from the merchant dashboard
X-SignatureAlphanumericYesHMAC SHA512 signature (128 chars) for request verification
X-TimestampNumericYesUnix timestamp in seconds when the request was sent
AuthorizationBearer <random_token>AlphanumericYesSystem-generated random bearer token; used as a component in the signature
The Authorization token is a randomly generated string — not a user access token. This webhook is triggered by a system event (link opened or session expired), not a user API call. Extract the token as-is and use it in the string to sign. See How to Validate Signature below.

Body Parameters

status
number
required
HTTP status code. Example: 200
success
boolean
required
Indicates whether the webhook was sent successfully. Example: true
event
string
required
Event type identifier. payment_link.inquiry when a link is opened; payment_link.inquiry.expired when a history session expires.
timestamp
string
required
Event timestamp in format "d M Y H:i:s". Example: "26 Dec 2025 13:35:45"
data
object
required
Container for payment link and history details.

Payload Examples

{
  "status": 200,
  "success": true,
  "event": "payment_link.inquiry",
  "timestamp": "26 Dec 2025 13:35:45",
  "data": {
    "payment_link_history": {
      "id": 12345,
      "reff_no": "PLH-20251226-ABC123",
      "status": "pending",
      "amount": {
        "value": 50000,
        "currency": "IDR"
      },
      "vendor_fee": null,
      "our_margin": null,
      "net_amount": null,
      "payment_method_name": null,
      "payment_method_value": null,
      "payment_method_additional": null,
      "customer_name": null,
      "customer_email": null,
      "customer_phone": null,
      "ip_address": "103.123.45.67",
      "expired_at": "2025-12-26 14:35:45",
      "created_at": "2025-12-26 13:35:45",
      "updated_at": "2025-12-26 13:35:45"
    },
    "payment_link": {
      "id": 678,
      "reff_no": "PL-20251220-XYZ789",
      "title": "Donasi Amal",
      "description": "Donasi untuk kegiatan sosial",
      "status": "active",
      "total_amount": {
        "value": 50000,
        "currency": "IDR"
      },
      "max_usage": 100,
      "current_usage": 25,
      "payment_url": "https://pay.singapay.id/pl/abc123",
      "required_customer_detail": true,
      "expired_at": "2025-12-31 23:59:59",
      "created_at": "2025-12-20 10:00:00",
      "updated_at": "2025-12-26 13:35:45"
    }
  }
}


Security and responses

Return HTTP 200 promptly after validating the request. For retry behavior, see Webhook retry mechanism. Verify every webhook using Security and signature validation. Use your configured callback path when building StringToSign. Handle duplicate deliveries idempotently using stable identifiers from the payload (for example transaction_id or reff_no).

When to use this webhook

Use when

  • Track views and engagement on payment links
  • Monitor conversion (views vs completed payments)
  • Alert when high-value links are accessed
  • Detect abandonment when inquiry sessions expire
  • Trigger follow-up when email was captured but payment was not completed

Skip when

  • You only need completed payment notifications (Payment Link Transaction)
  • You do not need view or inquiry tracking
  • Simple payment links without analytics requirements

Event types

EventWhen it fires
payment_link.inquiryCustomer opens or views the payment link; creates a payment_link_history with status pending
payment_link.inquiry.expiredInquiry session timeout reached; payment_link_history status becomes expired

Data structure

The payload contains two objects:
  1. payment_link_history — This inquiry or payment attempt (unique per link open)
  2. payment_link — Parent link configuration and usage counters (current_usage / max_usage)
Handle inquiry and expired events
<?php
$payload = json_decode($requestBody, true);

if ($payload['event'] === 'payment_link.inquiry') {
    $historyId     = $payload['data']['payment_link_history']['id'];
    $paymentLinkId = $payload['data']['payment_link']['id'];
    $currentUsage  = $payload['data']['payment_link']['current_usage'];
    $maxUsage      = $payload['data']['payment_link']['max_usage'];

    logInquiry($historyId, $paymentLinkId);

    if ($maxUsage && $currentUsage >= $maxUsage * 0.9) {
        sendAlert('Payment link is 90% full');
    }
}

if ($payload['event'] === 'payment_link.inquiry.expired') {
    $historyReffNo = $payload['data']['payment_link_history']['reff_no'];
    logAbandonedPayment($historyReffNo);

    $email = $payload['data']['payment_link_history']['customer_email'] ?? null;
    if ($email !== null) {
        sendFollowUpEmail($email);
    }
}
?>

Customer data handling

Customer fields in payment_link_history may be null on initial inquiry and populated after the customer submits the form or selects a payment method. Always check for null:
$customerName  = $payload['data']['payment_link_history']['customer_name'] ?? 'Unknown';
$customerEmail = $payload['data']['payment_link_history']['customer_email'];

if ($customerEmail !== null) {
    sendEmail($customerEmail, 'Thank you for viewing our payment link');
}

$paymentMethod = $payload['data']['payment_link_history']['payment_method_name'];
if ($paymentMethod !== null) {
    trackMethodSelection($paymentMethod);
}

Timestamp format

This webhook uses human-readable datetime strings, not Unix timestamps in the body.
FieldFormatExample
timestamp (root)d M Y H:i:s26 Dec 2025 13:35:45
payment_link_history.*_atY-m-d H:i:s2025-12-26 13:35:45
payment_link.*_atY-m-d H:i:s2025-12-20 10:00:00
For signature validation, use X-Timestamp (Unix seconds in the header), not the body timestamp field.
Parse body timestamps
$timestamp = $payload['timestamp']; // "26 Dec 2025 13:35:45"
$date = DateTime::createFromFormat('d M Y H:i:s', $timestamp);

// Carbon (Laravel)
$date = \Carbon\Carbon::createFromFormat('d M Y H:i:s', $timestamp);

Access token format

The Authorization header contains a random system-generated token, not a user JWT:
Authorization: Bearer a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
Extract it normally and include it in the string to sign. Validation uses your Client Secret as the HMAC key.

Idempotency

Use payment_link_history.reff_no to detect duplicate deliveries:
$historyReffNo = $payload['data']['payment_link_history']['reff_no'];

if (findInquiryByReffNo($historyReffNo)) {
    http_response_code(200);
    exit;
}

processNewInquiry($payload);

Usage tracking

$paymentLink   = $payload['data']['payment_link'];
$currentUsage  = $paymentLink['current_usage'];
$maxUsage      = $paymentLink['max_usage'];

if ($maxUsage !== null) {
    $usagePercentage = ($currentUsage / $maxUsage) * 100;

    if ($usagePercentage >= 90) {
        sendAlert("Payment link '{$paymentLink['title']}' is at {$usagePercentage}% capacity");
    }

    if ($currentUsage >= $maxUsage) {
        sendAlert("Payment link '{$paymentLink['title']}' has reached maximum usage");
    }
}

Expiration handling

Both the payment link and individual history sessions can expire:
$paymentLinkExpiredAt = $payload['data']['payment_link']['expired_at'];
if ($paymentLinkExpiredAt !== null) {
    $expiryDate = DateTime::createFromFormat('Y-m-d H:i:s', $paymentLinkExpiredAt);
    if ($expiryDate < new DateTime()) {
        logExpiredLink($payload['data']['payment_link']['id']);
    }
}

$historyExpiredAt = $payload['data']['payment_link_history']['expired_at'];
// When the current inquiry session will expire