What happened?
When creating a simulation with a custom payload via paddle.simulations.create(), camelCase keys inside custom_data are converted to snake_case before being sent to the Paddle API. The delivered simulation webhook then contains the snake_case keys instead of the original camelCase keys, breaking webhook handlers that expect the keys they originally stored.
This does not affect real (non-simulation) webhooks. Keys inside custom_data are preserved correctly when using paddle.subscriptions.create(), paddle.transactions.create(), etc.
Steps to reproduce
- Create a subscription with camelCase keys in
customData:
await paddle.subscriptions.create({
customData: { tenantId: "abc-123", type: "subscription" },
// ...other fields
});
// Real webhooks correctly deliver: custom_data: { tenantId: "abc-123", type: "subscription" }
- Create a simulation for that subscription with matching
custom_data:
await paddle.simulations.create({
notificationSettingId: "ntfset_01...",
name: "test renewal",
type: "transaction.completed",
payload: {
data: {
custom_data: { tenantId: "abc-123", type: "subscription" },
// ...other fields matching a transaction.completed event
},
},
});
- Run the simulation:
await paddle.simulations.createRun({ simulationId: "ntfsim_01..." });
- Inspect the delivered webhook payload --
custom_data contains { tenant_id: "abc-123" } instead of { tenantId: "abc-123" }.
What did you expect to happen?
custom_data is documented as free-form JSON. Keys inside it should be preserved exactly as provided, matching the behavior of real webhooks. The delivered simulation webhook should contain custom_data: { tenantId: "abc-123", type: "subscription" }.
Logs
No error logs. The conversion is silent. The symptom is that webhook handlers reading event.data.customData.tenantId get undefined because the key was converted to tenant_id.
Root Cause Analysis
Client.post() calls convertToSnakeCase(requestBody) on every outbound POST body.
convertToSnakeCase has a carve-out that preserves customData contents, but only when customData is a top-level key on the request body. This works fine for normal API requests, but when creating a simulation with customData in the payload, the customData gets converted.
A fix could be to add another customData carve-out for simulation creation. Another might be to make it depth-insensitive, so that customData objects aren't converted at any level of depth.
What happened?
When creating a simulation with a custom payload via
paddle.simulations.create(), camelCase keys insidecustom_dataare converted to snake_case before being sent to the Paddle API. The delivered simulation webhook then contains the snake_case keys instead of the original camelCase keys, breaking webhook handlers that expect the keys they originally stored.This does not affect real (non-simulation) webhooks. Keys inside
custom_dataare preserved correctly when usingpaddle.subscriptions.create(),paddle.transactions.create(), etc.Steps to reproduce
customData:custom_data:custom_datacontains{ tenant_id: "abc-123" }instead of{ tenantId: "abc-123" }.What did you expect to happen?
custom_datais documented as free-form JSON. Keys inside it should be preserved exactly as provided, matching the behavior of real webhooks. The delivered simulation webhook should containcustom_data: { tenantId: "abc-123", type: "subscription" }.Logs
No error logs. The conversion is silent. The symptom is that webhook handlers reading
event.data.customData.tenantIdgetundefinedbecause the key was converted totenant_id.Root Cause Analysis
Client.post()callsconvertToSnakeCase(requestBody)on every outbound POST body.convertToSnakeCasehas a carve-out that preservescustomDatacontents, but only whencustomDatais a top-level key on the request body. This works fine for normal API requests, but when creating a simulation withcustomDatain thepayload, thecustomDatagets converted.A fix could be to add another
customDatacarve-out for simulation creation. Another might be to make it depth-insensitive, so thatcustomDataobjects aren't converted at any level of depth.