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

解决Firebase Cloud Functions作为Mpesa支付回调URL失效问题

Mpesa回调接收失败:Invalid request data 问题排查与修复

问题背景

使用Firebase Cloud Functions作为回调URL接收Safaricom Mpesa支付响应,调用API触发用户输入PIN的流程正常,但云函数无法正确接收支付响应,返回“Invalid request data”错误。


相关代码

Flutter调用Mpesa API代码

Future mPesaPaymentInAction() async {
  print("mpesa in action!");
  try {
    Mpesa mpesa = Mpesa(
      clientKey: myClientKey,
      clientSecret: myClientSecret,
      passKey: myPassKey,
      environment: myEnvironment,
    );
    await mpesa
        .lipaNaMpesa(
      amount: 1,
      phoneNumber: phoneNumberForPayment.text.toString(),
      businessShortCode: myBusinessShortCode,
      transactionDescription: transactionDescription,
      callbackUrl: callBackUrl,
    )
        .then((result) async {
      ScaffoldMessenger.of(context).showSnackBar(SnackBar(
        duration: const Duration(seconds: 5),
        backgroundColor: Colors.greenAccent,
        content: Text(
          "Mpesa Pin request sent by Safaricom, Please wait.",
          textAlign: TextAlign.center,
          style: GoogleFonts.lato(
            fontSize: 14,
          ),
        ),
      ));

      if (result.ResponseCode.toString() == "0" &&
          result.CustomerMessage.toString() ==
              "Success. Request accepted for processing" &&
          result.ResponseDescription ==
              "Success. Request accepted for processing") {
        setState(() {
          mCheckoutRequestID_waterServices = result.CheckoutRequestID;
        });
        try {
          final prefs = await SharedPreferences.getInstance();
          await prefs.setString("mCheckoutRequestID_waterServices",
              mCheckoutRequestID_waterServices.toString());
          await prefs.setString(
              "paymentDone_waterServices", paymentDone_waterServices);
        } catch (e) {
          print(e.toString());
        }
        print("mpesa done!");
      } else {
        throw Error();
      }
    }).catchError((error) {
      print(error);
      setState(() {
        loadingScreen = false;
      });
    });
  } catch (e) {
    ScaffoldMessenger.of(context).showSnackBar(SnackBar(
      duration: const Duration(seconds: 5),
      backgroundColor: Colors.redAccent,
      content: Text(
        "Please try again.",
        textAlign: TextAlign.center,
        style: GoogleFonts.lato(
          fontSize: 14,
        ),
      ),
    ));
    setState(() {
      loadingScreen = false;
      phoneNumberForPayment.clear();
    });
    if (kDebugMode) {
      print("Was inside mpesa in action:- ${e.toString()}");
    }
  }
}

获取回调响应数据的Flutter方法(需删除)

Future getMpesaData(String callbackUrl) async {
  try {
    print("GETTING DATA");
    final response = await http.post(Uri.parse(callbackUrl));
    print(response.body.toString());

    if (response.statusCode == 200) {
      final data = json.decode(response.body);

      final initiatorName = data['InitiatorName'];
      final securityCredential = data['SecurityCredential'];
      final commandID = data['CommandID'];
      final amount = data['Amount'];
      final mpesaReceiptNumber = data['MpesaReceiptNumber'];
      final transactionID = data['TransactionID'];
      final transactionDate = data['TransactionDate'];
      final phoneNumber = data['PhoneNumber'];
      final resultCode = data['ResultCode'];
      final resultDesc = data['ResultDesc'];

      print('Initiator Name: $initiatorName');
      print('Security Credential: $securityCredential');
      print('Command ID: $commandID');
      print('Amount: $amount');
      print('M-Pesa Receipt Number: $mpesaReceiptNumber');
      print('Transaction ID: $transactionID');
      print('Transaction Date: $transactionDate');
      print('Phone Number: $phoneNumber');
      print('Result Code: $resultCode');
      print('Result Description: $resultDesc');
      return initiatorName;
    } else {
      throw Exception('Failed to get M-Pesa data: ${response.reasonPhrase}');
    }
  } catch (e) {
    print("Failed to get M-Pesa data");
  }
}

原Firebase云函数代码

const functions = require("firebase-functions");
const express = require("express");
const app = express();
const cors = require("cors")({ origin: true });
const bodyParser = require("body-parser");
const axios = require("axios");

app.use(cors);
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

const CONSUMER_KEY = ""; 
const CONSUMER_SECRET = ""; 
const PASSKEY = "";
const SHORTCODE = "174379";

exports.mpesaCallback = functions.https.onRequest((req, res) => {
    const data = req.body;

    if (!data || !data.Body || !data.Body.stkCallback) {
        return res.status(400).json({ message: "Invalid request data" });
    }

    const payment = data.Body.stkCallback;

    if (!payment || payment.ResultCode != 0) {
        return res.status(400).json({ message: "Invalid payment data" });
    }

    axios({
        url: "https://api.safaricom.co.ke/mpesa/b2c/v1/paymentresulthttp",
        method: "post",
        headers: {
            Authorization:
                "Basic " +
                Buffer.from(CONSUMER_KEY + ":" + CONSUMER_SECRET).toString("base64"),
            "Content-Type": "application/json"
        },
        data: {
            InitiatorName: "",
            SecurityCredential: "",
            CommandID: "PaymentConfirmation",
            Amount: payment.CallbackMetadata.Item[0].Value,
            MpesaReceiptNumber: payment.CallbackMetadata.Item[1].Value,
            TransactionID: payment.CallbackMetadata.Item[2].Value,
            TransactionDate: payment.CallbackMetadata.Item[3].Value,
            PhoneNumber: payment.CallbackMetadata.Item[4].Value,
            ResultCode: 0,
            ResultDesc: "Payment successful"
        }
    })
        .then(response => {
            console.log(response.data);
            return res.status(200).json({ message: "Payment confirmed" });
        })
        .catch(error => {
            console.log(error);
            return res.status(500).json({ message: "Error sending confirmation" });
        });
});

exports.app = functions.https.onRequest(app);

报错信息

Flutter端打印

I/flutter (13136): {"message":"Invalid request data"}
I/flutter (13136): Failed to get M-Pesa data

GCP云函数日志

{
  "insertId": "1ny6725faqkk59",
  "labels": {},
  "logName": "insertName",
  "receiveTimestamp": "2023-03-28T11:56:32.298610539Z",
  "resource": {},
  "severity": "DEBUG",
  "textPayload": "Function execution took 73 ms, finished with status: 'error'",
  "timestamp": "2023-03-28T11:56:32.028351659Z",
  "trace": "projects/insertName-6de2c/traces/f1fa60c5b51f6ac7f8d2e16958dacf96"
}

问题分析与修复

核心问题

  1. 回调URL指向错误:云函数定义了mpesaCallback处理逻辑,但如果callBackUrl指向的是app这个未关联对应路由的Express应用,Safaricom的请求无法进入正确处理流程。
  2. 错误主动调用回调URL:Flutter中的getMpesaData主动给回调URL发POST请求,违背Mpesa回调机制——回调是Safaricom主动发起的,无需客户端主动请求。
  3. 云函数逻辑错误:
    • 错误调用B2C接口paymentresulthttp,Lipa Na Mpesa回调只需返回200响应即可,无需额外请求。
    • 参数校验过于严格且无日志,无法定位请求实际结构。
    • 未容错CallbackMetadata.Item的顺序或缺失情况。

修复步骤

1. 修正回调URL

确保callBackUrl指向mpesaCallback函数的正确地址,格式示例:
https://<region>-<your-project-id>.cloudfunctions.net/mpesaCallback

2. 删除错误的getMpesaData调用

在Flutter的mPesaPaymentInAction方法中,移除final data = await getMpesaData(callBackUrl);这一行,同时可以删除整个getMpesaData函数。

3. 重构云函数代码

const functions = require("firebase-functions");

exports.mpesaCallback = functions.https.onRequest((req, res) => {
    // 打印请求体,用于排查结构问题
    console.log("Received callback request:", JSON.stringify(req.body));

    // 基础校验,同时返回Safaricom要求的格式响应
    if (!req.body?.Body?.stkCallback) {
        console.error("Invalid request structure:", req.body);
        return res.status(200).json({ ResultCode: 0, ResultDesc: "Invalid request received" });
    }

    const stkCallback = req.body.Body.stkCallback;
    console.log("STK Callback details:", JSON.stringify(stkCallback));

    // 处理支付结果
    if (stkCallback.ResultCode === 0) {
        // 支付成功,提取元数据
        const metadataMap = {};
        stkCallback.CallbackMetadata?.Item?.forEach(item => {
            metadataMap[item.Name] = item.Value;
        });
        console.log("Payment successful - Data:", metadataMap);
        
        // 此处添加业务逻辑:写入数据库、发送通知等
    } else {
        // 支付失败
        console.error("Payment failed:", stkCallback.ResultDesc);
    }

    // 必须返回200响应,否则Safaricom会重复发送回调请求
    return res.status(200).json({ ResultCode: 0, ResultDesc: "Success" });
});

4. 查看云函数日志

支付完成后,查看GCP云函数的日志,确认Safaricom是否发送了请求,以及请求结构是否符合预期,以此进一步排查问题。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 00:37:02