Stripe webhooks на PHP: надзейны апрацоўшчык для плацяжоў
Webhook Stripe, які працуе на наўтбуку, яшчэ нельга давяраць грошы. У гэтым гайдзе разбіраны праверку подпісы, хуткі адказ, дэдуплікацыю падзей і паўторныя дастаўкі на чыстым PHP і Laravel, з кодам, які можна ўзяць у праект.
Коратка
- Крыніца праўды аб плацяжы - 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 застаецца ў кантуры.