Главная/Статьи/Интеграция с СберБанк Эквайринг API

Интеграция с СберБанк Эквайринг API

Подключаем интернет-эквайринг СберБанка: регистрация заказа, оплата, callback, возвраты. Работающие примеры для PHP и Node.js.

ДМ
Дмитрий Мещеряков
📅 29 июля 2026 г.📖 9 мин чтения

Эквайринг — интеграция, где код проверяется не тестами, а деньгами: ошибка означает либо отданный без оплаты товар, либо потерянный платёж. При этом сама схема несложная — зарегистрировать заказ, отправить пользователя на страницу оплаты, обработать возврат.

Разберём API и то, что определяет надёжность приёма платежей независимо от банка.

СберБанк Эквайринг

СберБанк предоставляет API для приёма платежей на сайте. Основные возможности:

  • Оплата банковскими картами
  • Apple Pay / Google Pay
  • СБП (Система быстрых платежей)
  • Рекуррентные платежи
💡 Совет

Два окружения:

  • Тестовое: https://3dsec.sberbank.ru
  • Боевое: https://securepayments.sberbank.ru

Регистрация и настройка

  1. Заключите договор эквайринга со СберБанком
  2. Получите логин и пароль для API
  3. Настройте callback URL в личном кабинете

PHP: Класс для работы с API

php
<?php
// /local/lib/Payment/SberBank.php
namespace Local\Payment;
class SberBank
{
    private string $login;
    private string $password;
    private string $baseUrl;
    private bool $isTest;
    public function __construct(bool $isTest = false)
    {
        $this->isTest = $isTest;
        $this->baseUrl = $isTest
            ? 'https://3dsec.sberbank.ru/payment/rest/'
            : 'https://securepayments.sberbank.ru/payment/rest/';
        $this->login = $isTest 
            ? getenv('SBER_TEST_LOGIN') 
            : getenv('SBER_PROD_LOGIN');
        $this->password = $isTest 
            ? getenv('SBER_TEST_PASSWORD') 
            : getenv('SBER_PROD_PASSWORD');
    }
    /**
     * Регистрация заказа
     * 
     * @return array{orderId: string, formUrl: string}
     */
    public function registerOrder(
        string $orderNumber,
        int $amountKopecks,
        string $returnUrl,
        string $failUrl = '',
        string $description = '',
        array $additionalParams = []
    ): array {
        $params = [
            'orderNumber' => $orderNumber,
            'amount' => $amountKopecks,
            'returnUrl' => $returnUrl,
            'failUrl' => $failUrl ?: $returnUrl,
            'description' => $description,
        ];
        // Дополнительные параметры
        if (!empty($additionalParams['email'])) {
            $params['email'] = $additionalParams['email'];
        }
        if (!empty($additionalParams['phone'])) {
            $params['phone'] = $additionalParams['phone'];
        }
        // Данные для чека (54-ФЗ)
        if (!empty($additionalParams['orderBundle'])) {
            $params['orderBundle'] = json_encode(
                $additionalParams['orderBundle'],
                JSON_UNESCAPED_UNICODE
            );
        }
        $response = $this->request('register.do', $params);
        if (!empty($response['errorCode'])) {
            throw new \RuntimeException(
                $response['errorMessage'] ?? 'Unknown error',
                (int) $response['errorCode']
            );
        }
        return [
            'orderId' => $response['orderId'],
            'formUrl' => $response['formUrl'],
        ];
    }
    /**
     * Проверка статуса заказа
     */
    public function getOrderStatus(string $orderId): array
    {
        $response = $this->request('getOrderStatusExtended.do', [
            'orderId' => $orderId,
        ]);
        return [
            'orderNumber' => $response['orderNumber'] ?? '',
            'orderStatus' => $response['orderStatus'] ?? 0,
            'actionCode' => $response['actionCode'] ?? 0,
            'actionCodeDescription' => $response['actionCodeDescription'] ?? '',
            'amount' => $response['amount'] ?? 0,
            'currency' => $response['currency'] ?? '643',
            'ip' => $response['ip'] ?? '',
            'cardAuthInfo' => $response['cardAuthInfo'] ?? [],
            'paymentAmountInfo' => $response['paymentAmountInfo'] ?? [],
        ];
    }
    /**
     * Возврат средств
     */
    public function refund(string $orderId, int $amountKopecks): bool
    {
        $response = $this->request('refund.do', [
            'orderId' => $orderId,
            'amount' => $amountKopecks,
        ]);
        return ($response['errorCode'] ?? '1') === '0';
    }
    /**
     * Отмена неоплаченного заказа
     */
    public function cancel(string $orderId): bool
    {
        $response = $this->request('reverse.do', [
            'orderId' => $orderId,
        ]);
        return ($response['errorCode'] ?? '1') === '0';
    }
    private function request(string $method, array $params): array
    {
        $params['userName'] = $this->login;
        $params['password'] = $this->password;
        $url = $this->baseUrl . $method;
        $ch = curl_init();
        curl_setopt_array($ch, [
            CURLOPT_URL => $url,
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => http_build_query($params),
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => 30,
            CURLOPT_SSL_VERIFYPEER => true,
        ]);
        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $error = curl_error($ch);
        curl_close($ch);
        if ($error) {
            throw new \RuntimeException("cURL error: {$error}");
        }
        if ($httpCode !== 200) {
            throw new \RuntimeException("HTTP error: {$httpCode}");
        }
        $data = json_decode($response, true);
        if (json_last_error() !== JSON_ERROR_NONE) {
            throw new \RuntimeException('Invalid JSON response');
        }
        return $data;
    }
}

Использование в Битрикс

php
<?php
// Создание заказа и редирект на оплату
use Local\Payment\SberBank;
$sber = new SberBank(isTest: true);
try {
    // Формируем данные для чека 54-ФЗ
    $orderBundle = [
        'cartItems' => [
            'items' => array_map(function ($item) {
                return [
                    'positionId' => $item['ID'],
                    'name' => mb_substr($item['NAME'], 0, 100),
                    'quantity' => [
                        'value' => $item['QUANTITY'],
                        'measure' => 'шт',
                    ],
                    'itemAmount' => $item['PRICE'] * $item['QUANTITY'] * 100,
                    'itemCode' => $item['PRODUCT_ID'],
                    'tax' => [
                        'taxType' => 6, // Без НДС
                    ],
                    'itemPrice' => $item['PRICE'] * 100,
                ];
            }, $basketItems),
        ],
    ];
    $result = $sber->registerOrder(
        orderNumber: 'ORDER-' . $orderId,
        amountKopecks: $totalPrice * 100,
        returnUrl: 'https://site.ru/payment/success?order=' . $orderId,
        failUrl: 'https://site.ru/payment/fail?order=' . $orderId,
        description: 'Заказ №' . $orderId,
        additionalParams: [
            'email' => $customerEmail,
            'phone' => $customerPhone,
            'orderBundle' => $orderBundle,
        ]
    );
    // Сохраняем orderId СберБанка
    savePaymentOrderId($orderId, $result['orderId']);
    // Редирект на форму оплаты
    LocalRedirect($result['formUrl']);
} catch (\Exception $e) {
    // Логируем ошибку
    AddMessage2Log('SberBank error: ' . $e->getMessage());
    // Показываем ошибку пользователю
    $APPLICATION->ThrowException('Ошибка создания платежа. Попробуйте позже.');
}

Обработка callback

⚠️ Важно

Три правила, без которых приём платежей небезопасен. Они одинаковы для любого эквайринга, и нарушение любого из них рано или поздно приводит к потерям.

Статус заказа меняется только после проверки на стороне банка. Возврат пользователя на страницу успеха ничего не подтверждает: адрес можно открыть вручную. Получив уведомление или вернувшегося пользователя, запрашивайте фактический статус методом проверки заказа — и доверяйте только его ответу.

Сумма и валюта сверяются с заказом в вашей базе. Проверять только «оплачено/не оплачено» недостаточно: расхождение в сумме означает либо ошибку в вашем коде, либо попытку заплатить меньше.

Обработчик идемпотентен. Уведомление придёт повторно — это штатное поведение при отсутствии быстрого ответа. Без защиты повторная обработка второй раз спишет товар, отправит письмо и начислит бонусы. Проверяйте, не обработан ли уже этот платёж, и возвращайте успех, ничего не делая.

php
<?php
// /local/tools/payment-callback.php
require_once $_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_before.php';
use Local\Payment\SberBank;
use Bitrix\Main\Web\Json;
// Логируем входящие данные
file_put_contents(
    $_SERVER['DOCUMENT_ROOT'] . '/local/logs/sber-callback.log',
    date('Y-m-d H:i:s') . ' ' . Json::encode($_REQUEST) . "\n",
    FILE_APPEND
);
$orderId = $_REQUEST['orderId'] ?? '';
$status = (int) ($_REQUEST['status'] ?? 0);
if (empty($orderId)) {
    http_response_code(400);
    exit('Missing orderId');
}
try {
    $sber = new SberBank(isTest: true);
    $orderStatus = $sber->getOrderStatus($orderId);
    // Находим наш заказ по orderId СберБанка
    $localOrderId = getLocalOrderIdByPaymentId($orderId);
    if (!$localOrderId) {
        throw new \RuntimeException('Order not found');
    }
    // Обрабатываем статус
    // 0 - заказ зарегистрирован, но не оплачен
    // 1 - предавторизованная сумма захолдирована
    // 2 - проведена полная авторизация суммы заказа
    // 3 - авторизация отменена
    // 4 - по транзакции была проведена операция возврата
    // 5 - инициирована авторизация через сервер контроля доступа
    // 6 - авторизация отклонена
    switch ($orderStatus['orderStatus']) {
        case 2: // Оплачен
            markOrderAsPaid($localOrderId);
            sendPaymentConfirmation($localOrderId);
            break;
        case 3: // Отменён
        case 6: // Отклонён
            markOrderAsFailed($localOrderId, $orderStatus['actionCodeDescription']);
            break;
        case 4: // Возврат
            markOrderAsRefunded($localOrderId);
            break;
    }
    echo 'OK';
} catch (\Exception $e) {
    file_put_contents(
        $_SERVER['DOCUMENT_ROOT'] . '/local/logs/sber-callback-errors.log',
        date('Y-m-d H:i:s') . ' ' . $e->getMessage() . "\n",
        FILE_APPEND
    );
    http_response_code(500);
    echo 'Error: ' . $e->getMessage();
}

TypeScript: Next.js интеграция

typescript
// src/lib/sberbank.ts
type SberConfig = {
  login: string;
  password: string;
  isTest: boolean;
};
type RegisterOrderParams = {
  orderNumber: string;
  amount: number; // в копейках
  returnUrl: string;
  failUrl?: string;
  description?: string;
  email?: string;
  phone?: string;
};
type OrderStatus = {
  orderNumber: string;
  orderStatus: number;
  amount: number;
  actionCode: number;
  actionCodeDescription: string;
};
export class SberBankClient {
  private baseUrl: string;
  private login: string;
  private password: string;
  constructor(config: SberConfig) {
    this.baseUrl = config.isTest
      ? 'https://3dsec.sberbank.ru/payment/rest/'
      : 'https://securepayments.sberbank.ru/payment/rest/';
    this.login = config.login;
    this.password = config.password;
  }
  async registerOrder(params: RegisterOrderParams): Promise<{
    orderId: string;
    formUrl: string;
  }> {
    const response = await this.request('register.do', {
      orderNumber: params.orderNumber,
      amount: params.amount,
      returnUrl: params.returnUrl,
      failUrl: params.failUrl || params.returnUrl,
      description: params.description || '',
      email: params.email,
      phone: params.phone,
    });
    if (response.errorCode) {
      throw new Error(response.errorMessage || 'Registration failed');
    }
    return {
      orderId: response.orderId,
      formUrl: response.formUrl,
    };
  }
  async getOrderStatus(orderId: string): Promise<OrderStatus> {
    const response = await this.request('getOrderStatusExtended.do', {
      orderId,
    });
    return {
      orderNumber: response.orderNumber || '',
      orderStatus: response.orderStatus || 0,
      amount: response.amount || 0,
      actionCode: response.actionCode || 0,
      actionCodeDescription: response.actionCodeDescription || '',
    };
  }
  async refund(orderId: string, amount: number): Promise<boolean> {
    const response = await this.request('refund.do', { orderId, amount });
    return response.errorCode === '0';
  }
  private async request(method: string, params: Record<string, unknown>) {
    const body = new URLSearchParams({
      userName: this.login,
      password: this.password,
      ...Object.fromEntries(
        Object.entries(params)
          .filter(([, v]) => v !== undefined)
          .map(([k, v]) => [k, String(v)])
      ),
    });
    const response = await fetch(`${this.baseUrl}${method}`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      body: body.toString(),
    });
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    return response.json();
  }
}

API Routes

typescript
// src/app/api/payment/create/route.ts
import { NextResponse } from 'next/server';
import { SberBankClient } from '@/lib/sberbank';
import { prisma } from '@/lib/prisma';
const sber = new SberBankClient({
  login: process.env.SBER_LOGIN!,
  password: process.env.SBER_PASSWORD!,
  isTest: process.env.NODE_ENV !== 'production',
});
export async function POST(request: Request) {
  const { orderId } = await request.json();
  const order = await prisma.order.findUnique({
    where: { id: orderId },
    include: { items: true },
  });
  if (!order) {
    return NextResponse.json({ error: 'Order not found' }, { status: 404 });
  }
  try {
    const result = await sber.registerOrder({
      orderNumber: `ORDER-${order.id}`,
      amount: Math.round(order.total * 100),
      returnUrl: `${process.env.NEXT_PUBLIC_URL}/payment/success?order=${order.id}`,
      failUrl: `${process.env.NEXT_PUBLIC_URL}/payment/fail?order=${order.id}`,
      description: `Заказ №${order.id}`,
      email: order.email,
    });
    // Сохраняем ID платежа
    await prisma.order.update({
      where: { id: orderId },
      data: { paymentId: result.orderId },
    });
    return NextResponse.json({ formUrl: result.formUrl });
  } catch (error) {
    console.error('Payment error:', error);
    return NextResponse.json(
      { error: 'Payment creation failed' },
      { status: 500 }
    );
  }
}
⚠️ Важно

Безопасность:

  • Храните логин/пароль в переменных окружения
  • Валидируйте callback по IP СберБанка
  • Всегда проверяйте статус через API, не доверяйте query-параметрам

Итоги

ОперацияМетод API
Создание платежаregister.do
Статус платежаgetOrderStatusExtended.do
Возвратrefund.do
Отменаreverse.do

Статусы заказа:

КодЗначение
0Зарегистрирован, но не оплачен
1Средства захолдированы (двухстадийная оплата)
2Оплачен — списание проведено
3Отменён
4Возврат
6Отклонён
⚠️ Важно

Не путайте статус 1 со статусом 2. При двухстадийной оплате 1 означает, что средства только заблокированы на карте покупателя, а не списаны: списание произойдёт при завершении операции, и до этого момента заказ оплаченным не является. Отгрузка товара по статусу 1 — ошибка, которая обнаруживается на сверке через месяц.

Итоги

Проверяйте статус запросом к банку, а не по возврату пользователя.

Сверяйте сумму — всегда.

Делайте обработчик идемпотентным — уведомления повторяются.

Логируйте каждый шаг: запрос, ответ, решение, время. В спорах о платежах это единственное доказательство, и заводить лог нужно с первого дня, а не после первого спора.

Проверьте возврат средств до запуска, а не когда его потребует клиент. Возврат — та часть интеграции, которую почти никогда не тестируют и которая почти всегда содержит ошибку.

Разделите тестовые и боевые ключи так, чтобы их нельзя было перепутать. Разные переменные окружения, разные имена, явная проверка окружения при старте. Боевой платёж, ушедший на тестовый контур, выглядит как успешный — и обнаруживается по отсутствию денег.

🚀

Хотите такое же решение?

Настрою окружение под ваш проект, учту специфику инфраструктуры и обучу команду.

Обсудить проект →
Бесплатная консультация · Ответ в течение дня

Комментарии

Система комментариев скоро будет подключена. А пока вы можете написать мне в Telegram или на email.