Registering your webhook

Register

In the PayGate app portal, open your app’s settings page (/apps/{id}) and fill in Webhook URL — an HTTPS endpoint on your own server that accepts POST requests. Save.

Then click Generate webhook secret (or Regenerate webhook secret if you already have one). The plaintext secret is shown exactly once — copy it now. PayGate stores only an encrypted copy; if you lose the plaintext, regenerate it (this invalidates the previous secret immediately).

There’s no separate “enable” step — as soon as webhook_url is set and a secret exists, deliveries start going out on the next qualifying event.

Delivery format

POST to your webhook_url, Content-Type: application/json:

{
  "event_type": "subscription.updated",
  "app_id": 2,
  "data": {
    "external_ref": "user-123",
    "paygate_customer_id": "cus_5d2e7b9a1c4f8036ab12de45",
    "merchant_customer_id": "crm-1001",
    "plan_id": "plan_3f9c1a7e5b2d4c8a91e0f6b2",
    "price_id": "price_8a1d6c3e9f024b7d5a2c1e90",
    "plan_ref": "premium",
    "status": "active",
    "current_period_end": "2026-10-15 05:55:56",
    "in_trial": false,
    "trial_ends_at": null
  },
  "created_at": "2026-09-15T05:55:56+00:00"
}

The envelope (event_type, app_id, data, created_at) is the same for every event; only data differs. The events are listed in 04 — Webhooks.

Headers:

Header Meaning
X-PayGate-Timestamp Unix timestamp (seconds) when the request was signed
X-PayGate-Signature hex(HMAC_SHA256("{timestamp}.{raw_body}", your_webhook_secret))

Payload examples

Only the data object is shown; it sits in the envelope above.

subscription.renewed:

{
  "external_ref": "user-123",
  "paygate_customer_id": "cus_5d2e7b9a1c4f8036ab12de45",
  "merchant_customer_id": "crm-1001",
  "plan_id": "plan_3f9c1a7e5b2d4c8a91e0f6b2",
  "price_id": "price_8a1d6c3e9f024b7d5a2c1e90",
  "plan_ref": "premium",
  "provider_code": "payos",
  "transaction_id": 57,
  "amount": 199000,
  "currency": "VND",
  "previous_period_end": "2026-10-15 05:55:56",
  "current_period_end": "2026-11-15 05:55:56",
  "was_trial": false
}

was_trial is true when the payment converted a trial into a paid period.

subscription.renewal_reminder_sent:

{
  "external_ref": "user-123",
  "paygate_customer_id": "cus_5d2e7b9a1c4f8036ab12de45",
  "merchant_customer_id": "crm-1001",
  "plan_id": "plan_3f9c1a7e5b2d4c8a91e0f6b2",
  "price_id": "price_8a1d6c3e9f024b7d5a2c1e90",
  "plan_ref": "premium",
  "reminder_day": 3,
  "period_end": "2026-10-15 05:55:56",
  "in_trial": false,
  "sent_at": "2026-10-12T05:55:56+00:00"
}

reminder_day is 3, 1, or 0 for the “expired” notice.

transaction.completed (one-time mode: "payment" only):

{
  "external_ref": "user-123",
  "plan_ref": "lifetime_pass",
  "amount": 199000,
  "currency": "VND",
  "status": "completed"
}

Verifying the signature

Recompute the signature over the exact raw request body (before any JSON re-encoding) and compare using a constant-time function:

$timestamp = $_SERVER['HTTP_X_PAYGATE_TIMESTAMP'];
$signature = $_SERVER['HTTP_X_PAYGATE_SIGNATURE'];
$rawBody = file_get_contents('php://input');

$expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $yourWebhookSecret);

if (!hash_equals($expected, $signature)) {
    http_response_code(400);
    exit;
}

Reject requests where $timestamp is too far from the current time (e.g. more than 5 minutes) to guard against replay of an intercepted payload.

Responding

Return any 2xx status to acknowledge. Anything else (including no response / timeout) is treated as a failure and retried.

Retries

Failed deliveries are retried up to 5 times with increasing backoff (1, 5, 15, 60, 240 minutes). After the final attempt, the delivery is marked failed and not retried further — if you need to recover a missed event, poll GET /api/v1/subscriptions/{external_ref} (or GET /api/v1/transactions/{transaction_id} for a one-time payment) instead.

Webhook delivery is fire-and-forget relative to the underlying operation — a delivery failure never rolls back or blocks the payment/subscription change itself.


This site uses Just the Docs, a documentation theme for Jekyll.