diff --git a/.changeset/cyan-poems-boil.md b/.changeset/cyan-poems-boil.md
new file mode 100644
index 0000000..ef19d7e
--- /dev/null
+++ b/.changeset/cyan-poems-boil.md
@@ -0,0 +1,8 @@
+---
+"@paymesh/dodo": minor
+"@paymesh/cli": patch
+---
+
+Ship the first-party `@paymesh/dodo` provider with catalog-driven hosted checkout, customer upsert/get, catalog sync, dashboard sync helpers, and verified webhook normalization for Dodo Payments.
+
+Update `@paymesh/cli` provider selection so new setups can choose Dodo directly.
diff --git a/README.md b/README.md
index d240fca..4a70e42 100644
--- a/README.md
+++ b/README.md
@@ -82,7 +82,7 @@ npm install paymesh @paymesh/stripe @paymesh/prisma @prisma/client
```
- Available providers currently include @paymesh/stripe and @paymesh/polar.
+ Available providers currently include @paymesh/stripe, @paymesh/polar, @paymesh/abacatepay, and @paymesh/dodo.
@@ -99,6 +99,10 @@ npm install paymesh @paymesh/stripe @paymesh/prisma @prisma/client
Native PIX flows live under paymesh.pix. Use paymesh.payments for generic checkout sessions and paymesh.pix when you need QR code, copia-e-cola, and expiration data as first-class fields.
+
+ @paymesh/dodo supports hosted BRL checkout links that can present Pix inside the Dodo checkout page, but it intentionally does not expose paymesh.pix because Dodo does not currently document a native QR-code-first backend PIX flow.
+
+
Database packages: @paymesh/postgres, @paymesh/drizzle, and @paymesh/prisma. Operational tooling: @paymesh/cli.
diff --git a/apps/web/.source/browser.ts b/apps/web/.source/browser.ts
index 7cf14f5..ecc2455 100644
--- a/apps/web/.source/browser.ts
+++ b/apps/web/.source/browser.ts
@@ -7,6 +7,6 @@ const create = browser();
const browserCollections = {
- docs: create.doc("docs", {"basic-usage.mdx": () => import("../content/docs/basic-usage.mdx?collection=docs"), "comparison.mdx": () => import("../content/docs/comparison.mdx?collection=docs"), "installation.mdx": () => import("../content/docs/installation.mdx?collection=docs"), "introduction.mdx": () => import("../content/docs/introduction.mdx?collection=docs"), "mcp.mdx": () => import("../content/docs/mcp.mdx?collection=docs"), "adapters/elysia.mdx": () => import("../content/docs/adapters/elysia.mdx?collection=docs"), "adapters/express.mdx": () => import("../content/docs/adapters/express.mdx?collection=docs"), "adapters/fastify.mdx": () => import("../content/docs/adapters/fastify.mdx?collection=docs"), "adapters/hono.mdx": () => import("../content/docs/adapters/hono.mdx?collection=docs"), "adapters/next.mdx": () => import("../content/docs/adapters/next.mdx?collection=docs"), "ai-resources/skills.mdx": () => import("../content/docs/ai-resources/skills.mdx?collection=docs"), "database/drizzle.mdx": () => import("../content/docs/database/drizzle.mdx?collection=docs"), "database/overview.mdx": () => import("../content/docs/database/overview.mdx?collection=docs"), "database/postgres.mdx": () => import("../content/docs/database/postgres.mdx?collection=docs"), "database/prisma.mdx": () => import("../content/docs/database/prisma.mdx?collection=docs"), "concepts/api.mdx": () => import("../content/docs/concepts/api.mdx?collection=docs"), "concepts/client.mdx": () => import("../content/docs/concepts/client.mdx?collection=docs"), "concepts/customers.mdx": () => import("../content/docs/concepts/customers.mdx?collection=docs"), "concepts/hooks.mdx": () => import("../content/docs/concepts/hooks.mdx?collection=docs"), "concepts/payment-providers.mdx": () => import("../content/docs/concepts/payment-providers.mdx?collection=docs"), "concepts/pix.mdx": () => import("../content/docs/concepts/pix.mdx?collection=docs"), "concepts/sandbox.mdx": () => import("../content/docs/concepts/sandbox.mdx?collection=docs"), "concepts/typescript.mdx": () => import("../content/docs/concepts/typescript.mdx?collection=docs"), "guides/create-a-database-adapter.mdx": () => import("../content/docs/guides/create-a-database-adapter.mdx?collection=docs"), "guides/create-a-provider.mdx": () => import("../content/docs/guides/create-a-provider.mdx?collection=docs"), "guides/create-your-first-plugin.mdx": () => import("../content/docs/guides/create-your-first-plugin.mdx?collection=docs"), "guides/database.mdx": () => import("../content/docs/guides/database.mdx?collection=docs"), "guides/optimize-performance.mdx": () => import("../content/docs/guides/optimize-performance.mdx?collection=docs"), "guides/plugins.mdx": () => import("../content/docs/guides/plugins.mdx?collection=docs"), "guides/provider-selection.mdx": () => import("../content/docs/guides/provider-selection.mdx?collection=docs"), "guides/testing.mdx": () => import("../content/docs/guides/testing.mdx?collection=docs"), "guides/webhooks-at-scale.mdx": () => import("../content/docs/guides/webhooks-at-scale.mdx?collection=docs"), "guides/webhooks.mdx": () => import("../content/docs/guides/webhooks.mdx?collection=docs"), "plugins/audit-logs.mdx": () => import("../content/docs/plugins/audit-logs.mdx?collection=docs"), "plugins/dash.mdx": () => import("../content/docs/plugins/dash.mdx?collection=docs"), "plugins/overview.mdx": () => import("../content/docs/plugins/overview.mdx?collection=docs"), "providers/abacatepay.mdx": () => import("../content/docs/providers/abacatepay.mdx?collection=docs"), "providers/polar.mdx": () => import("../content/docs/providers/polar.mdx?collection=docs"), "providers/stripe.mdx": () => import("../content/docs/providers/stripe.mdx?collection=docs"), "reference/cli.mdx": () => import("../content/docs/reference/cli.mdx?collection=docs"), "reference/client-options.mdx": () => import("../content/docs/reference/client-options.mdx?collection=docs"), "reference/database-schema.mdx": () => import("../content/docs/reference/database-schema.mdx?collection=docs"), "reference/errors.mdx": () => import("../content/docs/reference/errors.mdx?collection=docs"), "reference/plugin-api.mdx": () => import("../content/docs/reference/plugin-api.mdx?collection=docs"), }),
+ docs: create.doc("docs", {"basic-usage.mdx": () => import("../content/docs/basic-usage.mdx?collection=docs"), "comparison.mdx": () => import("../content/docs/comparison.mdx?collection=docs"), "installation.mdx": () => import("../content/docs/installation.mdx?collection=docs"), "introduction.mdx": () => import("../content/docs/introduction.mdx?collection=docs"), "mcp.mdx": () => import("../content/docs/mcp.mdx?collection=docs"), "adapters/elysia.mdx": () => import("../content/docs/adapters/elysia.mdx?collection=docs"), "adapters/express.mdx": () => import("../content/docs/adapters/express.mdx?collection=docs"), "adapters/fastify.mdx": () => import("../content/docs/adapters/fastify.mdx?collection=docs"), "adapters/hono.mdx": () => import("../content/docs/adapters/hono.mdx?collection=docs"), "adapters/next.mdx": () => import("../content/docs/adapters/next.mdx?collection=docs"), "ai-resources/skills.mdx": () => import("../content/docs/ai-resources/skills.mdx?collection=docs"), "concepts/api.mdx": () => import("../content/docs/concepts/api.mdx?collection=docs"), "concepts/client.mdx": () => import("../content/docs/concepts/client.mdx?collection=docs"), "concepts/customers.mdx": () => import("../content/docs/concepts/customers.mdx?collection=docs"), "concepts/hooks.mdx": () => import("../content/docs/concepts/hooks.mdx?collection=docs"), "concepts/payment-providers.mdx": () => import("../content/docs/concepts/payment-providers.mdx?collection=docs"), "concepts/pix.mdx": () => import("../content/docs/concepts/pix.mdx?collection=docs"), "concepts/sandbox.mdx": () => import("../content/docs/concepts/sandbox.mdx?collection=docs"), "concepts/typescript.mdx": () => import("../content/docs/concepts/typescript.mdx?collection=docs"), "database/drizzle.mdx": () => import("../content/docs/database/drizzle.mdx?collection=docs"), "database/memory.mdx": () => import("../content/docs/database/memory.mdx?collection=docs"), "database/overview.mdx": () => import("../content/docs/database/overview.mdx?collection=docs"), "database/postgres.mdx": () => import("../content/docs/database/postgres.mdx?collection=docs"), "database/prisma.mdx": () => import("../content/docs/database/prisma.mdx?collection=docs"), "guides/create-a-database-adapter.mdx": () => import("../content/docs/guides/create-a-database-adapter.mdx?collection=docs"), "guides/create-a-provider.mdx": () => import("../content/docs/guides/create-a-provider.mdx?collection=docs"), "guides/create-your-first-plugin.mdx": () => import("../content/docs/guides/create-your-first-plugin.mdx?collection=docs"), "guides/database.mdx": () => import("../content/docs/guides/database.mdx?collection=docs"), "guides/optimize-performance.mdx": () => import("../content/docs/guides/optimize-performance.mdx?collection=docs"), "guides/plugins.mdx": () => import("../content/docs/guides/plugins.mdx?collection=docs"), "guides/provider-selection.mdx": () => import("../content/docs/guides/provider-selection.mdx?collection=docs"), "guides/testing.mdx": () => import("../content/docs/guides/testing.mdx?collection=docs"), "guides/webhooks-at-scale.mdx": () => import("../content/docs/guides/webhooks-at-scale.mdx?collection=docs"), "guides/webhooks.mdx": () => import("../content/docs/guides/webhooks.mdx?collection=docs"), "providers/abacatepay.mdx": () => import("../content/docs/providers/abacatepay.mdx?collection=docs"), "providers/dodo.mdx": () => import("../content/docs/providers/dodo.mdx?collection=docs"), "providers/polar.mdx": () => import("../content/docs/providers/polar.mdx?collection=docs"), "providers/stripe.mdx": () => import("../content/docs/providers/stripe.mdx?collection=docs"), "plugins/audit-logs.mdx": () => import("../content/docs/plugins/audit-logs.mdx?collection=docs"), "plugins/dash.mdx": () => import("../content/docs/plugins/dash.mdx?collection=docs"), "plugins/overview.mdx": () => import("../content/docs/plugins/overview.mdx?collection=docs"), "reference/cli.mdx": () => import("../content/docs/reference/cli.mdx?collection=docs"), "reference/client-options.mdx": () => import("../content/docs/reference/client-options.mdx?collection=docs"), "reference/database-schema.mdx": () => import("../content/docs/reference/database-schema.mdx?collection=docs"), "reference/errors.mdx": () => import("../content/docs/reference/errors.mdx?collection=docs"), "reference/plugin-api.mdx": () => import("../content/docs/reference/plugin-api.mdx?collection=docs"), }),
};
export default browserCollections;
\ No newline at end of file
diff --git a/apps/web/.source/server.ts b/apps/web/.source/server.ts
index 481ba5c..a5e160a 100644
--- a/apps/web/.source/server.ts
+++ b/apps/web/.source/server.ts
@@ -1,37 +1,39 @@
// @ts-nocheck
-import { frontmatter as __fd_glob_43 } from "../content/docs/reference/plugin-api.mdx?collection=docs&only=frontmatter"
-import { frontmatter as __fd_glob_42 } from "../content/docs/reference/errors.mdx?collection=docs&only=frontmatter"
-import { frontmatter as __fd_glob_41 } from "../content/docs/reference/database-schema.mdx?collection=docs&only=frontmatter"
-import { frontmatter as __fd_glob_40 } from "../content/docs/reference/client-options.mdx?collection=docs&only=frontmatter"
-import { frontmatter as __fd_glob_39 } from "../content/docs/reference/cli.mdx?collection=docs&only=frontmatter"
-import { frontmatter as __fd_glob_38 } from "../content/docs/providers/stripe.mdx?collection=docs&only=frontmatter"
-import { frontmatter as __fd_glob_37 } from "../content/docs/providers/polar.mdx?collection=docs&only=frontmatter"
-import { frontmatter as __fd_glob_36 } from "../content/docs/providers/abacatepay.mdx?collection=docs&only=frontmatter"
-import { frontmatter as __fd_glob_35 } from "../content/docs/plugins/overview.mdx?collection=docs&only=frontmatter"
-import { frontmatter as __fd_glob_34 } from "../content/docs/plugins/dash.mdx?collection=docs&only=frontmatter"
-import { frontmatter as __fd_glob_33 } from "../content/docs/plugins/audit-logs.mdx?collection=docs&only=frontmatter"
-import { frontmatter as __fd_glob_32 } from "../content/docs/guides/webhooks.mdx?collection=docs&only=frontmatter"
-import { frontmatter as __fd_glob_31 } from "../content/docs/guides/webhooks-at-scale.mdx?collection=docs&only=frontmatter"
-import { frontmatter as __fd_glob_30 } from "../content/docs/guides/testing.mdx?collection=docs&only=frontmatter"
-import { frontmatter as __fd_glob_29 } from "../content/docs/guides/provider-selection.mdx?collection=docs&only=frontmatter"
-import { frontmatter as __fd_glob_28 } from "../content/docs/guides/plugins.mdx?collection=docs&only=frontmatter"
-import { frontmatter as __fd_glob_27 } from "../content/docs/guides/optimize-performance.mdx?collection=docs&only=frontmatter"
-import { frontmatter as __fd_glob_26 } from "../content/docs/guides/database.mdx?collection=docs&only=frontmatter"
-import { frontmatter as __fd_glob_25 } from "../content/docs/guides/create-your-first-plugin.mdx?collection=docs&only=frontmatter"
-import { frontmatter as __fd_glob_24 } from "../content/docs/guides/create-a-provider.mdx?collection=docs&only=frontmatter"
-import { frontmatter as __fd_glob_23 } from "../content/docs/guides/create-a-database-adapter.mdx?collection=docs&only=frontmatter"
-import { frontmatter as __fd_glob_22 } from "../content/docs/concepts/typescript.mdx?collection=docs&only=frontmatter"
-import { frontmatter as __fd_glob_21 } from "../content/docs/concepts/sandbox.mdx?collection=docs&only=frontmatter"
-import { frontmatter as __fd_glob_20 } from "../content/docs/concepts/pix.mdx?collection=docs&only=frontmatter"
-import { frontmatter as __fd_glob_19 } from "../content/docs/concepts/payment-providers.mdx?collection=docs&only=frontmatter"
-import { frontmatter as __fd_glob_18 } from "../content/docs/concepts/hooks.mdx?collection=docs&only=frontmatter"
-import { frontmatter as __fd_glob_17 } from "../content/docs/concepts/customers.mdx?collection=docs&only=frontmatter"
-import { frontmatter as __fd_glob_16 } from "../content/docs/concepts/client.mdx?collection=docs&only=frontmatter"
-import { frontmatter as __fd_glob_15 } from "../content/docs/concepts/api.mdx?collection=docs&only=frontmatter"
-import { frontmatter as __fd_glob_14 } from "../content/docs/database/prisma.mdx?collection=docs&only=frontmatter"
-import { frontmatter as __fd_glob_13 } from "../content/docs/database/postgres.mdx?collection=docs&only=frontmatter"
-import { frontmatter as __fd_glob_12 } from "../content/docs/database/overview.mdx?collection=docs&only=frontmatter"
-import { frontmatter as __fd_glob_11 } from "../content/docs/database/drizzle.mdx?collection=docs&only=frontmatter"
+import { frontmatter as __fd_glob_45 } from "../content/docs/reference/plugin-api.mdx?collection=docs&only=frontmatter"
+import { frontmatter as __fd_glob_44 } from "../content/docs/reference/errors.mdx?collection=docs&only=frontmatter"
+import { frontmatter as __fd_glob_43 } from "../content/docs/reference/database-schema.mdx?collection=docs&only=frontmatter"
+import { frontmatter as __fd_glob_42 } from "../content/docs/reference/client-options.mdx?collection=docs&only=frontmatter"
+import { frontmatter as __fd_glob_41 } from "../content/docs/reference/cli.mdx?collection=docs&only=frontmatter"
+import { frontmatter as __fd_glob_40 } from "../content/docs/plugins/overview.mdx?collection=docs&only=frontmatter"
+import { frontmatter as __fd_glob_39 } from "../content/docs/plugins/dash.mdx?collection=docs&only=frontmatter"
+import { frontmatter as __fd_glob_38 } from "../content/docs/plugins/audit-logs.mdx?collection=docs&only=frontmatter"
+import { frontmatter as __fd_glob_37 } from "../content/docs/providers/stripe.mdx?collection=docs&only=frontmatter"
+import { frontmatter as __fd_glob_36 } from "../content/docs/providers/polar.mdx?collection=docs&only=frontmatter"
+import { frontmatter as __fd_glob_35 } from "../content/docs/providers/dodo.mdx?collection=docs&only=frontmatter"
+import { frontmatter as __fd_glob_34 } from "../content/docs/providers/abacatepay.mdx?collection=docs&only=frontmatter"
+import { frontmatter as __fd_glob_33 } from "../content/docs/guides/webhooks.mdx?collection=docs&only=frontmatter"
+import { frontmatter as __fd_glob_32 } from "../content/docs/guides/webhooks-at-scale.mdx?collection=docs&only=frontmatter"
+import { frontmatter as __fd_glob_31 } from "../content/docs/guides/testing.mdx?collection=docs&only=frontmatter"
+import { frontmatter as __fd_glob_30 } from "../content/docs/guides/provider-selection.mdx?collection=docs&only=frontmatter"
+import { frontmatter as __fd_glob_29 } from "../content/docs/guides/plugins.mdx?collection=docs&only=frontmatter"
+import { frontmatter as __fd_glob_28 } from "../content/docs/guides/optimize-performance.mdx?collection=docs&only=frontmatter"
+import { frontmatter as __fd_glob_27 } from "../content/docs/guides/database.mdx?collection=docs&only=frontmatter"
+import { frontmatter as __fd_glob_26 } from "../content/docs/guides/create-your-first-plugin.mdx?collection=docs&only=frontmatter"
+import { frontmatter as __fd_glob_25 } from "../content/docs/guides/create-a-provider.mdx?collection=docs&only=frontmatter"
+import { frontmatter as __fd_glob_24 } from "../content/docs/guides/create-a-database-adapter.mdx?collection=docs&only=frontmatter"
+import { frontmatter as __fd_glob_23 } from "../content/docs/database/prisma.mdx?collection=docs&only=frontmatter"
+import { frontmatter as __fd_glob_22 } from "../content/docs/database/postgres.mdx?collection=docs&only=frontmatter"
+import { frontmatter as __fd_glob_21 } from "../content/docs/database/overview.mdx?collection=docs&only=frontmatter"
+import { frontmatter as __fd_glob_20 } from "../content/docs/database/memory.mdx?collection=docs&only=frontmatter"
+import { frontmatter as __fd_glob_19 } from "../content/docs/database/drizzle.mdx?collection=docs&only=frontmatter"
+import { frontmatter as __fd_glob_18 } from "../content/docs/concepts/typescript.mdx?collection=docs&only=frontmatter"
+import { frontmatter as __fd_glob_17 } from "../content/docs/concepts/sandbox.mdx?collection=docs&only=frontmatter"
+import { frontmatter as __fd_glob_16 } from "../content/docs/concepts/pix.mdx?collection=docs&only=frontmatter"
+import { frontmatter as __fd_glob_15 } from "../content/docs/concepts/payment-providers.mdx?collection=docs&only=frontmatter"
+import { frontmatter as __fd_glob_14 } from "../content/docs/concepts/hooks.mdx?collection=docs&only=frontmatter"
+import { frontmatter as __fd_glob_13 } from "../content/docs/concepts/customers.mdx?collection=docs&only=frontmatter"
+import { frontmatter as __fd_glob_12 } from "../content/docs/concepts/client.mdx?collection=docs&only=frontmatter"
+import { frontmatter as __fd_glob_11 } from "../content/docs/concepts/api.mdx?collection=docs&only=frontmatter"
import { frontmatter as __fd_glob_10 } from "../content/docs/ai-resources/skills.mdx?collection=docs&only=frontmatter"
import { frontmatter as __fd_glob_9 } from "../content/docs/adapters/next.mdx?collection=docs&only=frontmatter"
import { frontmatter as __fd_glob_8 } from "../content/docs/adapters/hono.mdx?collection=docs&only=frontmatter"
@@ -51,4 +53,4 @@ const create = server({"doc":{"passthroughs":["extractedReferences"]}});
-export const docs = await create.docsLazy("docs", "content/docs", {}, {"basic-usage.mdx": __fd_glob_0, "comparison.mdx": __fd_glob_1, "installation.mdx": __fd_glob_2, "introduction.mdx": __fd_glob_3, "mcp.mdx": __fd_glob_4, "adapters/elysia.mdx": __fd_glob_5, "adapters/express.mdx": __fd_glob_6, "adapters/fastify.mdx": __fd_glob_7, "adapters/hono.mdx": __fd_glob_8, "adapters/next.mdx": __fd_glob_9, "ai-resources/skills.mdx": __fd_glob_10, "database/drizzle.mdx": __fd_glob_11, "database/overview.mdx": __fd_glob_12, "database/postgres.mdx": __fd_glob_13, "database/prisma.mdx": __fd_glob_14, "concepts/api.mdx": __fd_glob_15, "concepts/client.mdx": __fd_glob_16, "concepts/customers.mdx": __fd_glob_17, "concepts/hooks.mdx": __fd_glob_18, "concepts/payment-providers.mdx": __fd_glob_19, "concepts/pix.mdx": __fd_glob_20, "concepts/sandbox.mdx": __fd_glob_21, "concepts/typescript.mdx": __fd_glob_22, "guides/create-a-database-adapter.mdx": __fd_glob_23, "guides/create-a-provider.mdx": __fd_glob_24, "guides/create-your-first-plugin.mdx": __fd_glob_25, "guides/database.mdx": __fd_glob_26, "guides/optimize-performance.mdx": __fd_glob_27, "guides/plugins.mdx": __fd_glob_28, "guides/provider-selection.mdx": __fd_glob_29, "guides/testing.mdx": __fd_glob_30, "guides/webhooks-at-scale.mdx": __fd_glob_31, "guides/webhooks.mdx": __fd_glob_32, "plugins/audit-logs.mdx": __fd_glob_33, "plugins/dash.mdx": __fd_glob_34, "plugins/overview.mdx": __fd_glob_35, "providers/abacatepay.mdx": __fd_glob_36, "providers/polar.mdx": __fd_glob_37, "providers/stripe.mdx": __fd_glob_38, "reference/cli.mdx": __fd_glob_39, "reference/client-options.mdx": __fd_glob_40, "reference/database-schema.mdx": __fd_glob_41, "reference/errors.mdx": __fd_glob_42, "reference/plugin-api.mdx": __fd_glob_43, }, {"basic-usage.mdx": () => import("../content/docs/basic-usage.mdx?collection=docs"), "comparison.mdx": () => import("../content/docs/comparison.mdx?collection=docs"), "installation.mdx": () => import("../content/docs/installation.mdx?collection=docs"), "introduction.mdx": () => import("../content/docs/introduction.mdx?collection=docs"), "mcp.mdx": () => import("../content/docs/mcp.mdx?collection=docs"), "adapters/elysia.mdx": () => import("../content/docs/adapters/elysia.mdx?collection=docs"), "adapters/express.mdx": () => import("../content/docs/adapters/express.mdx?collection=docs"), "adapters/fastify.mdx": () => import("../content/docs/adapters/fastify.mdx?collection=docs"), "adapters/hono.mdx": () => import("../content/docs/adapters/hono.mdx?collection=docs"), "adapters/next.mdx": () => import("../content/docs/adapters/next.mdx?collection=docs"), "ai-resources/skills.mdx": () => import("../content/docs/ai-resources/skills.mdx?collection=docs"), "database/drizzle.mdx": () => import("../content/docs/database/drizzle.mdx?collection=docs"), "database/overview.mdx": () => import("../content/docs/database/overview.mdx?collection=docs"), "database/postgres.mdx": () => import("../content/docs/database/postgres.mdx?collection=docs"), "database/prisma.mdx": () => import("../content/docs/database/prisma.mdx?collection=docs"), "concepts/api.mdx": () => import("../content/docs/concepts/api.mdx?collection=docs"), "concepts/client.mdx": () => import("../content/docs/concepts/client.mdx?collection=docs"), "concepts/customers.mdx": () => import("../content/docs/concepts/customers.mdx?collection=docs"), "concepts/hooks.mdx": () => import("../content/docs/concepts/hooks.mdx?collection=docs"), "concepts/payment-providers.mdx": () => import("../content/docs/concepts/payment-providers.mdx?collection=docs"), "concepts/pix.mdx": () => import("../content/docs/concepts/pix.mdx?collection=docs"), "concepts/sandbox.mdx": () => import("../content/docs/concepts/sandbox.mdx?collection=docs"), "concepts/typescript.mdx": () => import("../content/docs/concepts/typescript.mdx?collection=docs"), "guides/create-a-database-adapter.mdx": () => import("../content/docs/guides/create-a-database-adapter.mdx?collection=docs"), "guides/create-a-provider.mdx": () => import("../content/docs/guides/create-a-provider.mdx?collection=docs"), "guides/create-your-first-plugin.mdx": () => import("../content/docs/guides/create-your-first-plugin.mdx?collection=docs"), "guides/database.mdx": () => import("../content/docs/guides/database.mdx?collection=docs"), "guides/optimize-performance.mdx": () => import("../content/docs/guides/optimize-performance.mdx?collection=docs"), "guides/plugins.mdx": () => import("../content/docs/guides/plugins.mdx?collection=docs"), "guides/provider-selection.mdx": () => import("../content/docs/guides/provider-selection.mdx?collection=docs"), "guides/testing.mdx": () => import("../content/docs/guides/testing.mdx?collection=docs"), "guides/webhooks-at-scale.mdx": () => import("../content/docs/guides/webhooks-at-scale.mdx?collection=docs"), "guides/webhooks.mdx": () => import("../content/docs/guides/webhooks.mdx?collection=docs"), "plugins/audit-logs.mdx": () => import("../content/docs/plugins/audit-logs.mdx?collection=docs"), "plugins/dash.mdx": () => import("../content/docs/plugins/dash.mdx?collection=docs"), "plugins/overview.mdx": () => import("../content/docs/plugins/overview.mdx?collection=docs"), "providers/abacatepay.mdx": () => import("../content/docs/providers/abacatepay.mdx?collection=docs"), "providers/polar.mdx": () => import("../content/docs/providers/polar.mdx?collection=docs"), "providers/stripe.mdx": () => import("../content/docs/providers/stripe.mdx?collection=docs"), "reference/cli.mdx": () => import("../content/docs/reference/cli.mdx?collection=docs"), "reference/client-options.mdx": () => import("../content/docs/reference/client-options.mdx?collection=docs"), "reference/database-schema.mdx": () => import("../content/docs/reference/database-schema.mdx?collection=docs"), "reference/errors.mdx": () => import("../content/docs/reference/errors.mdx?collection=docs"), "reference/plugin-api.mdx": () => import("../content/docs/reference/plugin-api.mdx?collection=docs"), });
\ No newline at end of file
+export const docs = await create.docsLazy("docs", "content/docs", {}, {"basic-usage.mdx": __fd_glob_0, "comparison.mdx": __fd_glob_1, "installation.mdx": __fd_glob_2, "introduction.mdx": __fd_glob_3, "mcp.mdx": __fd_glob_4, "adapters/elysia.mdx": __fd_glob_5, "adapters/express.mdx": __fd_glob_6, "adapters/fastify.mdx": __fd_glob_7, "adapters/hono.mdx": __fd_glob_8, "adapters/next.mdx": __fd_glob_9, "ai-resources/skills.mdx": __fd_glob_10, "concepts/api.mdx": __fd_glob_11, "concepts/client.mdx": __fd_glob_12, "concepts/customers.mdx": __fd_glob_13, "concepts/hooks.mdx": __fd_glob_14, "concepts/payment-providers.mdx": __fd_glob_15, "concepts/pix.mdx": __fd_glob_16, "concepts/sandbox.mdx": __fd_glob_17, "concepts/typescript.mdx": __fd_glob_18, "database/drizzle.mdx": __fd_glob_19, "database/memory.mdx": __fd_glob_20, "database/overview.mdx": __fd_glob_21, "database/postgres.mdx": __fd_glob_22, "database/prisma.mdx": __fd_glob_23, "guides/create-a-database-adapter.mdx": __fd_glob_24, "guides/create-a-provider.mdx": __fd_glob_25, "guides/create-your-first-plugin.mdx": __fd_glob_26, "guides/database.mdx": __fd_glob_27, "guides/optimize-performance.mdx": __fd_glob_28, "guides/plugins.mdx": __fd_glob_29, "guides/provider-selection.mdx": __fd_glob_30, "guides/testing.mdx": __fd_glob_31, "guides/webhooks-at-scale.mdx": __fd_glob_32, "guides/webhooks.mdx": __fd_glob_33, "providers/abacatepay.mdx": __fd_glob_34, "providers/dodo.mdx": __fd_glob_35, "providers/polar.mdx": __fd_glob_36, "providers/stripe.mdx": __fd_glob_37, "plugins/audit-logs.mdx": __fd_glob_38, "plugins/dash.mdx": __fd_glob_39, "plugins/overview.mdx": __fd_glob_40, "reference/cli.mdx": __fd_glob_41, "reference/client-options.mdx": __fd_glob_42, "reference/database-schema.mdx": __fd_glob_43, "reference/errors.mdx": __fd_glob_44, "reference/plugin-api.mdx": __fd_glob_45, }, {"basic-usage.mdx": () => import("../content/docs/basic-usage.mdx?collection=docs"), "comparison.mdx": () => import("../content/docs/comparison.mdx?collection=docs"), "installation.mdx": () => import("../content/docs/installation.mdx?collection=docs"), "introduction.mdx": () => import("../content/docs/introduction.mdx?collection=docs"), "mcp.mdx": () => import("../content/docs/mcp.mdx?collection=docs"), "adapters/elysia.mdx": () => import("../content/docs/adapters/elysia.mdx?collection=docs"), "adapters/express.mdx": () => import("../content/docs/adapters/express.mdx?collection=docs"), "adapters/fastify.mdx": () => import("../content/docs/adapters/fastify.mdx?collection=docs"), "adapters/hono.mdx": () => import("../content/docs/adapters/hono.mdx?collection=docs"), "adapters/next.mdx": () => import("../content/docs/adapters/next.mdx?collection=docs"), "ai-resources/skills.mdx": () => import("../content/docs/ai-resources/skills.mdx?collection=docs"), "concepts/api.mdx": () => import("../content/docs/concepts/api.mdx?collection=docs"), "concepts/client.mdx": () => import("../content/docs/concepts/client.mdx?collection=docs"), "concepts/customers.mdx": () => import("../content/docs/concepts/customers.mdx?collection=docs"), "concepts/hooks.mdx": () => import("../content/docs/concepts/hooks.mdx?collection=docs"), "concepts/payment-providers.mdx": () => import("../content/docs/concepts/payment-providers.mdx?collection=docs"), "concepts/pix.mdx": () => import("../content/docs/concepts/pix.mdx?collection=docs"), "concepts/sandbox.mdx": () => import("../content/docs/concepts/sandbox.mdx?collection=docs"), "concepts/typescript.mdx": () => import("../content/docs/concepts/typescript.mdx?collection=docs"), "database/drizzle.mdx": () => import("../content/docs/database/drizzle.mdx?collection=docs"), "database/memory.mdx": () => import("../content/docs/database/memory.mdx?collection=docs"), "database/overview.mdx": () => import("../content/docs/database/overview.mdx?collection=docs"), "database/postgres.mdx": () => import("../content/docs/database/postgres.mdx?collection=docs"), "database/prisma.mdx": () => import("../content/docs/database/prisma.mdx?collection=docs"), "guides/create-a-database-adapter.mdx": () => import("../content/docs/guides/create-a-database-adapter.mdx?collection=docs"), "guides/create-a-provider.mdx": () => import("../content/docs/guides/create-a-provider.mdx?collection=docs"), "guides/create-your-first-plugin.mdx": () => import("../content/docs/guides/create-your-first-plugin.mdx?collection=docs"), "guides/database.mdx": () => import("../content/docs/guides/database.mdx?collection=docs"), "guides/optimize-performance.mdx": () => import("../content/docs/guides/optimize-performance.mdx?collection=docs"), "guides/plugins.mdx": () => import("../content/docs/guides/plugins.mdx?collection=docs"), "guides/provider-selection.mdx": () => import("../content/docs/guides/provider-selection.mdx?collection=docs"), "guides/testing.mdx": () => import("../content/docs/guides/testing.mdx?collection=docs"), "guides/webhooks-at-scale.mdx": () => import("../content/docs/guides/webhooks-at-scale.mdx?collection=docs"), "guides/webhooks.mdx": () => import("../content/docs/guides/webhooks.mdx?collection=docs"), "providers/abacatepay.mdx": () => import("../content/docs/providers/abacatepay.mdx?collection=docs"), "providers/dodo.mdx": () => import("../content/docs/providers/dodo.mdx?collection=docs"), "providers/polar.mdx": () => import("../content/docs/providers/polar.mdx?collection=docs"), "providers/stripe.mdx": () => import("../content/docs/providers/stripe.mdx?collection=docs"), "plugins/audit-logs.mdx": () => import("../content/docs/plugins/audit-logs.mdx?collection=docs"), "plugins/dash.mdx": () => import("../content/docs/plugins/dash.mdx?collection=docs"), "plugins/overview.mdx": () => import("../content/docs/plugins/overview.mdx?collection=docs"), "reference/cli.mdx": () => import("../content/docs/reference/cli.mdx?collection=docs"), "reference/client-options.mdx": () => import("../content/docs/reference/client-options.mdx?collection=docs"), "reference/database-schema.mdx": () => import("../content/docs/reference/database-schema.mdx?collection=docs"), "reference/errors.mdx": () => import("../content/docs/reference/errors.mdx?collection=docs"), "reference/plugin-api.mdx": () => import("../content/docs/reference/plugin-api.mdx?collection=docs"), });
\ No newline at end of file
diff --git a/apps/web/content/docs/basic-usage.mdx b/apps/web/content/docs/basic-usage.mdx
index e5ede22..3803aca 100644
--- a/apps/web/content/docs/basic-usage.mdx
+++ b/apps/web/content/docs/basic-usage.mdx
@@ -113,7 +113,7 @@ console.log(pix.expiresAt);
```
- In the current repository, native `pix` capability is implemented by `@paymesh/stripe` and `@paymesh/abacatepay`. The Polar provider intentionally does not advertise PIX support.
+ In the current repository, native `pix` capability is implemented by `@paymesh/stripe` and `@paymesh/abacatepay`. `@paymesh/polar` and `@paymesh/dodo` intentionally advertise `pix: false`. Dodo can still show Pix inside BRL hosted checkout links created through `paymesh.payments.create()`.
## Upsert and read a customer
diff --git a/apps/web/content/docs/concepts/payment-providers.mdx b/apps/web/content/docs/concepts/payment-providers.mdx
index 2a7591a..1727cec 100644
--- a/apps/web/content/docs/concepts/payment-providers.mdx
+++ b/apps/web/content/docs/concepts/payment-providers.mdx
@@ -54,6 +54,18 @@ AbacatePay currently implements:
AbacatePay does **not** expose subscriptions, refunds, or customer portal in the current repository.
+### Dodo
+
+Dodo currently implements:
+
+- hosted checkout payments through Dodo product carts
+- customers
+- catalog reads
+- subscription-aware dashboard sync
+- webhook verification and event mapping
+
+Dodo does **not** expose `paymesh.pix` in the current repository. BRL hosted checkouts can still present Pix inside the Dodo checkout page, but the provider does not currently return a native QR-code-first PIX object.
+
## Capability flags
Provider differences are carried through capability flags rather than hidden.
@@ -72,6 +84,7 @@ Some provider-specific differences still matter. For example:
- Polar requires `productIds` for checkout creation
- AbacatePay requires `productIds` for checkout creation and uses catalog-driven checkout
+- Dodo requires `productIds` for payment creation and can enable hosted Pix inside BRL checkouts
- Stripe PIX supports `amountIncludesIof` and expiration settings
- dashboard integrations vary by provider
diff --git a/apps/web/content/docs/concepts/pix.mdx b/apps/web/content/docs/concepts/pix.mdx
index 8072d0a..3c1cac5 100644
--- a/apps/web/content/docs/concepts/pix.mdx
+++ b/apps/web/content/docs/concepts/pix.mdx
@@ -64,12 +64,17 @@ In the current repository:
- `@paymesh/stripe` advertises `pix: true`
- `@paymesh/polar` advertises `pix: false`
- `@paymesh/abacatepay` advertises `pix: true`
+- `@paymesh/dodo` advertises `pix: false`
That means PIX-aware product code should either:
- choose a provider that exposes PIX
- or gate PIX UI behind provider capability detection
+
+ `@paymesh/dodo` can still surface Pix as a hosted payment method for BRL checkout links created through `paymesh.payments.create()`. It is not exposed through `paymesh.pix` because Dodo does not currently document a backend-native PIX flow that matches Paymesh's QR-code-first contract.
+
+
## Persistence model
With a database adapter attached, PIX state is stored separately in the built-in `pix` table. This is important because PIX-specific fields do not fit cleanly inside generic checkout rows.
diff --git a/apps/web/content/docs/installation.mdx b/apps/web/content/docs/installation.mdx
index 62a8805..4273b72 100644
--- a/apps/web/content/docs/installation.mdx
+++ b/apps/web/content/docs/installation.mdx
@@ -19,6 +19,10 @@ description: Install Paymesh, wire a provider, optionally add persistence, and m
+ If you need Dodo's catalog-driven hosted checkout flow:
+
+
+
You do not need a database on day one, but most real installations should add one quickly so webhook state, customers, PIX objects, and catalog snapshots are queryable locally.
@@ -29,7 +33,7 @@ description: Install Paymesh, wire a provider, optionally add persistence, and m
Put the client in a stable server-side module such as `src/lib/paymesh.ts`, `src/server/paymesh.ts`, or `app/lib/paymesh.ts`.
-
+
```ts title="src/lib/paymesh.ts"
import { createClient } from "paymesh";
@@ -69,7 +73,25 @@ description: Install Paymesh, wire a provider, optionally add persistence, and m
});
```
+
+ ```ts title="src/lib/paymesh.ts"
+ import { createClient } from "paymesh";
+ import { dodo } from "@paymesh/dodo";
+
+ export const paymesh = createClient({
+ provider: dodo({
+ apiKey: process.env.DODO_PAYMENTS_API_KEY!,
+ webhookSecret: process.env.DODO_PAYMENTS_WEBHOOK_KEY,
+ baseUrl: "https://test.dodopayments.com",
+ }),
+ });
+ ```
+
+
+
+ `@paymesh/dodo` is catalog-driven like Polar and AbacatePay checkout flows. You create hosted payments with `productIds`. For BRL payments, the Dodo hosted checkout can offer Pix, but the provider does not expose `paymesh.pix`.
+
diff --git a/apps/web/content/docs/introduction.mdx b/apps/web/content/docs/introduction.mdx
index 94f54d2..b286bdb 100644
--- a/apps/web/content/docs/introduction.mdx
+++ b/apps/web/content/docs/introduction.mdx
@@ -25,7 +25,7 @@ Paymesh exists to normalize those concerns.
At runtime, Paymesh is built from four layers:
1. A `client` created with `createClient`.
-2. A `provider` such as `@paymesh/stripe` or `@paymesh/polar`.
+2. A `provider` such as `@paymesh/stripe`, `@paymesh/polar`, or `@paymesh/dodo`.
3. An optional `database` adapter such as `@paymesh/memory`, `@paymesh/postgres`, `@paymesh/drizzle`, or `@paymesh/prisma`.
4. Optional `plugins` such as `@paymesh/dash` and `@paymesh/audit-logs`.
@@ -75,14 +75,14 @@ The current repository exposes these first-party modules:
| Area | Package |
| --- | --- |
| Core client | `paymesh` |
-| Providers | `@paymesh/stripe`, `@paymesh/polar`, `@paymesh/abacatepay` |
+| Providers | `@paymesh/stripe`, `@paymesh/polar`, `@paymesh/abacatepay`, `@paymesh/dodo` |
| Database adapters | `@paymesh/memory`, `@paymesh/postgres`, `@paymesh/drizzle`, `@paymesh/prisma` |
| Framework adapters | `@paymesh/next`, `@paymesh/express`, `@paymesh/fastify`, `@paymesh/hono`, `@paymesh/elysia` |
| Plugins | `@paymesh/dash`, `@paymesh/audit-logs` |
| Tooling | `@paymesh/cli` |
- The docs below stay anchored to what is actually exported in the repository. Planned providers like PayPal and Dodo are treated as planned work, not presented like shipped integrations.
+ The docs below stay anchored to what is actually exported in the repository. PayPal remains planned work. Dodo is shipped as a catalog-driven hosted checkout provider with customer, subscription, catalog, and webhook support.
## Read this documentation like a system
diff --git a/apps/web/content/docs/providers/dodo.mdx b/apps/web/content/docs/providers/dodo.mdx
new file mode 100644
index 0000000..d461e3c
--- /dev/null
+++ b/apps/web/content/docs/providers/dodo.mdx
@@ -0,0 +1,203 @@
+---
+title: Dodo
+description: "Detailed guide to the Dodo provider: setup options, catalog-driven hosted payments, customers, catalog reads, dashboard sync helpers, and webhook normalization."
+---
+
+## Overview
+
+`@paymesh/dodo` is the first-party Paymesh provider for Dodo Payments. It is the right choice when you want:
+
+- catalog-driven hosted checkout payments
+- normalized customer lifecycle methods
+- catalog reads from Dodo products
+- subscription-aware webhook mapping
+- dashboard sync helpers for customer, payment, and subscription state
+
+Unlike Stripe and AbacatePay, Dodo in this repository does **not** expose `paymesh.pix`. For BRL checkouts, the Dodo hosted checkout can still offer Pix inside the redirect-flow experience.
+
+## Installation
+
+
+
+## Provider options
+
+| Option | Required | Description |
+| --- | --- | --- |
+| `apiKey` | yes in practice | Dodo API key used for authenticated requests. Defaults to `process.env.DODO_PAYMENTS_API_KEY`. |
+| `webhookSecret` | recommended | Secret used to verify `webhook-id`, `webhook-timestamp`, and `webhook-signature` headers. Defaults to `process.env.DODO_PAYMENTS_WEBHOOK_KEY`. |
+| `sandbox` | no | Forces sandbox mode. Auto-detected from the base URL when omitted. |
+| `baseUrl` | no | Overrides the Dodo API base URL. Defaults to `https://live.dodopayments.com`. Use `https://test.dodopayments.com` for sandbox. |
+| `retry` | no | Retry policy forwarded to the shared request layer. |
+| `timeout` | no | Per-request timeout in milliseconds. |
+| `fetch` | no | Custom fetch implementation for tests or runtime-specific environments. |
+
+```ts title="src/lib/paymesh.ts"
+import { createClient } from "paymesh";
+import { dodo } from "@paymesh/dodo";
+
+export const paymesh = createClient({
+ provider: dodo({
+ apiKey: process.env.DODO_PAYMENTS_API_KEY!,
+ webhookSecret: process.env.DODO_PAYMENTS_WEBHOOK_KEY,
+ baseUrl: "https://test.dodopayments.com",
+ }),
+});
+```
+
+## Capability profile
+
+| Capability | Supported |
+| --- | --- |
+| `checkout` | yes |
+| `pix` | no |
+| `customers` | yes |
+| `subscriptions` | yes |
+| `webhooks` | yes |
+| `refunds` | no |
+| `customerPortal` | no |
+| `coupons` | no |
+
+## Hosted checkout payments
+
+Dodo payment creation is catalog-driven. You must pass `productIds`.
+
+```ts title="src/server/payments.ts"
+const payment = await paymesh.payments.create({
+ amount: 4900,
+ currency: "BRL",
+ productIds: ["prod_abc123"],
+ customer: {
+ email: "billing@example.com",
+ externalId: "order_42",
+ name: "Billing Team",
+ },
+ successUrl: "http://localhost:3000/success",
+ cancelUrl: "http://localhost:3000/cancel",
+ metadata: {
+ orderId: "order_42",
+ },
+});
+```
+
+### Important constraints
+
+- `productIds` is required
+- `customer.id` or `customer.email` is required
+- `amount` can only be passed when exactly one product id is provided
+
+### Request mapping details
+
+- `productIds` maps to `product_cart`
+- `amount` maps into the product cart entry when one product id is present
+- `currency` maps to `billing_currency`
+- `customer.id` maps to `customer.customer_id`
+- `customer.email`, `customer.name`, and `customer.phone` map into the embedded customer payload when no `customer.id` exists
+- `metadata` values are serialized to strings before sending to Dodo
+- `successUrl`, `cancelUrl`, and `returnUrl` resolve into one `return_url`
+
+For BRL checkouts, the provider also enables Dodo's hosted Pix-capable payment method set automatically.
+
+## PIX note
+
+`@paymesh/dodo` does not implement `paymesh.pix`.
+
+That is deliberate. Dodo can present Pix inside the hosted checkout page for BRL payments, but the current Dodo API documentation does not describe a backend-native PIX flow that returns Paymesh's QR-code-first PIX fields such as `copyPasteCode` and `qrCodeImageUrlPng`.
+
+If your product needs native PIX payload fields, choose `@paymesh/stripe` or `@paymesh/abacatepay` instead.
+
+## Customer operations
+
+Dodo customer methods are normalized through `paymesh.customers`.
+
+```ts title="src/server/customers.ts"
+const customer = await paymesh.customers.upsert({
+ email: "billing@example.com",
+ externalId: "org_42",
+ name: "ACME Corp",
+ phone: "+55 11 99999-9999",
+ metadata: {
+ segment: "enterprise",
+ },
+});
+```
+
+### Customer mapping notes
+
+- `externalId` is stored in Dodo metadata as `externalId`
+- create uses `POST /customers`
+- update uses `PATCH /customers/:id`
+- delete is intentionally unsupported by this provider package
+
+If you attempt to create a customer without `email`, the provider throws `invalid_request`.
+
+## Catalog reads
+
+Dodo exposes `provider.catalog.list()` and reads products from `/products`.
+
+Paymesh normalizes:
+
+- products
+- one synthetic price entry per product when price data exists
+- recurring interval metadata when Dodo exposes recurring pricing fields
+
+```ts title="src/server/catalog.ts"
+const catalog = await paymesh.provider.catalog?.list();
+
+console.log(catalog?.products.length, catalog?.prices.length);
+```
+
+## Dashboard helpers
+
+Dodo ships `provider.dashboard` with:
+
+- customer sync
+- payment sync
+- subscription sync
+
+`getResourceUrl()` currently returns `null`, so operational dashboards should not assume Dodo deep links are available.
+
+## Webhook verification
+
+The provider verifies Dodo webhooks using:
+
+- `webhook-id`
+- `webhook-timestamp`
+- `webhook-signature`
+
+It rebuilds the signed message in the standard Dodo format and checks the `v1` HMAC-SHA256 signature against the configured webhook secret.
+
+If `webhookSecret` is missing, verification returns `false`.
+
+## Webhook normalization
+
+The provider currently normalizes these Dodo event families:
+
+| Dodo event | Paymesh event |
+| --- | --- |
+| `payment.succeeded` | `payment.succeeded` |
+| `payment.failed` | `payment.failed` |
+| `payment.processing` | `payment.created` |
+| `payment.cancelled` | `payment.canceled` |
+| `refund.succeeded` | `payment.refunded` |
+| `refund.failed` | `payment.failed` |
+| `subscription.active` | `subscription.created` |
+| `subscription.renewed` | `subscription.updated` |
+| `subscription.on_hold` | `subscription.updated` |
+| `subscription.paused` | `subscription.updated` |
+| `subscription.cancelled` | `subscription.canceled` |
+| `subscription.failed` | `subscription.updated` |
+| `subscription.expired` | `subscription.canceled` |
+| `subscription.plan_changed` | `subscription.updated` |
+| `subscription.updated` | `subscription.updated` |
+
+Pix-shaped Dodo payment payloads are still normalized as payments with `method: "pix"` when the webhook includes Pix-specific fields.
+
+## Choosing Dodo
+
+Use Dodo when:
+
+- your checkout flow is catalog-driven
+- you want Dodo Payments as the upstream billing system
+- you need hosted BRL checkout links that may offer Pix without building a native PIX flow
+
+Do not choose Dodo when your product requires `paymesh.pix` with QR code, copia-e-cola, or explicit PIX expiration fields as first-class backend data.
diff --git a/apps/web/public/llms.txt b/apps/web/public/llms.txt
index fb05132..6e07f04 100644
--- a/apps/web/public/llms.txt
+++ b/apps/web/public/llms.txt
@@ -9,7 +9,7 @@ For canonical documentation, prefer the docs pages linked below over inferring b
## Canonical project facts
- Core package: `paymesh`
-- Official live providers: `@paymesh/stripe`, `@paymesh/polar`, `@paymesh/abacatepay`
+- Official live providers: `@paymesh/stripe`, `@paymesh/polar`, `@paymesh/abacatepay`, `@paymesh/dodo`
- Official webhook adapters: `@paymesh/next`, `@paymesh/express`, `@paymesh/fastify`, `@paymesh/hono`, `@paymesh/elysia`
- Official database adapters: `@paymesh/postgres`, `@paymesh/drizzle`, `@paymesh/prisma`
- Official plugins: `@paymesh/dash`, `@paymesh/audit-logs`
@@ -166,12 +166,30 @@ Important limitation:
- `@paymesh/abacatepay` does not currently support subscriptions, refunds, or customer portal
+### Dodo
+
+Package: `@paymesh/dodo`
+
+Implemented capabilities:
+
+- checkout payments (catalog-driven with product IDs)
+- customers
+- webhooks
+- subscriptions
+- catalog reads
+- dashboard sync helpers for customers, payments, and subscriptions
+
+Important limitations:
+
+- `@paymesh/dodo` currently advertises `pix: false`
+- Dodo can expose Pix inside BRL hosted checkout, but not through `paymesh.pix`
+- customer delete is intentionally unsupported
+
### Planned providers
These appear in docs/navigation as planned work and should not be described as shipped integrations:
- PayPal
-- Dodo
## PIX
@@ -184,7 +202,7 @@ PIX is a first-class concept in Paymesh core, with normalized fields such as:
- `instructionsUrl`
- `expiresAt`
-PIX support is provider-specific. Stripe supports PIX. Polar currently does not. AbacatePay supports PIX through transparent charges.
+PIX support is provider-specific. Stripe supports PIX. Polar currently does not. AbacatePay supports PIX through transparent charges. Dodo can show hosted Pix for BRL checkouts but does not expose native `paymesh.pix`.
PIX create input supports:
@@ -475,7 +493,7 @@ Supported commands:
- Prefer the docs pages above when answering questions about Paymesh.
- Do not describe planned providers as implemented.
-- Treat Stripe, Polar, and AbacatePay as the official live providers.
+- Treat Stripe, Polar, AbacatePay, and Dodo as the official live providers.
- Treat PIX support as provider-specific, not universal.
- Mention raw-body requirements when discussing Express or Fastify webhooks.
- Mention that plugins are real runtime modules, not just helper functions.
diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx
index e2386d7..d6660e5 100644
--- a/apps/web/src/app/page.tsx
+++ b/apps/web/src/app/page.tsx
@@ -127,12 +127,12 @@ const providerCards: ProviderCard[] = [
{
id: '05',
name: 'Dodo Payments',
- status: 'coming soon',
- headline: 'Dodo is queued.',
+ status: 'supported',
+ headline: 'Dodo is live.',
description:
- 'Internet-native billing for teams that want optionality without integration churn.',
+ 'Catalog-driven hosted payments, customers, subscriptions, and webhook normalization through the same Paymesh client.',
icon: 'dodo',
- tags: ['global', 'merchant', 'tax'],
+ tags: ['catalog', 'hosted', 'webhooks'],
},
{
id: '06',
diff --git a/apps/web/src/components/docs/mdx-components.tsx b/apps/web/src/components/docs/mdx-components.tsx
index 585f993..344fdb7 100644
--- a/apps/web/src/components/docs/mdx-components.tsx
+++ b/apps/web/src/components/docs/mdx-components.tsx
@@ -12,6 +12,7 @@ const packageDocsLinks: Record = {
paymesh: '/docs/introduction',
'@paymesh/stripe': '/docs/providers/stripe',
'@paymesh/polar': '/docs/providers/polar',
+ '@paymesh/dodo': '/docs/providers/dodo',
'@paymesh/next': '/docs/adapters/next',
'@paymesh/express': '/docs/adapters/express',
'@paymesh/fastify': '/docs/adapters/fastify',
diff --git a/apps/web/src/lib/docs-navigation.ts b/apps/web/src/lib/docs-navigation.ts
index f0d8a05..f793647 100644
--- a/apps/web/src/lib/docs-navigation.ts
+++ b/apps/web/src/lib/docs-navigation.ts
@@ -114,8 +114,8 @@ export const docsNavigation: DocNavGroup[] = [
},
{
label: 'Dodo',
- description: 'Planned provider',
- status: 'planned',
+ href: '/docs/providers/dodo',
+ description: 'Catalog-driven Dodo Payments provider.',
icon: 'dodo',
},
],
diff --git a/apps/web/src/lib/docs.tsx b/apps/web/src/lib/docs.tsx
index 3021d09..3ecb46f 100644
--- a/apps/web/src/lib/docs.tsx
+++ b/apps/web/src/lib/docs.tsx
@@ -429,6 +429,47 @@ export const app = new Elysia().post(
}),
);`;
+const dodoSetupSnippet = `import { createClient } from "paymesh";
+import { dodo } from "@paymesh/dodo";
+
+export const client = createClient({
+ provider: dodo({
+ apiKey: process.env.DODO_PAYMENTS_API_KEY!,
+ webhookSecret: process.env.DODO_PAYMENTS_WEBHOOK_KEY,
+ baseUrl: "https://test.dodopayments.com",
+ }),
+});`;
+
+const dodoPaymentsSnippet = `const payment = await client.payments.create({
+ amount: 4900,
+ currency: "BRL",
+ productIds: ["prod_abc123"],
+ customer: {
+ email: "billing@example.com",
+ externalId: "order_42",
+ name: "Billing Team",
+ },
+ successUrl: "https://example.com/success",
+ cancelUrl: "https://example.com/cancel",
+ metadata: {
+ orderId: "order_42",
+ },
+});`;
+
+const dodoWebhookSnippet = `import { Elysia } from "elysia";
+import { Webhooks } from "@paymesh/elysia";
+
+export const app = new Elysia().post(
+ "/webhooks/dodo",
+ Webhooks({
+ client,
+
+ async onPaymentSucceeded(event) {
+ console.log("payment.succeeded", event.id);
+ },
+ }),
+);`;
+
const postgresSnippet = `import { createClient } from "paymesh";
import { postgres } from "@paymesh/postgres";
import { stripe } from "@paymesh/stripe";
@@ -827,8 +868,8 @@ const guidePages: DocPage[] = [
],
[
'Dodo',
- 'Another billing route for teams that want optionality.',
- 'Roadmap-only in the current repo build.',
+ 'Catalog-driven hosted checkout with Dodo Payments.',
+ 'No native paymesh.pix flow; Pix only appears inside BRL hosted checkout.',
],
]}
/>
@@ -1904,7 +1945,7 @@ const docsNavigation: DocNavGroup[] = [
status: 'coming-soon',
},
{ label: 'PayPal', slug: ['providers', 'paypal'], status: 'coming-soon' },
- { label: 'Dodo', slug: ['providers', 'dodo'], status: 'coming-soon' },
+ { label: 'Dodo', slug: ['providers', 'dodo'] },
],
},
{
@@ -2126,6 +2167,65 @@ const polarCapabilityRows: ReactNode[][] = [
],
];
+const dodoCapabilityRows: ReactNode[][] = [
+ [
+ checkout,
+
+ Available
+ ,
+ 'Creates Dodo hosted payments through catalog-driven product carts.',
+ ],
+ [
+ customers,
+
+ Available
+ ,
+ 'Upserts and fetches Dodo customers.',
+ ],
+ [
+ webhooks,
+
+ Available
+ ,
+ 'Verifies webhook-id, webhook-timestamp, and webhook-signature headers before dispatching normalized hooks.',
+ ],
+ [
+ subscriptions,
+
+ Advertised
+ ,
+ 'Subscription webhook events and dashboard sync are implemented.',
+ ],
+ [
+ pix,
+
+ False
+ ,
+ 'Dodo can offer hosted Pix inside BRL checkout links, but it does not expose native paymesh.pix flows.',
+ ],
+ [
+ refunds,
+
+ False
+ ,
+ 'Refund webhooks are normalized, but the provider does not expose refund helpers as a public capability.',
+ ],
+ [
+ customerPortal,
+
+ False
+ ,
+ 'No dedicated customer portal helper is exposed by the package.',
+ ],
+ [
+ coupons,
+
+ False
+ ,
+ 'No coupon capability is exposed by the package.',
+ ],
+];
+
const polarEventRows: ReactNode[][] = [
['checkout.created', 'payment.created', 'onPaymentCreated'],
['checkout.updated', 'checkout.completed', 'onCheckoutCompleted'],
@@ -2275,6 +2375,10 @@ const docsPages: DocPage[] = [
@paymesh/polar,
'You are using Polar as the upstream merchant-of-record provider.',
],
+ [
+ @paymesh/dodo,
+ 'You are using Dodo Payments for catalog-driven hosted checkout flows.',
+ ],
[
@paymesh/next,
'Your webhook endpoint runs in a Next.js App Router route.',
@@ -3277,10 +3381,10 @@ rawPayment.raw; // provider payload`}
],
[
'Dodo',
-
- Coming Soon
+
+ Available
,
- 'Planned additional modern billing route.',
+ 'Shipped catalog-driven provider with hosted checkout, customers, webhooks, catalog sync, and subscription sync.',
],
]}
/>
@@ -3702,6 +3806,120 @@ rawPayment.raw; // provider payload`}
},
],
},
+ {
+ group: 'Providers',
+ slug: ['providers', 'dodo'],
+ title: 'Dodo',
+ description:
+ 'Dodo is a shipped Paymesh provider for catalog-driven hosted checkout, customer operations, catalog sync, and normalized webhook handling.',
+ sections: [
+ {
+ id: 'setup',
+ title: 'Setup',
+ content: (
+
+
+ The Dodo provider lives in @paymesh/dodo{' '}
+ and reads DODO_PAYMENTS_API_KEY and
+ optional DODO_PAYMENTS_WEBHOOK_KEY by
+ default.
+
+
+
+ ),
+ },
+ {
+ id: 'capabilities',
+ title: 'Capabilities',
+ content: (
+
+
+
+ ),
+ },
+ {
+ id: 'payments',
+ title: 'Hosted Checkout Flow',
+ content: (
+
+
+ Dodo payment creation is catalog-driven. You must provide at least
+ one productIds entry, and{' '}
+ amount is only accepted when exactly one
+ product id is present.
+
+
+
+ For BRL hosted checkout links, the provider enables Dodo payment
+ methods that can include Pix inside the hosted checkout page. This
+ does not expose the dedicated client.pix{' '}
+ surface.
+
+
+ ),
+ },
+ {
+ id: 'customers',
+ title: 'Customer Operations',
+ content: (
+
+
+ The Dodo provider supports normalized{' '}
+ upsert and get{' '}
+ for customers. Delete is intentionally unsupported because the
+ package does not expose a provider-side customer delete endpoint.
+
+
+
+ ),
+ },
+ {
+ id: 'webhooks',
+ title: 'Webhook Mapping',
+ content: (
+
+
+ Dodo webhook verification reconstructs the standard signed message
+ using webhook-id,{' '}
+ webhook-timestamp, and the raw request
+ body before comparing the expected HMAC-SHA256 signature with the
+ v1 signature.
+
+
+
+
+ ),
+ },
+ ],
+ },
...[
{
slug: ['providers', 'abacatepay'],
@@ -3739,24 +3957,6 @@ rawPayment.raw; // provider payload`}
['refunds', 'planned'],
] as const,
},
- {
- slug: ['providers', 'dodo'],
- title: 'Dodo',
- packageName: '@paymesh/dodo',
- notes:
- 'Planned additional modern billing route for teams that want optionality beyond the first provider.',
- targets: [
- 'Consistent payment and customer primitives through the same core client.',
- 'Webhook normalization into Paymesh event hooks.',
- 'Provider capability modeling that matches the rest of the ecosystem.',
- ],
- capabilities: [
- ['checkout', 'planned'],
- ['customers', 'planned'],
- ['webhooks', 'planned'],
- ['subscriptions', 'planned'],
- ] as const,
- },
].map((provider) => ({
group: 'Providers',
slug: provider.slug,
diff --git a/bun.lock b/bun.lock
index 743b8d3..b791dc1 100644
--- a/bun.lock
+++ b/bun.lock
@@ -89,6 +89,16 @@
"paymesh": ">=0.0.0",
},
},
+ "packages/dodo": {
+ "name": "@paymesh/dodo",
+ "version": "0.0.0",
+ "devDependencies": {
+ "paymesh": "workspace:*",
+ },
+ "peerDependencies": {
+ "paymesh": ">=0.0.0",
+ },
+ },
"packages/drizzle": {
"name": "@paymesh/drizzle",
"version": "0.0.0",
@@ -514,6 +524,8 @@
"@paymesh/dash": ["@paymesh/dash@workspace:packages/dash"],
+ "@paymesh/dodo": ["@paymesh/dodo@workspace:packages/dodo"],
+
"@paymesh/drizzle": ["@paymesh/drizzle@workspace:packages/drizzle"],
"@paymesh/elysia": ["@paymesh/elysia@workspace:packages/elysia"],
diff --git a/package.json b/package.json
index 0d69c1a..c51e66d 100644
--- a/package.json
+++ b/package.json
@@ -14,7 +14,7 @@
"check": "biome check .",
"check:write": "biome check --write .",
"typecheck": "bun run typecheck:packages",
- "typecheck:packages": "bun run --filter './packages/{paymesh,audit-logs,memory,postgres,drizzle,prisma,stripe,polar,abacatepay,express,fastify,hono,elysia,next,cli,mcp}' typecheck",
+ "typecheck:packages": "bun run --filter './packages/{paymesh,audit-logs,memory,postgres,drizzle,prisma,stripe,polar,abacatepay,dodo,express,fastify,hono,elysia,next,cli,mcp}' typecheck",
"typecheck:apps": "bun run --filter './apps/*' typecheck",
"types": "bun run typecheck",
"test": "bun test --pass-with-no-tests",
diff --git a/packages/cli/src/shared/providers.ts b/packages/cli/src/shared/providers.ts
index 33a05a0..d2f663a 100644
--- a/packages/cli/src/shared/providers.ts
+++ b/packages/cli/src/shared/providers.ts
@@ -13,6 +13,13 @@ export const PROVIDERS = {
webhookSecretEnv: 'POLAR_WEBHOOK_SECRET',
paramName: 'accessToken',
},
+ dodo: {
+ label: 'Dodo Payments',
+ package: '@paymesh/dodo',
+ apiKeyEnv: 'DODO_PAYMENTS_API_KEY',
+ webhookSecretEnv: 'DODO_PAYMENTS_WEBHOOK_KEY',
+ paramName: 'apiKey',
+ },
custom: {
label: 'Custom — I will configure later',
package: null,
diff --git a/packages/dodo/README.md b/packages/dodo/README.md
new file mode 100644
index 0000000..a8afa12
--- /dev/null
+++ b/packages/dodo/README.md
@@ -0,0 +1,69 @@
+
+
+@paymesh/dodo
+
+
+ Dodo Payments for Paymesh, designed for catalog-driven global checkout flows.
+
+
+
+ Launch Dodo hosted payments, manage customers, normalize webhooks, and keep the rest of your application on the standard Paymesh client contract.
+
+
+
+ Installation ·
+ Usage ·
+ What You Get ·
+ Important Notes
+
+
+
+
+Installation
+
+```bash
+npm install paymesh @paymesh/dodo
+```
+
+Usage
+
+```ts
+import { createClient } from "paymesh";
+import { dodo } from "@paymesh/dodo";
+
+const paymesh = createClient({
+ provider: dodo({
+ apiKey: process.env.DODO_PAYMENTS_API_KEY!,
+ webhookSecret: process.env.DODO_PAYMENTS_WEBHOOK_KEY!,
+ baseUrl: "https://test.dodopayments.com",
+ }),
+});
+
+const payment = await paymesh.payments.create({
+ productIds: ["prod_abc123"],
+ currency: "BRL",
+ customer: {
+ email: "ada@example.com",
+ name: "Ada Lovelace",
+ },
+ metadata: {
+ orderId: "order_123",
+ },
+ successUrl: "https://example.com/billing",
+ cancelUrl: "https://example.com/cancel",
+});
+
+console.log(payment.id, payment.checkoutUrl);
+```
+
+What You Get
+
+
+ @paymesh/dodo implements catalog-driven hosted payments, customer reads and upserts, catalog sync, dashboard sync helpers, webhook verification, and normalized Dodo event handling.
+
+
+Important Notes
+
+
+ Dodo can expose Pix as a hosted payment method for BRL checkouts, and this provider enables that automatically for BRL payment links. The current Paymesh pix contract is intentionally not exposed here because Dodo does not document a native backend Pix creation flow that matches Paymesh's QR-code-first shape.
+
diff --git a/packages/dodo/package.json b/packages/dodo/package.json
new file mode 100644
index 0000000..09cd2cf
--- /dev/null
+++ b/packages/dodo/package.json
@@ -0,0 +1,36 @@
+{
+ "name": "@paymesh/dodo",
+ "version": "0.0.0",
+ "description": "Dodo Payments provider for Paymesh.",
+ "type": "module",
+ "license": "MIT",
+ "sideEffects": false,
+ "files": [
+ "dist"
+ ],
+ "exports": {
+ ".": {
+ "import": {
+ "types": "./dist/index.d.mts",
+ "default": "./dist/index.mjs"
+ },
+ "require": {
+ "types": "./dist/index.d.cts",
+ "default": "./dist/index.cjs"
+ }
+ }
+ },
+ "main": "./dist/index.cjs",
+ "module": "./dist/index.mjs",
+ "types": "./dist/index.d.mts",
+ "scripts": {
+ "build": "tsdown",
+ "typecheck": "tsc --noEmit"
+ },
+ "peerDependencies": {
+ "paymesh": ">=0.0.0"
+ },
+ "devDependencies": {
+ "paymesh": "workspace:*"
+ }
+}
diff --git a/packages/dodo/src/index.ts b/packages/dodo/src/index.ts
new file mode 100644
index 0000000..f4df809
--- /dev/null
+++ b/packages/dodo/src/index.ts
@@ -0,0 +1,333 @@
+import {
+ type CustomerUpsertData,
+ defineProvider,
+ type PaymentCreateData,
+ PaymeshError,
+ type PaymeshEvent,
+ type ProviderRequestOptions,
+ type ProviderWebhookHandleOptions,
+ type ProviderWebhookHandleResult,
+ request,
+ withRaw,
+} from 'paymesh';
+import {
+ DODO_CAPABILITIES,
+ DODO_LIVE_BASE_URL,
+ DODO_PIX_METHOD_TYPES,
+ DODO_TEST_BASE_URL,
+} from './shared/constants';
+import {
+ mapDodoCatalogPrice,
+ mapDodoCatalogProduct,
+ mapDodoCustomer,
+ mapDodoPayment,
+} from './shared/mapper';
+import {
+ syncDodoCustomer,
+ syncDodoPayment,
+ syncDodoSubscription,
+} from './shared/sync';
+import {
+ buildDodoCustomerRequest,
+ buildDodoProductCart,
+ isRecord,
+ serializeMetadata,
+ verifyDodoWebhookSignature,
+} from './shared/utils';
+import {
+ resolveDodoWebhookData,
+ resolveDodoWebhookEventId,
+ resolveDodoWebhookHook,
+ resolveDodoWebhookType,
+} from './shared/webhooks';
+import type {
+ DodoCustomer,
+ DodoPaginatedResponse,
+ DodoPayment,
+ DodoProductListResponse,
+ DodoProviderOptions,
+ DodoWebhookPayload,
+} from './types';
+
+export type * from './types';
+
+export const dodo = ({
+ apiKey = process.env.DODO_PAYMENTS_API_KEY,
+ webhookSecret = process.env.DODO_PAYMENTS_WEBHOOK_KEY,
+ baseUrl = process.env.DODO_PAYMENTS_BASE_URL ?? DODO_LIVE_BASE_URL,
+ sandbox,
+ retry,
+ timeout,
+ fetch,
+}: DodoProviderOptions = {}) => {
+ const resolveProviderSandbox = () =>
+ typeof sandbox === 'boolean' ? sandbox : baseUrl === DODO_TEST_BASE_URL;
+
+ const headers = {
+ authorization: `Bearer ${apiKey}`,
+ 'content-type': 'application/json',
+ };
+
+ const baseRequestOptions = {
+ baseUrl,
+ fetch,
+ headers,
+ retry,
+ timeout,
+ };
+
+ const resolveRequestOptions = (
+ options?: ProviderRequestOptions,
+ ) => ({
+ baseUrl: options?.baseUrl ?? baseRequestOptions.baseUrl,
+ timeout: options?.timeout ?? baseRequestOptions.timeout,
+ retry: options?.retry ?? baseRequestOptions.retry,
+ fetch: options?.fetch ?? baseRequestOptions.fetch,
+ headers: baseRequestOptions.headers,
+ });
+
+ return defineProvider({
+ id: 'dodo',
+ isSandbox: resolveProviderSandbox,
+ capabilities: DODO_CAPABILITIES,
+ payments: {
+ async create(
+ data: PaymentCreateData,
+ options?: ProviderRequestOptions,
+ ) {
+ const customer = buildDodoCustomerRequest({
+ customer: data.customer,
+ provider: 'dodo',
+ });
+ const metadata = serializeMetadata(data.metadata);
+ const payment = await request('/payments', {
+ provider: 'dodo',
+ ...resolveRequestOptions(options),
+ method: 'POST',
+ body: {
+ billing: {
+ country: 'BR',
+ },
+ customer,
+ product_cart: buildDodoProductCart({
+ amount: data.amount,
+ productIds: data.productIds ?? [],
+ provider: 'dodo',
+ }),
+ allowed_payment_method_types:
+ data.currency?.toUpperCase() === 'BRL'
+ ? [...DODO_PIX_METHOD_TYPES]
+ : undefined,
+ billing_currency: data.currency?.toUpperCase(),
+ metadata,
+ payment_link: true,
+ return_url: data.returnUrl ?? data.successUrl ?? data.cancelUrl,
+ show_saved_payment_methods: Boolean(data.customer?.id),
+ },
+ });
+
+ const normalized = mapDodoPayment(payment, resolveProviderSandbox());
+ const result = {
+ id: normalized.id,
+ provider: normalized.provider,
+ sandbox: normalized.sandbox,
+ amount: normalized.amount,
+ currency: normalized.currency,
+ status: normalized.status,
+ checkoutUrl: normalized.checkoutUrl,
+ customer: normalized.customer,
+ metadata: normalized.metadata,
+ };
+
+ return withRaw(result, payment, options?.includeRaw);
+ },
+ },
+ customers: {
+ async get(
+ id: string,
+ options?: ProviderRequestOptions,
+ ) {
+ const customer = await request(
+ `/customers/${encodeURIComponent(id)}`,
+ {
+ provider: 'dodo',
+ ...resolveRequestOptions(options),
+ },
+ );
+
+ return withRaw(
+ mapDodoCustomer(customer, resolveProviderSandbox()),
+ customer,
+ options?.includeRaw,
+ );
+ },
+ async upsert(
+ data: CustomerUpsertData,
+ options?: ProviderRequestOptions,
+ ) {
+ if (!data.id && !data.email) {
+ throw new PaymeshError({
+ code: 'invalid_request',
+ message: 'Provider "dodo" requires "email" when creating customers',
+ provider: 'dodo',
+ });
+ }
+
+ const metadata = serializeMetadata({
+ ...data.metadata,
+ ...(data.externalId ? { externalId: data.externalId } : {}),
+ });
+
+ const customer = await request(
+ data.id ? `/customers/${encodeURIComponent(data.id)}` : '/customers',
+ {
+ provider: 'dodo',
+ ...resolveRequestOptions(options),
+ method: data.id ? 'PATCH' : 'POST',
+ body: data.id
+ ? {
+ email: data.email,
+ name: data.name,
+ phone_number: data.phone,
+ metadata,
+ }
+ : {
+ email: data.email,
+ name: data.name ?? data.email!,
+ phone_number: data.phone,
+ metadata,
+ },
+ },
+ );
+
+ return withRaw(
+ mapDodoCustomer(customer, resolveProviderSandbox()),
+ customer,
+ options?.includeRaw,
+ );
+ },
+ async delete(
+ id: string,
+ options?: ProviderRequestOptions,
+ ) {
+ throw new PaymeshError({
+ code: 'unsupported_capability',
+ message: 'Provider "dodo" does not support deleting customers.',
+ provider: 'dodo',
+ cause: { id, includeRaw: options?.includeRaw },
+ });
+ },
+ },
+ catalog: {
+ async list() {
+ const response = await request<
+ DodoPaginatedResponse
+ >('/products', {
+ provider: 'dodo',
+ ...baseRequestOptions,
+ query: {
+ page_size: 100,
+ },
+ });
+ const products = response.items ?? response.data ?? [];
+
+ return {
+ products: products.map((product) =>
+ mapDodoCatalogProduct(product, resolveProviderSandbox()),
+ ),
+ prices: products
+ .filter(
+ (product) =>
+ typeof product.price === 'number' ||
+ typeof product.price_detail?.price === 'number',
+ )
+ .map((product) =>
+ mapDodoCatalogPrice(product, resolveProviderSandbox()),
+ ),
+ };
+ },
+ },
+ dashboard: {
+ getResourceUrl() {
+ return null;
+ },
+ syncCustomer: (input) =>
+ syncDodoCustomer({
+ ...input,
+ requestOptions: baseRequestOptions,
+ sandbox: resolveProviderSandbox(),
+ }),
+ syncPayment: (input) =>
+ syncDodoPayment({
+ ...input,
+ requestOptions: baseRequestOptions,
+ sandbox: resolveProviderSandbox(),
+ }),
+ syncSubscription: (input) =>
+ syncDodoSubscription({
+ ...input,
+ requestOptions: baseRequestOptions,
+ sandbox: resolveProviderSandbox(),
+ }),
+ },
+ webhooks: {
+ async verify({ request }) {
+ const payload = await request.text();
+ return verifyDodoWebhookSignature({
+ headers: request.headers,
+ payload,
+ secret: webhookSecret,
+ });
+ },
+ async handle(
+ options: ProviderWebhookHandleOptions,
+ ): Promise> {
+ const { request: incoming, includeRaw = false } = options;
+ const payload = await incoming.text();
+
+ let event: DodoWebhookPayload;
+
+ try {
+ event = JSON.parse(payload) as DodoWebhookPayload;
+ } catch (error) {
+ throw new PaymeshError({
+ code: 'webhook_parse_error',
+ message: 'Dodo webhook payload must be valid JSON.',
+ provider: 'dodo',
+ cause: error,
+ });
+ }
+
+ if (!isRecord(event) || typeof event.type !== 'string') {
+ throw new TypeError('Dodo webhook payload must be a JSON object.');
+ }
+
+ const type = resolveDodoWebhookType(event.type);
+ const hook = resolveDodoWebhookHook(type);
+ const data = resolveDodoWebhookData(
+ event,
+ type,
+ includeRaw,
+ resolveProviderSandbox(),
+ );
+ const id = resolveDodoWebhookEventId(event);
+
+ return {
+ deliveryId: incoming.headers.get('webhook-id') ?? undefined,
+ hook,
+ event: withRaw(
+ {
+ id,
+ type,
+ provider: 'dodo',
+ sandbox: resolveProviderSandbox(),
+ data,
+ },
+ event,
+ includeRaw,
+ ) as PaymeshEvent,
+ };
+ },
+ },
+ });
+};
diff --git a/packages/dodo/src/shared/constants.ts b/packages/dodo/src/shared/constants.ts
new file mode 100644
index 0000000..7573a22
--- /dev/null
+++ b/packages/dodo/src/shared/constants.ts
@@ -0,0 +1,72 @@
+import type {
+ PaymentStatus,
+ PaymeshEventType,
+ ProviderCapabilities,
+} from 'paymesh';
+
+export const DODO_LIVE_BASE_URL = 'https://live.dodopayments.com';
+export const DODO_TEST_BASE_URL = 'https://test.dodopayments.com';
+
+export const DODO_CAPABILITIES = {
+ checkout: true,
+ pix: false,
+ coupons: false,
+ refunds: false,
+ subscriptions: true,
+ webhooks: true,
+ customerPortal: false,
+ customers: true,
+} satisfies ProviderCapabilities;
+
+export const DODO_PAYMENT_STATUSES: Record = {
+ succeeded: 'paid',
+ failed: 'failed',
+ cancelled: 'canceled',
+ processing: 'processing',
+ requires_customer_action: 'pending',
+ requires_merchant_action: 'pending',
+ requires_payment_method: 'failed',
+ requires_confirmation: 'pending',
+ requires_capture: 'processing',
+ partially_captured: 'processing',
+ partially_captured_and_capturable: 'processing',
+ pending: 'pending',
+ active: 'paid',
+ on_hold: 'processing',
+ expired: 'canceled',
+};
+
+export const DODO_EVENTS: Record = {
+ 'payment.succeeded': 'payment.succeeded',
+ 'payment.failed': 'payment.failed',
+ 'payment.processing': 'payment.created',
+ 'payment.cancelled': 'payment.canceled',
+ 'refund.succeeded': 'payment.refunded',
+ 'refund.failed': 'payment.failed',
+ 'subscription.active': 'subscription.created',
+ 'subscription.renewed': 'subscription.updated',
+ 'subscription.on_hold': 'subscription.updated',
+ 'subscription.paused': 'subscription.updated',
+ 'subscription.cancelled': 'subscription.canceled',
+ 'subscription.failed': 'subscription.updated',
+ 'subscription.expired': 'subscription.canceled',
+ 'subscription.plan_changed': 'subscription.updated',
+ 'subscription.updated': 'subscription.updated',
+} satisfies Record;
+
+export const DODO_HOOKS: Record = {
+ 'payment.created': 'onPaymentCreated',
+ 'payment.succeeded': 'onPaymentSucceeded',
+ 'payment.failed': 'onPaymentFailed',
+ 'payment.canceled': 'onPaymentCanceled',
+ 'payment.refunded': 'onPaymentRefunded',
+ 'customer.created': 'onCustomerCreated',
+ 'customer.updated': 'onCustomerUpdated',
+ 'customer.deleted': 'onCustomerDeleted',
+ 'subscription.created': 'onSubscriptionCreated',
+ 'subscription.updated': 'onSubscriptionUpdated',
+ 'subscription.canceled': 'onSubscriptionCanceled',
+ 'checkout.completed': 'onCheckoutCompleted',
+};
+
+export const DODO_PIX_METHOD_TYPES = ['pix', 'credit', 'debit'] as const;
diff --git a/packages/dodo/src/shared/mapper.ts b/packages/dodo/src/shared/mapper.ts
new file mode 100644
index 0000000..4d18c63
--- /dev/null
+++ b/packages/dodo/src/shared/mapper.ts
@@ -0,0 +1,176 @@
+import type { BaseCustomer, BasePayment, BasePix } from 'paymesh';
+import type {
+ DodoCustomer,
+ DodoPayment,
+ DodoProductListResponse,
+ DodoRefund,
+ DodoSubscription,
+} from '../types';
+import {
+ getDodoExternalId,
+ isDodoPixPayment,
+ mapDodoIntentStatus,
+ mapDodoRefundStatusToPaymentStatus,
+} from './utils';
+
+export function mapDodoCustomer(
+ customer: DodoCustomer,
+ sandbox: boolean,
+): BaseCustomer {
+ return {
+ id: customer.customer_id,
+ provider: 'dodo',
+ sandbox,
+ externalId: getDodoExternalId(customer.metadata),
+ name: customer.name,
+ email: customer.email,
+ phone: customer.phone_number ?? void 0,
+ metadata: customer.metadata ?? void 0,
+ };
+}
+
+export function mapDodoPayment(
+ payment: DodoPayment,
+ sandbox: boolean,
+): BasePayment | BasePix {
+ const customer = payment.customer
+ ? {
+ id: payment.customer.customer_id,
+ externalId: getDodoExternalId(payment.customer.metadata),
+ name: payment.customer?.name,
+ email: payment.customer.email,
+ phone: payment.customer.phone_number ?? void 0,
+ }
+ : void 0;
+
+ if (isDodoPixPayment(payment)) {
+ return {
+ id: payment.payment_id,
+ provider: 'dodo',
+ sandbox,
+ amount: payment.total_amount,
+ currency: payment.currency.toLowerCase(),
+ status: mapDodoIntentStatus(payment.status),
+ method: 'pix',
+ customer,
+ metadata: payment.metadata ?? void 0,
+ checkoutUrl: payment.payment_link ?? void 0,
+ };
+ }
+
+ return {
+ id: payment.payment_id,
+ provider: 'dodo',
+ sandbox,
+ amount: payment.total_amount,
+ currency: payment.currency.toLowerCase(),
+ status: mapDodoIntentStatus(payment.status),
+ checkoutUrl: payment.payment_link ?? void 0,
+ customer,
+ metadata: payment.metadata ?? void 0,
+ };
+}
+
+export function mapDodoRefundToPayment(
+ refund: DodoRefund,
+ sandbox: boolean,
+): BasePayment {
+ return {
+ id: refund.payment_id,
+ provider: 'dodo',
+ sandbox,
+ amount: refund.amount ?? 0,
+ currency: refund.currency?.toLowerCase() ?? 'usd',
+ status: mapDodoRefundStatusToPaymentStatus(refund.status),
+ customer: refund.customer
+ ? {
+ id: refund.customer.customer_id,
+ externalId: getDodoExternalId(refund.customer.metadata),
+ name: refund.customer.name,
+ email: refund.customer.email,
+ phone: refund.customer.phone_number ?? void 0,
+ }
+ : void 0,
+ metadata: {
+ ...(refund.metadata ?? {}),
+ refundId: refund.refund_id,
+ },
+ };
+}
+
+export function mapDodoSubscription(
+ subscription: DodoSubscription,
+ sandbox: boolean,
+) {
+ return {
+ id: subscription.subscription_id,
+ provider: 'dodo' as const,
+ sandbox,
+ customer: subscription.customer
+ ? {
+ id: subscription.customer.customer_id,
+ externalId: getDodoExternalId(subscription.customer.metadata),
+ name: subscription?.customer?.name,
+ email: subscription.customer.email,
+ phone: subscription.customer.phone_number ?? void 0,
+ }
+ : void 0,
+ productId: subscription.product_id,
+ amount: subscription.recurring_pre_tax_amount,
+ currency: subscription.currency.toLowerCase(),
+ status: subscription.status,
+ cancelAtPeriodEnd: subscription.cancel_at_next_billing_date,
+ metadata: subscription.metadata ?? void 0,
+ raw: subscription,
+ };
+}
+
+export function mapDodoCatalogProduct(
+ product: DodoProductListResponse,
+ sandbox: boolean,
+) {
+ return {
+ id: product.product_id,
+ sandbox,
+ name: product.name ?? void 0,
+ description: product.description ?? void 0,
+ active: true,
+ metadata: product.metadata ?? void 0,
+ version: product.metadata?.version ?? void 0,
+ raw: product,
+ };
+}
+
+export function mapDodoCatalogPrice(
+ product: DodoProductListResponse,
+ sandbox: boolean,
+) {
+ const price = product.price_detail;
+ const type =
+ price?.type === 'recurring_price'
+ ? 'recurring'
+ : price?.type === 'usage_based_price'
+ ? 'usage_based'
+ : 'one_time';
+
+ return {
+ id: `${product.product_id}_price`,
+ sandbox,
+ productId: product.product_id,
+ active: true,
+ type,
+ currency: (price?.currency ?? product.currency ?? void 0)?.toLowerCase(),
+ amount: price?.price ?? product.price ?? void 0,
+ interval:
+ price?.type === 'recurring_price' || price?.type === 'usage_based_price'
+ ? price.payment_frequency_interval?.toLowerCase()
+ : void 0,
+ intervalCount:
+ price?.type === 'recurring_price' || price?.type === 'usage_based_price'
+ ? price.payment_frequency_count
+ : void 0,
+ metadata: product.metadata ?? void 0,
+ version: product.metadata?.version ?? void 0,
+ raw: product,
+ };
+}
diff --git a/packages/dodo/src/shared/sync.ts b/packages/dodo/src/shared/sync.ts
new file mode 100644
index 0000000..0780930
--- /dev/null
+++ b/packages/dodo/src/shared/sync.ts
@@ -0,0 +1,115 @@
+import type { ProviderDashboardSyncInput } from 'paymesh';
+import { request, withRaw } from 'paymesh';
+import type { DodoCustomer, DodoPayment, DodoSubscription } from '../types';
+import { mapDodoCustomer, mapDodoPayment, mapDodoSubscription } from './mapper';
+
+type RequestOptions = Parameters[1];
+
+export const syncDodoCustomer = async ({
+ id,
+ schema,
+ database,
+ sandbox,
+ requestOptions,
+}: ProviderDashboardSyncInput & {
+ requestOptions?: RequestOptions;
+ sandbox: boolean;
+}) => {
+ const customer = await request(
+ `/customers/${encodeURIComponent(id)}`,
+ {
+ provider: 'dodo',
+ ...requestOptions,
+ },
+ );
+
+ const normalized = withRaw(
+ mapDodoCustomer(customer, sandbox),
+ customer,
+ true,
+ );
+
+ await database.repositories.customers.upsert(schema, normalized);
+
+ return normalized;
+};
+
+export const syncDodoPayment = async ({
+ id,
+ schema,
+ database,
+ sandbox,
+ requestOptions,
+}: ProviderDashboardSyncInput & {
+ requestOptions?: RequestOptions;
+ sandbox: boolean;
+}) => {
+ const payment = await request(
+ `/payments/${encodeURIComponent(id)}`,
+ {
+ provider: 'dodo',
+ ...requestOptions,
+ },
+ );
+ const normalizedPayment = mapDodoPayment(payment, sandbox);
+ const normalized = withRaw(normalizedPayment, payment, true);
+
+ const promises = [database.repositories.invoices.upsert(schema, normalized)];
+
+ if ('method' in normalizedPayment && normalizedPayment.method === 'pix')
+ promises.push(
+ database.repositories.pix.upsert(
+ schema,
+ normalized as Extract,
+ ),
+ );
+
+ await Promise.all(promises);
+
+ return normalized;
+};
+
+export const syncDodoSubscription = async ({
+ id,
+ schema,
+ database,
+ sandbox,
+ requestOptions,
+}: ProviderDashboardSyncInput & {
+ requestOptions?: RequestOptions;
+ sandbox: boolean;
+}) => {
+ const subscription = await request(
+ `/subscriptions/${encodeURIComponent(id)}`,
+ {
+ provider: 'dodo',
+ ...requestOptions,
+ },
+ );
+ const type =
+ subscription.status === 'cancelled' || subscription.status === 'expired'
+ ? ('subscription.canceled' as const)
+ : ('subscription.updated' as const);
+
+ const event = withRaw(
+ {
+ id,
+ type,
+ sandbox,
+ provider: 'dodo',
+ data: withRaw(
+ {
+ ...mapDodoSubscription(subscription, sandbox),
+ },
+ subscription,
+ true,
+ ),
+ },
+ subscription,
+ true,
+ );
+
+ await database.repositories.subscriptions.upsert(schema, event);
+
+ return event.data as unknown as Record;
+};
diff --git a/packages/dodo/src/shared/utils.ts b/packages/dodo/src/shared/utils.ts
new file mode 100644
index 0000000..1763b67
--- /dev/null
+++ b/packages/dodo/src/shared/utils.ts
@@ -0,0 +1,231 @@
+import { createHmac, timingSafeEqual } from 'node:crypto';
+import { PaymeshError } from 'paymesh';
+import type {
+ DodoIntentStatus,
+ DodoMetadata,
+ DodoPayment,
+ DodoRefund,
+ DodoSubscription,
+} from '../types';
+import { DODO_PAYMENT_STATUSES } from './constants';
+
+export function getDodoExternalId(metadata?: DodoMetadata | null) {
+ return typeof metadata?.externalId === 'string' &&
+ metadata.externalId.length > 0
+ ? metadata.externalId
+ : undefined;
+}
+
+export function serializeMetadata(
+ metadata?: Record,
+) {
+ const entries = Object.entries(metadata ?? {}).filter(
+ ([, value]) => value !== null,
+ );
+ if (entries.length === 0) return undefined;
+
+ return Object.fromEntries(
+ entries.map(([key, value]) => [key, String(value)]),
+ ) as Record;
+}
+
+export function buildDodoCustomerRequest(input?: {
+ customer?: {
+ email?: string;
+ id?: string;
+ name?: string;
+ phone?: string;
+ };
+ provider?: string;
+}) {
+ const customer = input?.customer;
+
+ if (!customer?.id && !customer?.email) {
+ throw new PaymeshError({
+ code: 'invalid_request',
+ message:
+ 'Provider "dodo" requires either "customer.id" or "customer.email" when creating payments.',
+ provider: input?.provider ?? 'dodo',
+ });
+ }
+
+ if (customer.id) {
+ return { customer_id: customer.id };
+ }
+
+ return {
+ email: customer.email!,
+ name: customer.name ?? customer.email!,
+ phone_number: customer.phone,
+ };
+}
+
+export function isDodoPixPayment(
+ payment:
+ | DodoPayment
+ | Pick
+ | {
+ currency?: string | null;
+ payment_method?: string | null;
+ payment_method_type?: string | null;
+ },
+) {
+ return (
+ 'payment_method_type' in payment &&
+ (payment.payment_method_type === 'pix' ||
+ payment.payment_method === 'real_time_payment')
+ );
+}
+
+export function mapDodoIntentStatus(status?: DodoIntentStatus | null) {
+ return !status ? 'pending' : (DODO_PAYMENT_STATUSES[status] ?? 'pending');
+}
+
+export function parseDodoWebhookSecret(secret: string) {
+ const value = secret.startsWith('whsec_')
+ ? secret.slice('whsec_'.length)
+ : secret;
+ return Buffer.from(value, 'base64');
+}
+
+export function signDodoWebhook(options: {
+ payload: string;
+ secret: string;
+ timestamp: string;
+ webhookId: string;
+}) {
+ const body = `${options.webhookId}.${options.timestamp}.${options.payload}`;
+ const expected = createHmac('sha256', parseDodoWebhookSecret(options.secret))
+ .update(body)
+ .digest('base64');
+
+ return `v1,${expected}`;
+}
+
+export function verifyDodoWebhookSignature(options: {
+ headers: Headers;
+ payload: string;
+ secret?: string;
+}) {
+ if (!options.secret) return false;
+
+ const webhookId = options.headers.get('webhook-id');
+ const timestamp = options.headers.get('webhook-timestamp');
+ const signatureHeader = options.headers.get('webhook-signature');
+
+ if (!webhookId || !timestamp || !signatureHeader) return false;
+
+ const timestampValue = Number.parseInt(timestamp, 10);
+ if (!Number.isFinite(timestampValue)) return false;
+
+ const now = Math.floor(Date.now() / 1000);
+ if (Math.abs(now - timestampValue) > 5 * 60) return false;
+
+ const expected = signDodoWebhook({
+ payload: options.payload,
+ secret: options.secret,
+ timestamp,
+ webhookId,
+ });
+
+ const expectedSignature = expected.split(',')[1];
+ if (!expectedSignature) return false;
+
+ for (const part of signatureHeader.split(/\s+/)) {
+ const [version, signature] = part.split(',');
+ if (version !== 'v1' || !signature) continue;
+ if (
+ signature.length === expectedSignature.length &&
+ timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature))
+ ) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+export function isRecord(value: unknown): value is Record {
+ return typeof value === 'object' && value !== null;
+}
+
+export function resolveDodoSubscriptionEventType(
+ subscription: DodoSubscription,
+) {
+ if (
+ subscription.status === 'cancelled' ||
+ subscription.status === 'expired'
+ ) {
+ return 'subscription.canceled' as const;
+ }
+
+ return 'subscription.updated' as const;
+}
+
+export function ensureDodoProductIds(productIds?: string[]) {
+ if (!productIds || productIds.length === 0) {
+ throw new PaymeshError({
+ code: 'invalid_request',
+ message:
+ 'Provider "dodo" requires at least one product id in "productIds"',
+ provider: 'dodo',
+ });
+ }
+}
+
+export function buildDodoProductCart(input: {
+ amount?: number;
+ productIds: string[];
+ provider?: string;
+}) {
+ ensureDodoProductIds(input.productIds);
+
+ if (typeof input.amount === 'number' && input.productIds.length > 1)
+ throw new PaymeshError({
+ code: 'invalid_request',
+ message:
+ 'Provider "dodo" only accepts "amount" when exactly one product id is provided.',
+ provider: input.provider ?? 'dodo',
+ });
+
+ return input.productIds.map((productId) => ({
+ product_id: productId,
+ quantity: 1,
+ amount: typeof input.amount === 'number' ? input.amount : undefined,
+ }));
+}
+
+export function resolveDodoCustomerFromInput(input?: {
+ email?: string;
+ externalId?: string;
+ id?: string;
+ name?: string;
+ phone?: string;
+}) {
+ if (!input) return undefined;
+
+ if (
+ !input.id &&
+ !input.email &&
+ !input.externalId &&
+ !input.name &&
+ !input.phone
+ ) {
+ return undefined;
+ }
+
+ return {
+ id: input.id,
+ externalId: input.externalId,
+ name: input.name,
+ email: input.email,
+ phone: input.phone,
+ };
+}
+
+export function mapDodoRefundStatusToPaymentStatus(status?: string | null) {
+ if (status === 'succeeded') return 'refunded' as const;
+ if (status === 'failed') return 'failed' as const;
+ if (status === 'review') return 'processing' as const;
+ return 'pending' as const;
+}
diff --git a/packages/dodo/src/shared/webhooks.ts b/packages/dodo/src/shared/webhooks.ts
new file mode 100644
index 0000000..74987e8
--- /dev/null
+++ b/packages/dodo/src/shared/webhooks.ts
@@ -0,0 +1,106 @@
+import type { PaymeshEventType } from 'paymesh';
+import { withRaw } from 'paymesh';
+import type { DodoWebhookPayload } from '../types';
+import { DODO_EVENTS, DODO_HOOKS } from './constants';
+import {
+ mapDodoPayment,
+ mapDodoRefundToPayment,
+ mapDodoSubscription,
+} from './mapper';
+
+function isDodoPaymentWebhook(event: DodoWebhookPayload): event is Extract<
+ DodoWebhookPayload,
+ {
+ type:
+ | 'payment.succeeded'
+ | 'payment.failed'
+ | 'payment.processing'
+ | 'payment.cancelled';
+ }
+> {
+ return (
+ event.type === 'payment.succeeded' ||
+ event.type === 'payment.failed' ||
+ event.type === 'payment.processing' ||
+ event.type === 'payment.cancelled'
+ );
+}
+
+function isDodoRefundWebhook(
+ event: DodoWebhookPayload,
+): event is Extract<
+ DodoWebhookPayload,
+ { type: 'refund.succeeded' | 'refund.failed' }
+> {
+ return event.type === 'refund.succeeded' || event.type === 'refund.failed';
+}
+
+function isDodoSubscriptionWebhook(event: DodoWebhookPayload): event is Extract<
+ DodoWebhookPayload,
+ {
+ type:
+ | 'subscription.active'
+ | 'subscription.renewed'
+ | 'subscription.on_hold'
+ | 'subscription.paused'
+ | 'subscription.cancelled'
+ | 'subscription.failed'
+ | 'subscription.expired'
+ | 'subscription.plan_changed'
+ | 'subscription.updated';
+ }
+> {
+ return event.type.startsWith('subscription.');
+}
+
+export function resolveDodoWebhookType(type: string): PaymeshEventType {
+ return DODO_EVENTS[type] ?? 'payment.created';
+}
+
+export function resolveDodoWebhookHook(type: PaymeshEventType) {
+ return DODO_HOOKS[type];
+}
+
+export function resolveDodoWebhookData(
+ event: DodoWebhookPayload,
+ _type: PaymeshEventType,
+ includeRaw: boolean,
+ sandbox: boolean,
+) {
+ if (isDodoPaymentWebhook(event)) {
+ return withRaw(mapDodoPayment(event.data, sandbox), event.data, includeRaw);
+ }
+
+ if (isDodoRefundWebhook(event)) {
+ return withRaw(
+ mapDodoRefundToPayment(event.data, sandbox),
+ event.data,
+ includeRaw,
+ );
+ }
+
+ if (isDodoSubscriptionWebhook(event)) {
+ return withRaw(
+ mapDodoSubscription(event.data, sandbox),
+ event.data,
+ includeRaw,
+ );
+ }
+
+ return withRaw(
+ {
+ ...event.data,
+ provider: 'dodo',
+ sandbox,
+ },
+ event.data,
+ includeRaw,
+ );
+}
+
+export function resolveDodoWebhookEventId(event: DodoWebhookPayload) {
+ if (isDodoPaymentWebhook(event)) return event.data.payment_id;
+ if (isDodoRefundWebhook(event)) return event.data.refund_id;
+ if (isDodoSubscriptionWebhook(event)) return event.data.subscription_id;
+ return `${event.type}:${event.timestamp}`;
+}
diff --git a/packages/dodo/src/types.ts b/packages/dodo/src/types.ts
new file mode 100644
index 0000000..ebacef4
--- /dev/null
+++ b/packages/dodo/src/types.ts
@@ -0,0 +1,219 @@
+import type { RetryOptions } from 'paymesh';
+
+export interface DodoProviderOptions {
+ apiKey?: string;
+ sandbox?: boolean;
+ webhookSecret?: string;
+ baseUrl?: string;
+ retry?: RetryOptions;
+ timeout?: number;
+ fetch?: typeof fetch;
+}
+
+export type DodoMetadata = Record | null;
+
+export interface DodoCustomer {
+ business_id?: string;
+ created_at?: string;
+ customer_id: string;
+ email: string;
+ name: string;
+ metadata?: DodoMetadata;
+ phone_number?: string | null;
+}
+
+export type DodoIntentStatus =
+ | 'succeeded'
+ | 'failed'
+ | 'cancelled'
+ | 'processing'
+ | 'requires_customer_action'
+ | 'requires_merchant_action'
+ | 'requires_payment_method'
+ | 'requires_confirmation'
+ | 'requires_capture'
+ | 'partially_captured'
+ | 'partially_captured_and_capturable';
+
+export interface DodoPayment {
+ billing?: {
+ country?: string;
+ city?: string | null;
+ state?: string | null;
+ street?: string | null;
+ zipcode?: string | null;
+ };
+ brand_id?: string;
+ business_id?: string;
+ created_at?: string;
+ currency: string;
+ customer: DodoCustomer;
+ digital_products_delivered?: boolean;
+ disputes?: unknown[];
+ metadata: DodoMetadata;
+ payment_id: string;
+ payment_provider?: 'stripe' | 'adyen' | 'dodo';
+ refunds?: DodoRefund[];
+ retry_attempt?: number;
+ settlement_amount?: number;
+ settlement_currency?: string;
+ total_amount: number;
+ card_holder_name?: string | null;
+ card_issuing_country?: string | null;
+ card_last_four?: string | null;
+ card_network?: string | null;
+ card_type?: string | null;
+ checkout_session_id?: string | null;
+ custom_field_responses?: Array<{ key: string; value: string }> | null;
+ discount_id?: string | null;
+ discounts?: unknown[] | null;
+ error_code?: string | null;
+ error_message?: string | null;
+ invoice_id?: string | null;
+ invoice_url?: string | null;
+ payment_link?: string | null;
+ payment_method?: string | null;
+ payment_method_type?: string | null;
+ product_cart?: Array<{
+ product_id: string;
+ quantity: number;
+ amount?: number | null;
+ }> | null;
+ refund_status?: string | null;
+ status?: DodoIntentStatus | null;
+ subscription_id?: string | null;
+}
+
+export interface DodoRefund {
+ amount?: number | null;
+ business_id?: string;
+ created_at?: string;
+ customer?: DodoCustomer;
+ is_partial?: boolean;
+ metadata?: DodoMetadata;
+ payment_id: string;
+ refund_id: string;
+ status: 'succeeded' | 'failed' | 'pending' | 'review';
+ currency?: string | null;
+ reason?: string | null;
+}
+
+export interface DodoSubscription {
+ addons?: Array<{
+ addon_id: string;
+ name?: string;
+ quantity: number;
+ }>;
+ billing?: DodoPayment['billing'];
+ cancel_at_next_billing_date: boolean;
+ created_at?: string;
+ currency: string;
+ customer: DodoCustomer;
+ metadata: DodoMetadata;
+ next_billing_date?: string;
+ on_demand?: boolean;
+ payment_frequency_count?: number;
+ payment_frequency_interval?: string;
+ previous_billing_date?: string;
+ product_id: string;
+ quantity: number;
+ recurring_pre_tax_amount: number;
+ status: 'pending' | 'active' | 'on_hold' | 'cancelled' | 'failed' | 'expired';
+ subscription_id: string;
+ subscription_period_count?: number;
+ subscription_period_interval?: string;
+ tax_inclusive?: boolean;
+ trial_period_days?: number;
+ cancellation_comment?: string | null;
+ cancellation_feedback?: string | null;
+ cancelled_at?: string | null;
+ custom_field_responses?: Array<{ key: string; value: string }> | null;
+ customer_business_name?: string | null;
+ discount_id?: string | null;
+ discounts?: unknown[] | null;
+ expires_at?: string | null;
+ payment_method_id?: string | null;
+ scheduled_change?: unknown | null;
+ tax_id?: string | null;
+}
+
+export interface DodoCheckoutResponse {
+ session_id: string;
+ checkout_url?: string | null;
+ client_secret?: string | null;
+ payment_id?: string | null;
+ publishable_key?: string | null;
+}
+
+export interface DodoProductPrice {
+ type: 'one_time_price' | 'recurring_price' | 'usage_based_price';
+ currency: string;
+ price: number;
+ payment_frequency_count?: number;
+ payment_frequency_interval?: 'Day' | 'Week' | 'Month' | 'Year';
+ subscription_period_count?: number;
+ subscription_period_interval?: 'Day' | 'Week' | 'Month' | 'Year';
+ tax_inclusive?: boolean | null;
+}
+
+export interface DodoProductListResponse {
+ business_id?: string;
+ created_at?: string;
+ entitlements?: unknown[];
+ is_recurring: boolean;
+ metadata: DodoMetadata;
+ product_id: string;
+ tax_category?: string;
+ updated_at?: string;
+ currency?: string | null;
+ description?: string | null;
+ image?: string | null;
+ name?: string | null;
+ price?: number | null;
+ price_detail?: DodoProductPrice | null;
+ tax_inclusive?: boolean | null;
+}
+
+export interface DodoPaginatedResponse {
+ items?: T[];
+ data?: T[];
+}
+
+export type DodoWebhookPayload =
+ | {
+ business_id: string;
+ data: DodoPayment;
+ timestamp: string;
+ type:
+ | 'payment.succeeded'
+ | 'payment.failed'
+ | 'payment.processing'
+ | 'payment.cancelled';
+ }
+ | {
+ business_id: string;
+ data: DodoRefund;
+ timestamp: string;
+ type: 'refund.succeeded' | 'refund.failed';
+ }
+ | {
+ business_id: string;
+ data: DodoSubscription;
+ timestamp: string;
+ type:
+ | 'subscription.active'
+ | 'subscription.renewed'
+ | 'subscription.on_hold'
+ | 'subscription.paused'
+ | 'subscription.cancelled'
+ | 'subscription.failed'
+ | 'subscription.expired'
+ | 'subscription.plan_changed'
+ | 'subscription.updated';
+ }
+ | {
+ business_id?: string;
+ data: Record;
+ timestamp: string;
+ type: string;
+ };
diff --git a/packages/dodo/test/catalog.test.ts b/packages/dodo/test/catalog.test.ts
new file mode 100644
index 0000000..65393a0
--- /dev/null
+++ b/packages/dodo/test/catalog.test.ts
@@ -0,0 +1,82 @@
+import { describe, expect, test } from 'bun:test';
+import { dodo } from '../src';
+
+describe('dodo catalog', () => {
+ test('lists products and maps one-time and recurring prices', async () => {
+ const provider = dodo({
+ apiKey: 'dodo_test_123',
+ baseUrl: 'https://test.dodopayments.com',
+ fetch: (async (input) => {
+ expect(String(input)).toBe(
+ 'https://test.dodopayments.com/products?page_size=100',
+ );
+
+ return Response.json({
+ items: [
+ {
+ product_id: 'prod_one',
+ name: 'One Time',
+ description: 'One-time product',
+ is_recurring: false,
+ currency: 'USD',
+ price: 4900,
+ price_detail: {
+ type: 'one_time_price',
+ currency: 'USD',
+ price: 4900,
+ },
+ metadata: {
+ version: 'v1',
+ },
+ },
+ {
+ product_id: 'prod_recurring',
+ name: 'Recurring',
+ description: 'Recurring product',
+ is_recurring: true,
+ currency: 'EUR',
+ price: 2900,
+ price_detail: {
+ type: 'recurring_price',
+ currency: 'EUR',
+ price: 2900,
+ payment_frequency_count: 1,
+ payment_frequency_interval: 'Month',
+ subscription_period_count: 1,
+ subscription_period_interval: 'Month',
+ },
+ metadata: {},
+ },
+ ],
+ });
+ }) as typeof fetch,
+ });
+
+ const catalog = await provider.catalog?.list();
+
+ expect(catalog?.products).toHaveLength(2);
+ expect(catalog?.products[0]).toMatchObject({
+ id: 'prod_one',
+ sandbox: true,
+ name: 'One Time',
+ description: 'One-time product',
+ active: true,
+ version: 'v1',
+ });
+ expect(catalog?.prices).toHaveLength(2);
+ expect(catalog?.prices[0]).toMatchObject({
+ id: 'prod_one_price',
+ type: 'one_time',
+ currency: 'usd',
+ amount: 4900,
+ });
+ expect(catalog?.prices[1]).toMatchObject({
+ id: 'prod_recurring_price',
+ type: 'recurring',
+ currency: 'eur',
+ amount: 2900,
+ interval: 'month',
+ intervalCount: 1,
+ });
+ });
+});
diff --git a/packages/dodo/test/customers.test.ts b/packages/dodo/test/customers.test.ts
new file mode 100644
index 0000000..c783432
--- /dev/null
+++ b/packages/dodo/test/customers.test.ts
@@ -0,0 +1,179 @@
+import { describe, expect, test } from 'bun:test';
+import { createClient } from 'paymesh';
+import { dodo } from '../src';
+
+function expectType(_value: T) {}
+
+describe('dodo customers', () => {
+ test('manages Dodo customers through the client', async () => {
+ const requests: Array<{
+ input: string;
+ method?: string;
+ body?: Record;
+ }> = [];
+ const provider = dodo({
+ apiKey: 'dodo_test_123',
+ });
+ const client = createClient({
+ provider,
+ baseUrl: 'https://dodo.customers.test',
+ timeout: 1234,
+ fetch: (async (input, init) => {
+ requests.push({
+ input: String(input),
+ method: init?.method,
+ body: init?.body ? JSON.parse(String(init.body)) : undefined,
+ });
+
+ if (String(input).endsWith('/customers') && init?.method === 'POST') {
+ return Response.json({
+ customer_id: 'cus_create',
+ email: 'ana@example.com',
+ name: 'Ana',
+ phone_number: '+5511999999999',
+ metadata: {
+ externalId: 'user_ext_123',
+ plan: 'pro',
+ },
+ });
+ }
+
+ if (String(input).endsWith('/customers/cus_create')) {
+ if (init?.method === 'PATCH') {
+ return Response.json({
+ customer_id: 'cus_create',
+ email: 'ana@example.com',
+ name: 'Ana Silva',
+ phone_number: '+5511999999999',
+ metadata: {
+ externalId: 'user_ext_123',
+ plan: 'business',
+ },
+ });
+ }
+
+ return Response.json({
+ customer_id: 'cus_create',
+ email: 'ana@example.com',
+ name: 'Ana',
+ phone_number: '+5511999999999',
+ metadata: {
+ externalId: 'user_ext_123',
+ plan: 'pro',
+ },
+ });
+ }
+
+ return new Response('not found', { status: 404 });
+ }) as typeof fetch,
+ });
+
+ const created = await client.customers.upsert({
+ name: 'Ana',
+ email: 'ana@example.com',
+ phone: '+5511999999999',
+ externalId: 'user_ext_123',
+ metadata: {
+ plan: 'pro',
+ },
+ });
+ const found = await client.customers.get('cus_create');
+ const updated = await client.customers.upsert({
+ id: 'cus_create',
+ name: 'Ana Silva',
+ metadata: {
+ plan: 'business',
+ },
+ });
+
+ expect(requests[0]).toMatchObject({
+ input: 'https://dodo.customers.test/customers',
+ method: 'POST',
+ });
+ expect(requests[0]?.body).toEqual({
+ email: 'ana@example.com',
+ name: 'Ana',
+ phone_number: '+5511999999999',
+ metadata: {
+ plan: 'pro',
+ externalId: 'user_ext_123',
+ },
+ });
+ expect(requests[1]).toMatchObject({
+ input: 'https://dodo.customers.test/customers/cus_create',
+ method: undefined,
+ });
+ expect(requests[2]).toMatchObject({
+ input: 'https://dodo.customers.test/customers/cus_create',
+ method: 'PATCH',
+ });
+ expect(requests[2]?.body).toEqual({
+ email: undefined,
+ name: 'Ana Silva',
+ phone_number: undefined,
+ metadata: {
+ plan: 'business',
+ },
+ });
+ expect(created).toMatchObject({
+ id: 'cus_create',
+ provider: 'dodo',
+ externalId: 'user_ext_123',
+ name: 'Ana',
+ email: 'ana@example.com',
+ phone: '+5511999999999',
+ metadata: {
+ externalId: 'user_ext_123',
+ plan: 'pro',
+ },
+ });
+ expect(found.id).toBe('cus_create');
+ expect(updated.name).toBe('Ana Silva');
+ });
+
+ test('supports raw customer payloads and explicit delete failures', async () => {
+ const provider = dodo({
+ apiKey: 'dodo_test_123',
+ baseUrl: 'https://dodo.customers.test',
+ fetch: (async () =>
+ Response.json({
+ customer_id: 'cus_raw',
+ email: 'ana@example.com',
+ name: 'Ana',
+ metadata: {
+ externalId: 'user_ext_123',
+ },
+ })) as unknown as typeof fetch,
+ });
+ const rawClient = createClient({ provider, includeRaw: true });
+
+ const customer = await rawClient.customers.get('cus_raw');
+
+ expectType(customer.raw);
+ expect(customer.raw).toMatchObject({
+ customer_id: 'cus_raw',
+ });
+ expect(customer.externalId).toBe('user_ext_123');
+
+ await expect(rawClient.customers.delete('cus_raw')).rejects.toMatchObject({
+ code: 'unsupported_capability',
+ message: 'Provider "dodo" does not support deleting customers.',
+ provider: 'dodo',
+ });
+ });
+
+ test('requires email when creating customers', async () => {
+ const provider = dodo({
+ apiKey: 'dodo_test_123',
+ });
+
+ await expect(
+ provider.customers.upsert({
+ name: 'Ana',
+ }),
+ ).rejects.toMatchObject({
+ code: 'invalid_request',
+ message: 'Provider "dodo" requires "email" when creating customers',
+ });
+ });
+});
diff --git a/packages/dodo/test/dashboard.test.ts b/packages/dodo/test/dashboard.test.ts
new file mode 100644
index 0000000..7a0aca5
--- /dev/null
+++ b/packages/dodo/test/dashboard.test.ts
@@ -0,0 +1,226 @@
+import { describe, expect, test } from 'bun:test';
+import {
+ type CompiledQuery,
+ type PaymeshDatabaseDriver,
+ resolveDatabaseSchema,
+} from 'paymesh';
+import { dodo } from '../src';
+
+describe('dodo dashboard adapter', () => {
+ test('syncs customer, payment, pix-shaped payment, and subscriptions', async () => {
+ let syncedCustomerId: string | undefined;
+ let syncedInvoiceId: string | undefined;
+ let syncedPixId: string | undefined;
+ let syncedSubscriptionId: string | undefined;
+ const provider = dodo({
+ apiKey: 'dodo_test_123',
+ baseUrl: 'https://test.dodopayments.com',
+ fetch: (async (input) => {
+ if (String(input).endsWith('/customers/cus_123')) {
+ return Response.json({
+ customer_id: 'cus_123',
+ email: 'ana@example.com',
+ name: 'Ana',
+ metadata: {
+ externalId: 'user_123',
+ },
+ });
+ }
+
+ if (String(input).endsWith('/payments/pay_123')) {
+ return Response.json({
+ payment_id: 'pay_123',
+ total_amount: 4200,
+ currency: 'USD',
+ customer: {
+ customer_id: 'cus_123',
+ email: 'ana@example.com',
+ name: 'Ana',
+ },
+ metadata: {},
+ payment_link: 'https://pay.dodo.test/pay_123',
+ status: 'succeeded',
+ });
+ }
+
+ if (String(input).endsWith('/payments/pay_pix_123')) {
+ return Response.json({
+ payment_id: 'pay_pix_123',
+ total_amount: 3100,
+ currency: 'BRL',
+ customer: {
+ customer_id: 'cus_123',
+ email: 'ana@example.com',
+ name: 'Ana',
+ },
+ metadata: {},
+ payment_link: 'https://pay.dodo.test/pay_pix_123',
+ payment_method_type: 'pix',
+ status: 'processing',
+ });
+ }
+
+ if (String(input).endsWith('/subscriptions/sub_123')) {
+ return Response.json({
+ subscription_id: 'sub_123',
+ product_id: 'prod_123',
+ recurring_pre_tax_amount: 3900,
+ currency: 'USD',
+ status: 'active',
+ cancel_at_next_billing_date: false,
+ customer: {
+ customer_id: 'cus_123',
+ email: 'ana@example.com',
+ name: 'Ana',
+ },
+ metadata: {},
+ });
+ }
+
+ return new Response('not found', { status: 404 });
+ }) as typeof fetch,
+ });
+ const database = createDatabaseStub({
+ onCustomerUpsert(id) {
+ syncedCustomerId = id;
+ },
+ onInvoiceUpsert(id) {
+ syncedInvoiceId = id;
+ },
+ onPixUpsert(id) {
+ syncedPixId = id;
+ },
+ onSubscriptionUpsert(id) {
+ syncedSubscriptionId = id;
+ },
+ });
+
+ const customer = await provider.dashboard?.syncCustomer?.({
+ database,
+ id: 'cus_123',
+ schema: createSchema(),
+ });
+ const payment = await provider.dashboard?.syncPayment?.({
+ database,
+ id: 'pay_123',
+ schema: createSchema(),
+ });
+ const pix = await provider.dashboard?.syncPayment?.({
+ database,
+ id: 'pay_pix_123',
+ schema: createSchema(),
+ });
+ const subscription = await provider.dashboard?.syncSubscription?.({
+ database,
+ id: 'sub_123',
+ schema: createSchema(),
+ });
+
+ expect(
+ provider.dashboard?.getResourceUrl?.({ type: 'payment', id: 'pay_123' }),
+ ).toBeNull();
+ expect(customer?.id).toBe('cus_123');
+ expect(payment?.id).toBe('pay_123');
+ expect(pix).toMatchObject({
+ id: 'pay_pix_123',
+ method: 'pix',
+ status: 'processing',
+ });
+ expect(subscription?.id).toBe('sub_123');
+ expect(syncedCustomerId).toBe('cus_123');
+ expect(syncedInvoiceId).toBe('pay_pix_123');
+ expect(syncedPixId).toBe('pay_pix_123');
+ expect(syncedSubscriptionId).toBe('sub_123');
+ });
+});
+
+function createDatabaseStub(options: {
+ onCustomerUpsert(id: string): void;
+ onInvoiceUpsert(id: string): void;
+ onPixUpsert(id: string): void;
+ onSubscriptionUpsert(id: string): void;
+}) {
+ return {
+ id: 'stub-db',
+ type: 'database',
+ dialect: 'postgres',
+ persistRaw: true,
+ repositories: {
+ customers: {
+ async findByProviderId() {
+ return null;
+ },
+ async upsert(_schema: unknown, customer: { id: string }) {
+ options.onCustomerUpsert(customer.id);
+ },
+ async list() {
+ return { data: [], next: null, previous: null, total: 0 };
+ },
+ async markDeleted() {},
+ },
+ pix: {
+ async findByProviderId() {
+ return null;
+ },
+ async upsert(_schema: unknown, pix: { id: string }) {
+ options.onPixUpsert(pix.id);
+ },
+ },
+ checkouts: {
+ async findByProviderId() {
+ return null;
+ },
+ async upsert() {},
+ },
+ invoices: {
+ async findByProviderId() {
+ return null;
+ },
+ async upsert(_schema: unknown, payment: { id: string }) {
+ options.onInvoiceUpsert(payment.id);
+ },
+ },
+ subscriptions: {
+ async findByProviderId() {
+ return null;
+ },
+ async upsert(_schema: unknown, event: { data?: { id?: string } }) {
+ options.onSubscriptionUpsert(event.data?.id ?? 'missing');
+ },
+ },
+ webhookEvents: {
+ async acquire() {
+ return { duplicate: false };
+ },
+ async markProcessed() {},
+ async markFailed() {},
+ },
+ products: {
+ async upsertMany() {},
+ },
+ prices: {
+ async upsertMany() {},
+ },
+ migrations: {
+ async ensureTable() {},
+ async listApplied() {
+ return [];
+ },
+ async recordApplied() {},
+ },
+ },
+ async query(_query: CompiledQuery) {
+ return [] as Row[];
+ },
+ async execute(_query: CompiledQuery) {},
+ async transaction(
+ callback: (database: PaymeshDatabaseDriver) => Promise,
+ ) {
+ return callback(this as unknown as PaymeshDatabaseDriver);
+ },
+ } as unknown as PaymeshDatabaseDriver;
+}
+
+function createSchema() {
+ return resolveDatabaseSchema();
+}
diff --git a/packages/dodo/test/dodo.test.ts b/packages/dodo/test/dodo.test.ts
new file mode 100644
index 0000000..cdd221d
--- /dev/null
+++ b/packages/dodo/test/dodo.test.ts
@@ -0,0 +1,250 @@
+import { describe, expect, test } from 'bun:test';
+import { createClient } from 'paymesh';
+import { dodo } from '../src';
+
+function expectType(_value: T) {}
+
+describe('dodo provider', () => {
+ test('creates a hosted payment link using Dodo product carts', async () => {
+ const provider = dodo({
+ apiKey: 'dodo_test_123',
+ baseUrl: 'https://test.dodopayments.com',
+ fetch: (async (input, init) => {
+ expect(String(input)).toBe('https://test.dodopayments.com/payments');
+ expect(init?.method).toBe('POST');
+ expect(init?.headers).toEqual({
+ authorization: 'Bearer dodo_test_123',
+ 'content-type': 'application/json',
+ });
+
+ const body = JSON.parse(String(init?.body));
+ expect(body).toEqual({
+ billing: {
+ country: 'BR',
+ },
+ customer: {
+ email: 'ana@example.com',
+ name: 'Ana',
+ phone_number: '+5511999999999',
+ },
+ product_cart: [{ product_id: 'prod_123', quantity: 1, amount: 4900 }],
+ allowed_payment_method_types: ['pix', 'credit', 'debit'],
+ billing_currency: 'BRL',
+ metadata: {
+ externalId: 'user_123',
+ orderId: 'order_123',
+ },
+ payment_link: true,
+ return_url: 'https://app.test/success',
+ show_saved_payment_methods: false,
+ });
+
+ return Response.json({
+ payment_id: 'pay_123',
+ total_amount: 4900,
+ currency: 'BRL',
+ payment_link: 'https://pay.dodo.test/pay_123',
+ metadata: {
+ externalId: 'user_123',
+ orderId: 'order_123',
+ },
+ customer: {
+ customer_id: 'cus_123',
+ email: 'ana@example.com',
+ name: 'Ana',
+ phone_number: '+5511999999999',
+ metadata: {
+ externalId: 'user_123',
+ },
+ },
+ status: 'processing',
+ });
+ }) as typeof fetch,
+ });
+
+ const payment = await provider.payments.create({
+ amount: 4900,
+ currency: 'BRL',
+ productIds: ['prod_123'],
+ customer: {
+ email: 'ana@example.com',
+ name: 'Ana',
+ phone: '+5511999999999',
+ externalId: 'user_123',
+ },
+ successUrl: 'https://app.test/success',
+ metadata: {
+ externalId: 'user_123',
+ orderId: 'order_123',
+ },
+ });
+
+ expect(payment).toMatchObject({
+ id: 'pay_123',
+ provider: 'dodo',
+ sandbox: true,
+ amount: 4900,
+ currency: 'brl',
+ status: 'processing',
+ checkoutUrl: 'https://pay.dodo.test/pay_123',
+ customer: {
+ id: 'cus_123',
+ email: 'ana@example.com',
+ name: 'Ana',
+ externalId: 'user_123',
+ },
+ metadata: {
+ externalId: 'user_123',
+ orderId: 'order_123',
+ },
+ });
+ expect(payment.raw).toBeNull();
+ });
+
+ test('uses client request options and supports raw payload toggles', async () => {
+ let attempts = 0;
+ let timeoutSignal: AbortSignal | undefined;
+ const provider = dodo({
+ apiKey: 'dodo_test_123',
+ });
+ const client = createClient({
+ provider,
+ baseUrl: 'https://dodo.client.test',
+ timeout: 1234,
+ retry: {
+ max: 1,
+ },
+ fetch: (async (input, init) => {
+ attempts += 1;
+ timeoutSignal = init?.signal as AbortSignal;
+
+ expect(String(input)).toBe('https://dodo.client.test/payments');
+
+ if (attempts === 1) {
+ return new Response('busy', { status: 500 });
+ }
+
+ return Response.json({
+ payment_id: 'pay_retry',
+ total_amount: 1200,
+ currency: 'USD',
+ customer: {
+ customer_id: 'cus_retry',
+ email: 'retry@example.com',
+ name: 'Retry',
+ },
+ metadata: {},
+ payment_link: 'https://pay.dodo.test/retry',
+ status: 'succeeded',
+ });
+ }) as typeof fetch,
+ });
+ const rawClient = createClient({ provider, includeRaw: true });
+
+ const payment = await client.payments.create({
+ productIds: ['prod_123'],
+ customer: {
+ email: 'retry@example.com',
+ },
+ });
+ const rawPayment = await rawClient.payments.create(
+ {
+ productIds: ['prod_123'],
+ customer: {
+ email: 'raw@example.com',
+ },
+ },
+ {
+ includeRaw: true,
+ baseUrl: 'https://dodo.raw.test',
+ fetch: (async () =>
+ Response.json({
+ payment_id: 'pay_raw',
+ total_amount: 900,
+ currency: 'USD',
+ customer: {
+ customer_id: 'cus_raw',
+ email: 'raw@example.com',
+ name: 'Raw',
+ },
+ metadata: {},
+ payment_link: 'https://pay.dodo.test/raw',
+ status: 'processing',
+ })) as unknown as typeof fetch,
+ },
+ );
+
+ expect(attempts).toBe(2);
+ expect(timeoutSignal).toBeInstanceOf(AbortSignal);
+ expect(payment.status).toBe('paid');
+ expect(payment.raw).toBeNull();
+ expectType(rawPayment.raw);
+ expect(rawPayment.raw).toMatchObject({
+ payment_id: 'pay_raw',
+ });
+ });
+
+ test('requires productIds and customer identity', async () => {
+ const provider = dodo({
+ apiKey: 'dodo_test_123',
+ });
+
+ expect(
+ provider.payments.create({
+ customer: {
+ email: 'ana@example.com',
+ },
+ }),
+ ).rejects.toMatchObject({
+ code: 'invalid_request',
+ message:
+ 'Provider "dodo" requires at least one product id in "productIds"',
+ });
+
+ expect(
+ provider.payments.create({
+ productIds: ['prod_1', 'prod_2'],
+ amount: 1000,
+ customer: {
+ email: 'ana@example.com',
+ },
+ }),
+ ).rejects.toMatchObject({
+ code: 'invalid_request',
+ message:
+ 'Provider "dodo" only accepts "amount" when exactly one product id is provided.',
+ });
+
+ expect(
+ provider.payments.create({
+ productIds: ['prod_123'],
+ }),
+ ).rejects.toMatchObject({
+ code: 'invalid_request',
+ message:
+ 'Provider "dodo" requires either "customer.id" or "customer.email" when creating payments.',
+ });
+ });
+
+ test('infers sandbox from the configured base URL', () => {
+ expect(dodo({ baseUrl: 'https://test.dodopayments.com' }).isSandbox()).toBe(
+ true,
+ );
+ expect(dodo({ baseUrl: 'https://live.dodopayments.com' }).isSandbox()).toBe(
+ false,
+ );
+ });
+
+ test('advertises the supported Dodo capability surface', () => {
+ expect(dodo().capabilities).toEqual({
+ checkout: true,
+ pix: false,
+ coupons: false,
+ refunds: false,
+ subscriptions: true,
+ webhooks: true,
+ customerPortal: false,
+ customers: true,
+ });
+ });
+});
diff --git a/packages/dodo/test/webhooks.test.ts b/packages/dodo/test/webhooks.test.ts
new file mode 100644
index 0000000..d9d7211
--- /dev/null
+++ b/packages/dodo/test/webhooks.test.ts
@@ -0,0 +1,203 @@
+import { describe, expect, test } from 'bun:test';
+import { dodo } from '../src';
+import { signDodoWebhook } from '../src/shared/utils';
+
+describe('dodo webhooks', () => {
+ test('verifies valid webhook signatures and handles payment events', async () => {
+ const provider = dodo({
+ webhookSecret: 'whsec_c2VjcmV0X2Rlc2lnbg==',
+ baseUrl: 'https://test.dodopayments.com',
+ });
+ const request = dodoWebhookRequest(
+ {
+ business_id: 'biz_123',
+ type: 'payment.succeeded',
+ timestamp: new Date().toISOString(),
+ data: {
+ payment_id: 'pay_123',
+ total_amount: 1200,
+ currency: 'USD',
+ customer: {
+ customer_id: 'cus_123',
+ email: 'ana@example.com',
+ name: 'Ana',
+ metadata: {
+ externalId: 'user_123',
+ },
+ },
+ metadata: {},
+ payment_link: 'https://pay.dodo.test/pay_123',
+ status: 'succeeded',
+ },
+ },
+ 'whsec_c2VjcmV0X2Rlc2lnbg==',
+ );
+
+ const valid = await provider.webhooks?.verify({
+ request: request.clone(),
+ });
+ const handled = await provider.webhooks?.handle({
+ request,
+ includeRaw: true,
+ });
+
+ expect(valid).toBe(true);
+ expect(handled?.deliveryId).toBe('msg_123');
+ expect(handled?.hook).toBe('onPaymentSucceeded');
+ expect(handled?.event).toMatchObject({
+ id: 'pay_123',
+ type: 'payment.succeeded',
+ provider: 'dodo',
+ sandbox: true,
+ data: {
+ id: 'pay_123',
+ status: 'paid',
+ customer: {
+ id: 'cus_123',
+ externalId: 'user_123',
+ },
+ raw: {
+ payment_id: 'pay_123',
+ },
+ },
+ raw: {
+ type: 'payment.succeeded',
+ },
+ });
+ });
+
+ test('maps Pix-shaped payment and subscription cancellation webhooks', async () => {
+ const provider = dodo({
+ webhookSecret: 'whsec_c2VjcmV0X2Rlc2lnbg==',
+ baseUrl: 'https://test.dodopayments.com',
+ });
+ const pixHandled = await provider.webhooks?.handle({
+ request: new Request('https://app.test/webhooks', {
+ method: 'POST',
+ headers: {
+ 'content-type': 'application/json',
+ 'webhook-id': 'msg_pix',
+ },
+ body: JSON.stringify({
+ business_id: 'biz_123',
+ type: 'payment.processing',
+ timestamp: new Date().toISOString(),
+ data: {
+ payment_id: 'pay_pix_123',
+ total_amount: 3100,
+ currency: 'BRL',
+ customer: {
+ customer_id: 'cus_123',
+ email: 'ana@example.com',
+ name: 'Ana',
+ },
+ metadata: {},
+ payment_link: 'https://pay.dodo.test/pay_pix_123',
+ payment_method_type: 'pix',
+ status: 'processing',
+ },
+ }),
+ }),
+ });
+ const subscriptionHandled = await provider.webhooks?.handle({
+ request: new Request('https://app.test/webhooks', {
+ method: 'POST',
+ headers: {
+ 'content-type': 'application/json',
+ 'webhook-id': 'msg_sub',
+ },
+ body: JSON.stringify({
+ business_id: 'biz_123',
+ type: 'subscription.cancelled',
+ timestamp: new Date().toISOString(),
+ data: {
+ subscription_id: 'sub_123',
+ product_id: 'prod_123',
+ recurring_pre_tax_amount: 3900,
+ currency: 'USD',
+ status: 'cancelled',
+ cancel_at_next_billing_date: true,
+ customer: {
+ customer_id: 'cus_123',
+ email: 'ana@example.com',
+ name: 'Ana',
+ },
+ metadata: {},
+ },
+ }),
+ }),
+ });
+
+ expect(pixHandled?.event).toMatchObject({
+ type: 'payment.created',
+ data: {
+ id: 'pay_pix_123',
+ method: 'pix',
+ status: 'processing',
+ },
+ });
+ expect(pixHandled?.hook).toBe('onPaymentCreated');
+ expect(subscriptionHandled?.event).toMatchObject({
+ id: 'sub_123',
+ type: 'subscription.canceled',
+ data: {
+ id: 'sub_123',
+ status: 'cancelled',
+ },
+ });
+ expect(subscriptionHandled?.hook).toBe('onSubscriptionCanceled');
+ });
+
+ test('rejects invalid webhook signatures', async () => {
+ const provider = dodo({
+ webhookSecret: 'whsec_c2VjcmV0X2Rlc2lnbg==',
+ });
+ const valid = await provider.webhooks?.verify({
+ request: dodoWebhookRequest(
+ {
+ business_id: 'biz_123',
+ type: 'payment.failed',
+ timestamp: new Date().toISOString(),
+ data: {
+ payment_id: 'pay_123',
+ total_amount: 1200,
+ currency: 'USD',
+ customer: {
+ customer_id: 'cus_123',
+ email: 'ana@example.com',
+ name: 'Ana',
+ },
+ metadata: {},
+ status: 'failed',
+ },
+ },
+ 'whsec_d3Jvbmdfa2V5',
+ ),
+ });
+
+ expect(valid).toBe(false);
+ });
+});
+
+function dodoWebhookRequest(payload: unknown, secret: string) {
+ const body = JSON.stringify(payload);
+ const webhookId = 'msg_123';
+ const timestamp = String(Math.floor(Date.now() / 1000));
+ const signature = signDodoWebhook({
+ payload: body,
+ secret,
+ timestamp,
+ webhookId,
+ });
+
+ return new Request('https://app.test/webhooks', {
+ method: 'POST',
+ headers: {
+ 'content-type': 'application/json',
+ 'webhook-id': webhookId,
+ 'webhook-signature': signature,
+ 'webhook-timestamp': timestamp,
+ },
+ body,
+ });
+}
diff --git a/packages/dodo/tsconfig.json b/packages/dodo/tsconfig.json
new file mode 100644
index 0000000..d6b0121
--- /dev/null
+++ b/packages/dodo/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "paymesh": ["../paymesh/src/index.ts"]
+ },
+ "types": ["bun"]
+ },
+ "include": ["src/**/*.ts", "test/**/*.ts"]
+}
diff --git a/packages/dodo/tsdown.config.mjs b/packages/dodo/tsdown.config.mjs
new file mode 100644
index 0000000..21f6bf4
--- /dev/null
+++ b/packages/dodo/tsdown.config.mjs
@@ -0,0 +1,12 @@
+import { defineConfig } from 'tsdown';
+
+export default defineConfig({
+ entry: ['./src/index.ts'],
+ format: ['esm', 'cjs'],
+ dts: true,
+ clean: true,
+ sourcemap: false,
+ minify: true,
+ target: 'node20',
+ outDir: 'dist',
+});
diff --git a/packages/paymesh/README.md b/packages/paymesh/README.md
index 4371b6e..9ac456c 100644
--- a/packages/paymesh/README.md
+++ b/packages/paymesh/README.md
@@ -29,6 +29,10 @@ npm install paymesh @paymesh/stripe @paymesh/postgres
For tests, CI, and local examples, use @paymesh/memory instead of a production database adapter.
+
+ Official provider packages currently include @paymesh/stripe, @paymesh/polar, @paymesh/abacatepay, and @paymesh/dodo.
+
+
Quickstart
@@ -77,6 +81,10 @@ console.log(payment.checkoutUrl, customer.email);
For native PIX flows, use paymesh.pix. This is separate from paymesh.payments so providers can expose QR code, copia-e-cola, and expiration details directly.
+
+ @paymesh/dodo supports hosted BRL checkout links that can offer Pix inside the redirect flow, but it intentionally does not expose paymesh.pix because Dodo does not currently document a native QR-code-first backend PIX flow.
+
+
```ts
const pix = await paymesh.pix.create({
amount: 3500,