Блог

Integrations 8 хв чытання

Stripe webhooks на PHP: надзейны апрацоўшчык для плацяжоў

Webhook Stripe, які працуе на наўтбуку, яшчэ нельга давяраць грошы. У гэтым гайдзе разбіраны праверку подпісы, хуткі адказ, дэдуплікацыю падзей і паўторныя дастаўкі на чыстым PHP і Laravel, з кодам, які можна ўзяць у праект.

Вокладка артыкула: Stripe webhooks на PHP: надзейны апрацоўшчык для плацяжоў

Коратка

  • Крыніца праўды аб плацяжы - webhook, а не рэдырэкт браўзэра пасля аплаты.
  • Чытайце сырое цела запыту і правярайце загаловак Stripe-Signature да любой працы з дадзенымі.
  • Адказвайце 2xx хутка, а асноўную працу рабіце ў чарзе ці пасля захавання падзеі.
  • Захоўвайце ID падзеі з унікальным індэксам: Stripe можа даслаць адну і тую ж падзею некалькі разоў і ў іншым парадку.
  • Перад запускам пратэстуйце ўсё праз Stripe CLI (stripe listen і stripe trigger).

Калі вы прымаеце плацяжы праз Stripe, менавіта Stripe webhooks на PHP вырашаюць, ці будзе замова пазначаны аплачаным, ці актывуецца падпіска і ці патрапіць зварот у вашу базу. Старонка аплаты - толькі ўваходныя дзверы: кліенты зачыняюць укладкі, банкі пацвярджаюць з затрымкай, а некаторыя спосабы аплаты праходзяць праз хвіліны ці дні. Webhook - гэтае паведамленне ад Stripe аб тым, што адбылося насамрэч.

Мы робім і суправаджаем плацежныя інтэграцыі для кліентаў і для ўласных прадуктаў (напрыклад, Bill прымае аплату праз Stripe і PayPal на публічнай старонцы). Ніжэй - чэк-ліст, па якім мы пішам endpoint для Stripe webhooks на PHP.

Як працуюць Stripe webhooks за хвіліну

Вы рэгіструеце HTTPS-адрас у Stripe Dashboard (ці праз API) і выбіраеце тыпы падзей. Калі нешта адбываецца - завяршылася Checkout Session, аплачаны рахунак, зроблены зварот - Stripe адпраўляе на гэты адрас POST з JSON-аб'ектам Event. Ваш сервер павінен адказаць статутам 2xx. Калі не, Stripe паўтарае дастаўку з экспанентнай затрымкай да трох дзён у live-рэжыме.

З гэтага выцякаюць тры факты, якія вызначаюць увесь дызайн:

  • Падзея можа прыйсці больш за адзін раз. Паўторы і сеткавыя збоі - нармальная сітуацыя.
  • Падзеі могуць прыйсці не па парадку. Вы можаце атрымаць invoice.paid раней, чым customer.subscription.created.
  • POST на ваш адрас можа адправіць хто заўгодна. Без праверкі подпісу зламыснік сам адзначыць сваю замову аплачанай.

Крок 1. Усталюйце SDK і стварыце endpoint

Выкарыстоўвайце афіцыйную бібліятэку stripe/stripe-php, а сакрэт endpoint (ён пачынаецца з whsec_) захоўваеце ў зменнай асяроддзі, але не ў рэпазітары.

composer require stripe/stripe-php

# .env
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...

У кожнага endpoint у Dashboard свой сакрэт подпісы, а ў stripe listen у распрацоўцы - яшчэ адзін. Самая частая прычына памылкі "signature verification failed" - проста не той сакрэт.

Крок 2. Праверце подпіс па волкім целе

Stripe падпісвае кожны запыт. Загаловак Stripe-Signature змяшчае пазнаку часу і адну або некалькі подпісаў - HMAC-SHA256 ад радка timestamp.payload з сакрэтам endpoint. Бібліятэка правярае подпіс і па змаўчанні адхіляе падзеі старэйшыя за пяць хвілін.

Важная дэталь: для праверкі патрэбныя тыя ж самыя байты, якія адправіў Stripe. Калі фрэймворк распарыў JSON, а вы закадзіравалі яго назад, прабелы зменяцца і подпіс не супадзе.

<?php
// public/webhooks/stripe.php (plain PHP)
require __DIR__ . '/../../vendor/autoload.php';

$payload   = file_get_contents('php://input');
$sigHeader = $_SERVER['HTTP_STRIPE_SIGNATURE'] ?? '';
$secret    = getenv('STRIPE_WEBHOOK_SECRET');

try {
    $event = \Stripe\Webhook::constructEvent($payload, $sigHeader, $secret);
} catch (\UnexpectedValueException $e) {
    http_response_code(400);   // invalid JSON
    exit;
} catch (\Stripe\Exception\SignatureVerificationException $e) {
    http_response_code(400);   // bad or missing signature
    exit;
}

У Laravel чытайце $request->getContent(), а не $request->all(), і выключыце маршрут з CSRF-абароны (у webhook няма CSRF-токена, абаронай служыць подпіс). Па магчымасці выносьце маршрут з групы web з сесіямі.

Крок 3. Зрабіце апрацоўшчык ідэмпатэнтным

Ідэмпатэнтнасць - гэта калі паўторная апрацоўка падзеі дае той жа вынік, што і першая. Самы просты надзейны спосаб – табліца апрацаваных ID падзей з унікальным індэксам. Спачатку ўстаўляемы запіс, і калі ўстаўка падае з-за дубліката, значыць, падзея ўжо была.

CREATE TABLE stripe_events (
  id            BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  event_id      VARCHAR(64) NOT NULL,
  type          VARCHAR(100) NOT NULL,
  payload       JSON NOT NULL,
  status        ENUM('received','processed','failed') NOT NULL DEFAULT 'received',
  received_at   TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  processed_at  TIMESTAMP NULL,
  UNIQUE KEY uniq_event (event_id)
);
try {
    $pdo->prepare('INSERT INTO stripe_events (event_id, type, payload) VALUES (?, ?, ?)')
        ->execute([$event->id, $event->type, $payload]);
} catch (PDOException $e) {
    if ($e->getCode() === '23000') {   // duplicate key: already received
        http_response_code(200);
        exit;
    }
    throw $e;
}

Захаваны payload - гэта часопіс для разбору: калі кліент пытаецца "чаму з мяне спісалі двойчы?", вы можаце сапраўды паказаць, што паведаміў Stripe. А яшчэ так можна паўторна апрацаваць падзеі, якія ўпалі пасля выпраўлення бага.

Крок 4. Адказвайце хутка, апрацоўвайце асобна

Stripe чакае адказ на працягу некалькіх секунд. Калі апрацоўшчык прама ў запыце адпраўляе лісты, ходзіць у іншыя API ці будуе PDF, ён упрэцца ў таймаўт, Stripe паўторыць дастаўку, і праца выканаецца яшчэ раз. У запыце рабіце мінімум - праверыць, захаваць, адказаць 200, - а логіку выконвайце ў чарзе (чаргі Laravel, Symfony Messenger, cron, які чытае запісы са status = 'received', або worker пад supervisor).

Калі падзея наогул не атрымалася захаваць (упала база), вярніце 5xx. Гэта адзіны выпадак, калі вы хочаце, каб Stripe паўтарыў пазней.

Крок 5. Апрацуйце патрэбныя падзеі

Усе тыпы падзей не патрэбны. Падпісвайцеся толькі на тыя, што выкарыстоўвае ваша логіка: менш шуму і менш паверхня атакі. Тыповы набор:

  • checkout.session.completed - кліент завяршыў Checkout. Знайдзіце замову па client_reference_id або metadata, якія вы задалі пры стварэнні сесіі.
  • payment_intent.succeeded і payment_intent.payment_failed - для ўласных аплатных формаў.
  • invoice.paid і invoice.payment_failed - білінг падпісак.
  • customer.subscription.updated і customer.subscription.deleted - змена тарыфу і адмена.
  • charge.refunded і charge.dispute.created - грошы сыходзяць зваротна.
switch ($event->type) {
    case 'checkout.session.completed':
        $session = $event->data->object;
        $orderId = $session->client_reference_id;
        if ($session->payment_status === 'paid') {
            markOrderPaid($orderId, $session->payment_intent);
        }
        break;

    case 'invoice.payment_failed':
        notifyBillingProblem($event->data->object->customer);
        break;

    case 'charge.refunded':
        markOrderRefunded($event->data->object->payment_intent);
        break;

    default:
        // unknown or unsubscribed type: acknowledge and ignore
        break;
}

Дзве звычкі прадухіляюць большасць памылак. Па-першае, не давярайце сумам з браўзэра: параўноўвайце аплачаную суму і валюту з падзеі з замовай у вашай базе. Па-другое, паколькі парадак падзей не гарантаваны, рабіце змены стану манатоннымі: не перакладаеце замову з «аплачаны» назад у «чакае» толькі таму, што запознела старую падзею. Калі ёсць сумневы, запытайце актуальны аб'ект праз API, а не спадзявайцеся на здымак з падзеі.

Крок 6. Тэстуйце лакальна праз Stripe CLI

Stripe CLI перасылае сапраўдныя тэставыя падзеі на вашу машыну, публічны тунэль не патрэбен:

stripe login
stripe listen --forward-to http://localhost:8000/webhooks/stripe
# in another terminal:
stripe trigger checkout.session.completed
stripe trigger invoice.payment_failed

stripe listen друкуе часовы сакрэт whsec_, яго трэба пакласці ў лакальны .env. Праверце і дрэнныя сцэнары спецыяльна: адпраўце запыт з няправільным подпісам (чакаем 400), адпраўце адну падзею двойчы (павінен быць адзін эфект у базе) і спыніце worker чарзе на хвіліну, каб пераканацца, што падзеі збіраюцца, а не знікаюць.

Частыя памылкі, якія мы знаходзім у аўдытах

  • Рэдырэкт лічыцца доказам аплаты. Старонку success_url можа адкрыць хто заўгодна; паказвайце "пацвярджаем аплату", пакуль не прыйдзе webhook.
  • JSON парсіцца да праверкі. Перакадаванае цела ламае подпіс, часты вінаваты - body parser фрэймворка.
  • Адзін сакрэт для test і Live. У тэставага і баявога рэжымаў розныя ключы і розныя webhook endpoint.
  • Няма логаў. Без захаваных падзей і прыкметнага канала памылак багі плацяжоў знаходзяць кліенты.
  • Павольныя апрацоўшчыкі. Усё, што даўжэй пары секунд, выклікае паўторныя дастаўкі і падвойную працу.
  • Stripe блакуецца на файрволе ці WAF. Калі вы выкарыстоўваеце Cloudflare ці іншы WAF, дазвольце шлях webhook і праверце, што правілы для робатаў не аддаюць 403 серверам Stripe.

Маніторынг: як зразумець, што ўсё працягвае працаваць

Webhook можа зламацца праз месяцы пасля запуску: дэплой пераназваў калонку, ратавалі сакрэт, скончыўся сертыфікат. Stripe Dashboard паказвае спробы дастаўкі і памылкі па кожным endpoint, а пры сталых збоях Stripe дасылае ліст. Са свайго боку наладзьце алерты на тры рэчы: якая расце лік падзей у статутах received і failed, рэзкае падзенне ліку падзей у гадзіну, а таксама дзель адказаў 4xx і 5xx на маршруце webhook. Невялікая штодзённая зверка аплачаных заказаў у базе са спісам паспяховых плацяжоў у Stripe закрые астатняе.

Чым мы можам дапамагчы

Мы праектуем і робім плацежныя і webhook-інтэграцыі для прадакшэну: праверка подпісы, ідэмпатэнтнасць, паўторныя дастаўкі і логі, зразумелыя вашай камандзе. Калі Stripe-струмень ужо ёсць і патрэбен свежы погляд, хутчэй за ўсё знайсці слабыя месцы на кансультацыі ці архітэктурным рэўю. Аб тым, як выносіць крытычныя падзеі з Zapier і Make, чытайце ў артыкуле калі no-code ламаецца, а пра іншага правайдэра - у гайдзе па інтэграцыі WayForPay на PHP.

Частыя пытанні

Чаму праверка подпісу Stripe webhook не праходзіць?

Звычайна чыннік у няслушным сакрэце подпісу (test і live, CLI і endpoint з Dashboard), у праверцы распарсенного ці перакадаванага цела замест волкага або ў проксі, які змяняе payload. Правярайце дакладныя волкія байты сакрэтам менавіта гэтага endpoint (whsec_).

Як не апрацаваць падзею Stripe двойчы?

Захоўвайце ID кожнай падзеі ў табліцы з унікальным індэксам і ўстаўляйце запіс да пачатку працы. Калі ўстаўка падае з-за дубліката, вярніце 200 і прапусціце апрацоўку. Само бізнэс-дзеянне таксама павінна быць бяспечным пры паўторы.

Які код адказу павінен вяртаць webhook Stripe ?

Код 2xx (200 ці 204) адразу пасля праверкі і захавання падзеі. 400 - пры няслушным подпісы, 5xx - толькі калі падзея не ўдалося захаваць і вы хочаце, каб Stripe паўтарыў дастаўку.

Як доўга Stripe паўтарае няўдалыя webhook?

У live-рэжыме Stripe паўтарае дастаўку з экспанентнай затрымкай да трох дзён. У тэставым рэжыме акно паўтораў нашмат карацей, таму не судзіце аб паводзінах прадакшэна па паўторах у test.

Ці можна тэсціраваць Stripe webhooks без публічнага адрасу?

Так. Запусціце stripe listen --forward-to у Stripe CLI, каб перасылаць тэставыя падзеі на localhost, а stripe trigger стварае прыклады падзей.

Бясплатны званок для выяўлення

Прынясіце бязладны стэк ці завіслую інтэграцыю - Integrono намячае самы разумны першы крок.

Вы атрымліваеце экспертную ацэнку бюджэтных пастак, залежнасцей і таго, ці варта купляць, склейваць ці будаваць - да пачатку платнай працы. CEO застаецца ў кантуры.