PHP
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,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);
}
Обработка ошибок
Все методы могут бросать следующие исключения:
| Исключение | Когда |
|---|---|
ApiException | API вернул 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-статусу ответа. Для удобства есть методы-предикаты:
| HTTP | ErrorCode | Метод | Значение |
|---|---|---|---|
| 400 | BAD_REQUEST | isBadRequest() | Ошибка валидации запроса, в т.ч. не передан обязательный payerBank |
| 403 | FORBIDDEN | isForbidden() | Мерчант заблокирован / трафик выключен |
| 404 | NOT_FOUND | isNotFound() | Платёж не найден |
| 429 | RATE_LIMITED | isRateLimited() | Превышен лимит запросов |
| 5xx | SERVER_ERROR | isServerError() | Ошибка на стороне сервера |
| иное | 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