Search by

cuongcds / paygate-php

cuongcds

PHP SDK for the PayGate payment gateway API

Package info

github.com/cuongcds/paygate-php

pkg:composer/cuongcds/paygate-php

Statistics

Installs: 40

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-09-15 07:27 UTC

This package is auto-updated.

Last update: 2026-10-03 03:49:38 UTC


README

PHP client for PayGate, a multi-tenant payment gateway. See that repo for the full, language-agnostic API reference this SDK wraps.

Install

composer require cuongcds/paygate-php

Usage — HMAC (server-to-server)

Use this when your own backend calls PayGate — never embed api_secret in a mobile app or browser bundle.

use PayGate\Client;
use PayGate\Exceptions\PayGateException;

$client = Client::withHmac(
    'https://payments.example.com',
    'pgk_your_api_key',
    'your_api_secret'
);

try {
    $result = $client->createCheckoutSession([
        'external_ref' => 'user-42',
        'mode' => 'subscription',
        'price_id' => 'price_abc123', // from createPlan()/listPlans()
        'customer_id' => 'cus_abc123', // from createCustomer()
        'success_url' => 'https://yourapp.com/success',
        'cancel_url' => 'https://yourapp.com/cancel',
    ]);

    header('Location: ' . $result['checkout_url']);
} catch (PayGateException $e) {
    // $e->getErrorCode() is one of the codes in
    // https://github.com/cuongcds/paygate-docs/blob/main/documents/05-errors.md
    echo "Could not start checkout: {$e->getErrorCode()} — {$e->getMessage()}";
}

Usage — Firebase ID Token (client calls PayGate directly)

Use this when your client already authenticates end users with Firebase Auth. external_ref is derived from the token automatically — never pass it.

use PayGate\Client;

$client = Client::withFirebaseIdToken(
    'https://payments.example.com',
    $firebaseIdToken // from your client, e.g. a mobile app's Authorization header
);

$subscription = $client->getSubscription($currentUserUid);

Checking subscription status

use PayGate\Client;
use PayGate\Exceptions\PayGateException;

try {
    $subscription = $client->getSubscription('user-42');
    // [
    //   'plan_ref' => 'premium', 'plan_id' => 'plan_…', 'price_id' => 'price_…', 'customer_id' => 'cus_…',
    //   'status' => 'active', 'current_period_end' => '2026-04-15 00:00:00',
    //   'in_trial' => false, 'trial_ends_at' => null,
    // ]
} catch (PayGateException $e) {
    if ($e->getErrorCode() === 'not_found') {
        // user has never checked out — not necessarily an error in your flow
    }
}

Customers

A subscription checkout needs a customer. Writes use HMAC auth.

$customer = $client->createCustomer([
    'name' => 'Nguyen Van A',
    'email' => 'a@example.com',
    'cc_emails' => ['billing@example.com'],
    'merchant_customer_id' => 'user-42', // your own id, optional
    'locale' => 'vi', // 'en' | 'vi'
]);

$client->getCustomer($customer['id']);
$client->updateCustomer($customer['id'], ['locale' => 'en']); // partial update

Plans and prices

A plan holds many prices on sale at once (e.g. monthly, half-year, yearly). Plan writes are HMAC only.

$plan = $client->createPlan([
    'code' => 'premium',
    'name' => 'Premium',
    'prices' => [
        [
            'name' => 'Monthly',
            'amount' => 199000,
            'currency' => 'VND',
            'interval_unit' => 'month',
            'interval_count' => 1,
            'trial' => ['amount' => 0, 'interval_unit' => 'day', 'interval_count' => 7],
        ],
        ['name' => 'Half-year', 'amount' => 1090000, 'currency' => 'VND', 'interval_unit' => 'month', 'interval_count' => 6],
        ['name' => 'Yearly', 'amount' => 1990000, 'currency' => 'VND', 'interval_unit' => 'year', 'interval_count' => 1],
    ],
]);

$active = $client->listPlans(['status' => 'active']); // or listPlans() for all
$client->getPlan($plan['id']);
$client->updatePlan($plan['id'], ['description' => 'All features']);
$client->addPlanPrice($plan['id'], ['amount' => 99000, 'currency' => 'VND', 'interval_unit' => 'week', 'interval_count' => 1]);
$client->updatePlanPrice($plan['id'], $plan['prices'][0]['id'], ['status' => 'archived']);

Subscription checkout with a price

For mode: 'subscription', send price_id + customer_id (optionally skip_trial, payment_method, success_url, cancel_url, external_ref). Amount, currency and interval come from the price — do not send plan_ref, amount, currency, interval, interval_count or plan_id. The response is ['checkout_url' => ..., 'activated' => ..., 'trial' => ...].

$session = $client->createCheckoutSession([
    'mode' => 'subscription',
    'price_id' => $plan['prices'][2]['id'],
    'customer_id' => $customer['id'],
    'external_ref' => 'user-42',
    'skip_trial' => false,
]);
// $session['activated'] — true when no payment was needed; $session['trial'] — the applied trial or null

One-time payments (mode: 'payment') keep plan_ref, amount, currency, success_url and cancel_url.

Changing a subscription's customer or price

$client->setSubscriptionCustomer('user-42', 'cus_abc123');
$client->setSubscriptionPrice('user-42', 'price_xyz789'); // HMAC only, effective from the next renewal

Verifying a checkout redirect

success_url/cancel_url come back with paygate_transaction_id/paygate_external_ref/paygate_status appended — but that redirect alone is never proof of payment (it's a client-side navigation, not a signed confirmation). Verify server-side before unlocking anything:

use PayGate\Client;
use PayGate\Exceptions\PayGateException;

// From your success_url handler: $_GET['paygate_transaction_id'], $_GET['paygate_external_ref']
try {
    $transaction = $client->getTransaction($_GET['paygate_transaction_id']);

    if ($transaction['external_ref'] !== $_GET['paygate_external_ref']) {
        throw new RuntimeException('Transaction does not belong to the expected user.');
    }
    if ($transaction['status'] !== 'completed') {
        // 'pending'/'failed'/'canceled' — do not unlock anything yet
    }
} catch (PayGateException $e) {
    // 'not_found' — id doesn't exist, or belongs to a different app; treat as unverified
}

This also covers one-time payments (mode: 'payment'), which never create a subscription record — getSubscription() alone can't verify those.

Error handling

Every non-2xx PayGate response throws PayGate\Exceptions\PayGateException:

try {
    $client->createCheckoutSession([...]);
} catch (PayGateException $e) {
    $e->getErrorCode();   // e.g. "invalid_card_details"
    $e->getMessage();     // human-readable message from PayGate
    $e->getHttpStatus();  // e.g. 400
}

A request that never reached PayGate at all (DNS, timeout, connection refused) throws PayGate\Exceptions\TransportException instead — treat this as a network problem, not a PayGate-side rejection.

Testing your integration

createCheckoutSession() never accepts a test_card_code and never resolves a payment result itself — it only ever returns a checkout_url. To test without a real Stripe account, call it on an app whose environment allows the Test Payment Method (test/staging), with or without payment_method: 'test'; open the returned checkout_url (PayGate's own hosted picker page) and choose Test Payment Method there with one of the documented card codes — see Testing without a real Stripe account.

$session = $client->createCheckoutSession([
    'external_ref' => 'user-42',
    'plan_ref' => 'premium_1m',
    'amount' => 100000,
    'currency' => 'VND',
    'mode' => 'payment',
    'success_url' => 'https://yourapp.com/success',
    'cancel_url' => 'https://yourapp.com/cancel',
    'payment_method' => 'test',
]);
// Open $session['checkout_url'] in a browser to pick Test Payment Method there.

Requirements

  • PHP >= 7.4
  • ext-curl, ext-json

More examples

See example/ for runnable scripts, including subscription-flow.php (customer, plan with 3 prices, checkout, poll) and verify-transaction.php for the redirect-verification flow above.