Skip to content

[Bug]: convertToSnakeCase recursively converts keys inside custom_data on simulation payloads #220

Description

@benjamincburns

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

  1. 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" }
  1. 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
    },
  },
});
  1. Run the simulation:
await paddle.simulations.createRun({ simulationId: "ntfsim_01..." });
  1. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions