Payment-integrated form development for website

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.
Development and maintenance of all types of websites:
Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:
Development stages
Latest works
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    823
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    847
  • image_website-sbh_0.png
    Website development for SBH Partners
    999
  • image_website-_0.png
    Website development for Red Pear
    451

Payment Form Development with Payment Integration

A payment form is the point where a user becomes a customer or leaves forever. There's no room for redirects to third-party pages with unfamiliar design, unexplained errors, or fields that reset after failed attempt. The goal is to embed payment collection directly into site interface so user stays on page and doesn't lose context.

Embedded Form Architecture

Modern payment gateways offer two integration variants: redirect to gateway page and embedded form. The latter is preferable for most sites because it preserves visual context and increases trust.

Typical Stripe scheme:

// Initialize 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');

// Handle submit
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);
  }
});

For Russian market, YooKassa (former Yandex.Kassa) or CloudPayments is more common. CloudPayments provides own SDK for embedded form:

var widget = new cp.CloudPayments();
widget.charge(
  {
    publicId: 'pk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
    description: 'Order #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);
  }
);

Server-side: Payment Intent Creation

No amount logic should come from client. Browser amount from hidden field is untrustworthy — it can be replaced. Server creates payment intent with real amount from database:

// Laravel — create PaymentIntent via Stripe
use Stripe\StripeClient;

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

    // Check order belongs to current user
    abort_if($order->user_id !== auth()->id(), 403);

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

    $intent = $stripe->paymentIntents->create([
        'amount'   => $order->total_cents, // in kopecks/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 gets only client_secret — it contains no amount, cannot be used to change parameters.

Webhook and Payment Confirmation

Form on frontend reports success, but that's not guarantee. Money can hang, bank can decline after redirect. Only reliable source of truth is webhook from payment gateway.

// Stripe webhook handler
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()]);

    // Send receipt, initiate shipping, notify
    dispatch(new SendReceiptJob($order));
    dispatch(new InitiateShippingJob($order));
}

For YooKassa similar scheme via HMAC-signature:

$body = $request->getContent();
$key = config('services.yookassa.secret_key');
// YooKassa doesn't use signature for webhook — verify via API
$notification = new \YooKassa\Model\Notification\NotificationSucceeded(
    json_decode($body, true)
);
$payment = $notification->getObject();

UX Details That Affect Conversion

Real-time validation. Card number should format into groups of 4 digits during input. Expiry date — auto-add /. If Luhn check fails — report immediately, don't wait for submit.

Save progress. If user filled email, name, address — and payment form crashed with error, all fields should remain. Clear only CVV (PCI DSS requirement).

Status indication. "Pay" button should show spinner during request and disable from re-clicking. Double payment is real problem.

Mobile keyboard. Card number field should open numeric keyboard (inputmode="numeric"), not letter. Small detail forgotten in half cases.

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

Security and PCI DSS Compliance

Card data should never pass through your server — only through payment gateway's iframe or its JavaScript library. This is PCI DSS SAQ A level (simplest for merchant).

If card data touched your app even for millisecond — you automatically jump to SAQ D level with yearly audit, pentest, hundreds of mandatory requirements.

Content Security Policy for payment form pages:

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;

Multiple Payment Methods Support

Stripe Payment Element shows cards, Apple Pay, Google Pay, SEPA, Klarna and dozen more methods — automatically, depending on customer's country and browser.

CloudPayments supports cards, SBP (Fast Payment System), Tinkoff Pay. SBP especially useful: lower commission, high conversion on mobile, no card data entry needed.

To setup Apple Pay via CloudPayments requires domain verification — place file at /.well-known/apple-developer-merchantid-domain-association. This is Apple requirement.

Timeline and Stages

Typical integration for one gateway with test environment takes 3–5 business days. This includes: merchant account setup, server-side (payment intent creation, webhook), client-side form, test card testing, production switch.

Adding second gateway (e.g., for failover) — another 2–3 days for payment routing logic.

Fiscalization integration (54-ФЗ, receipt sending via OFD) — separate task, 2–4 days, depends if gateway has built-in support (YooKassa and CloudPayments do).