Handling WeChat Pay APIv3 Callback Notifications in ASP.NET Core

When procesisng WeChat Pay callback notifications in ASP.NET Core, two primary challenges arise: reading the request body and processing the encrypted payload. Instead of strictly following the official signature verification steps, a pragmatic approach involves decrypting the callback data directly to retrieve the order number and then validating the transaction state via the Order Query API.

Data Transfer Objects (DTOs)

Define the necessary classes to map the JSON structure sent by WeChat.

public class CallbackNotification
{
    public string id { get; set; }
    public string create_time { get; set; }
    public string event_type { get; set; }
    public string resource_type { get; set; }
    public EncryptedResource resource { get; set; }
    public string summary { get; set; }
}

public class EncryptedResource
{
    public string algorithm { get; set; }
    public string ciphertext { get; set; }
    public string associated_data { get; set; }
    public string original_type { get; set; }
    public string nonce { get; set; }
}

public class DecryptedTransaction
{
    public string appid { get; set; }
    public string mchid { get; set; }
    public string out_trade_no { get; set; }
    public string transaction_id { get; set; }
    public string trade_state { get; set; }
    public string trade_state_desc { get; set; }
    public PayerInfo payer { get; set; }
}

public class PayerInfo
{
    public string openid { get; set; }
}

public class CallbackResponse
{
    public string code { get; set; } = "SUCCESS";
    public string message { get; set; } = "";
}

Decryption Utility

Use BouncyCastle to handle AES-256-GCM decryption required by APIv3.

using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Engines;
using Org.BouncyCastle.Crypto.Modes;
using Org.BouncyCastle.Crypto.Parameters;

public class PaymentDecryptor
{
    private const int MacBitSize = 128;
    private readonly string _apiKey;

    public PaymentDecryptor(string apiKey)
    {
        _apiKey = apiKey;
    }

    public string DecryptPayload(string associatedData, string nonceStr, string encryptedText)
    {
        byte[] key = Encoding.UTF8.GetBytes(_apiKey);
        byte[] nonce = Encoding.UTF8.GetBytes(nonceStr);
        byte[] data = Convert.FromBase64String(encryptedText);
        byte[] aad = Encoding.UTF8.GetBytes(associatedData);

        var cipher = new GcmBlockCipher(new AesEngine());
        var parameters = new AeadParameters(new KeyParameter(key), MacBitSize, nonce, aad);
        cipher.Init(false, parameters);

        byte[] output = new byte[cipher.GetOutputSize(data.Length)];
        int length = cipher.ProcessBytes(data, 0, data.Length, output, 0);
        cipher.DoFinal(output, length);

        return Encoding.UTF8.GetString(output);
    }
}

Controller Implementation

Read the raw JSON body from the Request, decrypt the resource, and verify the transaction.

public async Task<CallbackResponse> ProcessPaymentCallback()
{
    // 1. Read Request Body in ASP.NET Core
    using var reader = new StreamReader(Request.Body);
    string rawJson = await reader.ReadToEndAsync();
    
    // 2. Deserialize Notification
    var notification = JsonConvert.DeserializeObject<CallbackNotification>(rawJson);
    
    if (notification?.resource == null)
    {
        return new CallbackResponse { code = "FAIL", message = "Invalid Resource" };
    }

    // 3. Decrypt Data
    var decryptor = new PaymentDecryptor("your_api_v3_key_here");
    string plainJson = decryptor.DecryptPayload(
        notification.resource.associated_data,
        notification.resource.nonce,
        notification.resource.ciphertext
    );

    var transactionData = JsonConvert.DeserializeObject<DecryptedTransaction>(plainJson);

    // 4. Verify by calling Query API (Bypassing local signature check)
    string queryUrl = $"https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/{transactionData.out_trade_no}?mchid={YourMchId}";
    
    // Assume HttpClient is configured with WeChat Pay auth handler
    var response = await _httpClient.GetAsync(queryUrl);
    
    if (response.IsSuccessStatusCode)
    {
        // Process successful payment logic here
        return new CallbackResponse();
    }

    return new CallbackResponse { code = "FAIL", message = "Verification Failed" };
}

Legacy .NET Framework Note

In older .NET Framework applications (non-Core), the stream reading method differs slightly:

var httpRequest = HttpContext.Current.Request;
using (var stream = httpRequest.InputStream)
{
    stream.Position = 0;
    using (var reader = new StreamReader(stream, Encoding.UTF8))
    {
        string body = reader.ReadToEnd();
        // Process body...
    }
}

Tags: ASP.NET Core WeChat Pay APIv3 AES-256-GCM Payment Callback

Posted on Fri, 07 Aug 2026 16:25:12 +0000 by jamesm87