Skip to content

WIP: feat(providers): add dodopayments provider - #158

Closed
ayushsharma74 wants to merge 1 commit into
getpaykit:mainfrom
ayushsharma74:feat/dodopayments-provider
Closed

WIP: feat(providers): add dodopayments provider#158
ayushsharma74 wants to merge 1 commit into
getpaykit:mainfrom
ayushsharma74:feat/dodopayments-provider

Conversation

@ayushsharma74

@ayushsharma74 ayushsharma74 commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Summary

fixes #104

This PR will add support for dodopayments in paykit

Summary by CodeRabbit

  • New Features
    • Introduced DodoPayments payment provider integration for PayKit ecosystem.
    • Enables customer account creation with email validation and metadata support.
    • Supports subscription checkout sessions with customizable product quantities.
    • Provides subscription management capabilities including updates, scheduling, resumption, and cancellation.
    • Implements webhook event handling for subscription lifecycle events.
    • Includes provider health verification and configuration status checks.

@vercel

vercel Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

@ayushsharma74 is attempting to deploy a commit to the maxktz Team on Vercel.

A member of the Team first needs to authorize it.

@ayushsharma74
ayushsharma74 marked this pull request as draft April 28, 2026 10:56
@coderabbitai

coderabbitai Bot commented Apr 28, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: f99536cb-535f-454c-bf26-b766a4141e41

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Introduces the @paykitjs/dodopayments package, a new payment provider implementation that integrates Dodo Payments with PayKit. The package includes configuration files, build tooling setup, and a provider adapter implementing customer management, subscription operations, webhook handling, and provider status verification.

Changes

Cohort / File(s) Summary
Package Configuration
packages/dodopayments/package.json, packages/dodopayments/tsdown.config.ts
Establishes npm package metadata, ESM module configuration with exports mapping, build scripts using tsdown and tsc, and declares runtime dependencies (dodopayments, paykitjs) and dev dependencies (tsdown, vitest).
Provider Implementation
packages/dodopayments/src/dodopayments-provider.ts
Implements createDodopaymentsProvider adapter with customer CRUD (create/update/list), subscription management (checkout, update, schedule, resume, cancel), active subscription listing, portal sessions, webhook event unwrapping, provider health checks, and consistent error handling for unsupported operations.
Package Entry Point
packages/dodopayments/src/index.ts
Re-exports dodopayments factory function and DodopaymentsOptions configuration type for public consumption.

Sequence Diagrams

sequenceDiagram
    participant App as PayKit Application
    participant Provider as DodoPayments Provider
    participant DodoAPI as Dodo Payments API

    App->>Provider: createCustomer(email, name, metadata)
    Provider->>DodoAPI: POST /customers (create)
    DodoAPI-->>Provider: Customer created or ConflictError
    alt Conflict - Email exists
        Provider->>DodoAPI: GET /customers (list by email)
        DodoAPI-->>Provider: Existing customer
        Provider->>DodoAPI: PATCH /customers (relink/update)
        DodoAPI-->>Provider: Customer updated
    end
    Provider-->>App: Customer ID
Loading
sequenceDiagram
    participant App as PayKit Application
    participant Provider as DodoPayments Provider
    participant DodoAPI as Dodo Payments API

    App->>Provider: createSubscriptionCheckout(customerId, items)
    Provider->>DodoAPI: POST /checkout/sessions (create with items)
    DodoAPI-->>Provider: Checkout session with URL
    Provider-->>App: Checkout URL
    Note over App,DodoAPI: User completes checkout at Dodo
    Note over Provider,DodoAPI: Webhook event fires
Loading
sequenceDiagram
    participant DodoAPI as Dodo Payments API
    participant Provider as DodoPayments Provider
    participant App as PayKit Application

    DodoAPI->>Provider: POST /webhooks (signed event)
    Provider->>Provider: Extract headers & signature
    Provider->>DodoAPI: client.webhooks.unwrap(event)
    DodoAPI-->>Provider: Unwrapped event or error
    alt Signature valid
        Provider->>Provider: Map event type (subscription.active, etc.)
        Provider-->>App: Handled events array
    else Signature invalid
        Provider-->>DodoAPI: PayKitError (signature failure)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 Hops and cheers, a payment gateway so fine,
DodoPayments dancing in perfect align,
Subscriptions and webhooks, customers galore,
PayKit enriched with features and more! 🎉

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding a dodopayments provider to the paykit library.
Linked Issues check ✅ Passed The pull request implements the dodopayments provider feature referenced in issue #104, including provider configuration, customer management, subscription handling, and webhook support.
Out of Scope Changes check ✅ Passed All changes are directly related to adding the dodopayments provider: package configuration, provider implementation, exports, and build configuration.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (1)
packages/dodopayments/src/dodopayments-provider.ts (1)

3-9: Split paykitjs value imports from type-only imports.

This import mixes runtime and type-only symbols. Please separate it to match the repo TS rules.

♻️ Suggested import split
-import {
-  PAYKIT_ERROR_CODES,
-  PayKitError,
-  type NormalizedWebhookEvent,
-  type PayKitProviderConfig,
-  type PaymentProvider,
-} from "paykitjs";
+import { PAYKIT_ERROR_CODES, PayKitError } from "paykitjs";
+import type {
+  NormalizedWebhookEvent,
+  PayKitProviderConfig,
+  PaymentProvider,
+} from "paykitjs";

As per coding guidelines "Use import type with separated style and Node.js protocol (node:fs, node:path) for built-in modules".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/dodopayments/src/dodopayments-provider.ts` around lines 3 - 9, Split
the mixed runtime and type-only imports from "paykitjs": import the runtime
values PAYKIT_ERROR_CODES and PayKitError with a normal import (e.g., import {
PAYKIT_ERROR_CODES, PayKitError } from "paykitjs";) and import the types
NormalizedWebhookEvent, PayKitProviderConfig, and PaymentProvider using an
import type (e.g., import type { NormalizedWebhookEvent, PayKitProviderConfig,
PaymentProvider } from "paykitjs";) so runtime symbols and type-only symbols are
separated per the repo TS rules.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/dodopayments/src/dodopayments-provider.ts`:
- Around line 113-117: The code uses non-null assertions on
providerProduct.productId (e.g., in createSubscriptionCheckout and
scheduleSubscriptionChange) which can cause opaque runtime failures; replace the
exclamation marks with explicit validation: check that data.providerProduct and
data.providerProduct.productId are present (and non-empty) before using them,
and if missing throw or return a clear provider error (with a descriptive
message and appropriate error type/return shape) instead of proceeding; apply
the same validation and error handling for the other occurrences referenced (the
product_cart construction and the lines around scheduleSubscriptionChange) so no
code assumes productId exists.
- Around line 277-279: The syncProducts method currently returns an empty
success ({ results: [] }) which silently hides unsupported behavior and leads to
unmapped products; update async syncProducts(data) in dodopayments-provider.ts
to either delegate to a standardized fallback (e.g., call a
notSupported('syncProducts') helper or throw a clear NotSupportedError) or
implement a minimal product sync that returns mapped results with
providerProduct identifiers; ensure the returned shape matches the caller's
expectations (include errors or a non-empty results array with providerProduct)
so downstream code can handle unsupported providers explicitly.
- Around line 282-320: The switch in handleWebhook currently falls through so
"subscription.active" and "subscription.renewed" hit default and return [],
dropping all events; update the switch in handleWebhook to handle those cases
explicitly (e.g., create and return the appropriate NormalizedWebhookEvent array
for subscription activation/renewal) instead of falling through to default —
locate the switch on the local variable event in handleWebhook and implement
case "subscription.active" and case "subscription.renewed" to build/return the
normalized payload (or call an existing normalizer helper) with relevant fields
(subscription id, customer id, status/timestamps) so webhook-driven subscription
state is processed.
- Around line 89-94: updateCustomer currently overwrites provider metadata with
data.metadata ?? {} and can drop the stored paykitCustomerId used by check();
before calling client.customers.update(data.providerCustomerId, ...), fetch the
existing customer (e.g., via client.customers.retrieve(data.providerCustomerId))
read its metadata.paykitCustomerId, and merge it into the metadata you send so
the resulting metadata is something like {...existing.metadata,
...data.metadata} (ensuring paykitCustomerId from existing.metadata is preserved
if present) when calling updateCustomer.
- Around line 282-304: The DodoPayments client is not given the webhook secret
so signature verification in handleWebhook (which calls
client.webhooks.unwrap(...)) will fail; update the DodoPayments constructor
usage to accept and forward the webhookSecret from DodopaymentsOptions into the
underlying client setup and ensure that when calling client.webhooks.unwrap in
handleWebhook you pass the webhookKey/webhookSecret parameter (use the same
secret field name expected by the client) so the HMAC SHA256 verification can
succeed; locate the DodoPayments constructor and the handleWebhook method to add
the missing option and pass it through to the client and unwrap call.

---

Nitpick comments:
In `@packages/dodopayments/src/dodopayments-provider.ts`:
- Around line 3-9: Split the mixed runtime and type-only imports from
"paykitjs": import the runtime values PAYKIT_ERROR_CODES and PayKitError with a
normal import (e.g., import { PAYKIT_ERROR_CODES, PayKitError } from
"paykitjs";) and import the types NormalizedWebhookEvent, PayKitProviderConfig,
and PaymentProvider using an import type (e.g., import type {
NormalizedWebhookEvent, PayKitProviderConfig, PaymentProvider } from
"paykitjs";) so runtime symbols and type-only symbols are separated per the repo
TS rules.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a6b47ed4-4d14-4006-9e54-62324a17b8fa

📥 Commits

Reviewing files that changed from the base of the PR and between b04e0ad and a2b0a94.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • packages/dodopayments/package.json
  • packages/dodopayments/src/dodopayments-provider.ts
  • packages/dodopayments/src/index.ts
  • packages/dodopayments/tsdown.config.ts

Comment on lines +89 to +94
async updateCustomer(data) {
await client.customers.update(data.providerCustomerId, {
email: data.email,
name: data.name,
metadata: data.metadata ?? {},
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate and read the dodopayments-provider.ts file
find . -name "dodopayments-provider.ts" -type f

Repository: getpaykit/paykit

Length of output: 113


🏁 Script executed:

# Get the file structure to understand the context
fd -t f "dodopayments" | head -20

Repository: getpaykit/paykit

Length of output: 111


🏁 Script executed:

# Search for the file more broadly
find . -path "*dodopayments*" -name "*.ts" -type f

Repository: getpaykit/paykit

Length of output: 191


🏁 Script executed:

cat -n ./packages/dodopayments/src/dodopayments-provider.ts

Repository: getpaykit/paykit

Length of output: 13970


Preserve paykitCustomerId in customer updates.

updateCustomer() sends metadata: data.metadata ?? {}, which will overwrite the existing metadata and lose the paykitCustomerId stored during customer creation. This breaks the reconciliation logic in check() (line 337) that relies on reading this mapping. Update the metadata to preserve the existing paykitCustomerId, similar to how the conflict-resolution code handles it at lines 78-81.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/dodopayments/src/dodopayments-provider.ts` around lines 89 - 94,
updateCustomer currently overwrites provider metadata with data.metadata ?? {}
and can drop the stored paykitCustomerId used by check(); before calling
client.customers.update(data.providerCustomerId, ...), fetch the existing
customer (e.g., via client.customers.retrieve(data.providerCustomerId)) read its
metadata.paykitCustomerId, and merge it into the metadata you send so the
resulting metadata is something like {...existing.metadata, ...data.metadata}
(ensuring paykitCustomerId from existing.metadata is preserved if present) when
calling updateCustomer.

Comment on lines +113 to +117
async createSubscriptionCheckout(data) {
const checkout = await client.checkoutSessions.create({
// Our core type do not allow product quantity, but dodo requires it
// Need to decide wether to change the core type or default product to 1
product_cart: [{ product_id: data.providerProduct.productId!, quantity: 1 }],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate providerProduct.productId instead of asserting it.

These paths all rely on productId!, but the adapter contract only gives you Record<string, string>, and scheduleSubscriptionChange() even permits providerProduct?: ... | null. Bad or missing mappings will surface as opaque runtime failures instead of a provider error.

Also applies to: 140-141, 176-177

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/dodopayments/src/dodopayments-provider.ts` around lines 113 - 117,
The code uses non-null assertions on providerProduct.productId (e.g., in
createSubscriptionCheckout and scheduleSubscriptionChange) which can cause
opaque runtime failures; replace the exclamation marks with explicit validation:
check that data.providerProduct and data.providerProduct.productId are present
(and non-empty) before using them, and if missing throw or return a clear
provider error (with a descriptive message and appropriate error type/return
shape) instead of proceeding; apply the same validation and error handling for
the other occurrences referenced (the product_cart construction and the lines
around scheduleSubscriptionChange) so no code assumes productId exists.

Comment on lines +277 to +279
async syncProducts(data) {
return { results: [] };
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't silently noop syncProducts().

Returning { results: [] } makes sync look successful while leaving every product unmapped. Downstream code will only discover the problem later when providerProduct is missing.

Do you want me to sketch a minimal notSupported(...) fallback or a first-pass Dodo product sync implementation?

🧰 Tools
🪛 GitHub Check: lint

[warning] 277-277: eslint(no-unused-vars)
Parameter 'data' is declared but never used. Unused parameters should start with a '_'.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/dodopayments/src/dodopayments-provider.ts` around lines 277 - 279,
The syncProducts method currently returns an empty success ({ results: [] })
which silently hides unsupported behavior and leads to unmapped products; update
async syncProducts(data) in dodopayments-provider.ts to either delegate to a
standardized fallback (e.g., call a notSupported('syncProducts') helper or throw
a clear NotSupportedError) or implement a minimal product sync that returns
mapped results with providerProduct identifiers; ensure the returned shape
matches the caller's expectations (include errors or a non-empty results array
with providerProduct) so downstream code can handle unsupported providers
explicitly.

Comment on lines +282 to +320
async handleWebhook(data): Promise<NormalizedWebhookEvent[]> {
const headers = data.headers;

const getHeader = (name: string) =>
Object.keys(headers).find((k) => k.toLowerCase() === name.toLowerCase())
? headers[Object.keys(headers).find((k) => k.toLowerCase() === name.toLowerCase())!]
: undefined;

const webhookId = getHeader("webhook-id") ?? "";
const webhookSignature = getHeader("webhook-signature");
const webhookTimestamp = getHeader("webhook-timestamp");

let event: WebhookEventType;

const webhookHeaders = {
"webhook-id": webhookId as string,
"webhook-signature": webhookSignature as string,
"webhook-timestamp": webhookTimestamp as string,
};

try {
const wh = client.webhooks.unwrap(data.body.toString(), { headers: webhookHeaders });
event = wh.type;
} catch (error) {
// HANDLE OTHER ERRORS HERE
throw PayKitError.from(
"BAD_REQUEST",
PAYKIT_ERROR_CODES.PROVIDER_SIGNATURE_MISSING,
"Invalid DodoPayments webhook signature",
);
}

switch (event) {
case "subscription.active":
case "subscription.renewed":

default:
return [];
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

handleWebhook() currently drops every event.

"subscription.active" and "subscription.renewed" both fall through to default, so this always returns []. That means webhook-driven subscription state never gets normalized or synced.

🧰 Tools
🪛 GitHub Check: lint

[warning] 305-305: eslint(no-unused-vars)
Catch parameter 'error' is caught but never used.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/dodopayments/src/dodopayments-provider.ts` around lines 282 - 320,
The switch in handleWebhook currently falls through so "subscription.active" and
"subscription.renewed" hit default and return [], dropping all events; update
the switch in handleWebhook to handle those cases explicitly (e.g., create and
return the appropriate NormalizedWebhookEvent array for subscription
activation/renewal) instead of falling through to default — locate the switch on
the local variable event in handleWebhook and implement case
"subscription.active" and case "subscription.renewed" to build/return the
normalized payload (or call an existing normalizer helper) with relevant fields
(subscription id, customer id, status/timestamps) so webhook-driven subscription
state is processed.

Comment on lines +282 to +304
async handleWebhook(data): Promise<NormalizedWebhookEvent[]> {
const headers = data.headers;

const getHeader = (name: string) =>
Object.keys(headers).find((k) => k.toLowerCase() === name.toLowerCase())
? headers[Object.keys(headers).find((k) => k.toLowerCase() === name.toLowerCase())!]
: undefined;

const webhookId = getHeader("webhook-id") ?? "";
const webhookSignature = getHeader("webhook-signature");
const webhookTimestamp = getHeader("webhook-timestamp");

let event: WebhookEventType;

const webhookHeaders = {
"webhook-id": webhookId as string,
"webhook-signature": webhookSignature as string,
"webhook-timestamp": webhookTimestamp as string,
};

try {
const wh = client.webhooks.unwrap(data.body.toString(), { headers: webhookHeaders });
event = wh.type;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

find . -name "dodopayments-provider.ts" -type f

Repository: getpaykit/paykit

Length of output: 113


🏁 Script executed:

wc -l packages/dodopayments/src/dodopayments-provider.ts

Repository: getpaykit/paykit

Length of output: 115


🏁 Script executed:

cat -n packages/dodopayments/src/dodopayments-provider.ts | head -100

Repository: getpaykit/paykit

Length of output: 3470


🏁 Script executed:

cat -n packages/dodopayments/src/dodopayments-provider.ts | sed -n '280,310p'

Repository: getpaykit/paykit

Length of output: 1409


🏁 Script executed:

cat -n packages/dodopayments/src/dodopayments-provider.ts | sed -n '360,375p'

Repository: getpaykit/paykit

Length of output: 611


🏁 Script executed:

cat -n packages/dodopayments/package.json | grep -A 5 -B 5 dodopayments

Repository: getpaykit/paykit

Length of output: 634


🏁 Script executed:

find . -path "*/node_modules/dodopayments" -name "*.d.ts" -o -name "*.ts" 2>/dev/null | head -20

Repository: getpaykit/paykit

Length of output: 847


🏁 Script executed:

rg "webhookKey|webhook-key" --type ts --type tsx -i

Repository: getpaykit/paykit

Length of output: 88


🏁 Script executed:

find . -name "*test*" -o -name "*spec*" | grep -i dodo | head -20

Repository: getpaykit/paykit

Length of output: 42


🏁 Script executed:

rg "webhookKey|webhook-key" -i

Repository: getpaykit/paykit

Length of output: 42


🏁 Script executed:

rg "DodoPayments" -A 5 | head -50

Repository: getpaykit/paykit

Length of output: 2396


🏁 Script executed:

find . -name "*.test.*" -o -name "*.spec.*" | grep -i dodo

Repository: getpaykit/paykit

Length of output: 42


🏁 Script executed:

cat packages/dodopayments/src/dodopayments-provider.ts | sed -n '1,50p'

Repository: getpaykit/paykit

Length of output: 1462


🌐 Web query:

DodoPayments SDK webhookKey webhook configuration initialization

💡 Result:

To configure and initialize the DodoPayments SDK with the webhookKey for webhook handling: 1. Obtain the webhook secret key from the Dodo Payments dashboard settings page. Keep it secure and use environment variables (e.g., DODO_PAYMENTS_WEBHOOK_KEY). 2. Initialize the SDK client by passing the webhookKey in the constructor options, along with bearerToken and environment. Examples from official docs: TypeScript/Node.js: import DodoPayments from 'dodopayments'; const client = new DodoPayments({ bearerToken: process.env.DODO_PAYMENTS_API_KEY, environment: process.env.DODO_PAYMENTS_ENVIRONMENT, webhookKey: process.env.DODO_PAYMENTS_WEBHOOK_KEY, }); Python: from dodopayments import DodoPayments import os client = DodoPayments( bearer_token=os.getenv("DODO_PAYMENTS_API_KEY"), environment=os.getenv("DODO_PAYMENTS_ENVIRONMENT"), webhook_key=os.getenv("DODO_PAYMENTS_WEBHOOK_KEY"), ) Go: client := dodopayments.NewClient( option.WithBearerToken(os.Getenv("DODO_PAYMENTS_API_KEY")), option.WithEnvironment(os.Getenv("DODO_PAYMENTS_ENVIRONMENT")), option.WithWebhookKey(os.Getenv("DODO_PAYMENTS_WEBHOOK_KEY")), ) 3. Handle incoming webhooks by using client.webhooks.unwrap to verify the signature with the required headers (webhook-id, webhook-signature, webhook-timestamp) and raw body. Example (TypeScript): app.post('/webhook', async (req, res) => { try { const unwrapped = client.webhooks.unwrap(req.body.toString, { headers: { 'webhook-id': req.headers['webhook-id'] as string, 'webhook-signature': req.headers['webhook-signature'] as string, 'webhook-timestamp': req.headers['webhook-timestamp'] as string, }, }); // Process unwrapped event res.json({ received: true }); } catch (error) { res.status(401).json({ error: 'Invalid signature' }); } }); The webhookKey enables built-in signature verification following the Standard Webhooks spec (HMAC SHA256 of webhook-id.webhook-timestamp.payload using the secret).

Citations:


🌐 Web query:

DodoPayments "webhooks.unwrap" signature verification

💡 Result:

DodoPayments webhooks use HMAC SHA256 signatures for verification, following the Standard Webhooks specification. Each webhook includes headers: webhook-id, webhook-timestamp, and webhook-signature. Official SDKs provide client.webhooks.unwrap(rawBody, { headers }) to verify the signature using your webhook secret key (set via DODO_PAYMENTS_WEBHOOK_KEY) and parse the payload. If verification fails, it throws an error. Example (Node.js): app.post('/webhook', async (req, res) => { try { const unwrapped = client.webhooks.unwrap(req.body.toString, { headers: { 'webhook-id': req.headers['webhook-id'], 'webhook-signature': req.headers['webhook-signature'], 'webhook-timestamp': req.headers['webhook-timestamp'], }, }); // Process unwrapped event res.json({ received: true }); } catch (error) { res.status(401).json({ error: 'Invalid signature' }); } }); Similar examples exist for Python, Go, etc. Manual verification: Concatenate webhook-id.webhook-timestamp.payload (raw string), compute HMAC SHA256 with secret, compare to webhook-signature using constant-time comparison. Use unsafe_unwrap only for testing without signatures. Get secret via Dashboard or API.

Citations:


🏁 Script executed:

rg "new DodoPayments" -A 10

Repository: getpaykit/paykit

Length of output: 777


🏁 Script executed:

cat packages/dodopayments/src/dodopayments-provider.ts | sed -n '300,320p'

Repository: getpaykit/paykit

Length of output: 590


Pass the webhook secret into the Dodo client.

handleWebhook() calls client.webhooks.unwrap(...), which requires the webhookKey parameter to verify signatures using HMAC SHA256. The DodoPayments constructor at lines 365-368 only accepts bearerToken and environment—the webhookSecret from DodopaymentsOptions is never passed. Signature validation will fail without this configuration.

Fix
     createAdapter(): PaymentProvider {
       const client = new DodoPayments({
         bearerToken: dodopaymentsOptions.bearerToken,
         environment: dodopaymentsOptions.environment ?? "live_mode",
+        webhookKey: dodopaymentsOptions.webhookSecret,
       });
 
       return createDodopaymentsProvider(client, dodopaymentsOptions);
     },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/dodopayments/src/dodopayments-provider.ts` around lines 282 - 304,
The DodoPayments client is not given the webhook secret so signature
verification in handleWebhook (which calls client.webhooks.unwrap(...)) will
fail; update the DodoPayments constructor usage to accept and forward the
webhookSecret from DodopaymentsOptions into the underlying client setup and
ensure that when calling client.webhooks.unwrap in handleWebhook you pass the
webhookKey/webhookSecret parameter (use the same secret field name expected by
the client) so the HMAC SHA256 verification can succeed; locate the DodoPayments
constructor and the handleWebhook method to add the missing option and pass it
through to the client and unwrap call.

@ayushsharma74
ayushsharma74 force-pushed the feat/dodopayments-provider branch from 00d682a to 79b2a78 Compare April 30, 2026 20:47
@vercel

vercel Bot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
paykit Skipped Skipped Apr 30, 2026 8:47pm

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(pay): Dodo Payments Provider

1 participant