> ## Documentation Index
> Fetch the complete documentation index at: https://docs.singapay.id/llms.txt
> Use this file to discover all available pages before exploring further.

# Payment Link Inquiry Webhook

> Optional real-time notifications when someone opens or views a payment link, or when an inquiry session expires. Track engagement and conversion without affecting payment link functionality.

<Note>
  **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](/api-reference/webhooks/payment-link) webhook instead.
</Note>

## Information

<div className="overflow-x-auto">
  | Method                                                                                                              | Path                                | Format | Authentication        |
  | ------------------------------------------------------------------------------------------------------------------- | ----------------------------------- | ------ | --------------------- |
  | <span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-bold bg-blue-500 text-white">POST</span> | `https://your-webhook-url/callback` | json   | HMAC SHA512 Signature |
</div>

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`)

<Info>
  **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](/api-reference/webhooks/product-expiration). For batch unpaid transaction expiration, see [Transaction Money-In Expiration](/api-reference/webhooks/transaction-moneyin-expiration).
</Info>

***

## Request Details

### Headers

| Field           | Value                     | Type         | Mandatory | Description                                                                |
| --------------- | ------------------------- | ------------ | :-------: | -------------------------------------------------------------------------- |
| `Content-Type`  | `application/json`        | Alphabetic   |    Yes    | Specifies JSON format for the request body                                 |
| `User-Agent`    | `SingaPaymentGateway/1.0` | Alphabetic   |    Yes    | Identifies the source of the webhook                                       |
| `Accept`        | `application/json`        | Alphabetic   |    Yes    | Expected response format                                                   |
| `X-PARTNER-ID`  | —                         | Alphanumeric |    Yes    | Your API Key from the merchant dashboard                                   |
| `X-Signature`   | —                         | Alphanumeric |    Yes    | HMAC SHA512 signature (128 chars) for request verification                 |
| `X-Timestamp`   | —                         | Numeric      |    Yes    | Unix timestamp in **seconds** when the request was sent                    |
| `Authorization` | `Bearer <random_token>`   | Alphanumeric |    Yes    | System-generated random bearer token; used as a component in the signature |

<Note>
  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](#how-to-validate-signature) below.
</Note>

***

### Body Parameters

<ParamField body="status" type="number" required>
  HTTP status code. Example: `200`
</ParamField>

<ParamField body="success" type="boolean" required>
  Indicates whether the webhook was sent successfully. Example: `true`
</ParamField>

<ParamField body="event" type="string" required>
  Event type identifier. `payment_link.inquiry` when a link is opened; `payment_link.inquiry.expired` when a history session expires.
</ParamField>

<ParamField body="timestamp" type="string" required>
  Event timestamp in format `"d M Y H:i:s"`. Example: `"26 Dec 2025 13:35:45"`
</ParamField>

<ParamField body="data" type="object" required>
  Container for payment link and history details.

  <Expandable title="data fields">
    <ParamField body="payment_link_history" type="object" required>
      Current payment link history (this inquiry or transaction attempt).

      <Expandable title="payment_link_history fields">
        <ParamField body="id" type="number" required>Payment link history ID. Example: `12345`</ParamField>
        <ParamField body="reff_no" type="string" required>Unique reference for this history. Example: `"PLH-20251226-ABC123"`</ParamField>
        <ParamField body="status" type="string" required>History status. E.g. `"pending"`, `"expired"`, `"paid"`, `"failed"`</ParamField>

        <ParamField body="amount" type="object" required>
          Transaction amount for this attempt.

          <Expandable title="amount fields">
            <ParamField body="value" type="number" required>Amount value. Example: `50000`</ParamField>
            <ParamField body="currency" type="string" required>ISO 4217 currency code. Example: `"IDR"`</ParamField>
          </Expandable>
        </ParamField>

        <ParamField body="vendor_fee" type="number">Vendor fee. `null` if not calculated yet.</ParamField>
        <ParamField body="our_margin" type="number">Platform margin. `null` if not calculated yet.</ParamField>
        <ParamField body="net_amount" type="number">Net amount. `null` if not calculated yet.</ParamField>
        <ParamField body="payment_method_name" type="string">Selected payment method name. E.g. `"QRIS"`. `null` if not selected.</ParamField>
        <ParamField body="payment_method_value" type="string">Payment method value. E.g. `"qris"`. `null` if not selected.</ParamField>
        <ParamField body="payment_method_additional" type="object">Additional payment method info. `null` if none.</ParamField>
        <ParamField body="customer_name" type="string">Customer name. `null` if not provided yet.</ParamField>
        <ParamField body="customer_email" type="string">Customer email. `null` if not provided yet.</ParamField>
        <ParamField body="customer_phone" type="string">Customer phone. `null` if not provided yet.</ParamField>
        <ParamField body="ip_address" type="string">Customer IP address. Example: `"103.123.45.67"`</ParamField>
        <ParamField body="expired_at" type="string">History expiration time. Example: `"2025-12-26 14:35:45"`</ParamField>
        <ParamField body="created_at" type="string" required>History creation time.</ParamField>
        <ParamField body="updated_at" type="string" required>History last update time.</ParamField>
      </Expandable>
    </ParamField>

    <ParamField body="payment_link" type="object" required>
      Parent payment link details (shared across inquiries).

      <Expandable title="payment_link fields">
        <ParamField body="id" type="number" required>Payment link ID. Example: `678`</ParamField>
        <ParamField body="reff_no" type="string" required>Payment link reference number. Example: `"PL-20251220-XYZ789"`</ParamField>
        <ParamField body="title" type="string" required>Payment link title. Example: `"Donasi Amal"`</ParamField>
        <ParamField body="description" type="string">Payment link description. `null` if none.</ParamField>
        <ParamField body="status" type="string" required>Payment link status. Example: `"active"`</ParamField>

        <ParamField body="total_amount" type="object" required>
          Payment link amount.

          <Expandable title="total_amount fields">
            <ParamField body="value" type="number" required>Amount value. Example: `50000`</ParamField>
            <ParamField body="currency" type="string" required>ISO 4217 currency code. Example: `"IDR"`</ParamField>
          </Expandable>
        </ParamField>

        <ParamField body="max_usage" type="number">Maximum usage limit. `null` if unlimited.</ParamField>
        <ParamField body="current_usage" type="number" required>Current usage count. Example: `25`</ParamField>
        <ParamField body="payment_url" type="string" required>Full payment link URL.</ParamField>
        <ParamField body="required_customer_detail" type="boolean" required>Whether customer details are required at checkout.</ParamField>
        <ParamField body="expired_at" type="string">Payment link expiration. `null` if no expiry.</ParamField>
        <ParamField body="created_at" type="string" required>Payment link creation time.</ParamField>
        <ParamField body="updated_at" type="string" required>Payment link last update time.</ParamField>
      </Expandable>
    </ParamField>
  </Expandable>
</ParamField>

***

### Payload Examples

<CodeGroup>
  ```json Inquiry theme={null}
  {
    "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"
      }
    }
  }
  ```

  ```json Expired theme={null}
  {
    "status": 200,
    "success": true,
    "event": "payment_link.inquiry.expired",
    "timestamp": "26 Dec 2025 14:35:45",
    "data": {
      "payment_link_history": {
        "id": 12345,
        "reff_no": "PLH-20251226-ABC123",
        "status": "expired",
        "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 14: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 14:35:45"
      }
    }
  }
  ```
</CodeGroup>

***

***

## Security and responses

Return HTTP `200` promptly after validating the request. For retry behavior, see [Webhook retry mechanism](/api-reference/webhooks/retry-mechanism).

Verify every webhook using [Security and signature validation](/api-reference/webhooks/security-and-signature). 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`).

***

## Payment Link Inquiry specific notes

### When to use this webhook

<CardGroup cols={2}>
  <Card title="Use when" icon="circle-check">
    * 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
  </Card>

  <Card title="Skip when" icon="circle-xmark">
    * You only need completed payment notifications ([Payment Link Transaction](/api-reference/webhooks/payment-link))
    * You do not need view or inquiry tracking
    * Simple payment links without analytics requirements
  </Card>
</CardGroup>

### Event types

| Event                          | When it fires                                                                                    |
| ------------------------------ | ------------------------------------------------------------------------------------------------ |
| `payment_link.inquiry`         | Customer opens or views the payment link; creates a `payment_link_history` with status `pending` |
| `payment_link.inquiry.expired` | Inquiry 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`)

```php Handle inquiry and expired events theme={null}
<?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`:

```php theme={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

<Warning>
  This webhook uses **human-readable datetime strings**, not Unix timestamps in the body.
</Warning>

| Field                       | Format        | Example                |
| --------------------------- | ------------- | ---------------------- |
| `timestamp` (root)          | `d M Y H:i:s` | `26 Dec 2025 13:35:45` |
| `payment_link_history.*_at` | `Y-m-d H:i:s` | `2025-12-26 13:35:45`  |
| `payment_link.*_at`         | `Y-m-d H:i:s` | `2025-12-20 10:00:00`  |

For signature validation, use **`X-Timestamp`** (Unix seconds in the header), not the body `timestamp` field.

```php Parse body timestamps theme={null}
$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:

```php theme={null}
$historyReffNo = $payload['data']['payment_link_history']['reff_no'];

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

processNewInquiry($payload);
```

### Usage tracking

```php theme={null}
$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:

```php theme={null}
$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
```
