PHPackages                             novapay-ua/novapay - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Payment Processing](/categories/payments)
4. /
5. novapay-ua/novapay

ActiveLibrary[Payment Processing](/categories/payments)

novapay-ua/novapay
==================

NovaPay Internet Acquiring and Checkout API client for PHP

v1.0.1(yesterday)01↑2900%MITPHPPHP &gt;=8.1CI passing

Since Jul 28Pushed todayCompare

[ Source](https://github.com/NovaPay/novapay-php)[ Packagist](https://packagist.org/packages/novapay-ua/novapay)[ Docs](https://github.com/NovaPay/novapay-php)[ RSS](/packages/novapay-ua-novapay/feed)WikiDiscussions main Synced today

READMEChangelogDependencies (3)Versions (3)Used By (0)

NovaPay PHP Library
===================

[](#novapay-php-library)

[![CI](https://github.com/NovaPay/novapay-php/actions/workflows/ci.yml/badge.svg)](https://github.com/NovaPay/novapay-php/actions/workflows/ci.yml)[![packagist](https://camo.githubusercontent.com/92547a29e90427b4311b21c77d5f1a15dcc22be00d4c6108b7ebd05ebc593a96/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6e6f76617061792d75612f6e6f76617061792e737667)](https://packagist.org/packages/novapay-ua/novapay)[![license](https://camo.githubusercontent.com/f373b036fe858d5c6a6c89d4cbdc753414923438e0db123a7f8b59e15bdd910e/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f6e6f76617061792d75612f6e6f76617061792e737667)](LICENSE)

PHP client for the **NovaPay external API** — [Internet Acquiring](https://novapay.readme.io/reference/acquiring-requests) and [Checkout](https://novapay.readme.io/reference/checkout-requests). Requests are signed for you, postbacks are verified for you, every payload is documented.

📖 [Документація українською](README.uk.md)

Requirements
------------

[](#requirements)

- **PHP 8.1+** with `ext-curl`, `ext-json`, `ext-openssl`
- No runtime dependencies

Install
-------

[](#install)

```
composer require novapay-ua/novapay
```

Quickstart
----------

[](#quickstart)

```
use NovaPay\Environment;
use NovaPay\NovaPayClient;

// getenv() returns string|false. Under `strict_types` an unset variable would be a TypeError
// rather than a readable error, so check it here.
$privateKeyPem = getenv('MERCHANT_PRIVATE_KEY_PEM');
if (false === $privateKeyPem) {
    throw new RuntimeException('MERCHANT_PRIVATE_KEY_PEM is not set');
}

$client = new NovaPayClient(
    privateKeyPem: $privateKeyPem,
    novapayPublicKeyPem: ((string) getenv('NOVAPAY_PUBLIC_KEY_PEM')) ?: null,
    environment: Environment::Production,
);

$session = $client->acquiring->createSession([
    'merchant_id' => '',
    'client_phone' => '+380501112233',
    'callback_url' => 'https://your.api/novapay/postback',
]);

$payment = $client->acquiring->addPayment([
    'merchant_id' => '',
    'session_id' => $session->id,
    'amount' => 100.5,
]);

echo 'Redirect the customer to: '.$payment->url;
```

`createSession` returns an object with `id` — that `id` is the session id you pass everywhere else.

Pass `'use_hold' => true` to `addPayment` to authorize now and capture later with `completeHold`.

### Checkout

[](#checkout)

Same client, same host, different paths. Checkout also collects delivery details.

```
$session = $client->checkout->createSession([
    'merchant_id' => '',
    'callback_url' => 'https://your.api/novapay/checkout-postback',
    'client_phone' => '+380501112233',
]);

$payment = $client->checkout->addPayment([
    'merchant_id' => '',
    'session_id' => $session->id,
    'amount' => 250,
]);
```

Responses
---------

[](#responses)

Every response is a read-only object you can read as a property or as an array key, dump as JSON, or convert back to a plain array. A field NovaPay adds tomorrow stays readable instead of breaking your integration:

```
$status = $client->acquiring->getStatus([...]);

$status->status;                          // 'paid'
$status['status'];                        // the same
$status->operations[0]->transaction_id;   // nested objects are wrapped too
$status->toArray();                       // plain nested array, for logging or storage
json_encode($status);                     // back to JSON
```

Reads never fail: an unknown field is `null`, exactly like a field NovaPay sent as `null`. `isset()`follows PHP semantics and is `false` for both, so it cannot tell "absent" from "null" — if you need that distinction, go through `toArray()` and `array_key_exists()`.

Postbacks
---------

[](#postbacks)

NovaPay signs postbacks with **its own** RSA public key (not your merchant key) and sends the signature in the **`x-sign-v2`** header — a different header from the `x-sign` on outgoing requests.

> **The signature covers the raw request body.** Verify before parsing. `json_encode($_POST)` or a re-encoded array will not match and verification will fail.

```
use NovaPay\Constants;
use NovaPay\Exception\ExceptionInterface;

$rawBody = file_get_contents('php://input');
$xSign = $_SERVER['HTTP_X_SIGN_V2'] ?? '';

try {
    $postback = $client->parsePostback($rawBody, $xSign);
} catch (ExceptionInterface $e) {
    http_response_code(401);
    exit('invalid postback signature');
}

error_log($postback->id.' → '.$postback->status);
http_response_code(200);
```

`parsePostback` verifies the signature and then decodes — in that order. If you only need the boolean, `verifyPostback($rawBody, $xSign)` does the check alone; both throw if you did not pass `novapayPublicKeyPem` to the constructor. To verify without a client:

```
use NovaPay\Signature;

$ok = Signature::verify($rawBody, $xSign, getenv('NOVAPAY_PUBLIC_KEY_PEM'));
```

The header name is exported as `Constants::WEBHOOK_HEADER_X_SIGN`. Payload fields of both v3 postback shapes (acquiring and checkout, current as of 2025-10-01) are documented on `NovaPay\Postback` — a checkout postback is an acquiring one plus `delivery`.

### Handle postbacks idempotently

[](#handle-postbacks-idempotently)

`x-sign-v2` signs **the body and nothing else** — no nonce, no timestamp. A valid postback therefore stays valid forever, and anyone who captured one can replay it. Verification will pass, correctly, on every replay. Deduplication is your job, not the SDK's:

```
$postback = $client->parsePostback($rawBody, $xSign);

// Look up what you already know about this session and only move forward through the lifecycle.
$known = $orders->findBySessionId($postback->id);
if (null === $known || !$known->statusIsNewerThan($postback->status)) {
    http_response_code(200);   // a replay is not an error — a non-2xx just makes NovaPay retry
    exit;
}

$orders->apply($postback);     // side effects (ship the goods, send the email) live behind this
http_response_code(200);
```

Three rules that follow from it:

- **Key on `id`** (the session id) plus `status`. The same pair arriving twice is a replay.
- **Never walk the lifecycle backwards.** `paid` → `holded` means a stale or replayed delivery.
- **Put side effects behind the dedup check**, not in front of it. Shipping twice is the failure mode.

`example/public/index.php` implements exactly this, in ~10 lines.

API
---

[](#api)

Every method takes one array and returns a response object.

MethodPathReturns`createSession($params)``POST /v1/session``Session``addPayment($params)``POST /v1/payment``Payment``getStatus($params)``POST /v1/get-status``SessionStatus``completeHold($params)``POST /v1/complete-hold``void``voidSession($params)``POST /v1/void``void``expireSession($params)``POST /v1/expire``void``$client->checkout` exposes the same six methods. `createSession` and `addPayment` use `/v1/checkout/*`, the rest share the acquiring endpoints.

Two checkout differences worth knowing:

- `checkout->addPayment` returns `CheckoutPayment` — **`url` and `session_id`, no `id`**. The transaction id only appears later in `getStatus` under `operations[0]->transaction_id`.
- A checkout session starts in status `precreated`, an acquiring one in `created`.

**`completeHold`, `voidSession` and `expireSession` return no data** — the response body is a literal `null`. They succeeded if they didn't throw. `getStatus` is the only way to observe what they did.

### Session lifecycle

[](#session-lifecycle)

```
createSession ──▶ created / precreated
                       │
              customer pays
                       ├── use_hold: true ──▶ holded ──completeHold──▶ paid
                       └── use_hold: false ─────────────────────────▶ paid
                                                                       │
                                                                  voidSession
                                                                       ▼
                                                                     voided

```

`expireSession` moves an unpaid session to `expired`. `voidSession` works on a direct charge and on a captured hold alike.

### `SessionStatus`

[](#sessionstatus)

```
$s = $client->acquiring->getStatus(['merchant_id' => $merchantId, 'session_id' => $sessionId]);

$s->status;              // 'created' | 'precreated' | 'holded' | 'paid' | 'voided' | 'expired'
$s->transaction_status;  // 'APPROVED' | 'REFUNDED' | null
$s->amount;              // '1.00' — decimal string, not a number
$s->pan;                 // '424242xxxxxx4242' — masked, null before payment
$s->paytype;             // 'card'; empty string (not null) before payment
$s->card_type;           // 'VISA'
$s->approval_code;       // '1785182032.057'
$s->operations;          // [{ transaction_id, external_id, amount, refunded_amount, status }]
```

Three things that bite:

- **Amounts are decimal strings** (`'1.00'`), not numbers. Don't compare them with `===` against a number.
- **`refunded_amount` stays `null` even after a successful void.** Detect refunds via `status === 'voided'` or `transaction_status === 'REFUNDED'`.
- **`rrn` stayed `null` even on a paid session**, and `processing_result` is `null` before payment but `''` after. Neither is a reliable success signal — use `status`.

Request fields are documented as array shapes on every method, so PHPStan and your IDE check them for you.

**Note on `metadata`:** `createSession()` stamps SDK identity into it — `source_name`(`novapay_php`), `version` (this package's release) and `runtime` (`php/8.3.10`). Your own keys are kept, those three win on a name collision. Because of that, create-session `metadata` is always sent as an object; elsewhere an empty PHP array still serializes to `[]`, not `{}`.

Configuration
-------------

[](#configuration)

```
new NovaPayClient(
    privateKeyPem: $pem,          // required — merchant RSA private key, signs outgoing requests
    novapayPublicKeyPem: $pem,    // NovaPay RSA public key, verifies incoming postbacks
    environment: Environment::Test, // Test (default) | Production
    acquiringBaseUrl: null,       // override the resolved host (staging, mocks)
    checkoutBaseUrl: null,        // override the resolved host (staging, mocks)
    httpClient: null,             // custom transport — proxies, instrumentation, tests
    timeout: 30.0,                // per-request timeout, seconds
);
```

Acquiring and Checkout share one host per environment:

`environment`ConstantHost`Environment::Test` *(default)*`Constants::TEST_BASE_URL``https://api-qecom.novapay.ua``Environment::Production``Constants::PRODUCTION_BASE_URL``https://api-ecom.novapay.ua``Environment::Test->baseUrl()` resolves the host outside a client.

**Note on keys in env vars:** PEM keys are multi-line. In a `.env` file either quote the whole value and keep real newlines, or store it base64-encoded and decode at startup — a PEM with literal `\n` will not parse.

### Custom transport

[](#custom-transport)

`httpClient` takes any `NovaPay\HttpClient\ClientInterface` — one method. Wrap Guzzle, add retries you control, or record calls in tests:

```
use NovaPay\HttpClient\ClientInterface;

final class LoggingClient implements ClientInterface
{
    public function __construct(private ClientInterface $inner, private \Psr\Log\LoggerInterface $log) {}

    public function post(string $url, array $headers, string $body, float $timeout): array
    {
        $this->log->info('NovaPay '.$url);

        return $this->inner->post($url, $headers, $body, $timeout);
    }
}
```

Errors
------

[](#errors)

Every exception implements `NovaPay\Exception\ExceptionInterface`.

ExceptionWhen`ProcessingException`4xx with `type: processing` — the request was valid but rejected`ValidationException`4xx with `type: validation` — the body failed schema validation`ApiException`any other non-2xx (5xx, gateway errors, non-JSON bodies)`ApiConnectionException`no HTTP response at all — DNS, TLS, connection reset, timeout`SignatureVerificationException`a postback did not match its `x-sign-v2``InvalidArgumentException`bad input on your side: unreadable key, missing option```
use NovaPay\Exception\ApiException;
use NovaPay\Exception\ProcessingException;
use NovaPay\Exception\ValidationException;

try {
    $client->acquiring->voidSession([...]);
} catch (ProcessingException $e) {
    $e->getErrorCode();      // 'SessionAlreadyRefundedError' | 'SessionNotFoundError' | 'NotFoundError' | …
    $e->getErrorMessage();   // 'session already refunded'
} catch (ValidationException $e) {
    $e->getErrors();         // [['path' => 'client_phone', 'code' => 'invalid_type', 'message' => '…']]
} catch (ApiException $e) {
    $e->getHttpStatus();     // 502
    $e->getHttpBody();       // raw response text
    $e->getJsonBody();       // decoded body, or null when it wasn't JSON
}
```

Branch on `getErrorCode()`, never on the message. Every 4xx body carries a `uuid` — `$e->getUuid()`, quote it in support tickets.

**There are no automatic retries.** `addPayment` is not idempotent — a blind retry can charge the customer twice. On a timeout or 5xx, call `getStatus` to find out what actually happened before retrying.

Example app
-----------

[](#example-app)

A runnable Slim 4 storefront — hold and direct-charge buttons, success/fail pages, signed postbacks, and a purchases list wired to `completeHold` / `voidSession`:

```
cd example && composer install
cp .env.example .env   # PUBLIC_URL + your QE key pair
ngrok http 3000
set -a && . ./.env && set +a
php -S 127.0.0.1:3000 -t public public/index.php
```

See [example/README.md](example/README.md).

Development
-----------

[](#development)

```
composer install
composer ci        # everything below, in order
composer test      # phpunit
composer analyse   # phpstan level 10
composer lint      # php-cs-fixer (PER-CS), dry run
composer fix       # php-cs-fixer, apply
```

No PHP locally? Everything runs in Docker:

```
docker run --rm -v "$PWD":/app -w /app php:8.1-cli php vendor/bin/phpunit
```

Issues and pull requests: [github.com/NovaPay/novapay-php](https://github.com/NovaPay/novapay-php/issues).

API reference
-------------

[](#api-reference)

Full field lists, status values and postback schemas: [NovaPay API Reference](https://novapay.readme.io/reference).

License
-------

[](#license)

[MIT](LICENSE)

###  Health Score

39

—

LowBetter than 84% of packages

Maintenance100

Actively maintained with recent releases

Popularity2

Limited adoption so far

Community2

Small or concentrated contributor base

Maturity43

Maturing project, gaining track record

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

2

Last Release

1d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/d265fc1aad5a534f17360b7fc1a2aeb4e520712a6951356bfbe5762dce72c237?d=identicon)[novapay-ua](/maintainers/novapay-ua)

---

Tags

novapaynovapay-sdkphpsdkpaymentsapi clientcheckoutacquiringukrainenovapay

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/novapay-ua-novapay/health.svg)

```
[![Health](https://phpackages.com/badges/novapay-ua-novapay/health.svg)](https://phpackages.com/packages/novapay-ua-novapay)
```

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
