Payin H2H

PHP

PHP SDK для интеграции с Merchant Payment API.

PHP

PHP SDK для интеграции с Merchant Payment API.

Требования

  • PHP >= 8.1
  • Расширения: ext-sodium, ext-curl, ext-json (входят в стандартную поставку PHP)

Установка

Добавьте VCS-репозиторий и пакет в composer.json:

{
    "repositories": [
        {
            "type": "path",
            "url": "./sdk-payin/php"
        }
    ],
    "require": {
        "unitewt/payin-sdk": "*"
    }
}

Или клонируйте и подключите локально:

git clone https://github.com/unitewt/sdk-payin.git
cd your-project
composer config repositories.payin-sdk path ./sdk-payin/php
composer require unitewt/payin-sdk:*

Генерация ключей ed25519

Перед началом работы необходимо сгенерировать пару ключей и передать публичный ключ в систему uniteplat:

$keypair    = sodium_crypto_sign_keypair();
$publicKey  = sodium_crypto_sign_publickey($keypair);
$privateKey = sodium_crypto_sign_secretkey($keypair);

echo 'Public key (передать в uniteplat): ' . base64_encode($publicKey) . PHP_EOL;
echo 'Private key (хранить у себя):      ' . base64_encode($privateKey) . PHP_EOL;

Быстрый старт

Инициализация клиента

use PayinSDK\Config;
use PayinSDK\PayinClient;

$client = new PayinClient(new Config(
    baseUrl:         'https://merchant.uniteplat.org',
    keyId:           'ваш-key-id',
    privateKey:      base64_decode('ваш-приватный-ключ-в-base64'),
    serverPublicKey: base64_decode('публичный-ключ-сервера-в-base64'), // опционально, для верификации ответов
));

Создание платежа (синхронный режим)

Реквизиты возвращаются сразу в ответе:

use PayinSDK\Enums\Currency;
use PayinSDK\Enums\PaymentMethod;

$payment = $client->newPayment(
    sum:             100.50,
    currency:        Currency::RUB,
    paymentMethod:   PaymentMethod::CARD,    // обязательно: PaymentMethod::CARD или PaymentMethod::SBP
    customerAccount: 'user_123',
    statusWebhookUrl: 'https://your-site.com/webhook/status', // опционально
    payerBank:       'sberbank',                              // опционально (для части мерчантов обязательно)
    redirectUrl:     'https://your-site.com/return',          // опционально
    email:           'buyer@example.com',                     // опционально
    userCreatedAt:   '2024-01-02T15:04:05Z',                  // опционально, RFC3339
);

echo $payment->orderId;           // ID заказа для дальнейших операций
echo $payment->payeeAccount;      // Номер карты/счёта для оплаты
echo $payment->payeeAccountBank;  // Название банка (может быть null)
echo $payment->payeeName;         // Имя получателя (может быть null)
echo $payment->sum;               // Сумма к оплате

Поля payeeAccountBank, payeeName и deeplink могут быть null — проверяйте перед использованием.

Создание платежа (асинхронный режим)

Реквизиты придут на credentialsWebhookUrl после назначения:

$payment = $client->newPaymentAsync(
    sum:                   100.50,
    currency:              Currency::RUB,
    paymentMethod:         PaymentMethod::SBP,
    customerAccount:       'user_123',
    credentialsWebhookUrl: 'https://your-site.com/webhook/credentials', // обязательно
    statusWebhookUrl:      'https://your-site.com/webhook/status',
);

echo $payment->orderId; // Показать пользователю страницу ожидания

credentialsWebhookUrl обязателен для асинхронного режима: именно на него придут реквизиты после назначения. Методы newPayment и newPaymentAsync также принимают опциональные redirectUrl, email, userCreatedAt (RFC3339).

Доступные суммы (/available)

Запрос доступных сумм до создания платежа:

$available = $client->getAvailableAmounts(
    sum:             1000.00,
    currency:        Currency::RUB,
    paymentMethod:   PaymentMethod::CARD,
    customerAccount: 'user_123', // опционально
    sumDriftUp:      100.0,      // опционально — допустимое отклонение вверх
    sumDriftDown:    50.0,       // опционально — допустимое отклонение вниз
);

foreach ($available->availableAmounts as $amount) {
    echo $amount . PHP_EOL; // float[]
}

Подтверждение оплаты

Вызывается после того, как пользователь сообщает, что совершил перевод:

$result = $client->confirm('abc123');
echo $result->orderId;

Отмена платежа

$result = $client->cancel('abc123');

Отменить платёж можно, только пока он ожидает подтверждения оплаты пользователем либо находится на стадии арбитража (включая receiptRequired). Финальные статусы (doneSuccess, doneFail, arbitrageCancelledByMerchant) отменить нельзя.

Получение статуса

use PayinSDK\Enums\PaymentStatus;

$status = $client->getStatus('abc123');

echo $status->status->value; // 'pending', 'doneSuccess', etc.

if ($status->deeplink !== null) {
    echo $status->deeplink; // Ссылка-диплинк для оплаты (если настроена у мерчанта)
}

if ($status->isSuccessful()) {
    // платёж завершён успешно
}

if ($status->requiresReceipt()) {
    // необходимо прикрепить чек
}

if ($status->isArbitrageCancelledByMerchant()) {
    // платёж отменён мерчантом в арбитраже (финальный статус)
}

if ($status->isDone()) {
    // платёж в финальном статусе (doneSuccess, doneFail или arbitrageCancelledByMerchant)
}

Возможные статусы: free, pending, receiptRequired, doneSuccess, doneFail, arbitrageCancelledByMerchant. Последний — финальный (isDone() вернёт true). Неизвестные значения статуса не приводят к ошибке: $status->status будет null.

Прикрепление чека

Требуется при статусе receiptRequired:

// Один файл
$client->attachReceipt('abc123', '/path/to/receipt.pdf');

// Несколько файлов
$client->attachReceipt('abc123', '/path/to/receipt1.pdf', '/path/to/receipt2.jpg');

Обработка вебхуков

Credentials webhook (credentialsWebhookUrl)

// webhook_credentials.php
use PayinSDK\Webhook\WebhookVerifier;
use PayinSDK\Exceptions\SignatureException;

$verifier = new WebhookVerifier(base64_decode('публичный-ключ-сервера-в-base64'));

try {
    $payload = $verifier->verifyCredentialsWebhook(
        body:           file_get_contents('php://input'),
        signatureBase64: $_SERVER['HTTP_X_SIGNATURE'] ?? '',
    );

    // Реквизиты готовы — показать пользователю
    echo $payload->orderId;
    echo $payload->payeeAccount;
    echo $payload->payeeAccountBank;
    echo $payload->payeeName;
    if ($payload->deeplink !== null) {
        echo $payload->deeplink; // Ссылка-диплинк, если настроена у мерчанта
    }

    http_response_code(200);
} catch (SignatureException $e) {
    http_response_code(403);
}

Status webhook (statusWebhookUrl)

// webhook_status.php
use PayinSDK\Webhook\WebhookVerifier;
use PayinSDK\Enums\PaymentStatus;
use PayinSDK\Exceptions\SignatureException;

$verifier = new WebhookVerifier(base64_decode('публичный-ключ-сервера-в-base64'));

try {
    $payload = $verifier->verifyStatusWebhook(
        body:           file_get_contents('php://input'),
        signatureBase64: $_SERVER['HTTP_X_SIGNATURE'] ?? '',
    );

    match ($payload->status) {
        PaymentStatus::DONE_SUCCESS                   => handleSuccess($payload->orderId),
        PaymentStatus::DONE_FAIL                      => handleFailure($payload->orderId),
        PaymentStatus::RECEIPT_REQUIRED               => handleReceiptRequired($payload->orderId),
        PaymentStatus::ARBITRAGE_CANCELLED_BY_MERCHANT => handleFailure($payload->orderId),
        default                                       => null, // null при неизвестном статусе
    };

    http_response_code(200);
} catch (SignatureException $e) {
    http_response_code(403);
}

Обработка ошибок

Все методы могут бросать следующие исключения:

ИсключениеКогда
ApiExceptionAPI вернул success=false или неожиданный HTTP-статус
SignatureExceptionНе прошла верификация подписи сервера или вебхука
PayinExceptionБазовый класс; перехватывает все ошибки SDK
use PayinSDK\Exceptions\ApiException;
use PayinSDK\Exceptions\SignatureException;
use PayinSDK\Exceptions\PayinException;

try {
    $payment = $client->newPayment(
        sum:             100.50,
        currency:        Currency::RUB,
        paymentMethod:   PaymentMethod::CARD,
        customerAccount: 'user_123',
    );
} catch (ApiException $e) {
    // Ошибка API: $e->getMessage(), $e->orderId, $e->getCode() (HTTP-статус), $e->errorCode
} catch (SignatureException $e) {
    // Проблема с подписью
} catch (PayinException $e) {
    // Любая другая ошибка SDK
}

Классификация ошибок по HTTP-статусу

ApiException несёт поле errorCode (PayinSDK\Exceptions\ErrorCode), которое заполняется по HTTP-статусу ответа. Для удобства есть методы-предикаты:

HTTPErrorCodeМетодЗначение
400BAD_REQUESTisBadRequest()Ошибка валидации запроса, в т.ч. не передан обязательный payerBank
403FORBIDDENisForbidden()Мерчант заблокирован / трафик выключен
404NOT_FOUNDisNotFound()Платёж не найден
429RATE_LIMITEDisRateLimited()Превышен лимит запросов
5xxSERVER_ERRORisServerError()Ошибка на стороне сервера
иноеUNKNOWNПрочее
try {
    $payment = $client->newPayment(
        sum:             100.50,
        currency:        Currency::RUB,
        paymentMethod:   PaymentMethod::CARD,
        customerAccount: 'user_123',
        email:           'buyer@example.com',
    );
} catch (ApiException $e) {
    if ($e->isRateLimited()) {
        // 429 — слишком много запросов, повторите позже
    } elseif ($e->isForbidden()) {
        // 403 — мерчант заблокирован или трафик выключен
    } elseif ($e->isBadRequest()) {
        // 400 — ошибка валидации, в т.ч. не передан обязательный payerBank
    }
}

Структура пакета

src/
├── PayinClient.php          # Главный клиент
├── Config.php               # Параметры подключения
├── Signer.php               # Подпись/верификация ed25519
├── HttpClient.php           # HTTP-клиент на cURL
├── Enums/
│   ├── Currency.php         # RUB
│   ├── PaymentMethod.php    # card | sbp
│   └── PaymentStatus.php    # free | pending | receiptRequired | doneSuccess | doneFail | arbitrageCancelledByMerchant
├── Models/
│   ├── NewPaymentRequest.php
│   ├── NewPaymentResponse.php
│   ├── NewAsyncPaymentRequest.php
│   ├── NewAsyncPaymentResponse.php
│   ├── AvailableAmountsRequest.php
│   ├── AvailableAmountsResponse.php
│   ├── ConfirmResponse.php
│   ├── CancelResponse.php
│   ├── StatusResponse.php
│   └── AttachReceiptResponse.php
├── Webhook/
│   ├── WebhookVerifier.php
│   ├── CredentialsPayload.php
│   └── StatusPayload.php
└── Exceptions/
    ├── PayinException.php
    ├── SignatureException.php
    ├── ErrorCode.php
    └── ApiException.php
Copyright © 2026