You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Stripe CLI触发的webhook POST请求中传递自定义变量

问题描述

需要实现自定义变量跨页面重载、跨第三方跳转后仍可正常传递调用,核心需求是读取req.session.variable的值,在Stripe CLI管控的router.post('/webhook', ...)接口中使用,当前存在两个问题:

  • 用户跳转至Stripe等第三方站点后,req.session内容无法在webhook POST请求中读取到
  • Stripe webhook的POST接口要求req.body必须为原始req.rawBody格式,常规JSON解析会导致签名校验失败

原有实现代码如下:

let express = require('express');
let router = express.Router();
let postMong = require('./post')
require("dotenv").config()
router.use(express.json());
const YOUR_DOMAIN = 'http://localhost:4242';
const stripe = require('stripe')(process.env.PUBLIC_KEY);
const endpointSecret = 'whsec_1c7320d5c8878c54d6f99e0d5dcd441008c79b76ddb0a09727a4fd977d3ac1ed';
const fulfillOrder = (ses) => {
  console.log("Order Completed")
}


router.post('/checkout/create-order', async (req, res) => {
  const price = req.body.order.stripe_price || undefined,
        product = req.body.order.stripe_product || undefined
  const session = await stripe.checkout.sessions.create({
    //shipping_address_collection: {
    //  allowed_countries: ['US', 'CA'],
    //},
    //shipping_options: [
    //  {
    //    shipping_rate_data: {
    //      type: 'fixed_amount',
    //      fixed_amount: {
    //        amount: 2499,
    //        currency: 'usd',
    //      },
    //      display_name: 'International Shipping',
    //      // Delivers between 5-7 business days
    //      delivery_estimate: {
    //        minimum: {
    //          unit: 'week',
    //          value: 2,
    //        },
    //      }
    //    }
    //  },
    //],
    line_items: [
      {
        price: price,
        quantity: 1,
      },
    ],
    payment_method_types: ["card", 'us_bank_account'],
    mode: 'payment',
    success_url: `${YOUR_DOMAIN}/success.html`,
    cancel_url: `${YOUR_DOMAIN}/index.html`,
  });

  res.json({url: session.url})
});


router.post('/webhook', (request, response) => {
  const payload = request.body;
let username = request.session.username; // returns undefined because the session is empty after the redirect
  const sig = request.headers['stripe-signature'];
  let event;
  try {
    event = stripe.webhooks.constructEvent(payload, sig, endpointSecret);
  } catch (err) {
    console.log(err.message)
    return response.status(400).send(`Webhook Error: ${err.message}`);
  }

  if (event.type === 'checkout.session.completed') {
    const session = event.data.object;
    fulfillOrder(session);
  }

  response.status(200);
});

module.exports = router
解决方案

核心原理说明

首先明确两个认知误区:

  1. Stripe webhook请求是Stripe官方服务器主动向你的服务端发起的回调请求,发起方不是用户浏览器,不会携带用户本地存储的session cookie,因此无论是否发生跳转,你都不可能在webhook接口中读到req.session的用户相关值,和跳转清空无关。
  2. Stripe webhook签名校验要求拿到原始未解析的请求body,全局挂载express.json()会把请求体提前解析为JSON对象,导致签名校验失败。

具体实现步骤

  • 拆分body解析逻辑:webhook路由单独使用express.raw()处理原始请求体,其余业务路由正常使用express.json()解析JSON格式请求。
  • 传递自定义变量不要依赖session:创建Stripe Checkout Session时,把需要传递的自定义字段全部存入Stripe自带的metadata字段,webhook收到事件后直接从返回的Checkout Session对象中读取metadata即可,不需要额外存储,也不会丢失。

修改后可直接运行的代码

let express = require('express');
let router = express.Router();
let postMong = require('./post')
require("dotenv").config()

// 移除全局的express.json()挂载,按路由分别配置解析规则
const YOUR_DOMAIN = 'http://localhost:4242';
const stripe = require('stripe')(process.env.PUBLIC_KEY);
const endpointSecret = 'whsec_1c7320d5c8878c54d6f99e0d5dcd441008c79b76ddb0a09727a4fd977d3ac1ed';

const fulfillOrder = (ses, username) => {
  // 这里可以直接拿到传入的username做后续业务处理
  console.log("Order Completed, user:", username)
}

// 非webhook路由正常使用json解析
router.post('/checkout/create-order', express.json(), async (req, res) => {
  const price = req.body.order.stripe_price || undefined,
        product = req.body.order.stripe_product || undefined,
        // 从当前请求的session里拿到要传递的自定义变量
        username = req.session.username;

  const session = await stripe.checkout.sessions.create({
    line_items: [
      {
        price: price,
        quantity: 1,
      },
    ],
    payment_method_types: ["card", 'us_bank_account'],
    mode: 'payment',
    success_url: `${YOUR_DOMAIN}/success.html`,
    cancel_url: `${YOUR_DOMAIN}/index.html`,
    // 把自定义变量存在metadata里,Stripe会在后续webhook事件中原样返回
    metadata: {
      username: username
    }
  });

  res.json({url: session.url})
});

// webhook路由使用raw格式解析body,满足签名校验要求
router.post('/webhook', express.raw({type: 'application/json'}), (request, response) => {
  const payload = request.body;
  const sig = request.headers['stripe-signature'];
  let event;
  try {
    event = stripe.webhooks.constructEvent(payload, sig, endpointSecret);
  } catch (err) {
    console.log(err.message)
    return response.status(400).send(`Webhook Error: ${err.message}`);
  }

  if (event.type === 'checkout.session.completed') {
    const session = event.data.object;
    // 直接从返回的session里读之前存入的metadata,拿到自定义变量
    const username = session.metadata.username;
    fulfillOrder(session, username);
  }

  response.status(200).end();
});

module.exports = router

注:metadata支持传入任意自定义键值对,只要值为字符串格式即可,单个Checkout Session的metadata总大小不超过500个键值对就可以正常使用,完全满足常规业务传值需求。

内容的提问来源于stack exchange,提问作者Cristian Babalau

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 18:21:56