Розробка форми з інтеграцією оплати на сайті

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Розробка форми з інтеграцією оплати на сайті
Середня
~3-5 робочих днів
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Розробка форми з інтеграцією оплати на сайті

Форма оплати — момент, коли користувач перетворюється на покупця або йде назавжди. Тут немає місця для перенаправлень на сторонні сторінки з чужим дизайном, для незрозумілих помилок без пояснень, для полів, що скидаються після невдалої спроби. Завдання — вбудувати прийом платежів прямо в інтерфейс сайту так, щоб користувач не покидав сторінку та не втрачав контексту.

Архітектура вбудованої форми

Сучасні платіжні шлюзи пропонують два варіанти інтеграції: перенаправлення на сторінку шлюзу та вбудована форма (embedded form). Другий варіант переважніший для більшості сайтів, оскільки зберігає візуальний контекст та підвищує довіру.

Типова схема для Stripe:

// Ініціалізація Stripe Elements
const stripe = Stripe('pk_live_...');
const elements = stripe.elements({
  appearance: {
    theme: 'flat',
    variables: {
      colorPrimary: '#0f172a',
      fontFamily: 'Inter, sans-serif',
    },
  },
});

const paymentElement = elements.create('payment', {
  layout: { type: 'tabs', defaultCollapsed: false },
});
paymentElement.mount('#payment-element');

// Обробка сабміту
const form = document.getElementById('payment-form');
form.addEventListener('submit', async (e) => {
  e.preventDefault();
  const { error } = await stripe.confirmPayment({
    elements,
    confirmParams: {
      return_url: 'https://example.com/order/complete',
    },
  });
  if (error) {
    showError(error.message);
  }
});

Для російського ринку частіше використовується ЮКаса (колишня Яндекс.Каса) або CloudPayments. CloudPayments надає власний SDK для вбудованої форми:

var widget = new cp.CloudPayments();
widget.charge(
  {
    publicId: 'pk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
    description: 'Заказ #12345',
    amount: 4990,
    currency: 'RUB',
    invoiceId: '12345',
    email: '[email protected]',
    skin: 'mini',
    data: { orderId: '12345', userId: 42 },
  },
  function (options) {
    // success callback
    updateOrderStatus(options.invoiceId, 'paid');
  },
  function (reason, options) {
    // fail callback
    logPaymentError(reason, options);
  }
);

Серверна частина: створення платіжного намірення

Ніяка логіка суми не повинна йти зі сторони клієнта. Браузер не довіряє сумі з hidden-поля — її можна замінити. Сервер створює платіжне намірення з реальною сумою з бази даних:

// Laravel — створення PaymentIntent через Stripe
use Stripe\StripeClient;

public function createPaymentIntent(Request $request): JsonResponse
{
    $order = Order::findOrFail($request->order_id);

    // Перевірка, що замовлення належить поточному користувачу
    abort_if($order->user_id !== auth()->id(), 403);

    $stripe = new StripeClient(config('services.stripe.secret'));

    $intent = $stripe->paymentIntents->create([
        'amount'   => $order->total_cents, // у копійках/центах
        'currency' => 'rub',
        'metadata' => [
            'order_id' => $order->id,
            'user_id'  => $order->user_id,
        ],
        'automatic_payment_methods' => ['enabled' => true],
    ]);

    return response()->json([
        'client_secret' => $intent->client_secret,
    ]);
}

Клієнт отримує лише client_secret — він не містить суми, не може бути використаний для зміни параметрів.

Webhook та підтвердження оплати

Форма на фронтенді повідомляє про успіх, але це не гарантія. Гроші можуть зависнути, банк може відхилити після перенаправлення. Єдиний надійний джерело істини — webhook від платіжного шлюзу.

// Обробник webhook Stripe
public function handleWebhook(Request $request): Response
{
    $payload = $request->getContent();
    $sigHeader = $request->header('Stripe-Signature');

    try {
        $event = \Stripe\Webhook::constructEvent(
            $payload,
            $sigHeader,
            config('services.stripe.webhook_secret')
        );
    } catch (\Stripe\Exception\SignatureVerificationException $e) {
        return response('Invalid signature', 400);
    }

    match ($event->type) {
        'payment_intent.succeeded'       => $this->handleSuccess($event->data->object),
        'payment_intent.payment_failed'  => $this->handleFailed($event->data->object),
        'charge.dispute.created'         => $this->handleDispute($event->data->object),
        default                          => null,
    };

    return response('OK', 200);
}

private function handleSuccess(\Stripe\PaymentIntent $intent): void
{
    $order = Order::where('stripe_payment_intent', $intent->id)->firstOrFail();
    $order->update(['status' => 'paid', 'paid_at' => now()]);

    // Відправка чека, запуск доставки, сповіщення
    dispatch(new SendReceiptJob($order));
    dispatch(new InitiateShippingJob($order));
}

Для ЮКаси аналогічна схема через HMAC-підпис:

$body = $request->getContent();
$key = config('services.yookassa.secret_key');
// ЮКаса не використовує підпис для webhook — перевіряємо через API
$notification = new \YooKassa\Model\Notification\NotificationSucceeded(
    json_decode($body, true)
);
$payment = $notification->getObject();

UX-деталі, що впливають на конверсію

Валідація в реальному часі. Номер карти повинен форматуватися групами по 4 цифри прямо під час введення. Термін дії — автоматично додавати /. Якщо Luhn-перевірка не пройдена — повідомляти відразу, не чекати сабміту.

Збереження прогресу. Якщо користувач заповнив email, ім'я, адресу — і форма оплати упала з помилкою, всі поля повинні залишитися. Очищувати тільки CVV (вимога PCI DSS).

Індикація стану. Кнопка «Оплатити» повинна показувати спіннер під час запиту та блокуватись від повторного натиснення. Подвійна оплата — реальна проблема.

Мобільна клавіатура. Поле номера карти повинне відкривати числову клавіатуру (inputmode="numeric"), а не літерну. Дрібниця, яку забувають у половині випадків.

<input
  type="text"
  inputmode="numeric"
  autocomplete="cc-number"
  placeholder="0000 0000 0000 0000"
  pattern="[0-9\s]{13,19}"
/>

Безпека та відповідність PCI DSS

Дані карти ніколи не повинні проходити через ваш сервер — тільки через iframe платіжного шлюзу або його JavaScript-бібліотеку. Це рівень PCI DSS SAQ A (найпростіший для мерчанта).

Якщо дані карти хоча б на мілісекунду опинилися у вашому додатку — ви автоматично переходите на рівень SAQ D з щорічним аудитом, пентестом та кількома сотнями обов'язкових вимог.

Content Security Policy для сторінок з формою оплати:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://js.stripe.com https://widget.cloudpayments.ru;
  frame-src https://js.stripe.com https://widget.cloudpayments.ru;
  connect-src 'self' https://api.stripe.com;

Підтримка кількох методів оплати

Stripe Payment Element з коробки показує карти, Apple Pay, Google Pay, SEPA, Klarna та ще десяток методів — автоматично, в залежності від країни користувача та його браузера.

CloudPayments підтримує карти, СБП (Система Швидких Платежів), Tinkoff Pay. СБП особливо корисна: менша комісія, висока конверсія на мобільних пристроях, немає необхідності вводити дані карти.

Для налаштування Apple Pay через CloudPayments потрібна верифікація домену — розмістити файл на /.well-known/apple-developer-merchantid-domain-association. Це обов'язкова вимога Apple.

Часові терміни та етапи

Типова інтеграція для одного шлюзу з тестовим окруженням займає 3–5 робочих днів. Це включає: налаштування аккаунту мерчанта, серверна частина (створення платіжного намірення, webhook), клієнтська форма, тестування на тестових картах, переключення на боевий режим.

Додавання другого шлюзу (наприклад, для резервування) — ще 2–3 дня на логіку маршрутизації платежів.

Інтеграція фіскалізації (54-ФЗ, відправка чеків через ОФД) — окремане завдання, 2–4 дня, залежить від того, чи є у шлюзу вбудована підтримка (у ЮКаси та CloudPayments є).