Stripe webhook payload not a string/buffer
My OpenSaas application implements Stripe for payment processing. I've followed the steps in the docs, including setting up a middleware to handle calls to the webhook to return raw request (instead of parsed), but when I test it by running "stripe trigger payment_intent.succeeded", I always get the error
"StripeSignatureVerificationError: Webhook payload must be provided as a string or a Buffer (https://nodejs.org/api/buffer.html) instance representing the raw request body.Payload was provided as a parsed JavaScript object instead.
[ Server ] Signature verification is impossible without access to the original signed material."
Here's the webhook definition in main.wasp
api stripeWebhooks {
fn: import { stripeWebhooks } from "@src/zigozi/apis",
middlewareConfigFn: import { stripeMiddlewareConfigFn } from "@src/payment/stripe/webhook",
httpRoute: (POST, "/api/stripe-webhooks")
}
and here's the middleware function
middlewareConfig.delete('express.json');
middlewareConfig.set('express.raw', express.raw({ type: 'application/json' }));
console.log('>> stripeMiddlewareConfigFn called');
return middlewareConfig;
How do I fix this issue?
20 Replies
Hi @Kynetix. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:
----------------------
Your approach is correct: Stripe requires the raw request body for webhook signature verification, so you must swap out the default JSON body parser for express.raw middleware on your webhook route. The error you’re seeing—"Webhook payload must be provided as a string or a Buffer... Payload was provided as a parsed JavaScript object instead"—means that somewhere, the request body is still being parsed as JSON before Stripe can verify the signature.
Based on the Wasp docs, your main.wasp and middleware function should look like this:
main.wasp
middleware function (TypeScript/JavaScript)
This matches the example in the docs, which shows how to swap out express.json for express.raw for a webhook endpoint. The key is to ensure that no other middleware (including global or per-path middleware) is parsing the body as JSON before your custom middleware runs. If you have global or per-path middleware that adds express.json, it could override your per-api configuration and cause this issue. Double-check that your global and per-path middleware do not add express.json for this route, as per the Wasp middleware customization docs:
You can override middleware for a specific api route (e.g. POST /webhook/callback). This is helpful if you want to disable JSON parsing for some callback, for example.Summary of steps to fix: 1. Ensure your middleware function is exactly as above and is being used for the correct API route. 2. Make sure no global or per-path middleware is re-adding express.json for this route. 3. Confirm that your Stripe webhook endpoint in Stripe’s dashboard matches the route in your app. If you follow these steps and still get the error, it’s likely that some other middleware is interfering. Review your middleware configuration at all levels (global, per-path, per-api) to ensure only express.raw is used for this endpoint. If you need more details, see the official Wasp docs on customizing API-specific middleware. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ---------------------- Sorry @Kynetix, I seem to be having a bit of trouble. Don't worry though, I've pinged my support and they will try to fix me up! :technologist: Perhaps try again in 15mins?
...
middlewareConfig.delete('express.json')
middlewareConfig.set('express.raw', express.raw({ type: '/' }))
Docs > Advanced > Configuring Middleware > 2. Customizeapi
-specific Middleware
@kapa.ai I found the issue. The webhook is in /api/stripe-webhooks, and there's already a middleware that applies to /api. How can I modify it so that the middleware for /api/stripe-webhooks is not affected by that of /api?
Let me check the knowledge base for you again. One moment! :books:
----------------------
To ensure that the middleware applied to /api does not affect your /api/stripe-webhooks endpoint, you should use the per-api middleware override feature in Wasp. According to the documentation, when you define a middlewareConfigFn directly on an api (as you have done for stripeWebhooks), it will override the middleware for that specific route—even if there is also a per-path (e.g., /api) middleware applied via apiNamespace.
However, if your per-path middleware (for /api) is still affecting /api/stripe-webhooks, double-check that your per-api middlewareConfigFn is correctly set up and that it removes or replaces any middleware you don't want (such as express.json). The per-api middlewareConfigFn should take precedence for that route:
- The per-api middlewareConfigFn should override the per-path middleware for that specific endpoint. - Make sure your per-api middlewareConfigFn explicitly deletes or replaces any middleware you don't want (e.g., express.json). - If you still see the per-path middleware being applied, double-check your middlewareConfigFn logic and ensure it is being used for the correct route. If you follow this pattern, the /api/stripe-webhooks endpoint will use only the middleware you specify in its middlewareConfigFn, and not inherit from the /api namespace middleware. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ---------------------- Sorry @Kynetix, I seem to be having a bit of trouble. Don't worry though, I've pinged my support and they will try to fix me up! :technologist: Perhaps try again in 15mins?
If you would like to modify the middleware for a single API, you can do something like:
... This gets installed on a per-method basis. Behind the scenes, this results in code like:
Docs > Advanced > Configuring Middleware > 2. Customize api
-specific Middleware
In summary:- The per-api middlewareConfigFn should override the per-path middleware for that specific endpoint. - Make sure your per-api middlewareConfigFn explicitly deletes or replaces any middleware you don't want (e.g., express.json). - If you still see the per-path middleware being applied, double-check your middlewareConfigFn logic and ensure it is being used for the correct route. If you follow this pattern, the /api/stripe-webhooks endpoint will use only the middleware you specify in its middlewareConfigFn, and not inherit from the /api namespace middleware. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ---------------------- Sorry @Kynetix, I seem to be having a bit of trouble. Don't worry though, I've pinged my support and they will try to fix me up! :technologist: Perhaps try again in 15mins?
@kapa.ai the existing middleware is this one
apiNamespace zigoziMiddlewareConfigFn {
middlewareConfigFn: import { zigoziMiddlewareConfigFn } from "@src/zigozi/apis",
path: "/api"
} in case it makes a difference, and it helps in fixing CORS issues
} in case it makes a difference, and it helps in fixing CORS issues
Let me check the knowledge base for you again. One moment! :books:
----------------------
Given your setup, where you have an
- This means your
You do not need to change your
apiNamespace
applying middleware to /api
, and you want /api/stripe-webhooks
to have its own middleware (for Stripe's raw body requirement), here’s how Wasp handles middleware precedence:
- Per-api middlewareConfigFn (set directly on the api
declaration) will override the per-path (apiNamespace
) middleware for that specific route.- This means your
stripeWebhooks
API, with its own middlewareConfigFn
, should not be affected by the /api
namespace middleware, as long as your per-api middleware removes or replaces any conflicting middleware (like express.json
) [Customize API-specific Middleware].
What you should do:
- Ensure your stripeWebhooks
API has its own middlewareConfigFn
that deletes express.json
and sets express.raw
.
- This per-api middleware will take precedence over the /api
namespace middleware for the /api/stripe-webhooks
route.
Example:
Summary:You do not need to change your
/api
namespace middleware. The per-api middlewareConfigFn
for stripeWebhooks
will override it for that route, ensuring Stripe receives the raw body as required [Customize API-specific Middleware].
If you still experience issues, double-check that your per-api middleware is correctly implemented and that no other global or per-path middleware is interfering [Customize Per-Path Middleware].
----------------------
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
----------------------
Sorry @Kynetix, I seem to be having a bit of trouble. Don't worry though, I've pinged my support and they will try to fix me up! :technologist:
Perhaps try again in 15mins?@kapa.ai I added the log, and it says that req.body is not a buffer (returns false).
I noticed that if I remove the apiNamespace middleware, the stripeWebhook works fine (though the CORS protection from the apiNamespace would no longer work). Is there another way to solve the CORS issue without using a middleware?
Let me check the knowledge base for you again. One moment! :books:
----------------------
Based on the Wasp documentation and issues, CORS protection for custom API endpoints in Wasp is typically implemented via middleware—specifically, by configuring the CORS middleware in your middlewareConfigFn, either globally, per-path (using apiNamespace), or per-api. There is no built-in way to enable CORS for custom API endpoints in Wasp without using middleware, as APIs do not utilize the default middleware stack by default [Making Sure CORS Works].
If your per-path (apiNamespace) middleware is interfering with the Stripe webhook (e.g., by adding express.json before your per-api express.raw), the only supported way to resolve this is to ensure your per-api middlewareConfigFn for the Stripe webhook removes or replaces the conflicting middleware (like express.json) with express.raw. This is the recommended and documented approach [Customize API-specific Middleware].
To summarize:
- CORS in Wasp is handled via middleware. There is no alternative built-in mechanism for CORS protection on custom endpoints.
- The correct solution is to use a per-api middlewareConfigFn for your Stripe webhook that removes express.json and sets express.raw, which will override the per-path middleware for that endpoint.
- If you remove the apiNamespace middleware, you lose CORS protection for all /api routes, which is not recommended.
If you need CORS only for certain endpoints, you can fine-tune your per-path or per-api middleware to apply CORS only where needed, but it must still be done via middleware [Middleware Customization].
There is no way to enable CORS for custom HTTP endpoints in Wasp without using middleware, according to the current documentation and issues.
----------------------
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
----------------------
Sorry @Kynetix, I seem to be having a bit of trouble. Don't worry though, I've pinged my support and they will try to fix me up! :technologist:
Perhaps try again in 15mins?
I was able to fix it by changing the endpoint to something else besides /api
@kapa.ai Now that I can access the stripe webhook, I want to add a record to the database, and the webhook needs to know that the user is authenticated, since the database functions (createTransaction(), etc) will return a 401 error if an unauthorized user attempts to run it. The webhook api includes a context parameter, but the context.user is null. How can I pass in an authenticated user into the webhook api?
Let me check the knowledge base for you again. One moment! :books:
----------------------
In Wasp, the
The knowledge sources do not mention a way to "authenticate" a webhook request as a user or to populate
You cannot set
context.user
object is automatically populated for authenticated requests made by users through the app (e.g., via the frontend). However, for webhooks like Stripe's, the request is coming from Stripe's servers, not from an authenticated user session. Therefore, context.user
will be null
in your webhook handler, and there is no built-in way to "pass in" an authenticated user for such requests.
The recommended approach is to identify the user based on information provided in the webhook payload (for example, a Stripe customer ID or email), and then fetch the corresponding user from your database manually within the webhook handler. You can then use that user’s ID or other information to create records or perform actions as needed.
Example approach:
1. Extract the relevant identifier (e.g., customer
or email
) from the Stripe webhook payload.
2. Query your database for the user associated with that identifier.
3. Use that user’s ID when creating records (e.g., transactions).
This pattern is common for webhooks, since they are not tied to a logged-in user session.
Note:The knowledge sources do not mention a way to "authenticate" a webhook request as a user or to populate
context.user
for webhooks. All examples and documentation show that context.user
is only set for authenticated user requests, not for third-party service webhooks [Accessing the logged-in user > Using the context.user object].
If you need to restrict webhook access, you should verify the webhook signature (as Stripe recommends) to ensure the request is legitimate, but this does not populate context.user
.
Summary:You cannot set
context.user
for a webhook request. Instead, look up the user in your database using information from the webhook payload, and use that user’s ID for your database operations.
----------------------
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
----------------------
Sorry @Kynetix, I seem to be having a bit of trouble. Don't worry though, I've pinged my support and they will try to fix me up! :technologist:
Perhaps try again in 15mins?@kapa.ai thanks, I can retrieve the userId via the metadata used in creating the Stripe checkout session, and pass it to the webhook. Technically, I don't need to insert the userId into the database. I believe the createTransaction() function can only be called by an authenticated user - otherwise, it'll return the 401 error. Is there a way to simulate an authenticated user in the webhook?
Let me check the knowledge base for you again. One moment! :books:
----------------------
Based on the available knowledge sources, there is no documented way in Wasp to simulate an authenticated user within a webhook handler by populating
- You cannot simulate an authenticated user in a webhook handler. - Extract shared logic into a plain function and call it from both your authenticated action/query and your webhook handler, passing the userId as needed. - Perform user lookups manually in the webhook handler using identifiers from the webhook payload. If you need to perform database operations as a result of a webhook, do so directly in the webhook handler after verifying the request and identifying the user. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ---------------------- Sorry @Kynetix, I seem to be having a bit of trouble. Don't worry though, I've pinged my support and they will try to fix me up! :technologist: Perhaps try again in 15mins?
context.user
or bypassing the usual authentication checks that actions/queries enforce for logged-in users.
Webhooks, such as those from Stripe, are not tied to a user session, so context.user
will always be null
in these handlers. The recommended approach is to manually look up the user using identifiers (like userId
from Stripe metadata) and then perform the necessary database operations directly, rather than calling functions (like createTransaction()
) that are protected by authentication and expect a user context.
If you need to reuse logic from an authenticated action/query in your webhook, the best practice is to extract the shared logic into a separate function that does not depend on context.user
, and call that function from both your action/query and your webhook handler. This way, you avoid the authentication check that would otherwise result in a 401 error when called from the webhook, as described in community discussions:
If there was logic in the action you wanted to reuse from Job, what I would do in this case is create a function for that logic, normal JS function somewhere in your server code, I would call it function X, and then your action can call that function X inside it, and also your job can call that same function X. That way you can share a lot of logic, as much as you want.Summary:
Discord support thread: Using actions inside workers - is it possible?
- You cannot simulate an authenticated user in a webhook handler. - Extract shared logic into a plain function and call it from both your authenticated action/query and your webhook handler, passing the userId as needed. - Perform user lookups manually in the webhook handler using identifiers from the webhook payload. If you need to perform database operations as a result of a webhook, do so directly in the webhook handler after verifying the request and identifying the user. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ---------------------- Sorry @Kynetix, I seem to be having a bit of trouble. Don't worry though, I've pinged my support and they will try to fix me up! :technologist: Perhaps try again in 15mins?
Hey @Kynetix, I think the last question is a little above our AI's paygrade. Let me check that and get back to you
Ok, I'm looking at the OpenSaas template now, and I don't see a
createTransaction
function. Is that one of yours?@Filip yes sorry, that's my function. Transaction is one of the schema models I created, and createTransaction is the one for adding new Transaction record
Is it an operation? If so, you'll want to use this: https://wasp.sh/docs/data-model/operations/actions#using-actions-on-the-server
I understand the problem is getting the
AuthUser
without having the context
. I remember us having a discussion over this and I think it can be done, give me a moment
Ah, I see what happend - I ran into some issues implementing that and decided to postpone it until it proves necessary: https://github.com/wasp-lang/wasp/issues/2066
Looks like you just proved it's necessary, so I'm adding this thread to the issue 😄
Here's what I suggest:
If you can make createTransactionForUser
work without the properties from AuthUser
(and it's probably possible), then this should work.
If not, you just need a couple of more db calls in stripeWebhook
to get all the data on the specific user you need.
I'm here if you get stuck 🙂Ah, thanks a lot man. I see what you did. So basically createTransactionForUser is an intermediate that stripeWebhook() can access, and that can call createTransaction(). I actually don't need the user object for the transaction - I was only mentioning that the createTransaction() can't be called by an unauthenticated user, hence the need for AuthUser.
What I'd done as a workaround was to do a direct prisma insert
await prisma.transaction.create({...})
I'll try your suggestion though, and that should work too.
I was only mentioning that the createTransaction() can't be called by an unauthenticated user, hence the need for AuthUser.Even better then, the intermediary function will be simpler. But I can imagine how frustrating it was to be blocked by the user which you didn't even need 😅 Anyway, yes, this should work and is a little nicer than directly calling prisma. Let me know how it turns out!
@Filip thanks for your help. I'm a little stuck and maybe you could help with the implementation.
Here's the createTransaction() function
// Transaction Operations
export const createTransaction: CreateTransaction<CreateTransactionInput, Transaction> = async (args, context) => {
if (!context.user) {
throw new HttpError(401);
}
// get the travelerId from the trip using the tripId
const traveler = await context.entities.Trip.findFirst({
where: {
id: args.tripId,
traveler: {
userId: context.user.id
}
}
});
if (!traveler) {
throw new HttpError(404, 'Traveler not found or you do not have permission to create a transaction for it');
}
// get the shipperId from the package using the packageId
const shipper = await context.entities.Package.findFirst({
where: {
id: args.packageId,
}
});
if (!shipper) {
throw new HttpError(404, 'Package not found or you do not have permission to create a transaction for it');
}
const transaction = await context.entities.Transaction.create({
data: {
tripId: args.tripId,
shipperId: shipper.shipperId,
pickupDate: args.pickupDate,
deliveryDate: args.deliveryDate,
status: args.status,
packageId: args.packageId,
stripePaymentIntentId: args.stripePaymentIntentId ?? null,
travelerId: traveler.travelerId,
code: args.code
}
});
return transaction;
};
Apparently, if I'm to move most of the logic into createTransactionForUser(), I'll need the context property to access the entities to do the insertion/retrieval, etc. I won't be able to get that from the stripe webhook.
Am I doing it correctly, or is there a better way?Hey @Kynetix, yes, I forgot about this.
Btw, you can use three backticks when pasting a lot of code to get better formatting.
Back to the problem at hand - yes, you're right, this is inconvenient. I recommend you fall back to what you had for now (until we fix the issue).
There is a way around this, but it's equally complicated as your initial idea and not much nicer
Sorry for the late response, got a little carried away with the other stuff
sounds good. Thanks
'ts all good.
We need to fix that issue ASAP though, so thanks for raising this!