# Astro Adaptor Source: https://docs.dodopayments.com/developer-resources/astro-adaptor Learn how to integrate Dodo Payments with your Astro App Router project using our Astro Adaptor. Covers checkout, customer portal, webhooks, and secure environment setup. Integrate Dodo Payments checkout into your Astro app. Allow customers to manage subscriptions and details. Receive and process Dodo Payments webhook events. ## Installation Run the following command in your project root: ```bash theme={null} npm install @dodopayments/astro ``` Create a .env file in your project root: ```env expandable theme={null} DODO_PAYMENTS_API_KEY=your-api-key DODO_PAYMENTS_WEBHOOK_KEY=your-webhook-secret DODO_PAYMENTS_ENVIRONMENT="test_mode" or "live_mode" DODO_PAYMENTS_RETURN_URL=https://yourdomain.com/checkout/success ``` Never commit your .env file or secrets to version control. ## Route Handler Examples All examples assume you are using the Astro App Router. Use this handler to integrate Dodo Payments checkout into your Astro app. Supports static (GET), dynamic (POST), and session (POST) payment flows. ```typescript Astro Route Handler expandable theme={null} // src/pages/api/checkout.ts import { Checkout } from "@dodopayments/astro"; export const prerender = false; export const GET = Checkout({ bearerToken: import.meta.env.DODO_PAYMENTS_API_KEY, returnUrl: import.meta.env.DODO_PAYMENTS_RETURN_URL, environment: import.meta.env.DODO_PAYMENTS_ENVIRONMENT, type: "static", // optional, defaults to 'static' }); export const POST = Checkout({ bearerToken: import.meta.env.DODO_PAYMENTS_API_KEY, returnUrl: import.meta.env.DODO_PAYMENTS_RETURN_URL, environment: import.meta.env.DODO_PAYMENTS_ENVIRONMENT, type: "dynamic", // for dynamic checkout }); export const POST = Checkout({ bearerToken: import.meta.env.DODO_PAYMENTS_API_KEY, returnUrl: import.meta.env.DODO_PAYMENTS_RETURN_URL, environment: import.meta.env.DODO_PAYMENTS_ENVIRONMENT, type: "session", // for checkout sessions }); ``` ```curl Static Checkout Curl example expandable theme={null} curl --request GET \ --url 'https://example.com/api/checkout?productId=pdt_fqJhl7pxKWiLhwQR042rh' \ --header 'User-Agent: insomnia/11.2.0' \ --cookie mode=test ``` ```curl Dynamic Checkout Curl example expandable theme={null} curl --request POST \ --url https://example.com/api/checkout \ --header 'Content-Type: application/json' \ --header 'User-Agent: insomnia/11.2.0' \ --cookie mode=test \ --data '{ "billing": { "city": "Texas", "country": "US", "state": "Texas", "street": "56, hhh", "zipcode": "560000" }, "customer": { "email": "test@example.com", "name": "test" }, "metadata": {}, "payment_link": true, "product_id": "pdt_QMDuvLkbVzCRWRQjLNcs", "quantity": 1, "billing_currency": "USD", "discount_codes": ["IKHZ23M9GQ"], "return_url": "https://example.com", "trial_period_days": 10 }' ``` ```curl Checkout Session Curl example expandable theme={null} curl --request POST \ --url https://example.com/api/checkout \ --header 'Content-Type: application/json' \ --header 'User-Agent: insomnia/11.2.0' \ --cookie mode=test \ --data '{ "product_cart": [ { "product_id": "pdt_QMDuvLkbVzCRWRQjLNcs", "quantity": 1 } ], "customer": { "email": "test@example.com", "name": "test" }, "return_url": "https://example.com/success" }' ``` Use this handler to allow customers to manage their subscriptions and details via the Dodo Payments customer portal. ```typescript Astro Route Handler expandable theme={null} // src/pages/api/customer-portal.ts import { CustomerPortal } from "@dodopayments/astro"; export const GET = CustomerPortal({ bearerToken: import.meta.env.DODO_PAYMENTS_API_KEY, environment: import.meta.env.DODO_PAYMENTS_ENVIRONMENT, }); ``` ```curl Customer Portal Curl example expandable theme={null} curl --request GET \ --url 'https://example.com/api/customer-portal?customer_id=cus_9VuW4K7O3GHwasENg31m&send_email=true' \ --header 'User-Agent: insomnia/11.2.0' \ --cookie mode=test ``` Use this handler to receive and process Dodo Payments webhook events securely in your Astro app. ```typescript expandable theme={null} // src/pages/api/webhook.ts import { Webhooks } from "@dodopayments/astro"; export const POST = Webhooks({ webhookKey: import.meta.env.DODO_PAYMENTS_WEBHOOK_KEY, onPayload: async (payload) => { // handle the payload }, // ... other event handlers for granular control }); ``` ## Checkout Route Handler Dodo Payments supports three types of payment flows for integrating payments into your website, this adaptor supports all types of payment flows. * **Static Payment Links:** Instantly shareable URLs for quick, no-code payment collection. * **Dynamic Payment Links:** Programmatically generate payment links with custom details using the API or SDKs. * **Checkout Sessions:** Create secure, customizable checkout experiences with pre-configured product carts and customer details. ### Supported Query Parameters Product identifier (e.g., ?productId=pdt\_nZuwz45WAs64n3l07zpQR). Quantity of the product. Customer's full name. Customer's first name. Customer's last name. Customer's email address. Customer's country. Customer's address line. Customer's city. Customer's state/province. Customer's zip/postal code. Disable full name field. Disable first name field. Disable last name field. Disable email field. Disable country field. Disable address line field. Disable city field. Disable state field. Disable zip code field. Specify the payment currency (e.g., USD). Show currency selector. Fixes the amount charged, in major currency units (e.g., 12.5 for \$12.50). Pay What You Want products only, and ignored if below the product's minimum price. Show discount fields. Any query parameter starting with metadata\_ will be passed as metadata. If productId is missing, the handler returns a 400 response. Invalid query parameters also result in a 400 response. ### Response Format Static checkout returns a JSON response with the checkout URL: ```json theme={null} { "checkout_url": "https://checkout.dodopayments.com/..." } ``` * Send parameters as a JSON body in a POST request. * Supports both one-time and recurring payments. * For a complete list of supported POST body fields, refer to: * [Request body for a One Time Payment Product](https://docs.dodopayments.com/api-reference/payments/post-payments) * [Request body for a Subscription Product](https://docs.dodopayments.com/api-reference/subscriptions/post-subscriptions) ### Response Format Dynamic checkout returns a JSON response with the checkout URL: ```json theme={null} { "checkout_url": "https://checkout.dodopayments.com/..." } ``` Checkout sessions provide a more secure, hosted checkout experience that handles the complete payment flow for both one-time purchases and subscriptions with full customization control. Refer to [Checkout Sessions Integration Guide](https://docs.dodopayments.com/developer-resources/checkout-session) for more details and a complete list of supported fields. ### Response Format Checkout sessions return a JSON response with the checkout URL: ```json theme={null} { "checkout_url": "https://checkout.dodopayments.com/session/..." } ``` ## Customer Portal Route Handler The Customer Portal Route Handler enables you to seamlessly integrate the Dodo Payments customer portal into your Astro application. ### Query Parameters The customer ID for the portal session (e.g., ?customer\_id=cus\_123). If set to true, sends an email to the customer with the portal link. Returns 400 if customer\_id is missing. ## Webhook Route Handler * **Method:** Only POST requests are supported. Other methods return 405. * **Signature Verification:** Verifies the webhook signature using webhookKey. Returns 401 if verification fails. * **Payload Validation:** Validated with Zod. Returns 400 for invalid payloads. * **Error Handling:** * 401: Invalid signature * 400: Invalid payload * 500: Internal error during verification * **Event Routing:** Calls the appropriate event handler based on the payload type. ### Supported Webhook Event Handlers ```typescript Typescript expandable theme={null} onPayload?: (payload: WebhookPayload) => Promise; onPaymentSucceeded?: (payload: WebhookPayload) => Promise; onPaymentFailed?: (payload: WebhookPayload) => Promise; onPaymentProcessing?: (payload: WebhookPayload) => Promise; onPaymentCancelled?: (payload: WebhookPayload) => Promise; onRefundSucceeded?: (payload: WebhookPayload) => Promise; onRefundFailed?: (payload: WebhookPayload) => Promise; onDisputeOpened?: (payload: WebhookPayload) => Promise; onDisputeExpired?: (payload: WebhookPayload) => Promise; onDisputeAccepted?: (payload: WebhookPayload) => Promise; onDisputeCancelled?: (payload: WebhookPayload) => Promise; onDisputeChallenged?: (payload: WebhookPayload) => Promise; onDisputeWon?: (payload: WebhookPayload) => Promise; onDisputeLost?: (payload: WebhookPayload) => Promise; onSubscriptionActive?: (payload: WebhookPayload) => Promise; onSubscriptionOnHold?: (payload: WebhookPayload) => Promise; onSubscriptionRenewed?: (payload: WebhookPayload) => Promise; onSubscriptionPlanChanged?: (payload: WebhookPayload) => Promise; onSubscriptionCancelled?: (payload: WebhookPayload) => Promise; onSubscriptionFailed?: (payload: WebhookPayload) => Promise; onSubscriptionExpired?: (payload: WebhookPayload) => Promise; onSubscriptionUpdated?: (payload: WebhookPayload) => Promise; onLicenseKeyCreated?: (payload: WebhookPayload) => Promise; onAbandonedCheckoutDetected?: (payload: WebhookPayload) => Promise; onAbandonedCheckoutRecovered?: (payload: WebhookPayload) => Promise; onDunningStarted?: (payload: WebhookPayload) => Promise; onDunningRecovered?: (payload: WebhookPayload) => Promise; onCreditAdded?: (payload: WebhookPayload) => Promise; onCreditDeducted?: (payload: WebhookPayload) => Promise; onCreditExpired?: (payload: WebhookPayload) => Promise; onCreditRolledOver?: (payload: WebhookPayload) => Promise; onCreditRolloverForfeited?: (payload: WebhookPayload) => Promise; onCreditOverageCharged?: (payload: WebhookPayload) => Promise; onCreditManualAdjustment?: (payload: WebhookPayload) => Promise; onCreditBalanceLow?: (payload: WebhookPayload) => Promise; ``` *** ## Prompt for LLM ``` You are an expert Astro developer assistant. Your task is to guide a user through integrating the @dodopayments/astro adapter into their existing Astro project. The @dodopayments/astro adapter provides route handlers for Dodo Payments' Checkout, Customer Portal, and Webhook functionalities, designed for the Astro App Router. First, install the necessary packages. Use the package manager appropriate for your project (npm, yarn, or bun) based on the presence of lock files (e.g., package-lock.json for npm, yarn.lock for yarn, bun.lockb for bun): npm install @dodopayments/astro Here's how you should structure your response: Ask the user which functionalities they want to integrate. "Which parts of the @dodopayments/astro adapter would you like to integrate into your project? You can choose one or more of the following: Checkout Route Handler (for handling product checkouts) Customer Portal Route Handler (for managing customer subscriptions/details) Webhook Route Handler (for receiving Dodo Payments webhook events) All (integrate all three)" Based on the user's selection, provide detailed integration steps for each chosen functionality. If Checkout Route Handler is selected: Purpose: This handler redirects users to the Dodo Payments checkout page. File Creation: Create a new file at app/checkout/route.ts in your Astro project. Code Snippet: // src/pages/api/checkout.ts import { Checkout } from "@dodopayments/astro"; export const prerender = false; export const GET = Checkout({ bearerToken: import.meta.env.DODO_PAYMENTS_API_KEY, returnUrl: import.meta.env.DODO_PAYMENTS_RETURN_URL, environment: import.meta.env.DODO_PAYMENTS_ENVIRONMENT, type: "static", // optional, defaults to 'static' }); export const POST = Checkout({ bearerToken: import.meta.env.DODO_PAYMENTS_API_KEY, returnUrl: import.meta.env.DODO_PAYMENTS_RETURN_URL, environment: import.meta.env.DODO_PAYMENTS_ENVIRONMENT, type: "dynamic", // for dynamic checkout }); export const POST = Checkout({ bearerToken: import.meta.env.DODO_PAYMENTS_API_KEY, returnUrl: import.meta.env.DODO_PAYMENTS_RETURN_URL, environment: import.meta.env.DODO_PAYMENTS_ENVIRONMENT, type: "session", // for checkout sessions }); Configuration & Usage: bearerToken: Your Dodo Payments API key. It's recommended to set this via the DODO_PAYMENTS_API_KEY environment variable. returnUrl: (Optional) The URL to redirect the user to after a successful checkout. environment: (Optional) Set to "test_mode" for testing, or omit/set to "live_mode" for production. type: (Optional) Set to "static" for GET/static checkout, "dynamic" for POST/dynamic checkout, or "session" for POST/checkout sessions. Static Checkout (GET) Query Parameters: productId (required): Product identifier (e.g., ?productId=pdt_nZuwz45WAs64n3l07zpQR) quantity (optional): Quantity of the product Customer Fields (optional): fullName, firstName, lastName, email, country, addressLine, city, state, zipCode Disable Flags (optional, set to true to disable): disableFullName, disableFirstName, disableLastName, disableEmail, disableCountry, disableAddressLine, disableCity, disableState, disableZipCode Advanced Controls (optional): paymentCurrency, showCurrencySelector, paymentAmount, showDiscounts Metadata (optional): Any query parameter starting with metadata_ (e.g., ?metadata_userId=abc123) Returns: {"checkout_url": "https://checkout.dodopayments.com/..."} Dynamic Checkout (POST) - Returns JSON with checkout_url: Parameters are sent as a JSON body. Supports both one-time and recurring payments. Returns: {"checkout_url": "https://checkout.dodopayments.com/..."}. For a complete list of supported POST body fields, refer to: Docs - One Time Payment Product: https://docs.dodopayments.com/api-reference/payments/post-payments Docs - Subscription Product: https://docs.dodopayments.com/api-reference/subscriptions/post-subscriptions Checkout Sessions (POST) - (Recommended) A more customizable checkout experience. Returns JSON with checkout_url: Parameters are sent as a JSON body. Supports both one-time and recurring payments. Returns: {"checkout_url": "https://checkout.dodopayments.com/session/..."}. For a complete list of supported fields, refer to: Checkout Sessions Integration Guide: https://docs.dodopayments.com/developer-resources/checkout-session Error Handling: If productId is missing or other query parameters are invalid, the handler will return a 400 response. If Customer Portal Route Handler is selected: Purpose: This handler redirects authenticated users to their Dodo Payments customer portal. File Creation: Create a new file at app/customer-portal/route.ts in your Astro project. Code Snippet: // src/pages/api/customer-portal.ts import { CustomerPortal } from "@dodopayments/astro"; export const GET = CustomerPortal({ bearerToken: import.meta.env.DODO_PAYMENTS_API_KEY, environment: import.meta.env.DODO_PAYMENTS_ENVIRONMENT, }); Query Parameters: customer_id (required): The customer ID for the portal session (e.g., ?customer_id=cus_123) send_email (optional, boolean): If set to true, sends an email to the customer with the portal link. Returns 400 if customer_id is missing. If Webhook Route Handler is selected: Purpose: This handler processes incoming webhook events from Dodo Payments, allowing your application to react to events like successful payments, refunds, or subscription changes. File Creation: Create a new file at app/api/webhook/dodo-payments/route.ts in your Astro project. Code Snippet: // src/pages/api/webhook.ts import { Webhooks } from "@dodopayments/astro"; export const POST = Webhooks({ webhookKey: import.meta.env.DODO_PAYMENTS_WEBHOOK_KEY, onPayload: async (payload) => { // handle the payload }, // ... other event handlers for granular control }); Handler Details: Method: Only POST requests are supported. Other methods return 405. Signature Verification: The handler verifies the webhook signature using the webhookKey and returns 401 if verification fails. Payload Validation: The payload is validated with Zod. Returns 400 for invalid payloads. Error Handling: 401: Invalid signature 400: Invalid payload 500: Internal error during verification Event Routing: Calls the appropriate event handler based on the payload type. Supported Webhook Event Handlers: onPayload?: (payload: WebhookPayload) => Promise onPaymentSucceeded?: (payload: WebhookPayload) => Promise onPaymentFailed?: (payload: WebhookPayload) => Promise onPaymentProcessing?: (payload: WebhookPayload) => Promise onPaymentCancelled?: (payload: WebhookPayload) => Promise onRefundSucceeded?: (payload: WebhookPayload) => Promise onRefundFailed?: (payload: WebhookPayload) => Promise onDisputeOpened?: (payload: WebhookPayload) => Promise onDisputeExpired?: (payload: WebhookPayload) => Promise onDisputeAccepted?: (payload: WebhookPayload) => Promise onDisputeCancelled?: (payload: WebhookPayload) => Promise onDisputeChallenged?: (payload: WebhookPayload) => Promise onDisputeWon?: (payload: WebhookPayload) => Promise onDisputeLost?: (payload: WebhookPayload) => Promise onSubscriptionActive?: (payload: WebhookPayload) => Promise onSubscriptionOnHold?: (payload: WebhookPayload) => Promise onSubscriptionRenewed?: (payload: WebhookPayload) => Promise onSubscriptionPlanChanged?: (payload: WebhookPayload) => Promise onSubscriptionCancelled?: (payload: WebhookPayload) => Promise onSubscriptionFailed?: (payload: WebhookPayload) => Promise onSubscriptionExpired?: (payload: WebhookPayload) => Promise onSubscriptionUpdated?: (payload: WebhookPayload) => Promise onLicenseKeyCreated?: (payload: WebhookPayload) => Promise onAbandonedCheckoutDetected?: (payload: WebhookPayload) => Promise onAbandonedCheckoutRecovered?: (payload: WebhookPayload) => Promise onDunningStarted?: (payload: WebhookPayload) => Promise onDunningRecovered?: (payload: WebhookPayload) => Promise onCreditAdded?: (payload: WebhookPayload) => Promise onCreditDeducted?: (payload: WebhookPayload) => Promise onCreditExpired?: (payload: WebhookPayload) => Promise onCreditRolledOver?: (payload: WebhookPayload) => Promise onCreditRolloverForfeited?: (payload: WebhookPayload) => Promise onCreditOverageCharged?: (payload: WebhookPayload) => Promise onCreditManualAdjustment?: (payload: WebhookPayload) => Promise onCreditBalanceLow?: (payload: WebhookPayload) => Promise Environment Variable Setup: To ensure the adapter functions correctly, you will need to manually set up the following environment variables in your Astro project's deployment environment (e.g., Vercel, Netlify, AWS, etc.): DODO_PAYMENTS_API_KEY: Your Dodo Payments API Key (required for Checkout and Customer Portal). RETURN_URL: (Optional) The URL to redirect to after a successful checkout (for Checkout handler). DODO_PAYMENTS_WEBHOOK_SECRET: Your Dodo Payments Webhook Secret (required for Webhook handler). Example .env file: DODO_PAYMENTS_API_KEY=your-api-key DODO_PAYMENTS_WEBHOOK_KEY=your-webhook-secret DODO_PAYMENTS_ENVIRONMENT="test_mode" or "live_mode" DODO_PAYMENTS_RETURN_URL=your-return-url Usage in your code: bearerToken: import.meta.env.DODO_PAYMENTS_API_KEY webhookKey: import.meta.env.DODO_PAYMENTS_WEBHOOK_KEY Important: Never commit sensitive environment variables directly into your version control. Use environment variables for all sensitive information. If the user needs assistance setting up environment variables for their specific deployment environment, ask them what platform they are using (e.g., Vercel, Netlify, AWS, etc.), and provide guidance. You can also add comments to their PR or chat depending on the context ``` # Better-Auth Adapter Source: https://docs.dodopayments.com/developer-resources/better-auth-adaptor This guide shows you how to integrate Dodo Payments into your authentication flow using the Better-Auth adaptor. # Overview The Better Auth Adapter for Dodo Payments provides: * Automatic customer creation on sign-up * Checkout Sessions (preferred) with product slug mapping * Self-service customer portal * Metered usage ingestion and reporting endpoints for usage-based billing * Real-time webhook event processing with signature verification * Full TypeScript support You need a Dodo Payments account and API keys to use this integration. # Prerequisites * Node.js 20+ * Access to your Dodo Payments Dashboard * Existing project using [better-auth](https://www.npmjs.com/package/better-auth) # Installation Run the following command in your project root: ```bash theme={null} npm install @dodopayments/better-auth dodopayments better-auth zod ``` All required packages are now installed. # Setup Add these to your .env file: ```env expandable theme={null} DODO_PAYMENTS_API_KEY=your_api_key_here DODO_PAYMENTS_WEBHOOK_SECRET=your_webhook_secret_here BETTER_AUTH_URL=http://localhost:3000 BETTER_AUTH_SECRET=your_better_auth_secret_here ``` Never commit API keys or secrets to version control. Create or update src/lib/auth.ts: ```typescript expandable theme={null} import { betterAuth } from "better-auth"; import { dodopayments, checkout, portal, webhooks, usage, } from "@dodopayments/better-auth"; import DodoPayments from "dodopayments"; export const dodoPayments = new DodoPayments({ bearerToken: process.env.DODO_PAYMENTS_API_KEY!, environment: "test_mode", // or "live_mode" for production }); export const { auth, endpoints, client } = BetterAuth({ plugins: [ dodopayments({ client: dodoPayments, createCustomerOnSignUp: true, use: [ checkout({ products: [ { productId: "pdt_xxxxxxxxxxxxxxxxxxxxx", slug: "premium-plan", }, ], successUrl: "/dashboard/success", authenticatedUsersOnly: true, }), portal(), webhooks({ webhookKey: process.env.DODO_PAYMENTS_WEBHOOK_SECRET!, onPayload: async (payload) => { console.log("Received webhook:", payload.type); }, }), usage(), ], }), ], }); ``` Set environment to live\_mode for production. Create or update src/lib/auth-client.ts: ```typescript expandable theme={null} import { createAuthClient } from "better-auth/react"; import { dodopaymentsClient } from "@dodopayments/better-auth/client"; export const authClient = createAuthClient({ baseURL: process.env.BETTER_AUTH_URL || "http://localhost:3000", plugins: [dodopaymentsClient()], }); ``` # Usage Examples Prefer authClient.dodopayments.checkoutSession for new integrations. The legacy checkout method is deprecated and kept only for backward compatibility. ## Creating a Checkout Session (Preferred) ```typescript Using a slug theme={null} const { data: session, error } = await authClient.dodopayments.checkoutSession({ // Option A: use a configured slug slug: "premium-plan", // Your internal id for reference referenceId: "order_123", }); if (session) { window.location.href = session.url; } ``` ```typescript Using product cart theme={null} const { data: session, error } = await authClient.dodopayments.checkoutSession({ // Option B: pass a product cart/product id directly product_cart: [ { product_id: "pdt_xxxxxxxxxxxxxxxxxxxxx", quantity: 1, } ], // Your internal id for reference referenceId: "order_123", }); if (session) { window.location.href = session.url; } ``` Unlike the legacy checkout method, checkoutSession does not require billing information upfront as it will be taken from the user at the checkout page. You can still override it by passing the billing key in the argument. Similar to the legacy checkout method, checkoutSession takes customer's email and name from their better-auth session, however, you can override it by passing the customer object with email and name. You can pass the same options in the argument as the request body for [Create Checkout Session](https://docs.dodopayments.com/api-reference/checkout-sessions/create) endpoint. The return URL is taken from the successUrl configured in the server plugin. You do not need to include return\_url in the client payload. ## Legacy Checkout (Deprecated) The authClient.dodopayments.checkout method is deprecated. Use checkoutSession instead for new implementations. ```typescript expandable theme={null} const { data: checkout, error } = await authClient.dodopayments.checkout({ slug: "premium-plan", customer: { email: "customer@example.com", name: "John Doe", }, billing: { city: "San Francisco", country: "US", state: "CA", street: "123 Market St", zipcode: "94103", }, referenceId: "order_123", }); if (checkout) { window.location.href = checkout.url; } ``` ## Accessing the Customer Portal ```typescript expandable theme={null} const { data: customerPortal, error } = await authClient.dodopayments.customer.portal(); if (customerPortal && customerPortal.redirect) { window.location.href = customerPortal.url; } ``` ## Listing Customer Data ```typescript expandable theme={null} // Get subscriptions const { data: subscriptions, error } = await authClient.dodopayments.customer.subscriptions.list({ query: { limit: 10, page: 1, status: "active", }, }); // Get payment history const { data: payments, error } = await authClient.dodopayments.customer.payments.list({ query: { limit: 10, page: 1, status: "succeeded", }, }); ``` ## Tracking Metered Usage Enable the usage() plugin on the server to capture metered events and let customers inspect their usage-based billing. * authClient.dodopayments.usage.ingest ingests events for the signed-in, email-verified user. * authClient.dodopayments.usage.meters.list lists recent usage for the meters tied to that customer’s subscriptions. ```typescript expandable theme={null} // Record a metered event (e.g., an API request) const { error: ingestError } = await authClient.dodopayments.usage.ingest({ event_id: crypto.randomUUID(), event_name: "api_request", metadata: { route: "/reports", method: "GET", }, // Optional Date; defaults to now if omitted timestamp: new Date(), }); if (ingestError) { console.error("Failed to record usage", ingestError); } // List recent usage for the current customer const { data: usage, error: usageError } = await authClient.dodopayments.usage.meters.list({ query: { page_size: 20, meter_id: "mtr_yourMeterId", // optional }, }); if (usage?.items) { usage.items.forEach((event) => { console.log(event.event_name, event.timestamp, event.metadata); }); } ``` Timestamps older than one hour or more than five minutes into the future are rejected when ingesting usage. If you omit meter\_id when listing usage meters, all meters tied to the customer’s subscriptions are returned. # Webhooks The webhooks plugin processes real-time payment events from Dodo Payments with secure signature verification. The default endpoint is /api/auth/dodopayments/webhooks. Generate a webhook secret for your endpoint URL (e.g., https\://\/api/auth/dodopayments/webhooks) in the Dodo Payments Dashboard and set it in your .env file: ```env theme={null} DODO_PAYMENTS_WEBHOOK_SECRET=your_webhook_secret_here ``` Example handler: ```typescript expandable theme={null} webhooks({ webhookKey: process.env.DODO_PAYMENTS_WEBHOOK_SECRET!, onPayload: async (payload) => { console.log("Received webhook:", payload.type); }, }); ``` ### Supported Webhook Event Handlers ```typescript Typescript expandable theme={null} onPayload?: (payload: WebhookPayload) => Promise; onPaymentSucceeded?: (payload: WebhookPayload) => Promise; onPaymentFailed?: (payload: WebhookPayload) => Promise; onPaymentProcessing?: (payload: WebhookPayload) => Promise; onPaymentCancelled?: (payload: WebhookPayload) => Promise; onRefundSucceeded?: (payload: WebhookPayload) => Promise; onRefundFailed?: (payload: WebhookPayload) => Promise; onDisputeOpened?: (payload: WebhookPayload) => Promise; onDisputeExpired?: (payload: WebhookPayload) => Promise; onDisputeAccepted?: (payload: WebhookPayload) => Promise; onDisputeCancelled?: (payload: WebhookPayload) => Promise; onDisputeChallenged?: (payload: WebhookPayload) => Promise; onDisputeWon?: (payload: WebhookPayload) => Promise; onDisputeLost?: (payload: WebhookPayload) => Promise; onSubscriptionActive?: (payload: WebhookPayload) => Promise; onSubscriptionOnHold?: (payload: WebhookPayload) => Promise; onSubscriptionRenewed?: (payload: WebhookPayload) => Promise; onSubscriptionPlanChanged?: (payload: WebhookPayload) => Promise; onSubscriptionCancelled?: (payload: WebhookPayload) => Promise; onSubscriptionFailed?: (payload: WebhookPayload) => Promise; onSubscriptionExpired?: (payload: WebhookPayload) => Promise; onSubscriptionUpdated?: (payload: WebhookPayload) => Promise; onLicenseKeyCreated?: (payload: WebhookPayload) => Promise; onAbandonedCheckoutDetected?: (payload: WebhookPayload) => Promise; onAbandonedCheckoutRecovered?: (payload: WebhookPayload) => Promise; onDunningStarted?: (payload: WebhookPayload) => Promise; onDunningRecovered?: (payload: WebhookPayload) => Promise; onCreditAdded?: (payload: WebhookPayload) => Promise; onCreditDeducted?: (payload: WebhookPayload) => Promise; onCreditExpired?: (payload: WebhookPayload) => Promise; onCreditRolledOver?: (payload: WebhookPayload) => Promise; onCreditRolloverForfeited?: (payload: WebhookPayload) => Promise; onCreditOverageCharged?: (payload: WebhookPayload) => Promise; onCreditManualAdjustment?: (payload: WebhookPayload) => Promise; onCreditBalanceLow?: (payload: WebhookPayload) => Promise; ``` # Configuration Reference * client (required): DodoPayments client instance * createCustomerOnSignUp (optional): Auto-create customers on user signup * use (required): Array of plugins to enable (checkout, portal, usage, webhooks) * getCustomerParams (optional): Function that receives the BetterAuth `User` and returns extra fields to attach to the DodoPayments customer on creation and update (e.g. `metadata`, `phone_number`) ```typescript theme={null} dodopayments({ client: dodoPayments, createCustomerOnSignUp: true, use: [portal()], getCustomerParams: (user) => ({ metadata: { userId: user.id }, phone_number: user.phoneNumber ?? null, }), }) ``` * products: Array of products or async function returning products - successUrl: URL to redirect after successful payment - authenticatedUsersOnly: Require user authentication (default: false) # Troubleshooting & Tips * Invalid API key: Double-check your DODO\_PAYMENTS\_API\_KEY in .env. - Webhook signature mismatch: Ensure the webhook secret matches the one set in the Dodo Payments Dashboard. - Customer not created: Confirm createCustomerOnSignUp is set to true. * Use environment variables for all secrets and keys. - Test in test\_mode before switching to live\_mode. - Log webhook events for debugging and auditing. # Prompt for LLMs ```text expandable theme={null} You are a skilled developer helping to integrate the @dodopayments/better-auth adapter into a typescript web application with better-auth. This adapter enables seamless payment processing through Dodo Payments with automatic customer management, checkout flows, and webhook handling. STAGE 1: BASIC SETUP This stage covers the foundational setup needed before implementing any plugins. Complete this stage first. STEP 1: Installation Install the required dependencies: npm install @dodopayments/better-auth dodopayments better-auth zod STEP 2: Environment Variables Setup You will need to complete these external setup tasks. I will provide you with a TODO list for the actions you need to take outside of the code: TODO LIST FOR USER: 1. Generate Dodo Payments API Key: - Go to your Dodo Payments Dashboard > Developer > API Keys - Create a new API key (or use existing) - Copy the API key value - Set environment variable: DODO_PAYMENTS_API_KEY=your_api_key_here 2. Generate Better Auth Secret: - Generate a random secret key (32+ characters) - Set environment variable: BETTER_AUTH_SECRET=your_better_auth_secret_here 3. Set Application URL: - For development: BETTER_AUTH_URL=http://localhost:3000 - For production: BETTER_AUTH_URL=https://your-domain.com 4. Webhook Secret (only if implementing webhooks plugin): - This will be provided after you specify your domain name in Stage 2 - Set environment variable: DODO_PAYMENTS_WEBHOOK_SECRET=your_webhook_secret_here Add these environment variables to your .env file: DODO_PAYMENTS_API_KEY=your_api_key_here DODO_PAYMENTS_WEBHOOK_SECRET=your_webhook_secret_here BETTER_AUTH_URL=http://localhost:3000 BETTER_AUTH_SECRET=your_better_auth_secret_here STEP 3: Server Configuration Create or update your better-auth setup file (src/lib/auth.ts): import { BetterAuth } from "better-auth"; import { dodopayments } from "@dodopayments/better-auth"; import DodoPayments from "dodopayments"; // Create DodoPayments client export const dodoPayments = new DodoPayments({ bearerToken: process.env.DODO_PAYMENTS_API_KEY!, environment: "test_mode", // Change to "live_mode" for production }); // Configure better-auth with dodopayments adapter export const { auth, endpoints, client } = BetterAuth({ plugins: [ dodopayments({ client: dodoPayments, createCustomerOnSignUp: true, // Auto-create customers on signup use: [], // We'll add plugins here in Stage 2 // Optional: attach metadata or phone_number to DodoPayments customer records // getCustomerParams: (user) => ({ // metadata: { userId: user.id }, // phone_number: user.phoneNumber ?? null, // }), }), ], }); STEP 4: Client Configuration Create or update your auth client file (src/lib/auth-client.ts): import { createAuthClient } from "better-auth/react"; import { dodopaymentsClient } from "@dodopayments/better-auth/client"; export const authClient = createAuthClient({ baseURL: process.env.BETTER_AUTH_URL || "http://localhost:3000", plugins: [dodopaymentsClient()], }); STAGE 2: PLUGIN IMPLEMENTATION After completing Stage 1, you can selectively implement any of these plugins based on your needs. Each plugin is independent and can be added or removed as needed. PLUGIN SELECTION: Before implementing any plugins, ask the user which plugins they want to implement: "Which plugins would you like to implement? You can choose any combination of: 1. CHECKOUT - Enables secure payment processing and checkout flows 2. PORTAL - Provides customer self-service portal for subscriptions and payments 3. USAGE - Tracks metered events and exposes usage history for billing beyond included allowances 4. WEBHOOKS - Handles real-time payment events from Dodo Payments Please specify which plugins you want (e.g., 'checkout and usage', 'all four', 'just portal', etc.)" If the user doesn't respond or you cannot prompt the user, implement all four plugins by default. Based on the user's selection, implement only the requested plugins from the sections below: CHECKOUT PLUGIN Purpose: Enables secure payment processing using Checkout Sessions (preferred) with product slug mapping. A legacy checkout endpoint is also exposed for backward compatibility but is deprecated. SETUP TODO LIST FOR USER: 1. Create products in Dodo Payments Dashboard: - Go to Dodo Payments Dashboard > Products - Create your products (e.g., subscription plans, one-time purchases) - Copy each product ID (starts with "pdt_") - Note down the product names for creating friendly slugs 2. Plan your checkout URLs: - Decide on your success URL (e.g., "/dashboard/success", "/thank-you") - Ensure this URL exists in your application Configuration: Add checkout to your imports in src/lib/auth.ts: import { dodopayments, checkout } from "@dodopayments/better-auth"; Add checkout plugin to the use array in your dodopayments configuration: use: [ checkout({ products: [ { productId: "pdt_xxxxxxxxxxxxxxxxxxxxx", // Your actual product ID from Dodo Payments slug: "premium-plan", // Friendly slug for checkout }, // Add more products as needed ], successUrl: "/dashboard/success", // Your success page URL authenticatedUsersOnly: true, // Require login for checkout }), ], Usage Example (Preferred - Checkout Sessions): const { data: session, error } = await authClient.dodopayments.checkoutSession({ // Use the slug from your configuration OR provide product_cart directly slug: "premium-plan", // product_cart: [{ product_id: "pdt_xxxxxxxxxxxxxxxxxxxxx", quantity: 1 }], referenceId: "order_123", // Optional reference }); if (session) { window.location.href = session.url; } Deprecated (Legacy Checkout): const { data: checkout, error } = await authClient.dodopayments.checkout({ slug: "premium-plan", customer: { email: "customer@example.com", name: "John Doe", }, billing: { city: "San Francisco", country: "US", state: "CA", street: "123 Market St", zipcode: "94103", }, }); if (checkout) { window.location.href = checkout.url; } Options: - products: Array of products or async function returning products - successUrl: URL to redirect after successful payment - authenticatedUsersOnly: Require user authentication (default: false) PORTAL PLUGIN Purpose: Provides customer self-service capabilities for managing subscriptions and viewing payment history. Configuration: Add portal to your imports in src/lib/auth.ts: import { dodopayments, portal } from "@dodopayments/better-auth"; Add portal plugin to the use array in your dodopayments configuration: use: [ portal(), ], Usage Examples: // Access customer portal const { data: customerPortal, error } = await authClient.dodopayments.customer.portal(); if (customerPortal && customerPortal.redirect) { window.location.href = customerPortal.url; } // List customer subscriptions const { data: subscriptions, error } = await authClient.dodopayments.customer.subscriptions.list({ query: { limit: 10, page: 1, active: true, }, }); // List customer payments const { data: payments, error } = await authClient.dodopayments.customer.payments.list({ query: { limit: 10, page: 1, status: "succeeded", }, }); Note: All portal methods require user authentication. USAGE PLUGIN (METERED BILLING) Purpose: Records metered events (like API requests) for usage-based plans and surfaces recent usage history per customer. SETUP TODO LIST FOR USER: 1. Create or identify usage meters in the Dodo Payments Dashboard: - Dashboard > Usage > Meters - Copy the meter IDs (prefixed with mtr_) used by your usage-based plans 2. Decide which event names and metadata you will capture (e.g., api_request, route, method) 3. Ensure BetterAuth users verify their email addresses (the plugin enforces this before ingesting usage) Configuration: Add usage to your imports in src/lib/auth.ts: import { dodopayments, usage } from "@dodopayments/better-auth"; Add the plugin to the use array: use: [ usage(), ], Usage Examples: // Record a metered event for the signed-in customer const { error: ingestError } = await authClient.dodopayments.usage.ingest({ event_id: crypto.randomUUID(), event_name: "api_request", metadata: { route: "/reports", method: "GET", }, // Optional Date or ISO string; defaults to now timestamp: new Date(), }); if (ingestError) { console.error("Failed to record usage", ingestError); } // List recent usage for the current customer const { data: usage, error: usageError } = await authClient.dodopayments.usage.meters.list({ query: { page_size: 20, meter_id: "mtr_yourMeterId", // optional filter }, }); if (usage?.items) { usage.items.forEach((event) => { console.log(event.event_name, event.timestamp, event.metadata); }); } The plugin exposes authClient.dodopayments.usage.ingest and authClient.dodopayments.usage.meters.list. Timestamps older than one hour or more than five minutes in the future are rejected. If you do not pass meter_id when listing usage meters, all meters tied to the customer’s active subscriptions are returned. WEBHOOKS PLUGIN Purpose: Handles real-time payment events from Dodo Payments with secure signature verification. BEFORE CONFIGURATION - Setup Webhook URL: First, I need your domain name to generate the webhook URL and provide you with setup instructions. STEP 1: Domain Name Input What is your domain name? Please provide: - For production: your domain name (e.g., "myapp.com", "api.mycompany.com") - For staging: your staging domain (e.g., "staging.myapp.com") - For development: use "localhost:3000" (or your local port) STEP 2: After receiving your domain name, I will: - Generate your webhook URL: https://[YOUR-DOMAIN]/api/auth/dodopayments/webhooks - Provide you with a TODO list for webhook setup in Dodo Payments dashboard - Give you the exact environment variable setup instructions WEBHOOK SETUP TODO LIST (will be provided after domain input): 1. Configure webhook in Dodo Payments Dashboard: - Go to Dodo Payments Dashboard > Developer > Webhooks - Click "Add Webhook" or "Create Webhook" - Enter webhook URL: https://[YOUR-DOMAIN]/api/auth/dodopayments/webhooks - Select events you want to receive (or select all) - Copy the generated webhook secret 2. Set webhook secret in your environment: - For production: Set DODO_PAYMENTS_WEBHOOK_SECRET in your hosting provider environment - For staging: Set DODO_PAYMENTS_WEBHOOK_SECRET in your staging environment - For development: Add DODO_PAYMENTS_WEBHOOK_SECRET=your_webhook_secret_here to your .env file 3. Deploy your application with the webhook secret configured STEP 3: Add webhooks to your imports in src/lib/auth.ts: import { dodopayments, webhooks } from "@dodopayments/better-auth"; STEP 4: Add webhooks plugin to the use array in your dodopayments configuration: use: [ webhooks({ webhookKey: process.env.DODO_PAYMENTS_WEBHOOK_SECRET!, // Generic handler for all webhook events onPayload: async (payload) => { console.log("Received webhook:", payload.type); }, // Payment event handlers onPaymentSucceeded: async (payload) => { console.log("Payment succeeded:", payload); }, onPaymentFailed: async (payload) => { console.log("Payment failed:", payload); }, onPaymentProcessing: async (payload) => { console.log("Payment processing:", payload); }, onPaymentCancelled: async (payload) => { console.log("Payment cancelled:", payload); }, // Refund event handlers onRefundSucceeded: async (payload) => { console.log("Refund succeeded:", payload); }, onRefundFailed: async (payload) => { console.log("Refund failed:", payload); }, // Dispute event handlers onDisputeOpened: async (payload) => { console.log("Dispute opened:", payload); }, onDisputeExpired: async (payload) => { console.log("Dispute expired:", payload); }, onDisputeAccepted: async (payload) => { console.log("Dispute accepted:", payload); }, onDisputeCancelled: async (payload) => { console.log("Dispute cancelled:", payload); }, onDisputeChallenged: async (payload) => { console.log("Dispute challenged:", payload); }, onDisputeWon: async (payload) => { console.log("Dispute won:", payload); }, onDisputeLost: async (payload) => { console.log("Dispute lost:", payload); }, // Subscription event handlers onSubscriptionActive: async (payload) => { console.log("Subscription active:", payload); }, onSubscriptionOnHold: async (payload) => { console.log("Subscription on hold:", payload); }, onSubscriptionRenewed: async (payload) => { console.log("Subscription renewed:", payload); }, onSubscriptionPlanChanged: async (payload) => { console.log("Subscription plan changed:", payload); }, onSubscriptionCancelled: async (payload) => { console.log("Subscription cancelled:", payload); }, onSubscriptionFailed: async (payload) => { console.log("Subscription failed:", payload); }, onSubscriptionExpired: async (payload) => { console.log("Subscription expired:", payload); }, onSubscriptionUpdated: async (payload) => { console.log("Subscription updated:", payload); }, // License key event handlers onLicenseKeyCreated: async (payload) => { console.log("License key created:", payload); }, // Abandoned checkout event handlers onAbandonedCheckoutDetected: async (payload) => { console.log("Abandoned checkout detected:", payload); }, onAbandonedCheckoutRecovered: async (payload) => { console.log("Abandoned checkout recovered:", payload); }, // Dunning event handlers onDunningStarted: async (payload) => { console.log("Dunning started:", payload); }, onDunningRecovered: async (payload) => { console.log("Dunning recovered:", payload); }, // Credit event handlers onCreditAdded: async (payload) => { console.log("Credit added:", payload); }, onCreditDeducted: async (payload) => { console.log("Credit deducted:", payload); }, onCreditExpired: async (payload) => { console.log("Credit expired:", payload); }, onCreditRolledOver: async (payload) => { console.log("Credit rolled over:", payload); }, onCreditRolloverForfeited: async (payload) => { console.log("Credit rollover forfeited:", payload); }, onCreditOverageCharged: async (payload) => { console.log("Credit overage charged:", payload); }, onCreditManualAdjustment: async (payload) => { console.log("Credit manual adjustment:", payload); }, onCreditBalanceLow: async (payload) => { console.log("Credit balance low:", payload); }, }), ], Supported Webhook Event Handlers: - onPayload: Generic handler for all webhook events - onPaymentSucceeded: Payment completed successfully - onPaymentFailed: Payment failed - onPaymentProcessing: Payment is being processed - onPaymentCancelled: Payment was cancelled - onRefundSucceeded: Refund completed successfully - onRefundFailed: Refund failed - onDisputeOpened: Dispute was opened - onDisputeExpired: Dispute expired - onDisputeAccepted: Dispute was accepted - onDisputeCancelled: Dispute was cancelled - onDisputeChallenged: Dispute was challenged - onDisputeWon: Dispute was won - onDisputeLost: Dispute was lost - onSubscriptionActive: Subscription became active - onSubscriptionOnHold: Subscription was put on hold - onSubscriptionRenewed: Subscription was renewed - onSubscriptionPlanChanged: Subscription plan was changed - onSubscriptionCancelled: Subscription was cancelled - onSubscriptionFailed: Subscription failed - onSubscriptionExpired: Subscription expired - onSubscriptionUpdated: Subscription was updated - onLicenseKeyCreated: License key was created - onAbandonedCheckoutDetected: Abandoned checkout was detected - onAbandonedCheckoutRecovered: Abandoned checkout was recovered - onDunningStarted: Dunning process started - onDunningRecovered: Dunning process recovered - onCreditAdded: Credits were added - onCreditDeducted: Credits were deducted - onCreditExpired: Credits expired - onCreditRolledOver: Credits were rolled over - onCreditRolloverForfeited: Credit rollover was forfeited - onCreditOverageCharged: Credit overage was charged - onCreditManualAdjustment: Manual credit adjustment was made - onCreditBalanceLow: Credit balance is low COMBINING SELECTED PLUGINS: After implementing the user's selected plugins, update your src/lib/auth.ts file to include all chosen plugins in the imports and use array: Example for all four plugins: import { dodopayments, checkout, portal, usage, webhooks, } from "@dodopayments/better-auth"; use: [ checkout({ // checkout configuration }), portal(), usage(), webhooks({ // webhook configuration }), ]; Example for checkout + portal + usage: import { dodopayments, checkout, portal, usage } from "@dodopayments/better-auth"; use: [ checkout({ // checkout configuration }), portal(), usage(), ]; Example for just webhooks: import { dodopayments, webhooks } from "@dodopayments/better-auth"; use: [ webhooks({ // webhook configuration }), ]; IMPORTANT NOTES: 1. Complete Stage 1 before implementing any plugins 2. Ask the user which plugins they want to implement, or implement all four if no response 3. Only implement the plugins the user specifically requested 4. ALWAYS provide TODO lists for external actions the user needs to complete: - API key generation and environment variable setup - Product creation in Dodo Payments dashboard (for checkout plugin) - Usage meter configuration and API event definition (for usage plugin) - Webhook setup in Dodo Payments dashboard (for webhooks plugin) - Domain name collection for webhook URL generation 5. For webhook plugin: Ask for the user's domain name and generate the exact webhook URL: https://[domain]/api/auth/dodopayments/webhooks 6. The usage plugin requires the BetterAuth session user to exist and have a verified email before ingesting or listing usage 7. All client methods return { data, error } objects for proper error handling 8. Use test_mode for development and live_mode for production 9. The webhook endpoint is automatically created and secured with signature verification (if webhooks plugin is selected) 10. Customer portal and subscription listing require user authentication (if portal plugin is selected) 11. Handle errors appropriately and test webhook functionality in development before going live 12. Present all external setup tasks as clear TODO lists with specific environment variable names 13. Use getCustomerParams to attach metadata or phone_number to DodoPayments customer records — the function receives the BetterAuth User object and runs on every customer creation and update ``` # Build with AI Coding Agents Source: https://docs.dodopayments.com/developer-resources/build-with-ai-coding-agents Use the Dodo Agent Plugin to build integrations with Claude Code, Codex CLI, Cursor, VS Code, Kiro, Gemini CLI, and OpenCode — MCP servers and skills in one install. The Dodo Agent Plugin wires two MCP servers and seventeen integration skills into your AI coding agent in a single install. It works with **Claude Code**, **Codex CLI**, **Cursor**, **VS Code / GitHub Copilot**, **Kiro**, and **OpenCode**. **Gemini CLI** gets the two MCP servers only. The MCP servers and skills CLI work with any MCP-compatible client. The plugin follows the [Agent Plugins 1.0.0](https://agent-plugins.org/specification) specification, so clients with native support load it directly from the root `plugin.json` and `mcp.json`. Clients that predate the spec load the same content through generated compatibility manifests. **Three primitives, one plugin.** The Agent Plugin bundles everything you need: * **API MCP server** — live access to payments, subscriptions, customers, products, refunds, licenses, and usage. Authenticates via browser OAuth (no local keys required). * **Knowledge MCP server** — semantic search across all Dodo Payments documentation. No credentials needed. * **Seventeen agent skills** — cheat sheets your agent loads on demand for checkout, subscriptions, webhooks, usage-based and credit-based billing, license keys, product catalog, discounts, localized pricing, mobile checkout, refunds and disputes, customer management, framework adapters, BillingSDK, Better Auth, testing and go-live, and best practices. ## Install the plugin Choose your coding agent below. Every install adds both MCP servers; all of them except Gemini CLI also add the seventeen skills. Install from the marketplace: ```bash theme={null} claude plugins marketplace add dodopayments/dodo-agent-plugin claude plugins install dodopayments@dodopayments ``` The API MCP server uses browser OAuth by default — no keys required at install time. The first time your agent calls a Dodo tool, you'll be prompted to sign in. Source code, configuration options, and local development instructions Codex installs plugins in two steps: register the marketplace from your shell, then install the plugin from inside the Codex TUI. ```bash theme={null} codex plugin marketplace add dodopayments/dodo-agent-plugin ``` Open Codex and run the `/plugins` slash command: ```bash theme={null} codex ``` Then type `/plugins`, switch to the **Dodo Payments** marketplace, select the **dodopayments** plugin, and choose **Install plugin**. Both MCP servers and all seventeen skills are registered automatically once the plugin is installed. Codex CLI does not have a `codex plugin install` subcommand — plugin installation always happens through the in-TUI `/plugins` flow. See the [official Codex plugins docs](https://developers.openai.com/codex/plugins). If you previously added the marketplace and the plugin doesn't appear under `/plugins`, refresh it: ```bash theme={null} codex plugin marketplace upgrade dodopayments ``` Manual install — clone the repo into Cursor's local plugins directory: ```bash theme={null} git clone https://github.com/dodopayments/dodo-agent-plugin.git ~/.cursor/plugins/local/dodo-agent-plugin ``` Restart Cursor. The plugin loads skills from `skills/` and MCP servers from `.mcp.json`, as declared in `.cursor-plugin/plugin.json`. Recent Cursor builds also recognize Agent Plugins 1.0.0 directly and accept either `.cursor-plugin/marketplace.json` or `.claude-plugin/marketplace.json` as a marketplace source. This plugin ships `.claude-plugin/marketplace.json` — use that path. The generated `.cursor-plugin/plugin.json` is kept for older builds. Versions before 0.5.0 shipped `skills/` as symlinks into a git submodule, so this clone produced a plugin with **no working skills**. Skills now ship as real files. If you installed an earlier version, delete the directory and re-clone. OpenCode distributes via npm. Add the plugin to your `opencode.json`: ```json theme={null} { "$schema": "https://opencode.ai/config.json", "plugin": ["@dodopayments/opencode-plugin"] } ``` Restart OpenCode. Both MCP servers (`dodopayments-api`, `dodo-knowledge`) are registered automatically via the plugin's config hook. No manual `mcp` block required. **Skills need one extra line.** OpenCode does not scan installed packages for skills, so point it at the package's `skills/` directory yourself: ```bash theme={null} npm install --save-dev @dodopayments/opencode-plugin ``` ```json theme={null} { "$schema": "https://opencode.ai/config.json", "plugin": ["@dodopayments/opencode-plugin"], "skills": { "paths": ["node_modules/@dodopayments/opencode-plugin/skills"] } } ``` Relative `skills.paths` entries resolve against the **project directory**, so the package must exist in that project's `node_modules` — OpenCode's own plugin cache is a different location. An absolute path works too, and avoids the local-install requirement. A skills path that does not exist is ignored **silently**. Verify rather than assume: ```bash theme={null} opencode run "List every skill available to you by name." ``` You should see all seventeen. Versions before 0.5.0 documented these skills as auto-discovered — they were not, so OpenCode users had MCP servers but no skills. Clone the repo anywhere, then register it: ```bash theme={null} git clone https://github.com/dodopayments/dodo-agent-plugin.git ``` Open the Chat view, go to **Plugins**, and add the cloned folder. You can also register it directly in `settings.json`: ```json theme={null} { "chat.pluginLocations": { "~/dodo-agent-plugin": true } } ``` The key is a path, the value enables it. Absolute paths and `~/` both work; a relative path resolves against each workspace folder. The setting is read live, so a running window picks it up without a restart. Skills load from `skills/`, and both MCP servers load from `.mcp.json`. `chat.pluginLocations` is marked **experimental** and is a *restricted* setting, so the workspace must be trusted for it to apply. Agent-plugin support overall is a preview feature governed by `chat.plugins.enabled`, which is **on by default** — you only need to set it if your organization disables it by policy. VS Code does not key off the Agent Plugins `$schema`. Its loader probes `.plugin/plugin.json`, then `.claude-plugin/plugin.json`, then a root `plugin.json`, and defaults MCP to `.mcp.json` rather than `mcp.json`. Because this repo ships a generated `.claude-plugin/plugin.json`, VS Code loads it through that compatibility branch and gets the `mcp-remote` bridge rather than the native transports in `mcp.json`. Everything works — the path differs. Kiro reads the Agent Plugins manifest natively and loads this as a Power. ```bash theme={null} git clone https://github.com/dodopayments/dodo-agent-plugin.git ``` In Kiro, open the Powers panel — the Ghosty icon with the lightning bolt. Choose **Add Custom Power → Import power from a folder**, select the cloned directory (the one containing `plugin.json`), and click **Install**. Skills load from `skills/`, MCP servers from `mcp.json`, and Kiro-specific presentation comes from the `dev.kiro` extension namespace in `plugin.json`. For plugins in the Agent Plugins format, Kiro manages MCP servers internally — they are **not** written to your user-level `~/.kiro/settings/mcp.json`, so don't expect to find them there. Kiro can also install a Power directly from a GitHub URL; see [Kiro's install docs](https://kiro.dev/docs/powers/installation/). Gemini CLI has no agent-skill primitive, so **only the two MCP servers are available** — the seventeen skills are not. `dodo-knowledge` still covers a good share of what the skills provide, and it stays current automatically. ```bash theme={null} git clone https://github.com/dodopayments/dodo-agent-plugin.git \ ~/.gemini/extensions/dodopayments ``` Restart Gemini CLI. `gemini-extension.json` at the repo root is the manifest. Using a different agent? The [MCP Server](/developer-resources/mcp-server) and [Agent Skills](/developer-resources/agent-skills) guides cover Claude Desktop, Windsurf, Cline, Zed, and any MCP-compatible client. ## What you get Once the plugin is installed, your agent has access to two MCP servers and seventeen skills. ### MCP servers | Server | Purpose | Auth | | ------------------ | ---------------------------------------------------------------------------------------- | --------------- | | `dodopayments-api` | Live API access — payments, subscriptions, customers, products, refunds, licenses, usage | OAuth (browser) | | `dodo-knowledge` | Semantic search across all Dodo Payments documentation | None | Both servers speak Streamable HTTP. The canonical `mcp.json` declares them natively (`type: "streamable-http"`), which is what spec-native clients such as Codex CLI and Kiro use. The generated compatibility manifest `.mcp.json` wires the same two endpoints through `mcp-remote` instead, so they also run in clients that cannot dial Streamable HTTP directly — Claude Code, VS Code, and Cursor via the git-clone install documented above, whose `.cursor-plugin/plugin.json` points at `.mcp.json`. ### Agent skills | Skill | Description | | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `dodo-best-practices` | SDK setup, environments, API keys, and the canonical checkout-to-webhook architecture | | `framework-adapters` | Official `@dodopayments/*` route handlers for Next.js, Express, Hono, Astro, Remix, SvelteKit, Nuxt, Fastify, TanStack, Bun, and Convex | | `testing-and-go-live` | Test mode, test payment methods, webhook testing, and the production launch checklist | | `checkout-integration` | Checkout Sessions, payment links, and overlay or inline checkout | | `subscription-integration` | Subscription lifecycle, trials, plan changes, proration, and on-demand charges | | `mobile-checkout` | In-app checkout for React Native, Flutter, iOS, and Android | | `webhook-integration` | Receiving and verifying webhooks using the Standard Webhooks specification | | `credit-based-billing` | Credit entitlements, balances, ledger, rollover, overage, and meter-based deduction | | `usage-based-billing` | Meters, event ingestion, aggregation, and per-unit pricing | | `license-keys` | License key activation, validation, and instance management | | `product-catalog-management` | Products, pricing, add-ons, collections, images, and digital product delivery | | `discounts-and-promotions` | Discount codes, eligibility rules, stacking, and subscription-cycle limits | | `localized-pricing` | Localized pricing, adaptive currency, and purchasing power parity | | `customer-management` | Customers, the self-service portal, saved payment methods, and wallets | | `refunds-and-disputes` | Issuing refunds, handling disputes and chargebacks, and reconciling access | | `billing-sdk` | BillingSDK React components for pricing tables and billing UI | | `better-auth-integration` | The `@dodopayments/better-auth` plugin for customer sync, checkout, and portal access | Skills load automatically — your agent picks the right one when it detects a relevant task. See the [Agent Skills documentation](/developer-resources/agent-skills) for the full list and individual installation. ### Try this prompt first Once the plugin is active, try: ``` Set up Dodo Payments webhook handlers in my Next.js app for payment.succeeded and subscription.active events. ``` Your agent will load the `webhook-integration` skill, use the `dodo-knowledge` MCP to pull the latest payload shapes, and write a handler with signature verification following the Standard Webhooks spec. ## Client support ### Clients with an Agent Plugin install One install wires both MCP servers, plus the skills on every client that has a skill primitive. | Agent | Install | Skills | MCP servers | | ---------------------------- | ------------------- | -----: | ----------: | | **Claude Code** | marketplace command | 17 | 2 | | **Codex CLI** | marketplace command | 17 | 2 | | **Cursor** | git clone | 17 | 2 | | **VS Code / GitHub Copilot** | git clone | 17 | 2 | | **Kiro** | git clone | 17 | 2 | | **OpenCode** | npm | 17 | 2 | | **Gemini CLI** | git clone | 0 | 2 | * **OpenCode** needs one extra `skills.paths` entry before its seventeen skills load — see the install section above. * **Gemini CLI** has no agent-skill primitive, so skills are unavailable there by design. ### Other MCP-compatible clients These have no Agent Plugin install. Configure the two MCP servers by hand, and add skills through the Skills CLI where supported. | Agent | MCP servers | Skills | | -------------------- | --------------------------------------------------- | ----------------------------------------------- | | Claude Desktop | [MCP Server guide](/developer-resources/mcp-server) | not supported | | Windsurf | [MCP Server guide](/developer-resources/mcp-server) | [Skills CLI](/developer-resources/agent-skills) | | Cline / Zed / others | [MCP Server guide](/developer-resources/mcp-server) | [Skills CLI](/developer-resources/agent-skills) | ## Docs built for agents Every Dodo Payments documentation page is available in a format optimized for AI consumption: * **Full docs index**: [`docs.dodopayments.com/llms.txt`](https://docs.dodopayments.com/llms.txt) — serves the complete documentation index for context ingestion. * **Plain markdown**: Append `.md` to any documentation URL to get the raw markdown version (e.g., `/api-reference/introduction.md`). * **Source repository**: [`github.com/dodopayments/dodo-docs`](https://github.com/dodopayments/dodo-docs) — clone for offline indexing. ## What your agent can do With the plugin installed, your coding agent can: * **Create checkout sessions and payment links** — [One-time payments](/features/one-time-payment-products) and [subscriptions](/features/subscription) * **Stand up subscription and usage-based billing end-to-end** — [Subscriptions](/features/subscription), [Usage-based billing](/features/usage-based-billing/introduction), [Credit-based billing](/features/credit-based-billing) * **Generate Standard Webhooks–compliant handlers** with signature verification — [Webhooks](/developer-resources/webhooks) * **Mount route handlers in your framework** using the official adapters — [Framework adaptors](/developer-resources/framework-adaptors) * **Wire BillingSDK React components** for pricing tables and subscription management — [BillingSDK](/developer-resources/billingsdk) * **Author license-key flows** for digital products — [License keys](/features/license-keys) * **Implement credit-based billing** with entitlements, balances, rollover, and overage — [Credits](/features/credit-based-billing) * **Build and manage your product catalog** with add-ons and collections — [Products](/features/products), [Add-ons](/features/addons), [Product collections](/features/product-collections) * **Add discounts and localized pricing** — [Discount codes](/features/discount-codes), [Localized pricing](/features/localized-pricing), [Adaptive currency](/features/adaptive-currency) * **Ship mobile in-app checkout** for React Native, Flutter, iOS, and Android — [Mobile integration](/developer-resources/mobile-integration) * **Handle refunds, disputes, and customer self-service** — [Refunds](/features/transactions/refunds), [Disputes](/features/transactions/disputes), [Customer portal](/features/customer-portal) * **Test the integration and go live safely** — [Test mode vs live mode](/miscellaneous/test-mode-vs-live-mode) ## Security and best practices **Never commit production API keys.** Use [test mode](/miscellaneous/test-mode-vs-live-mode) during development. The Agent Plugin uses browser OAuth by default — only switch to local API keys if your workflow requires it. * **Use test mode first.** Sandbox your integration with `dodo_test_...` keys before going live. See [Test Mode vs Live Mode](/miscellaneous/test-mode-vs-live-mode). * **OAuth is the default.** The Agent Plugin authenticates via browser OAuth (no local secrets). Only use API-key mode if you need it — see the Configure section below. * **Review agent-generated code.** Always verify webhook handlers include signature verification following the [Standard Webhooks spec](https://standardwebhooks.com/). ## Configure with an API key By default, the Agent Plugin uses the remote MCP server with browser OAuth — no local credentials needed. If your workflow requires a local API key (e.g., CI environments, headless servers), you can switch to stdio mode. Open `/plugins` in Claude Code, select **Dodo Payments**, and choose **Configure options**. Fill in: * `dodo_api_key` — your `dodo_test_...` or `dodo_live_...` key * `dodo_webhook_key` — your webhook signing secret * `dodo_environment` — `test_mode` or `live_mode` Then edit `.mcp.json` to point `dodopayments-api` at the local stdio server: ```json theme={null} { "mcpServers": { "dodopayments-api": { "type": "stdio", "command": "npx", "args": ["-y", "dodopayments-mcp@latest"], "env": { "DODO_PAYMENTS_API_KEY": "${user_config.dodo_api_key}", "DODO_PAYMENTS_WEBHOOK_KEY": "${user_config.dodo_webhook_key}", "DODO_PAYMENTS_ENVIRONMENT": "${user_config.dodo_environment}" } } } } ``` Run `/reload-plugins` to apply changes to your current session. Declare `dodopayments-api` yourself in `opencode.json` — your entry wins over the plugin's default remote server: ```json theme={null} { "$schema": "https://opencode.ai/config.json", "plugin": ["@dodopayments/opencode-plugin"], "mcp": { "dodopayments-api": { "type": "local", "command": ["npx", "-y", "dodopayments-mcp@latest"], "environment": { "DODO_PAYMENTS_API_KEY": "dodo_test_...", "DODO_PAYMENTS_WEBHOOK_KEY": "whsec_...", "DODO_PAYMENTS_ENVIRONMENT": "test_mode" }, "enabled": true } } } ``` Restart OpenCode to apply. ## Next steps Full reference for both MCP servers — all supported clients, configuration, and available tools Individual skill installation, skill reference, and per-agent setup instructions AI-powered billing assistant for VS Code, Cursor, and Windsurf — ask, build, and plan in your editor Complete OpenAPI reference for all Dodo Payments endpoints # Convex Component Source: https://docs.dodopayments.com/developer-resources/convex-component Learn how to integrate Dodo Payments with your Convex backend using our Convex Component. Covers checkout functions, customer portal, webhooks, and secure environment setup. Integrate Dodo Payments checkout with session-based flow. Allow customers to manage subscriptions and details. Receive and process Dodo Payments webhook events. ## Installation Run the following command in your project root: ```bash theme={null} npm install @dodopayments/convex ``` Add the Dodo Payments component to your Convex configuration: ```typescript theme={null} // convex/convex.config.ts import { defineApp } from "convex/server"; import dodopayments from "@dodopayments/convex/convex.config"; const app = defineApp(); app.use(dodopayments); export default app; ``` After editing `convex.config.ts`, run `npx convex dev` once to generate the necessary types. Set up environment variables in your Convex dashboard (**Settings** → **Environment Variables**). You can access the dashboard by running: ```bash theme={null} npx convex dashboard ``` Add the following environment variables: * `DODO_PAYMENTS_API_KEY` - Your Dodo Payments API key * `DODO_PAYMENTS_ENVIRONMENT` - Set to `test_mode` or `live_mode` * `DODO_PAYMENTS_WEBHOOK_SECRET` - Your webhook secret (required for webhook handling) Always use Convex environment variables for sensitive information. Never commit secrets to version control. ## Component Setup Examples First, create an internal query to fetch customers from your database. This will be used in the payment functions to identify customers. Before using this query, make sure to define the appropriate schema in your `convex/schema.ts` file or change the query to match your existing schema. ```typescript theme={null} // convex/customers.ts import { internalQuery } from "./_generated/server"; import { v } from "convex/values"; // Internal query to fetch customer by auth ID export const getByAuthId = internalQuery({ args: { authId: v.string() }, handler: async (ctx, { authId }) => { return await ctx.db .query("customers") .withIndex("by_auth_id", (q) => q.eq("authId", authId)) .first(); }, }); ``` ```typescript Convex Component Setup expandable theme={null} // convex/dodo.ts import { DodoPayments, DodoPaymentsClientConfig } from "@dodopayments/convex"; import { components } from "./_generated/api"; import { internal } from "./_generated/api"; export const dodo = new DodoPayments(components.dodopayments, { // This function maps your Convex user to a Dodo Payments customer // Customize it based on your authentication provider and database identify: async (ctx) => { const identity = await ctx.auth.getUserIdentity(); if (!identity) { return null; // User is not logged in } // Use ctx.runQuery() to lookup customer from your database const customer = await ctx.runQuery(internal.customers.getByAuthId, { authId: identity.subject, }); if (!customer) { return null; // Customer not found in database } return { dodoCustomerId: customer.dodoCustomerId, // Replace customer.dodoCustomerId with your field storing Dodo Payments customer ID }; }, apiKey: process.env.DODO_PAYMENTS_API_KEY!, environment: process.env.DODO_PAYMENTS_ENVIRONMENT as "test_mode" | "live_mode", } as DodoPaymentsClientConfig); // Export the API methods for use in your app export const { checkout, customerPortal } = dodo.api(); ``` Use this function to integrate Dodo Payments checkout into your Convex app. Uses session-based checkout with full feature support. ```typescript Checkout Action expandable theme={null} // convex/payments.ts import { action } from "./_generated/server"; import { v } from "convex/values"; import { checkout } from "./dodo"; export const createCheckout = action({ args: { product_cart: v.array(v.object({ product_id: v.string(), quantity: v.number(), })), returnUrl: v.optional(v.string()), }, handler: async (ctx, args) => { try { const session = await checkout(ctx, { payload: { product_cart: args.product_cart, return_url: args.returnUrl, billing_currency: "USD", feature_flags: { allow_discount_code: true, }, }, }); if (!session?.checkout_url) { throw new Error("Checkout session did not return a checkout_url"); } return session; } catch (error) { console.error("Failed to create checkout session", error); throw new Error("Unable to create checkout session. Please try again."); } }, }); ``` Use this function to allow customers to manage their subscriptions and details via the Dodo Payments customer portal. The customer is automatically identified via the `identify` function. ```typescript Customer Portal Action expandable theme={null} // convex/payments.ts (add to existing file) import { action } from "./_generated/server"; import { v } from "convex/values"; import { customerPortal } from "./dodo"; export const getCustomerPortal = action({ args: { send_email: v.optional(v.boolean()), }, handler: async (ctx, args) => { try { const portal = await customerPortal(ctx, args); if (!portal?.portal_url) { throw new Error("Customer portal did not return a portal_url"); } return portal; } catch (error) { console.error("Failed to generate customer portal link", error); throw new Error("Unable to generate customer portal link. Please try again."); } }, }); ``` Use this handler to receive and process Dodo Payments webhook events securely in your Convex app. All webhook handlers receive the Convex `ActionCtx` as the first parameter, allowing you to use `ctx.runQuery()` and `ctx.runMutation()` to interact with your database. ```typescript Convex HTTP Action expandable theme={null} // convex/http.ts import { createDodoWebhookHandler } from "@dodopayments/convex"; import { httpRouter } from "convex/server"; import { internal } from "./_generated/api"; const http = httpRouter(); http.route({ path: "/dodopayments-webhook", method: "POST", handler: createDodoWebhookHandler({ // Handle successful payments onPaymentSucceeded: async (ctx, payload) => { console.log("🎉 Payment Succeeded!"); // Use Convex context to persist payment data await ctx.runMutation(internal.webhooks.createPayment, { paymentId: payload.data.payment_id, businessId: payload.business_id, customerEmail: payload.data.customer.email, amount: payload.data.total_amount, currency: payload.data.currency, status: payload.data.status, webhookPayload: JSON.stringify(payload), }); }, // Handle subscription activation onSubscriptionActive: async (ctx, payload) => { console.log("🎉 Subscription Activated!"); // Use Convex context to persist subscription data await ctx.runMutation(internal.webhooks.createSubscription, { subscriptionId: payload.data.subscription_id, businessId: payload.business_id, customerEmail: payload.data.customer.email, status: payload.data.status, webhookPayload: JSON.stringify(payload), }); }, // Add other event handlers as needed }), }); export default http; ``` Make sure to define the corresponding database mutations in your Convex backend for each webhook event you want to handle. For example, create a createPayment mutation to record successful payments or a createSubscription mutation to manage subscription state. ## Checkout Function The Dodo Payments Convex component uses session-based checkout, providing a secure, customizable checkout experience with pre-configured product carts and customer details. This is the recommended approach for all payment flows. ### Usage ```typescript theme={null} const result = await checkout(ctx, { payload: { product_cart: [{ product_id: "pdt_123", quantity: 1 }], customer: { email: "user@example.com" }, return_url: "https://example.com/success" } }); ``` Refer [Checkout Sessions](/developer-resources/checkout-session) for more details and a complete list of supported fields. ### Response Format The checkout function returns a JSON response with the checkout URL: ```json theme={null} { "checkout_url": "https://checkout.dodopayments.com/session/..." } ``` ## Customer Portal Function The Customer Portal Function enables you to seamlessly integrate the Dodo Payments customer portal into your Convex application. ### Usage ```typescript theme={null} const result = await customerPortal(ctx, { send_email: false }); ``` ### Parameters If set to true, sends an email to the customer with the portal link. The customer is automatically identified using the `identify` function configured in your DodoPayments setup. This function should return the customer's `dodoCustomerId`. ## Webhook Handler * **Method:** Only POST requests are supported. Other methods return 405. * **Signature Verification:** Verifies the webhook signature using DODO\_PAYMENTS\_WEBHOOK\_SECRET. Returns 401 if verification fails. * **Payload Validation:** Validated with Zod. Returns 400 for invalid payloads. * **Error Handling:** * 401: Invalid signature * 400: Invalid payload * 500: Internal error during verification * **Event Routing:** Calls the appropriate event handler based on the payload type. ### Supported Webhook Event Handlers ```typescript Typescript expandable theme={null} onPayload?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise; onPaymentSucceeded?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise; onPaymentFailed?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise; onPaymentProcessing?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise; onPaymentCancelled?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise; onRefundSucceeded?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise; onRefundFailed?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise; onDisputeOpened?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise; onDisputeExpired?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise; onDisputeAccepted?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise; onDisputeCancelled?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise; onDisputeChallenged?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise; onDisputeWon?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise; onDisputeLost?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise; onSubscriptionActive?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise; onSubscriptionOnHold?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise; onSubscriptionRenewed?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise; onSubscriptionPlanChanged?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise; onSubscriptionCancelled?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise; onSubscriptionFailed?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise; onSubscriptionExpired?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise; onSubscriptionUpdated?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise; onLicenseKeyCreated?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise; onAbandonedCheckoutDetected?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise; onAbandonedCheckoutRecovered?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise; onDunningStarted?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise; onDunningRecovered?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise; onCreditAdded?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise; onCreditDeducted?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise; onCreditExpired?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise; onCreditRolledOver?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise; onCreditRolloverForfeited?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise; onCreditOverageCharged?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise; onCreditManualAdjustment?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise; onCreditBalanceLow?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise; ``` ## Frontend Usage Use the checkout function from your React components with Convex hooks. ```tsx React Checkout Component expandable theme={null} import { useAction } from "convex/react"; import { api } from "../convex/_generated/api"; export function CheckoutButton() { const createCheckout = useAction(api.payments.createCheckout); const handleCheckout = async () => { try { const { checkout_url } = await createCheckout({ product_cart: [{ product_id: "pdt_123", quantity: 1 }], returnUrl: "https://example.com/success" }); if (!checkout_url) { throw new Error("Missing checkout_url in response"); } window.location.href = checkout_url; } catch (error) { console.error("Failed to create checkout", error); throw new Error("Unable to create checkout. Please try again."); } }; return ; } ``` ```tsx Customer Portal Component expandable theme={null} import { useAction } from "convex/react"; import { api } from "../convex/_generated/api"; export function CustomerPortalButton() { const getPortal = useAction(api.payments.getCustomerPortal); const handlePortal = async () => { try { const { portal_url } = await getPortal({ send_email: false }); if (!portal_url) { throw new Error("Missing portal_url in response"); } window.location.href = portal_url; } catch (error) { console.error("Unable to open customer portal", error); alert("We couldn't open the customer portal. Please try again."); } }; return ; } ``` *** ## Prompt for LLM ``` You are an expert Convex developer assistant. Your task is to guide a user through integrating the @dodopayments/convex component into their existing Convex application. The @dodopayments/convex adapter provides a Convex component for Dodo Payments' Checkout, Customer Portal, and Webhook functionalities, built using the official Convex component architecture pattern. First, install the necessary package: npm install @dodopayments/convex Here's how you should structure your response: 1. Ask the user which functionalities they want to integrate. "Which parts of the @dodopayments/convex component would you like to integrate into your project? You can choose one or more of the following: - Checkout Function (for handling product checkouts) - Customer Portal Function (for managing customer subscriptions/details) - Webhook Handler (for receiving Dodo Payments webhook events) - All (integrate all three)" 2. Based on the user's selection, provide detailed integration steps for each chosen functionality. If Checkout Function is selected: Purpose: This function handles session-based checkout flows and returns checkout URLs for programmatic handling. Integration Steps: Step 1: Add the component to your Convex configuration. // convex/convex.config.ts import { defineApp } from "convex/server"; import dodopayments from "@dodopayments/convex/convex.config"; const app = defineApp(); app.use(dodopayments); export default app; Step 2: Guide the user to set up environment variables in the Convex dashboard. Instruct them to open the dashboard by running: npx convex dashboard Then add the required environment variables (e.g., DODO_PAYMENTS_API_KEY, DODO_PAYMENTS_ENVIRONMENT, DODO_PAYMENTS_WEBHOOK_SECRET) in **Settings → Environment Variables**. Do not use .env files for backend functions. Step 3: Create an internal query to fetch customers from your database. Note: Ensure the user has appropriate schema defined in their convex/schema.ts file or modify the query to match their existing schema. // convex/customers.ts import { internalQuery } from "./_generated/server"; import { v } from "convex/values"; // Internal query to fetch customer by auth ID export const getByAuthId = internalQuery({ args: { authId: v.string() }, handler: async (ctx, { authId }) => { return await ctx.db .query("customers") .withIndex("by_auth_id", (q) => q.eq("authId", authId)) .first(); }, }); Step 4: Create your payment functions file. // convex/dodo.ts import { DodoPayments, DodoPaymentsClientConfig } from "@dodopayments/convex"; import { components } from "./_generated/api"; import { internal } from "./_generated/api"; export const dodo = new DodoPayments(components.dodopayments, { // This function maps your Convex user to a Dodo Payments customer // Customize it based on your authentication provider and user database identify: async (ctx) => { const identity = await ctx.auth.getUserIdentity(); if (!identity) { return null; // User is not logged in } // Use ctx.runQuery() to lookup customer from your database const customer = await ctx.runQuery(internal.customers.getByAuthId, { authId: identity.subject, }); if (!customer) { return null; // Customer not found in database } return { dodoCustomerId: customer.dodoCustomerId, // Replace customer.dodoCustomerId with your field storing Dodo Payments customer ID }; }, apiKey: process.env.DODO_PAYMENTS_API_KEY!, environment: process.env.DODO_PAYMENTS_ENVIRONMENT as "test_mode" | "live_mode", } as DodoPaymentsClientConfig); // Export the API methods for use in your app export const { checkout, customerPortal } = dodo.api(); Step 5: Create actions that use the checkout function. // convex/payments.ts import { action } from "./_generated/server"; import { v } from "convex/values"; import { checkout } from "./dodo"; // Checkout session with full feature support export const createCheckout = action({ args: { product_cart: v.array(v.object({ product_id: v.string(), quantity: v.number(), })), returnUrl: v.optional(v.string()), }, handler: async (ctx, args) => { return await checkout(ctx, { payload: { product_cart: args.product_cart, return_url: args.returnUrl, billing_currency: "USD", feature_flags: { allow_discount_code: true, }, }, }); }, }); Step 6: Use in your frontend. // Your frontend component import { useAction } from "convex/react"; import { api } from "../convex/_generated/api"; export function CheckoutButton() { const createCheckout = useAction(api.payments.createCheckout); const handleCheckout = async () => { const { checkout_url } = await createCheckout({ product_cart: [{ product_id: "pdt_123", quantity: 1 }], }); window.location.href = checkout_url; }; return ; } Configuration Details: - `checkout()`: Checkout session with full feature support using session checkout. - Returns: `{"checkout_url": "https://checkout.dodopayments.com/..."}` For complete API documentation, refer to: - Checkout Sessions: https://docs.dodopayments.com/developer-resources/checkout-session - One-time Payments: https://docs.dodopayments.com/api-reference/payments/post-payments - Subscriptions: https://docs.dodopayments.com/api-reference/subscriptions/post-subscriptions If Customer Portal Function is selected: Purpose: This function allows customers to manage their subscriptions and payment methods. The customer is automatically identified via the `identify` function. Integration Steps: Follow Steps 1-4 from the Checkout Function section, then: Step 5: Create a customer portal action. // convex/payments.ts (add to existing file) import { action } from "./_generated/server"; import { v } from "convex/values"; import { customerPortal } from "./dodo"; export const getCustomerPortal = action({ args: { send_email: v.optional(v.boolean()), }, handler: async (ctx, args) => { try { const portal = await customerPortal(ctx, args); if (!portal?.portal_url) { throw new Error("Customer portal did not return a portal_url"); } return portal; } catch (error) { console.error("Failed to generate customer portal link", error); throw new Error("Unable to generate customer portal link. Please retry."); } }, }); Step 6: Use in your frontend. // Your frontend component import { useAction } from "convex/react"; import { api } from "../convex/_generated/api"; export function CustomerPortalButton() { const getPortal = useAction(api.payments.getCustomerPortal); const handlePortal = async () => { const { portal_url } = await getPortal({ send_email: false }); window.location.href = portal_url; }; return ; } Configuration Details: - Requires authenticated user (via `identify` function). - Customer identification is handled automatically by the `identify` function. - `send_email`: Optional boolean to send portal link via email. If Webhook Handler is selected: Purpose: This handler processes incoming webhook events from Dodo Payments, allowing your application to react to events like successful payments or subscription changes. Integration Steps: Step 1: Add the webhook secret to your environment variables in the Convex dashboard. You can open the dashboard by running: Guide the user to open the Convex dashboard by running: npx convex dashboard In the dashboard, go to **Settings → Environment Variables** and add: - `DODO_PAYMENTS_WEBHOOK_SECRET=whsec_...` Do not use .env files for backend functions; always set secrets in the Convex dashboard. Step 2: Create a file `convex/http.ts`: // convex/http.ts import { createDodoWebhookHandler } from "@dodopayments/convex"; import { httpRouter } from "convex/server"; import { internal } from "./_generated/api"; const http = httpRouter(); http.route({ path: "/dodopayments-webhook", method: "POST", handler: createDodoWebhookHandler({ // Handle successful payments onPaymentSucceeded: async (ctx, payload) => { console.log("🎉 Payment Succeeded!"); // Use Convex context to persist payment data await ctx.runMutation(internal.webhooks.createPayment, { paymentId: payload.data.payment_id, businessId: payload.business_id, customerEmail: payload.data.customer.email, amount: payload.data.total_amount, currency: payload.data.currency, status: payload.data.status, webhookPayload: JSON.stringify(payload), }); }, // Handle subscription activation onSubscriptionActive: async (ctx, payload) => { console.log("🎉 Subscription Activated!"); // Use Convex context to persist subscription data await ctx.runMutation(internal.webhooks.createSubscription, { subscriptionId: payload.data.subscription_id, businessId: payload.business_id, customerEmail: payload.data.customer.email, status: payload.data.status, webhookPayload: JSON.stringify(payload), }); }, // Add other event handlers as needed }), }); export default http; Note: Make sure to define the corresponding database mutations in your Convex backend for each webhook event you want to handle. For example, create a `createPayment` mutation to record successful payments or a `createSubscription` mutation to manage subscription state. Now, you can set the webhook endpoint URL in your Dodo Payments dashboard to `https:///dodopayments-webhook`. Environment Variable Setup: Set up the following environment variables in your Convex dashboard if you haven't already (Settings → Environment Variables): - `DODO_PAYMENTS_API_KEY` - Your Dodo Payments API key - `DODO_PAYMENTS_ENVIRONMENT` - Set to `test_mode` or `live_mode` - `DODO_PAYMENTS_WEBHOOK_SECRET` - Your webhook secret (required for webhook handling) Usage in your component configuration: apiKey: process.env.DODO_PAYMENTS_API_KEY environment: process.env.DODO_PAYMENTS_ENVIRONMENT as "test_mode" | "live_mode" Important: Never commit sensitive environment variables directly into your code. Always use Convex environment variables for all sensitive information. If the user needs assistance setting up environment variables or deployment, ask them about their specific setup and provide guidance accordingly. Run `npx convex dev` after setting up the component to generate the necessary types. ``` # Official SDKs Overview Source: https://docs.dodopayments.com/developer-resources/dodo-payments-sdks Official SDKs for TypeScript, Python, PHP, Go, Ruby, Java, Kotlin, C#, and Rust to integrate Dodo Payments into your applications Dodo Payments provides official SDKs for multiple programming languages, each designed with language-specific best practices and modern features for seamless payment integration. Always use the latest SDK version to access the newest features and improvements. Check your package manager for updates regularly to ensure you have access to all Dodo Payments capabilities. ## Available SDKs Choose the SDK that matches your tech stack: Type-safe integration for TypeScript and Node.js with promise-based API and auto-pagination Pythonic interface with async/await support for Python 3.7+ applications PSR-4 compliant SDK for modern PHP 8.1+ applications Idiomatic Go interface with context support and strong typing Elegant Ruby interface following Ruby conventions and best practices Robust and thread-safe SDK for Java 8+ with Maven and Gradle support Modern Kotlin SDK with coroutines, null safety, and extension functions Type-safe SDK for .NET 8+ with async Task-based API (Beta) Async-first SDK built on Tokio and reqwest with strong typing for Rust 1.75+ Command-line interface for interacting with the API from your terminal Building a mobile app? See [Mobile Checkout SDKs](#mobile-checkout-sdks) below for Android, iOS, React Native, and Flutter. ## Quick Start Get started with any SDK in minutes: Use your language's package manager to install the SDK ```bash theme={null} npm install dodopayments ``` ```bash theme={null} pip install dodopayments ``` ```bash theme={null} composer require dodopayments/client ``` ```bash theme={null} go get github.com/dodopayments/dodopayments-go ``` ```bash theme={null} cargo add dodopayments ``` Configure the client with your API key ```typescript theme={null} import DodoPayments from 'dodopayments'; const client = new DodoPayments({ bearerToken: 'your_api_key' }); ``` ```python theme={null} from dodopayments import DodoPayments client = DodoPayments(bearer_token="your_api_key") ``` ```php theme={null} use Dodopayments\Client; $client = new Client(bearerToken: 'your_api_key'); ``` ```go theme={null} import "github.com/dodopayments/dodopayments-go" client := dodopayments.NewClient(option.WithBearerToken("your_api_key")) ``` ```rust theme={null} use dodopayments::Client; let client = Client::from_env()?; // reads DODO_PAYMENTS_API_KEY ``` Always store your API keys securely using environment variables. Never commit them to version control. Create a checkout session or payment You're now ready to process payments! Visit the individual SDK pages for detailed guides and examples. ## Key Features All **backend** SDKs share these core capabilities. The mobile checkout SDKs below are deliberately narrower — they hold no API key and never call the API: * **Type Safety**: Strong typing for compile-time safety and better IDE support * **Error Handling**: Comprehensive exception handling with detailed error messages * **Authentication**: Simple API key authentication with environment variable support * **Async Support**: Modern async/await patterns where applicable * **Auto-Pagination**: Automatic pagination for list responses * **Usage-Based Billing**: Built-in support for tracking and ingesting usage events * **Testing**: Full Test Mode support for development and testing ## Mobile Checkout SDKs If you'd rather open Dodo's **hosted checkout page** than build a native payment sheet, use the mobile checkout SDK for your platform. All four share the same one-call contract: `start(...)` returns a typed `CheckoutResult`, none of them hold an API key, and Apple Pay/Google Pay work the same way they do on the open web since checkout opens in the platform's real browser surface (`SFSafariViewController` / Custom Tabs), not a `WebView`: Kotlin SDK that opens a Chrome Custom Tab. Requires `minSdk` 23 Swift SDK that opens `SFSafariViewController`. Requires iOS 16+ Turbo Module over both native cores. Requires React Native 0.76+ Pigeon channel over both native cores. Requires Flutter 3.44+ The end-to-end mobile payment flow, from backend checkout session to callback scheme setup ## Command-Line Interface For terminal-based workflows and automation: Auto-generated command-line interface with support for the Dodo Payments API **Features:** * Resource-based command structure for intuitive usage * Multiple output formats (JSON, YAML, pretty, interactive) * Shell completion for bash, zsh, and fish * Perfect for scripting and CI/CD automation ```bash theme={null} # Quick example dodopayments payments list --format json | jq '.items[] | {payment_id, total_amount}' ``` ## Migration from Node.js SDK We migrated from the Node.js SDK to the new TypeScript SDK. If you're using the legacy Node.js SDK, see the [migration guide](https://github.com/dodopayments/dodopayments-typescript/blob/main/MIGRATION.md) to update your integration. ## Framework Adapters Integrate in under 10 lines of code with our framework adapters. Choose from our recommended frameworks or explore all supported options. ### Recommended Frameworks React-based full-stack framework with App Router support Authentication framework with built-in integrations Open source Firebase alternative with Postgres and Auth Backend-as-a-Service with real-time capabilities ## Getting Help Need assistance with any SDK? * **Discord**: Join our [community server](https://discord.gg/bYqAp4ayYh) for real-time help * **Email**: Contact us at [support@dodopayments.com](mailto:support@dodopayments.com) * **GitHub**: Open an issue on the respective SDK repository * **Documentation**: Visit our [API reference](/api-reference/introduction) ## Contributing We welcome contributions to all our SDKs! Each repository has a `CONTRIBUTING.md` file with guidelines for: * Reporting bugs * Requesting features * Submitting pull requests * Running tests locally * Code style and conventions Visit the individual SDK pages to access their GitHub repositories and contribution guidelines. # Express Adaptor Source: https://docs.dodopayments.com/developer-resources/express-adaptor Learn how to integrate Dodo Payments with your Express App Router project using our Express Adaptor. Covers checkout, customer portal, webhooks, and secure environment setup. Integrate Dodo Payments checkout into your Express app. Allow customers to manage subscriptions and details. Receive and process Dodo Payments webhook events. ## Installation Run the following command in your project root: ```bash theme={null} npm install @dodopayments/express ``` Create a .env file in your project root: ```env expandable theme={null} DODO_PAYMENTS_API_KEY=your-api-key DODO_PAYMENTS_WEBHOOK_KEY=your-webhook-secret DODO_PAYMENTS_ENVIRONMENT="test_mode" or "live_mode" DODO_PAYMENTS_RETURN_URL=your-return-url ``` Never commit your .env file or secrets to version control. ## Route Handler Examples Use this handler to integrate Dodo Payments checkout into your Express app. Supports static (GET), dynamic (POST), and session (POST) payment flows. ```typescript Express Route Handler expandable theme={null} import { checkoutHandler } from '@dodopayments/express'; app.get('/api/checkout', checkoutHandler({ bearerToken: process.env.DODO_PAYMENTS_API_KEY, returnUrl: process.env.DODO_PAYMENTS_RETURN_URL, environment: process.env.DODO_PAYMENTS_ENVIRONMENT, type: "static" })) app.post('/api/checkout', checkoutHandler({ bearerToken: process.env.DODO_PAYMENTS_API_KEY, returnUrl: process.env.DODO_PAYMENTS_RETURN_URL, environment: process.env.DODO_PAYMENTS_ENVIRONMENT, type: "dynamic" })) app.post('/api/checkout', checkoutHandler({ bearerToken: process.env.DODO_PAYMENTS_API_KEY, returnUrl: process.env.DODO_PAYMENTS_RETURN_URL, environment: process.env.DODO_PAYMENTS_ENVIRONMENT, type: "session" })) ``` ```curl Static Checkout Curl example expandable theme={null} curl --request GET \ --url 'https://example.com/api/checkout?productId=pdt_fqJhl7pxKWiLhwQR042rh' \ --header 'User-Agent: insomnia/11.2.0' \ --cookie mode=test ``` ```curl Dynamic Checkout Curl example expandable theme={null} curl --request POST \ --url https://example.com/api/checkout \ --header 'Content-Type: application/json' \ --header 'User-Agent: insomnia/11.2.0' \ --cookie mode=test \ --data '{ "billing": { "city": "Texas", "country": "US", "state": "Texas", "street": "56, hhh", "zipcode": "560000" }, "customer": { "email": "test@example.com", "name": "test" }, "metadata": {}, "payment_link": true, "product_id": "pdt_QMDuvLkbVzCRWRQjLNcs", "quantity": 1, "billing_currency": "USD", "discount_codes": ["IKHZ23M9GQ"], "return_url": "https://example.com", "trial_period_days": 10 }' ``` ```curl Checkout Session Curl example expandable theme={null} curl --request POST \ --url https://example.com/api/checkout \ --header 'Content-Type: application/json' \ --header 'User-Agent: insomnia/11.2.0' \ --cookie mode=test \ --data '{ "product_cart": [ { "product_id": "pdt_QMDuvLkbVzCRWRQjLNcs", "quantity": 1 } ], "customer": { "email": "test@example.com", "name": "test" }, "return_url": "https://example.com/success" }' ``` Use this handler to allow customers to manage their subscriptions and details via the Dodo Payments customer portal. ```typescript Express Route Handler expandable theme={null} import { CustomerPortal } from "@dodopayments/express"; app.get('/api/customer-portal', CustomerPortal({ bearerToken: process.env.DODO_PAYMENTS_API_KEY, environment: process.env.DODO_PAYMENTS_ENVIRONMENT, })) ``` ```curl Customer Portal Curl example expandable theme={null} curl --request GET \ --url 'https://example.com/api/customer-portal?customer_id=cus_9VuW4K7O3GHwasENg31m&send_email=true' \ --header 'User-Agent: insomnia/11.2.0' \ --cookie mode=test ``` Use this handler to receive and process Dodo Payments webhook events securely in your Express app. ```typescript Express Route Handler expandable theme={null} import { Webhooks } from "@dodopayments/express"; app.post('/api/webhook',Webhooks({ webhookKey: process.env.DODO_PAYMENTS_WEBHOOK_KEY, onPayload: async (payload) => { // handle the payload }, // ... other event handlers for granular control })) ``` ## Checkout Route Handler Dodo Payments supports three types of payment flows for integrating payments into your website, this adaptor supports all types of payment flows. * **Static Payment Links:** Instantly shareable URLs for quick, no-code payment collection. * **Dynamic Payment Links:** Programmatically generate payment links with custom details using the API or SDKs. * **Checkout Sessions:** Create secure, customizable checkout experiences with pre-configured product carts and customer details. ### Supported Query Parameters Product identifier (e.g., ?productId=pdt\_nZuwz45WAs64n3l07zpQR). Quantity of the product. Customer's full name. Customer's first name. Customer's last name. Customer's email address. Customer's country. Customer's address line. Customer's city. Customer's state/province. Customer's zip/postal code. Disable full name field. Disable first name field. Disable last name field. Disable email field. Disable country field. Disable address line field. Disable city field. Disable state field. Disable zip code field. Specify the payment currency (e.g., USD). Show currency selector. Fixes the amount charged, in major currency units (e.g., 12.5 for \$12.50). Pay What You Want products only, and ignored if below the product's minimum price. Show discount fields. Any query parameter starting with metadata\_ will be passed as metadata. If productId is missing, the handler returns a 400 response. Invalid query parameters also result in a 400 response. ### Response Format Static checkout returns a JSON response with the checkout URL: ```json theme={null} { "checkout_url": "https://checkout.dodopayments.com/..." } ``` * Send parameters as a JSON body in a POST request. * Supports both one-time and recurring payments. * For a complete list of supported POST body fields, refer to: * [Request body for a One Time Payment Product](https://docs.dodopayments.com/api-reference/payments/post-payments) * [Request body for a Subscription Product](https://docs.dodopayments.com/api-reference/subscriptions/post-subscriptions) ### Response Format Dynamic checkout returns a JSON response with the checkout URL: ```json theme={null} { "checkout_url": "https://checkout.dodopayments.com/..." } ``` Checkout sessions provide a more secure, hosted checkout experience that handles the complete payment flow for both one-time purchases and subscriptions with full customization control. Refer to [Checkout Sessions Integration Guide](https://docs.dodopayments.com/developer-resources/checkout-session) for more details and a complete list of supported fields. ### Response Format Checkout sessions return a JSON response with the checkout URL: ```json theme={null} { "checkout_url": "https://checkout.dodopayments.com/session/..." } ``` ## Customer Portal Route Handler The Customer Portal Route Handler enables you to seamlessly integrate the Dodo Payments customer portal into your Express application. ### Query Parameters The customer ID for the portal session (e.g., ?customer\_id=cus\_123). If set to true, sends an email to the customer with the portal link. Returns 400 if customer\_id is missing. ## Webhook Route Handler * **Method:** Only POST requests are supported. Other methods return 405. * **Signature Verification:** Verifies the webhook signature using webhookKey. Returns 401 if verification fails. * **Payload Validation:** Validated with Zod. Returns 400 for invalid payloads. * **Error Handling:** * 401: Invalid signature * 400: Invalid payload * 500: Internal error during verification * **Event Routing:** Calls the appropriate event handler based on the payload type. ### Supported Webhook Event Handlers ```typescript Typescript expandable theme={null} onPayload?: (payload: WebhookPayload) => Promise; onPaymentSucceeded?: (payload: WebhookPayload) => Promise; onPaymentFailed?: (payload: WebhookPayload) => Promise; onPaymentProcessing?: (payload: WebhookPayload) => Promise; onPaymentCancelled?: (payload: WebhookPayload) => Promise; onRefundSucceeded?: (payload: WebhookPayload) => Promise; onRefundFailed?: (payload: WebhookPayload) => Promise; onDisputeOpened?: (payload: WebhookPayload) => Promise; onDisputeExpired?: (payload: WebhookPayload) => Promise; onDisputeAccepted?: (payload: WebhookPayload) => Promise; onDisputeCancelled?: (payload: WebhookPayload) => Promise; onDisputeChallenged?: (payload: WebhookPayload) => Promise; onDisputeWon?: (payload: WebhookPayload) => Promise; onDisputeLost?: (payload: WebhookPayload) => Promise; onSubscriptionActive?: (payload: WebhookPayload) => Promise; onSubscriptionOnHold?: (payload: WebhookPayload) => Promise; onSubscriptionRenewed?: (payload: WebhookPayload) => Promise; onSubscriptionPlanChanged?: (payload: WebhookPayload) => Promise; onSubscriptionCancelled?: (payload: WebhookPayload) => Promise; onSubscriptionFailed?: (payload: WebhookPayload) => Promise; onSubscriptionExpired?: (payload: WebhookPayload) => Promise; onSubscriptionUpdated?: (payload: WebhookPayload) => Promise; onLicenseKeyCreated?: (payload: WebhookPayload) => Promise; onAbandonedCheckoutDetected?: (payload: WebhookPayload) => Promise; onAbandonedCheckoutRecovered?: (payload: WebhookPayload) => Promise; onDunningStarted?: (payload: WebhookPayload) => Promise; onDunningRecovered?: (payload: WebhookPayload) => Promise; onCreditAdded?: (payload: WebhookPayload) => Promise; onCreditDeducted?: (payload: WebhookPayload) => Promise; onCreditExpired?: (payload: WebhookPayload) => Promise; onCreditRolledOver?: (payload: WebhookPayload) => Promise; onCreditRolloverForfeited?: (payload: WebhookPayload) => Promise; onCreditOverageCharged?: (payload: WebhookPayload) => Promise; onCreditManualAdjustment?: (payload: WebhookPayload) => Promise; onCreditBalanceLow?: (payload: WebhookPayload) => Promise; ``` *** ## Prompt for LLM ``` You are an expert Express.js developer assistant. Your task is to guide a user through integrating the @dodopayments/express adapter into their existing Express.js project. The @dodopayments/express adapter provides route handlers for Dodo Payments' Checkout, Customer Portal, and Webhook functionalities, designed to plug directly into an Express app. First, install the necessary package. Use the package manager appropriate for the user's project (npm, yarn, or bun): npm install @dodopayments/express --- Here's how you should structure your response: 1. Ask the user which functionalities they want to integrate. "Which parts of the @dodopayments/express adapter would you like to integrate into your project? You can choose one or more of the following: - Checkout Route Handler (for handling product checkouts) - Customer Portal Route Handler (for managing customer subscriptions/details) - Webhook Route Handler (for receiving Dodo Payments webhook events) - All (integrate all three)" --- 2. Based on the user's selection, provide detailed integration steps for each chosen functionality. --- **If Checkout Route Handler is selected:** **Purpose**: This handler manages different types of checkout flows. All checkout types (static, dynamic, and sessions) return JSON responses with checkout URLs for programmatic handling. **Integration**: Create routes in your Express app for static (GET), dynamic (POST), and checkout sessions (POST). import { checkoutHandler } from '@dodopayments/express'; app.get('/api/checkout', checkoutHandler({ bearerToken: process.env.DODO_PAYMENTS_API_KEY, returnUrl: process.env.DODO_PAYMENTS_RETURN_URL, environment: process.env.DODO_PAYMENTS_ENVIRONMENT, type: "static" })); app.post('/api/checkout', checkoutHandler({ bearerToken: process.env.DODO_PAYMENTS_API_KEY, returnUrl: process.env.DODO_PAYMENTS_RETURN_URL, environment: process.env.DODO_PAYMENTS_ENVIRONMENT, type: "dynamic" })); // For checkout sessions app.post('/api/checkout', checkoutHandler({ bearerToken: process.env.DODO_PAYMENTS_API_KEY, returnUrl: process.env.DODO_PAYMENTS_RETURN_URL, environment: process.env.DODO_PAYMENTS_ENVIRONMENT, type: "session" })); Config Options: bearerToken: Your Dodo Payments API key (recommended to be stored in DODO_PAYMENTS_API_KEY env variable). returnUrl (optional): URL to redirect the user after successful checkout. environment: "test_mode" or "live_mode" type: "static" (GET), "dynamic" (POST), or "session" (POST) GET (static checkout) expects query parameters: productId (required) quantity, customer fields (fullName, email, etc.), and metadata (metadata_*) are optional. Returns: {"checkout_url": "https://checkout.dodopayments.com/..."} POST (dynamic checkout) expects a JSON body with payment details (one-time or subscription). Reference the docs for the full POST schema: One-time payments: https://docs.dodopayments.com/api-reference/payments/post-payments Subscriptions: https://docs.dodopayments.com/api-reference/subscriptions/post-subscriptions Returns: {"checkout_url": "https://checkout.dodopayments.com/..."} POST (checkout sessions) - (Recommended) A more customizable checkout experience. Returns JSON with checkout_url: Parameters are sent as a JSON body. Supports both one-time and recurring payments. Returns: {"checkout_url": "https://checkout.dodopayments.com/session/..."}. For a complete list of supported fields, refer to: Checkout Sessions Integration Guide: https://docs.dodopayments.com/developer-resources/checkout-session If Customer Portal Route Handler is selected: Purpose: This route allows customers to manage their subscriptions via the Dodo Payments portal. Integration: import { CustomerPortal } from "@dodopayments/express"; app.get('/api/customer-portal', CustomerPortal({ bearerToken: process.env.DODO_PAYMENTS_API_KEY, environment: process.env.DODO_PAYMENTS_ENVIRONMENT, })); Query Parameters: customer_id (required): e.g., ?customer_id=cus_123 send_email (optional): if true, customer is emailed the portal link Returns 400 if customer_id is missing. If Webhook Route Handler is selected: Purpose: Processes incoming webhook events from Dodo Payments to trigger events in your app. Integration: import { Webhooks } from "@dodopayments/express"; app.post('/api/webhook', Webhooks({ webhookKey: process.env.DODO_PAYMENTS_WEBHOOK_KEY, onPayload: async (payload) => { // Handle generic payload }, // You can also provide fine-grained handlers for each event type below })); Features: Only POST method is allowed — others return 405 Signature verification is performed using webhookKey. Returns 401 if invalid. Zod-based payload validation. Returns 400 if invalid schema. All handlers are async functions. Supported Webhook Event Handlers: You may pass in any of the following handlers: onPayload onPaymentSucceeded onPaymentFailed onPaymentProcessing onPaymentCancelled onRefundSucceeded onRefundFailed onDisputeOpened, onDisputeExpired, onDisputeAccepted, onDisputeCancelled, onDisputeChallenged, onDisputeWon, onDisputeLost onSubscriptionActive, onSubscriptionOnHold, onSubscriptionRenewed, onSubscriptionPlanChanged, onSubscriptionCancelled, onSubscriptionFailed, onSubscriptionExpired, onSubscriptionUpdated onLicenseKeyCreated onAbandonedCheckoutDetected, onAbandonedCheckoutRecovered onDunningStarted, onDunningRecovered onCreditAdded, onCreditDeducted, onCreditExpired, onCreditRolledOver, onCreditRolloverForfeited, onCreditOverageCharged, onCreditManualAdjustment, onCreditBalanceLow Environment Variable Setup: Make sure to define these environment variables in your project: DODO_PAYMENTS_API_KEY=your-api-key DODO_PAYMENTS_WEBHOOK_KEY=your-webhook-secret DODO_PAYMENTS_ENVIRONMENT="test_mode" or "live_mode" DODO_PAYMENTS_RETURN_URL=your-return-url Use these inside your code as: process.env.DODO_PAYMENTS_API_KEY process.env.DODO_PAYMENTS_WEBHOOK_SECRET Security Note: Do NOT commit secrets to version control. Use .env files locally and secrets managers in deployment environments (e.g., AWS, Vercel, Heroku, etc.). ``` # Framework Adaptors Overview Source: https://docs.dodopayments.com/developer-resources/framework-adaptors Pre-built adaptors for popular web frameworks to integrate Dodo Payments in minutes with minimal code Dodo Payments provides official framework adaptors that simplify payment integration. Each adaptor is designed to work seamlessly with your framework's conventions, offering checkout, customer portal, and webhook handling out of the box. Framework adaptors let you integrate Dodo Payments in under 10 lines of code. They handle authentication, request parsing, and response formatting automatically. ## Available Framework Adaptors Choose the adaptor that matches your framework: App Router support with route handlers for checkout, portal, and webhooks Vue-based full-stack framework with server routes integration Middleware-based handlers for the popular Node.js framework High-performance Node.js framework with plugin architecture Ultrafast web framework for the edge, Cloudflare Workers, and more Content-focused framework with server endpoints support Full-stack Svelte framework with server hooks integration Full-stack React framework with loader and action handlers Type-safe full-stack React framework with server functions Authentication framework plugin for seamless auth + payments Backend-as-a-Service component for real-time payment sync Native Bun.serve() handlers for checkout, portal, and webhooks ## Core Features All framework adaptors provide these built-in capabilities: | Feature | Description | | ---------------------- | ------------------------------------------------------------- | | **Checkout Handler** | Support for static, dynamic, and session-based checkout flows | | **Customer Portal** | Pre-built handler for subscription and billing management | | **Webhook Handler** | Secure signature verification with typed event handlers | | **Environment Config** | Simple setup via environment variables | | **Type Safety** | Full TypeScript support with typed payloads | ## Quick Start Get started with any framework adaptor in three steps: Use your package manager to install the framework-specific adaptor: ```bash theme={null} npm install @dodopayments/nextjs ``` ```bash theme={null} npm install @dodopayments/nuxt ``` ```bash theme={null} npm install @dodopayments/express ``` ```bash theme={null} npm install @dodopayments/hono ``` ```bash theme={null} npm install @dodopayments/astro ``` ```bash theme={null} npm install @dodopayments/sveltekit ``` Add your Dodo Payments credentials to your environment: ```env theme={null} DODO_PAYMENTS_API_KEY=your-api-key DODO_PAYMENTS_WEBHOOK_KEY=your-webhook-secret DODO_PAYMENTS_RETURN_URL=https://yourdomain.com/checkout/success DODO_PAYMENTS_ENVIRONMENT="test_mode" # or "live_mode" ``` Never commit your `.env` file or secrets to version control. Set up your checkout, customer portal, and webhook routes: ```typescript theme={null} // app/checkout/route.ts import { Checkout } from "@dodopayments/nextjs"; export const GET = Checkout({ bearerToken: process.env.DODO_PAYMENTS_API_KEY, returnUrl: process.env.DODO_PAYMENTS_RETURN_URL, environment: process.env.DODO_PAYMENTS_ENVIRONMENT, }); ``` ```typescript theme={null} import { checkoutHandler } from '@dodopayments/express'; app.get('/api/checkout', checkoutHandler({ bearerToken: process.env.DODO_PAYMENTS_API_KEY, returnUrl: process.env.DODO_PAYMENTS_RETURN_URL, environment: process.env.DODO_PAYMENTS_ENVIRONMENT, })); ``` ```typescript theme={null} import { Checkout } from "@dodopayments/hono"; app.get('/checkout', Checkout({ bearerToken: process.env.DODO_PAYMENTS_API_KEY, returnUrl: process.env.DODO_PAYMENTS_RETURN_URL, environment: process.env.DODO_PAYMENTS_ENVIRONMENT, })); ``` You're now ready to process payments! Visit the individual adaptor pages for detailed guides and all available options. ## Checkout Flow Types All adaptors support three checkout flow types: Use static checkout for simple, shareable payment links. Pass the product ID as a query parameter: ``` /api/checkout?productId=pdt_xxx&quantity=1 ``` Supports optional customer prefill and customization via query parameters. Use dynamic checkout to programmatically create payments with custom details: ```json theme={null} { "product_id": "pdt_xxx", "customer": { "email": "customer@example.com", "name": "John Doe" }, "quantity": 1 } ``` Supports both one-time payments and subscriptions. Use checkout sessions for the most flexible checkout experience with cart support: ```json theme={null} { "product_cart": [ { "product_id": "pdt_xxx", "quantity": 1 }, { "product_id": "pdt_yyy", "quantity": 2 } ], "customer": { "email": "customer@example.com" } } ``` Learn more in the [Checkout Sessions Guide](/developer-resources/checkout-session). ## Webhook Event Handling All adaptors provide type-safe webhook handling with granular event callbacks: ```typescript theme={null} Webhooks({ webhookKey: process.env.DODO_PAYMENTS_WEBHOOK_KEY, onPayload: async (payload) => { // Handle any webhook event }, onPaymentSucceeded: async (payload) => { // Handle successful payments }, onSubscriptionActive: async (payload) => { // Handle new subscriptions }, // ... 20+ event types supported }); ``` All webhook handlers automatically verify signatures and validate payloads using Zod schemas. Invalid requests are rejected with appropriate error codes. ## Choosing the Right Adaptor | Framework | Best For | Runtime | | ------------------ | -------------------------------------- | -------------- | | **Next.js** | Full-stack React apps with App Router | Node.js, Edge | | **Nuxt** | Full-stack Vue.js applications | Node.js | | **Express** | REST APIs and traditional Node.js apps | Node.js | | **Fastify** | High-performance APIs | Node.js | | **Hono** | Edge deployments, Cloudflare Workers | Edge, Node.js | | **Astro** | Content sites with server endpoints | Node.js, Edge | | **SvelteKit** | Full-stack Svelte applications | Node.js | | **Remix** | Full-stack React with nested routing | Node.js | | **TanStack Start** | Type-safe full-stack React | Node.js | | **Better Auth** | Apps already using Better Auth | Various | | **Convex** | Apps using Convex for backend | Convex Runtime | | **Bun** | Native Bun server applications | Bun | ## Getting Help Need assistance with framework adaptors? * **Discord**: Join our [community server](https://discord.gg/bYqAp4ayYh) for real-time help * **Email**: Contact us at [support@dodopayments.com](mailto:support@dodopayments.com) * **GitHub**: Open an issue on the respective adaptor repository * **Documentation**: Visit our [API reference](/api-reference/introduction) # API Gateway Blueprint Source: https://docs.dodopayments.com/developer-resources/ingestion-blueprints/api-gateway Track API calls and gateway-level usage for billing. Perfect for API-as-a-service platforms with high-volume request tracking. ## Use Cases Explore common scenarios supported by the API Gateway Blueprint: Track usage per customer for API platforms and charge based on number of calls. Monitor API usage patterns and implement usage-based rate limiting. Track response times and error rates alongside billing data. Bill customers based on their API consumption across different endpoints. Ideal for tracking API endpoint usage, rate limiting, and implementing usage-based API billing. ## Quick Start Track API calls at the gateway level with automatic batching for high-volume scenarios: ```bash theme={null} npm install @dodopayments/ingestion-blueprints ``` * **Dodo Payments API Key**: Get it from [Dodo Payments Dashboard](https://app.dodopayments.com/developer/api-keys) Create a meter in your [Dodo Payments Dashboard](https://app.dodopayments.com/): * **Event Name**: `api_call` (or your preferred name) * **Aggregation Type**: `count` for tracking number of calls * Configure additional properties if tracking metadata like response times, status codes, etc. ```javascript Single API Call theme={null} import { Ingestion, trackAPICall } from '@dodopayments/ingestion-blueprints'; const ingestion = new Ingestion({ apiKey: process.env.DODO_PAYMENTS_API_KEY, environment: 'test_mode', eventName: 'api_call' }); // Track a single API call await trackAPICall(ingestion, { customerId: 'customer_123', metadata: { endpoint: '/api/v1/users', method: 'GET', status_code: 200, response_time_ms: 45 } }); ``` ```javascript High-Volume with Batching theme={null} import { Ingestion, createBatch } from '@dodopayments/ingestion-blueprints'; const ingestion = new Ingestion({ apiKey: process.env.DODO_PAYMENTS_API_KEY, environment: 'live_mode', eventName: 'api_call' }); // Create batch for high-volume tracking const batch = createBatch(ingestion, { maxSize: 100, // Flush after 100 events flushInterval: 5000 // Or flush every 5 seconds }); // Add API calls to batch batch.add({ customerId: 'customer_123', metadata: { endpoint: '/api/v1/products', method: 'GET', status_code: 200 } }); // Clean up when done await batch.cleanup(); ``` ```javascript Express.js Middleware theme={null} import express from 'express'; import { Ingestion, createBatch } from '@dodopayments/ingestion-blueprints'; const app = express(); const ingestion = new Ingestion({ apiKey: process.env.DODO_PAYMENTS_API_KEY, environment: 'live_mode', eventName: 'api_call' }); const batch = createBatch(ingestion, { maxSize: 50, flushInterval: 10000 }); // Middleware to track all API calls app.use((req, res, next) => { const startTime = Date.now(); res.on('finish', () => { const responseTime = Date.now() - startTime; batch.add({ customerId: req.user?.id || 'anonymous', metadata: { endpoint: req.path, method: req.method, status_code: res.statusCode, response_time_ms: responseTime } }); }); next(); }); // Cleanup on shutdown process.on('SIGTERM', async () => { await batch.cleanup(); process.exit(0); }); ``` ## Configuration ### Ingestion Configuration Your Dodo Payments API key from the dashboard. Environment mode: `test_mode` or `live_mode`. Event name that matches your meter configuration. ### Track API Call Options The customer ID for billing attribution. Optional metadata about the API call like endpoint, method, status code, response time, etc. ### Batch Configuration Maximum number of events before auto-flush. Default: `100`. Auto-flush interval in milliseconds. Default: `5000` (5 seconds). ## Best Practices **Use Batching for High Volume**: For applications handling more than 10 requests per second, use `createBatch()` to reduce overhead and improve performance. **Always Clean Up Batches**: Call `batch.cleanup()` on application shutdown to flush pending events and prevent data loss. # LLM Blueprint Source: https://docs.dodopayments.com/developer-resources/ingestion-blueprints/llm Effortlessly track LLM token usage for usage-based billing with automatic ingestion to Dodo Payments. Works with AI SDK, OpenAI, Anthropic, OpenRouter, Groq, and Google Gemini. Get started in 2 minutes with automatic token tracking. Complete API documentation for ingesting usage events. Learn how to create and configure meters for billing. Comprehensive guide to usage-based billing with meters. Perfect for SaaS apps, AI chatbots, content generation tools, and any LLM-powered application that needs usage-based billing. ## Quick Start Get started with automatic LLM token tracking in just 2 minutes: Install the Dodo Payments Ingestion Blueprints: ```bash theme={null} npm install @dodopayments/ingestion-blueprints ``` You'll need two API keys: * **Dodo Payments API Key**: Get it from [Dodo Payments Dashboard](https://app.dodopayments.com/developer/api-keys) * **LLM Provider API Key**: From AI SDK, OpenAI, Anthropic, Groq, etc. Store your API keys securely in environment variables. Never commit them to version control. Before tracking usage, create a meter in your Dodo Payments dashboard: 1. **Login** to [Dodo Payments Dashboard](https://app.dodopayments.com/) 2. **Navigate to** Products → Meters 3. **Click** "Create Meter" 4. **Configure your meter**: * **Meter Name**: Choose a descriptive name (e.g., "LLM Token Usage") * **Event Name**: Set a unique event identifier (e.g., `llm.chat_completion`) * **Aggregation Type**: Select `sum` to add up token counts * **Over Property**: Choose what to track: * `inputTokens` - Track input/prompt tokens * `outputTokens` - Track output/completion tokens (includes reasoning tokens when applicable) * `totalTokens` - Track combined input + output tokens The **Event Name** you set here must match exactly what you pass to the SDK (case-sensitive). For detailed instructions, see the [Usage-Based Billing Guide](/developer-resources/usage-based-billing-guide). Wrap your LLM client and start tracking automatically: ```javascript AI SDK theme={null} import { createLLMTracker } from '@dodopayments/ingestion-blueprints'; import { generateText } from 'ai'; import { google } from '@ai-sdk/google'; const llmTracker = createLLMTracker({ apiKey: process.env.DODO_PAYMENTS_API_KEY, environment: 'test_mode', eventName: 'aisdk.usage', }); const client = llmTracker.wrap({ client: { generateText }, customerId: 'customer_123' }); const response = await client.generateText({ model: google('gemini-2.0-flash'), prompt: 'Hello!', maxOutputTokens: 500 }); console.log('Usage:', response.usage); ``` ```javascript OpenRouter theme={null} import { createLLMTracker } from '@dodopayments/ingestion-blueprints'; import OpenAI from 'openai'; const openrouter = new OpenAI({ baseURL: 'https://openrouter.ai/api/v1', apiKey: process.env.OPENROUTER_API_KEY }); const llmTracker = createLLMTracker({ apiKey: process.env.DODO_PAYMENTS_API_KEY, environment: 'test_mode', eventName: 'openrouter.usage' }); const client = llmTracker.wrap({ client: openrouter, customerId: 'customer_123' }); const response = await client.chat.completions.create({ model: 'qwen/qwen3-max', messages: [{ role: 'user', content: 'Hello!' }], max_tokens: 500 }); console.log('Response:', response.choices[0].message.content); console.log('Usage:', response.usage); ``` ```javascript OpenAI theme={null} import { createLLMTracker } from '@dodopayments/ingestion-blueprints'; import OpenAI from 'openai'; // 1. Create your LLM client (normal way) const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); // 2. Create tracker ONCE at startup const tracker = createLLMTracker({ apiKey: process.env.DODO_PAYMENTS_API_KEY, environment: 'test_mode', // Use 'live_mode' for production eventName: 'llm.chat_completion' // Match your meter's event name }); // 3. Wrap & use - automatic tracking! const client = tracker.wrap({ client: openai, customerId: 'customer_123' }); // Every API call is now automatically tracked const response = await client.chat.completions.create({ model: 'gpt-4', messages: [{ role: 'user', content: 'Hello!' }] }); // ✨ Usage automatically sent to Dodo Payments! console.log('Tokens used:', response.usage); ``` ```javascript Anthropic theme={null} import { createLLMTracker } from '@dodopayments/ingestion-blueprints'; import Anthropic from '@anthropic-ai/sdk'; const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }); const tracker = createLLMTracker({ apiKey: process.env.DODO_PAYMENTS_API_KEY, environment: 'test_mode', eventName: 'anthropic.usage' }); const client = tracker.wrap({ client: anthropic, customerId: 'customer_123' }); const response = await client.messages.create({ model: 'claude-sonnet-4-0', max_tokens: 1024, messages: [{ role: 'user', content: 'Hello Claude!' }] }); console.log('Tokens used:', response.usage); ``` ```javascript Groq theme={null} import { createLLMTracker } from '@dodopayments/ingestion-blueprints'; import Groq from 'groq-sdk'; const groq = new Groq({ apiKey: process.env.GROQ_API_KEY }); const tracker = createLLMTracker({ apiKey: process.env.DODO_PAYMENTS_API_KEY, environment: 'test_mode', eventName: 'groq.usage' }); const client = tracker.wrap({ client: groq, customerId: 'customer_123' }); const response = await client.chat.completions.create({ model: 'llama-3.1-8b-instant', messages: [{ role: 'user', content: 'Hello!' }] }); console.log('Tokens:', response.usage); ``` ```javascript Google Gemini theme={null} import { createLLMTracker } from '@dodopayments/ingestion-blueprints'; import { GoogleGenAI } from '@google/genai'; const googleGenai = new GoogleGenAI({ apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY }); const llmTracker = createLLMTracker({ apiKey: process.env.DODO_PAYMENTS_API_KEY, environment: 'test_mode', eventName: 'gemini.usage' }); const client = llmTracker.wrap({ client: googleGenai, customerId: 'customer_123' }); const response = await client.models.generateContent({ model: 'gemini-2.5-flash', contents: 'Why is the sky blue?' }); console.log('Response:', response.text); console.log('Usage:', response.usageMetadata); ``` That's it! Every API call now automatically tracks token usage and sends events to Dodo Payments for billing. *** ## Configuration ### Tracker Configuration Create a tracker once at application startup with these required parameters: Your Dodo Payments API key. Get it from the [API Keys page](https://app.dodopayments.com/developer/api-keys). ```javascript theme={null} apiKey: process.env.DODO_PAYMENTS_API_KEY ``` The environment mode for the tracker. * `test_mode` - Use for development and testing * `live_mode` - Use for production ```javascript theme={null} environment: 'test_mode' // or 'live_mode' ``` Always use `test_mode` during development to avoid affecting production metrics. The event name that triggers your meter. Must match exactly what you configured in your Dodo Payments meter (case-sensitive). ```javascript theme={null} eventName: 'llm.chat_completion' ``` This event name links your tracked usage to the correct meter for billing calculations. ### Wrapper Configuration When wrapping your LLM client, provide these parameters: Your LLM client instance (OpenAI, Anthropic, Groq, etc.). ```javascript theme={null} client: openai ``` The unique customer identifier for billing. This should match your customer ID in Dodo Payments. ```javascript theme={null} customerId: 'customer_123' ``` Use your application's user ID or customer ID to ensure accurate billing per customer. Optional additional data to attach to the tracking event. Useful for filtering and analysis. ```javascript theme={null} metadata: { feature: 'chat', userTier: 'premium', sessionId: 'session_123', modelVersion: 'gpt-4' } ``` ### Complete Configuration Example ```javascript Full Configuration theme={null} import { createLLMTracker } from "@dodopayments/ingestion-blueprints"; import { generateText } from "ai"; import { google } from "@ai-sdk/google"; import "dotenv/config"; async function aiSdkExample() { console.log("🤖 AI SDK Simple Usage Example\n"); try { // 1. Create tracker const llmTracker = createLLMTracker({ apiKey: process.env.DODO_PAYMENTS_API_KEY!, environment: "test_mode", eventName: "your_meter_event_name", }); // 2. Wrap the ai-sdk methods const client = llmTracker.wrap({ client: { generateText }, customerId: "customer_123", metadata: { provider: "ai-sdk", }, }); // 3. Use the wrapped function const response = await client.generateText({ model: google("gemini-2.5-flash"), prompt: "Hello, I am a cool guy! Tell me a fun fact.", maxOutputTokens: 500, }); console.log(response); console.log(response.usage); console.log("✅ Automatically tracked for customer\n"); } catch (error) { console.error(error); } } aiSdkExample().catch(console.error); ``` **Automatic Tracking:** The SDK automatically tracks token usage in the background without modifying the response. Your code remains clean and identical to using the original provider SDKs. *** ## Supported Providers The LLM Blueprint works seamlessly with all major LLM providers and aggregators: Track usage with the Vercel AI SDK for universal LLM support. ```javascript AI SDK Integration theme={null} import { createLLMTracker } from '@dodopayments/ingestion-blueprints'; import { generateText } from 'ai'; import { google } from '@ai-sdk/google'; const llmTracker = createLLMTracker({ apiKey: process.env.DODO_PAYMENTS_API_KEY, environment: 'test_mode', eventName: 'aisdk.usage', }); const client = llmTracker.wrap({ client: { generateText }, customerId: 'customer_123', metadata: { model: 'gemini-2.0-flash', feature: 'chat' } }); const response = await client.generateText({ model: google('gemini-2.0-flash'), prompt: 'Explain neural networks', maxOutputTokens: 500 }); console.log('Usage:', response.usage); ``` **Tracked Metrics:** * `inputTokens` → `inputTokens` * `outputTokens` + `reasoningTokens` → `outputTokens` * `totalTokens` → `totalTokens` * Model name When using reasoning-capable models through AI SDK (like Google's Gemini 2.5 Flash with thinking mode), reasoning tokens are automatically included in the `outputTokens` count for accurate billing. Track token usage across 200+ models via OpenRouter's unified API. ```javascript OpenRouter Integration theme={null} import { createLLMTracker } from '@dodopayments/ingestion-blueprints'; import OpenAI from 'openai'; // OpenRouter uses OpenAI-compatible API const openrouter = new OpenAI({ baseURL: 'https://openrouter.ai/api/v1', apiKey: process.env.OPENROUTER_API_KEY }); const tracker = createLLMTracker({ apiKey: process.env.DODO_PAYMENTS_API_KEY, environment: 'test_mode', eventName: 'openrouter.usage' }); const client = tracker.wrap({ client: openrouter, customerId: 'user_123', metadata: { provider: 'openrouter' } }); const response = await client.chat.completions.create({ model: 'qwen/qwen3-max', messages: [{ role: 'user', content: 'What is machine learning?' }], max_tokens: 500 }); console.log('Response:', response.choices[0].message.content); console.log('Usage:', response.usage); ``` **Tracked Metrics:** * `prompt_tokens` → `inputTokens` * `completion_tokens` → `outputTokens` * `total_tokens` → `totalTokens` * Model name OpenRouter provides access to models from OpenAI, Anthropic, Google, Meta, and many more providers through a single API. Track token usage from OpenAI's GPT models automatically. ```javascript OpenAI Integration theme={null} import { createLLMTracker } from '@dodopayments/ingestion-blueprints'; import OpenAI from 'openai'; const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); const tracker = createLLMTracker({ apiKey: process.env.DODO_PAYMENTS_API_KEY, environment: 'test_mode', eventName: 'openai.usage' }); const client = tracker.wrap({ client: openai, customerId: 'user_123' }); // All OpenAI methods work automatically const response = await client.chat.completions.create({ model: 'gpt-4', messages: [{ role: 'user', content: 'Explain quantum computing' }] }); console.log('Total tokens:', response.usage.total_tokens); ``` **Tracked Metrics:** * `prompt_tokens` → `inputTokens` * `completion_tokens` → `outputTokens` * `total_tokens` → `totalTokens` * Model name Track token usage from Anthropic's Claude models. ```javascript Anthropic Integration theme={null} import { createLLMTracker } from '@dodopayments/ingestion-blueprints'; import Anthropic from '@anthropic-ai/sdk'; const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }); const tracker = createLLMTracker({ apiKey: process.env.DODO_PAYMENTS_API_KEY, environment: 'test_mode', eventName: 'anthropic.usage' }); const client = tracker.wrap({ client: anthropic, customerId: 'user_123' }); const response = await client.messages.create({ model: 'claude-sonnet-4-0', max_tokens: 1024, messages: [{ role: 'user', content: 'Explain machine learning' }] }); console.log('Input tokens:', response.usage.input_tokens); console.log('Output tokens:', response.usage.output_tokens); ``` **Tracked Metrics:** * `input_tokens` → `inputTokens` * `output_tokens` → `outputTokens` * Calculated `totalTokens` * Model name Track ultra-fast LLM inference with Groq. ```javascript Groq Integration theme={null} import { createLLMTracker } from '@dodopayments/ingestion-blueprints'; import Groq from 'groq-sdk'; const groq = new Groq({ apiKey: process.env.GROQ_API_KEY }); const tracker = createLLMTracker({ apiKey: process.env.DODO_PAYMENTS_API_KEY, environment: 'test_mode', eventName: 'groq.usage' }); const client = tracker.wrap({ client: groq, customerId: 'user_123' }); const response = await client.chat.completions.create({ model: 'llama-3.1-8b-instant', messages: [{ role: 'user', content: 'What is AI?' }] }); console.log('Tokens:', response.usage); ``` **Tracked Metrics:** * `prompt_tokens` → `inputTokens` * `completion_tokens` → `outputTokens` * `total_tokens` → `totalTokens` * Model name Track token usage from Google's Gemini models via the Google GenAI SDK. ```javascript Google Gemini Integration theme={null} import { createLLMTracker } from '@dodopayments/ingestion-blueprints'; import { GoogleGenAI } from '@google/genai'; const googleGenai = new GoogleGenAI({ apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY }); const tracker = createLLMTracker({ apiKey: process.env.DODO_PAYMENTS_API_KEY, environment: 'test_mode', eventName: 'gemini.usage' }); const client = tracker.wrap({ client: googleGenai, customerId: 'user_123' }); const response = await client.models.generateContent({ model: 'gemini-2.5-flash', contents: 'Explain quantum computing' }); console.log('Response:', response.text); console.log('Usage:', response.usageMetadata); ``` **Tracked Metrics:** * `promptTokenCount` → `inputTokens` * `candidatesTokenCount` + `thoughtsTokenCount` → `outputTokens` * `totalTokenCount` → `totalTokens` * Model version **Gemini Thinking Mode:** When using Gemini models with thinking/reasoning capabilities (like Gemini 2.5 Pro), the SDK automatically includes `thoughtsTokenCount` (reasoning tokens) in `outputTokens` to accurately reflect the full computational cost. *** ## Advanced Usage ### Multiple Providers Track usage across different LLM providers with separate trackers: ```javascript Multiple Provider Setup theme={null} import { createLLMTracker } from '@dodopayments/ingestion-blueprints'; import OpenAI from 'openai'; import Groq from 'groq-sdk'; import Anthropic from '@anthropic-ai/sdk'; import { GoogleGenAI } from '@google/genai'; // Create separate trackers for different providers const openaiTracker = createLLMTracker({ apiKey: process.env.DODO_PAYMENTS_API_KEY, environment: 'live_mode', eventName: 'openai.usage' }); const groqTracker = createLLMTracker({ apiKey: process.env.DODO_PAYMENTS_API_KEY, environment: 'live_mode', eventName: 'groq.usage' }); const anthropicTracker = createLLMTracker({ apiKey: process.env.DODO_PAYMENTS_API_KEY, environment: 'live_mode', eventName: 'anthropic.usage' }); const geminiTracker = createLLMTracker({ apiKey: process.env.DODO_PAYMENTS_API_KEY, environment: 'live_mode', eventName: 'gemini.usage' }); const openrouterTracker = createLLMTracker({ apiKey: process.env.DODO_PAYMENTS_API_KEY, environment: 'live_mode', eventName: 'openrouter.usage' }); // Initialize clients const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); const groq = new Groq({ apiKey: process.env.GROQ_API_KEY }); const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }); const googleGenai = new GoogleGenAI({ apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY }); const openrouter = new OpenAI({ baseURL: 'https://openrouter.ai/api/v1', apiKey: process.env.OPENROUTER_API_KEY }); // Wrap clients const trackedOpenAI = openaiTracker.wrap({ client: openai, customerId: 'user_123' }); const trackedGroq = groqTracker.wrap({ client: groq, customerId: 'user_123' }); const trackedAnthropic = anthropicTracker.wrap({ client: anthropic, customerId: 'user_123' }); const trackedGemini = geminiTracker.wrap({ client: googleGenai, customerId: 'user_123' }); const trackedOpenRouter = openrouterTracker.wrap({ client: openrouter, customerId: 'user_123' }); // Use whichever provider you need const response = await trackedOpenAI.chat.completions.create({...}); // or const geminiResponse = await trackedGemini.models.generateContent({...}); // or const openrouterResponse = await trackedOpenRouter.chat.completions.create({...}); ``` Use different event names for different providers to track usage separately in your meters. ### Express.js API Integration Complete example of integrating LLM tracking into an Express.js API: ```javascript Express.js Server theme={null} import express from 'express'; import { createLLMTracker } from '@dodopayments/ingestion-blueprints'; import OpenAI from 'openai'; const app = express(); app.use(express.json()); // Initialize OpenAI client const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); // Create tracker once at startup const tracker = createLLMTracker({ apiKey: process.env.DODO_PAYMENTS_API_KEY, environment: process.env.NODE_ENV === 'production' ? 'live_mode' : 'test_mode', eventName: 'api.chat_completion' }); // Chat endpoint with automatic tracking app.post('/api/chat', async (req, res) => { try { const { message, userId } = req.body; // Validate input if (!message || !userId) { return res.status(400).json({ error: 'Missing message or userId' }); } // Wrap client for this specific user const trackedClient = tracker.wrap({ client: openai, customerId: userId, metadata: { endpoint: '/api/chat', timestamp: new Date().toISOString() } }); // Make LLM request - automatically tracked const response = await trackedClient.chat.completions.create({ model: 'gpt-4', messages: [{ role: 'user', content: message }], temperature: 0.7 }); const completion = response.choices[0].message.content; res.json({ message: completion, usage: response.usage }); } catch (error) { console.error('Chat error:', error); res.status(500).json({ error: 'Internal server error' }); } }); app.listen(3000, () => { console.log('Server running on port 3000'); }); ``` *** ## What Gets Tracked Every LLM API call automatically sends a usage event to Dodo Payments with the following structure: ```json Event Structure theme={null} { "event_id": "llm_1673123456_abc123", "customer_id": "customer_123", "event_name": "llm.chat_completion", "timestamp": "2024-01-08T10:30:00Z", "metadata": { "inputTokens": 10, "outputTokens": 25, "totalTokens": 35, "model": "gpt-4", } } ``` ### Event Fields Unique identifier for this specific event. Automatically generated by the SDK. Format: `llm_[timestamp]_[random]` The customer ID you provided when wrapping the client. Used for billing. The event name that triggers your meter. Matches your tracker configuration. ISO 8601 timestamp when the event occurred. Token usage and additional tracking data: * `inputTokens` - Number of input/prompt tokens used * `outputTokens` - Number of output/completion tokens used (includes reasoning tokens when applicable) * `totalTokens` - Total tokens (input + output) * `model` - The LLM model used (e.g., "gpt-4") * `provider` - The LLM provider (if included in wrapper metadata) * Any custom metadata you provided when wrapping the client **Reasoning Tokens:** For models with reasoning capabilities, `outputTokens` automatically includes both the completion tokens and reasoning tokens. Your Dodo Payments meter uses the `metadata` fields (especially `inputTokens`, `outputTokens` or `totalTokens`) to calculate usage and billing. *** # Object Storage Blueprint Source: https://docs.dodopayments.com/developer-resources/ingestion-blueprints/object-storage Track file uploads and storage usage for S3, Google Cloud Storage, Azure Blob, and other object storage services. ## Use Cases Explore common scenarios supported by the Object Storage Blueprint: Bill customers based on total storage usage and upload volume. Track backup data uploads and charge per GB stored. Monitor media uploads and bill for storage and bandwidth. Track document uploads per customer for usage-based pricing. Perfect for billing based on storage uploads, file hosting, CDN usage, or backup services. ## Quick Start Track object storage uploads with bytes consumed: ```bash theme={null} npm install @dodopayments/ingestion-blueprints ``` * **Dodo Payments API Key**: Get it from [Dodo Payments Dashboard](https://app.dodopayments.com/developer/api-keys) * **Storage Provider API Key**: From AWS S3, Google Cloud Storage, Azure, etc. Create a meter in your [Dodo Payments Dashboard](https://app.dodopayments.com/): * **Event Name**: `object_storage_upload` (or your preferred name) * **Aggregation Type**: `sum` to track total bytes uploaded * **Over Property**: `bytes` to bill based on storage size ```javascript AWS S3 Upload theme={null} import { Ingestion, trackObjectStorage } from '@dodopayments/ingestion-blueprints'; import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; import fs from 'fs'; const ingestion = new Ingestion({ apiKey: process.env.DODO_PAYMENTS_API_KEY, environment: 'test_mode', eventName: 'object_storage_upload' }); const s3 = new S3Client({ region: 'us-east-1' }); // Read the file (example: from disk or request) const fileBuffer = fs.readFileSync('./document.pdf'); // Upload to S3 const command = new PutObjectCommand({ Bucket: 'my-bucket', Key: 'uploads/document.pdf', Body: fileBuffer }); await s3.send(command); // Track the upload await trackObjectStorage(ingestion, { customerId: 'customer_123', bytes: fileBuffer.length }); ``` ```javascript Google Cloud Storage theme={null} import { Ingestion, trackObjectStorage } from '@dodopayments/ingestion-blueprints'; import { Storage } from '@google-cloud/storage'; import fs from 'fs'; const ingestion = new Ingestion({ apiKey: process.env.DODO_PAYMENTS_API_KEY, environment: 'test_mode', eventName: 'object_storage_upload' }); const storage = new Storage(); const bucket = storage.bucket('my-bucket'); // Read the file const fileBuffer = fs.readFileSync('./image.png'); // Upload to GCS await bucket.file('uploads/image.png').save(fileBuffer); // Track the upload await trackObjectStorage(ingestion, { customerId: 'customer_456', bytes: fileBuffer.length, metadata: { bucket: 'my-bucket', key: 'uploads/image.png' } }); ``` ## Configuration ### Ingestion Configuration Your Dodo Payments API key from the dashboard. Environment mode: `test_mode` or `live_mode`. Event name that matches your meter configuration. ### Track Object Storage Options The customer ID for billing attribution. Number of bytes uploaded. Required for byte-based billing. Optional metadata about the upload like bucket name, content type, etc. ## Best Practices **Track Before or After Upload**: You can track the event before or after the actual upload depending on your error handling strategy. **Handle Upload Failures**: Only track successful uploads to avoid billing for failed operations. # Stream Blueprint Source: https://docs.dodopayments.com/developer-resources/ingestion-blueprints/stream Track streaming data consumption for video, audio, live streams, and real-time data transfer billing. ## Use Cases Explore common scenarios supported by the Stream Blueprint: Bill customers based on video bandwidth consumption and streaming quality. Track audio streaming usage per user for subscription tiers. Monitor live stream consumption and charge for bandwidth usage. Track real-time data transfer for IoT and telemetry applications. Perfect for video/audio streaming platforms, live streaming services, and real-time data applications. ## Quick Start Track streaming bytes consumed by your customers: ```bash theme={null} npm install @dodopayments/ingestion-blueprints ``` * **Dodo Payments API Key**: Get it from [Dodo Payments Dashboard](https://app.dodopayments.com/developer/api-keys) Create a meter in your [Dodo Payments Dashboard](https://app.dodopayments.com/): * **Event Name**: `stream_consumption` (or your preferred name) * **Aggregation Type**: `sum` to track total bytes streamed * **Over Property**: `bytes` to bill based on bandwidth usage ```javascript Video Streaming theme={null} import { Ingestion, trackStreamBytes } from '@dodopayments/ingestion-blueprints'; const ingestion = new Ingestion({ apiKey: process.env.DODO_PAYMENTS_API_KEY, environment: 'test_mode', eventName: 'stream_consumption' }); // Track video stream consumption await trackStreamBytes(ingestion, { customerId: 'customer_123', bytes: 10485760, // 10MB metadata: { stream_type: 'video', } }); ``` ## Configuration ### Ingestion Configuration Your Dodo Payments API key from the dashboard. Environment mode: `test_mode` or `live_mode`. Event name that matches your meter configuration. ### Track Stream Bytes Options The customer ID for billing attribution. Number of bytes consumed in the stream. Required for bandwidth-based billing. Optional metadata about the stream like stream type, quality, sessionId, etc. ## Best Practices **Track by Chunk**: For long streams, track consumption in chunks rather than waiting for the entire stream to complete. **Accurate Byte Counting**: Ensure byte counts include all overhead (headers, protocol overhead) if billing for total bandwidth. # Time Range Blueprint Source: https://docs.dodopayments.com/developer-resources/ingestion-blueprints/time-range Track resource consumption based on elapsed time for compute, serverless functions, containers, and runtime billing. ## Use Cases Explore common scenarios supported by the Time Range Blueprint: Bill based on function execution time and memory usage. Track container running time for usage-based billing. Monitor VM runtime and charge by the minute or hour. Track processing time for data exports, reports, and batch jobs. Perfect for billing based on compute time, function execution duration, container runtime, or any time-based usage. ## Quick Start Track resource usage by time duration: ```bash theme={null} npm install @dodopayments/ingestion-blueprints ``` * **Dodo Payments API Key**: Get it from [Dodo Payments Dashboard](https://app.dodopayments.com/developer/api-keys) Create a meter in your [Dodo Payments Dashboard](https://app.dodopayments.com/): * **Event Name**: `time_range_usage` (or your preferred name) * **Aggregation Type**: `sum` to track total duration * **Over Property**: `durationSeconds`, `durationMinutes`, or `durationMs` ```javascript Serverless Functions theme={null} import { Ingestion, trackTimeRange } from '@dodopayments/ingestion-blueprints'; const ingestion = new Ingestion({ apiKey: process.env.DODO_PAYMENTS_API_KEY, environment: 'test_mode', eventName: 'function_execution' }); // Track function execution time const startTime = Date.now(); // Execute your function (example: image processing) const result = await yourImageProcessingLogic(); const durationMs = Date.now() - startTime; await trackTimeRange(ingestion, { customerId: 'customer_123', durationMs: durationMs }); ``` ```javascript Container Runtime theme={null} import { Ingestion, trackTimeRange } from '@dodopayments/ingestion-blueprints'; const ingestion = new Ingestion({ apiKey: process.env.DODO_PAYMENTS_API_KEY, environment: 'test_mode', eventName: 'container_runtime' }); // Track container runtime in seconds await trackTimeRange(ingestion, { customerId: 'customer_456', durationSeconds: 120 }); ``` ```javascript VM Instance Runtime theme={null} import { Ingestion, trackTimeRange } from '@dodopayments/ingestion-blueprints'; const ingestion = new Ingestion({ apiKey: process.env.DODO_PAYMENTS_API_KEY, environment: 'test_mode', eventName: 'vm_runtime' }); // Track VM runtime in minutes await trackTimeRange(ingestion, { customerId: 'customer_789', durationMinutes: 60 }); ``` ## Configuration ### Ingestion Configuration Your Dodo Payments API key from the dashboard. Environment mode: `test_mode` or `live_mode`. Event name that matches your meter configuration. ### Track Time Range Options The customer ID for billing attribution. Duration in milliseconds. Use for sub-second precision. Duration in seconds. Most common for function execution and short tasks. Duration in minutes. Useful for longer-running resources like VMs. Optional metadata about the resource like CPU, memory, region, etc. ## Best Practices **Choose the Right Unit**: Use milliseconds for short operations, seconds for functions, and minutes for longer-running resources. **Accurate Timing**: Use `Date.now()` or `performance.now()` for accurate time tracking, especially for serverless functions. # Mobile Integration Guide Source: https://docs.dodopayments.com/developer-resources/mobile-integration Unified guide for integrating Dodo Payments into Android, iOS, React Native and Flutter mobile applications. Get your mobile payment integration running in 4 simple steps Complete code examples for Android, iOS, React Native, and Flutter Dodo Payments ships an official checkout SDK for **Android, iOS, React Native, and Flutter**. Each one wraps the pattern documented below (open the checkout URL, capture the return, parse the result) behind a single typed `start(...)` call, with abandoned-session recovery built in. Reach for a manual WebView only if none of them fits your stack. ## Prerequisites Before integrating Dodo Payments into your mobile app, ensure you have: * **Dodo Payments Account**: Active merchant account with API access * **API Credentials**: API key and webhook secret key from your dashboard * **Mobile App Project**: Android, iOS, React Native, or Flutter application * **Backend Server**: To securely handle checkout session creation ## Integration Workflow The mobile integration follows a secure 4-step process where your backend handles API calls and your mobile app manages the user experience. Learn how to create a checkout session in your backend using Node.js, Python, and more. See complete examples and parameter references in the dedicated Checkout Sessions API documentation. **Security**: Checkout sessions must be created on your backend server, never in the mobile app. This protects your API keys and ensures proper validation. Your mobile app calls your backend to get the checkout URL. Authenticate this request with the signed-in user's own session token. ```swift theme={null} func getCheckoutURL(productId: String, customerEmail: String, customerName: String) async throws -> String { let url = URL(string: "https://your-backend.com/api/create-checkout-session")! var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.setValue("Bearer \(userSessionToken)", forHTTPHeaderField: "Authorization") let requestData: [String: Any] = [ "productId": productId, "customerEmail": customerEmail, "customerName": customerName ] request.httpBody = try JSONSerialization.data(withJSONObject: requestData) let (data, _) = try await URLSession.shared.data(for: request) let response = try JSONDecoder().decode(CheckoutResponse.self, from: data) return response.checkout_url } ``` ```kotlin theme={null} suspend fun getCheckoutURL(productId: String, customerEmail: String, customerName: String): String { val client = OkHttpClient() val requestBody = JSONObject().apply { put("productId", productId) put("customerEmail", customerEmail) put("customerName", customerName) }.toString().toRequestBody("application/json".toMediaType()) val request = Request.Builder() .url("https://your-backend.com/api/create-checkout-session") .header("Authorization", "Bearer $userSessionToken") .post(requestBody) .build() val response = client.newCall(request).execute() val responseBody = response.body?.string() val jsonResponse = JSONObject(responseBody ?: "") return jsonResponse.getString("checkout_url") } ``` ```javascript theme={null} const getCheckoutURL = async (productId, customerEmail, customerName) => { try { const response = await fetch('https://your-backend.com/api/create-checkout-session', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${userSessionToken}`, }, body: JSON.stringify({ productId, customerEmail, customerName }) }); const data = await response.json(); return data.checkout_url; } catch (error) { console.error('Failed to get checkout URL:', error); throw error; } }; ``` ```dart theme={null} import 'dart:convert'; import 'package:http/http.dart' as http; Future getCheckoutUrl({ required String productId, required String customerEmail, required String customerName, }) async { final response = await http.post( Uri.parse('https://your-backend.com/api/create-checkout-session'), headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer $userSessionToken', }, body: jsonEncode({ 'productId': productId, 'customerEmail': customerEmail, 'customerName': customerName, }), ); if (response.statusCode != 200) { throw Exception('Failed to get checkout URL: ${response.statusCode}'); } return jsonDecode(response.body)['checkout_url'] as String; } ``` **Security**: Mobile apps only communicate with your backend, never directly with Dodo Payments API. Open the checkout URL in a secure in-app browser for payment processing. Or skip the manual setup entirely with the official checkout SDK for your platform. Install steps and setup instructions for Android, iOS, React Native, and Flutter. Process payment completion via webhooks and redirect URLs to confirm payment status. ## Choose Your SDK Every mobile SDK exposes the same contract: one `start(...)` call opens Dodo's hosted checkout in the platform's native browser surface and returns a typed `CheckoutResult` whose `status` is `succeeded`, `failed`, `cancelled`, `pending`, or `expired`. None of them holds an API key or calls the Dodo Payments API, and all four support abandoned-session recovery. `com.dodopayments.api:checkout-android` opens a Chrome Custom Tab. Requires `minSdk` 23. `dodopayments-mobile-sdk-ios` opens `SFSafariViewController`. Requires iOS 16+. `@dodopayments/react-native-checkout`, a Turbo Module over both native cores. Requires React Native 0.76+. `dodopayments_checkout`, a Pigeon channel over both native cores. Requires Flutter 3.44+. The `status` you get back is a UI hint, not proof of payment. Confirm every payment from your backend via the `payment.succeeded` / `subscription.active` webhook, or by retrieving the payment with your secret key. ### Registering a Callback URL Scheme All four SDKs hand control back to your app through a custom URL scheme that you choose, for example `myapp://checkout/return`. Register it once per platform: ```kotlin android/app/build.gradle theme={null} android { defaultConfig { manifestPlaceholders["dodoCallbackScheme"] = "myapp" } } ``` The SDK's own manifest already declares the redirect activity, so there is no manifest XML to add. ```xml Info.plist theme={null} CFBundleURLTypes CFBundleURLName myapp CFBundleURLSchemes myapp ``` On iOS you must also forward incoming URLs into the SDK, because `SFSafariViewController` cannot catch its own return URL. See the [iOS](/developer-resources/sdks/ios) or [React Native](/developer-resources/sdks/react-native) page for the exact handler. `@dodopayments/react-native-checkout` ships a config plugin that registers the scheme for both platforms at `prebuild`. Pass it a `scheme` option: ```json app.json theme={null} { "expo": { "scheme": "myapp", "plugins": [ [ "@dodopayments/react-native-checkout", { "scheme": "myappcheckout" } ] ] } } ``` `scheme` must match `returnUrl` and must differ from `expo.scheme`, which Expo already registers on `MainActivity`. See the [React Native SDK](/developer-resources/sdks/react-native) page for details. If `android/app/build.gradle` is hand-maintained without a standard `defaultConfig { }` block, set the placeholder manually instead (see the Android tab). Works with development builds only, not Expo Go. Run `npx expo prebuild --clean` after editing `app.json`. Prefer to build it yourself? Open the `checkout_url` in a WebView and intercept the navigation to your `return_url`, then read the `status` and `payment_id` query parameters. The SDKs above do this for you in the platform's real browser surface, which is why Apple Pay and Google Pay keep working. ## Appearance Customization Every SDK accepts an optional `customization` parameter on `start(...)` / `CheckoutParams` that controls the native browser surface's appearance and behavior — the toolbar, buttons, and presentation. This is separate from the checkout page's own theme, which you configure server-side via [`customization.theme_config`](/developer-resources/checkout-session) on the checkout session. Options are grouped by platform because Android's Custom Tab and iOS's `SFSafariViewController` expose different native controls. All fields are optional; omitting `customization` entirely uses each platform's default appearance. Toolbar background color. Navigation bar color. Divider color above the navigation bar. `default` shows the system "X" icon; `back` draws a back arrow instead. Which side of the toolbar the close button appears on. Shows the toolbar's share icon. Shows the page title under the URL in the toolbar. Lets the toolbar auto-hide as the page scrolls. Shows "Bookmark this page" in the overflow menu. Shows "Download page" in the overflow menu. Forces light or dark appearance regardless of the device's system setting. Label or icon for the dismiss button. `pageSheet` presents as a card with swipe-to-dismiss; `fullScreen` covers the whole screen. Lets the toolbar collapse on scroll. Only visible when `presentationStyle` is `fullScreen` — `pageSheet` keeps the bars pinned regardless of this setting. Forces light or dark appearance regardless of the device's system setting. ```typescript theme={null} const result = await DodoCheckout.start({ checkoutUrl, returnUrl: 'myapp://checkout/return', customization: { android: { toolbarColor: '#6366F1', closeButtonStyle: 'back' }, ios: { presentationStyle: 'fullScreen', colorScheme: 'dark' }, }, }); ``` ```dart theme={null} final result = await DodoCheckout.instance.start( CheckoutParams( checkoutUrl: Uri.parse(checkoutUrl), returnUrl: Uri.parse('myapp://checkout/return'), customization: BrowserCustomization( android: AndroidBrowserOptions( toolbarColor: Color(0xFF6366F1), closeButtonStyle: CloseButtonStyle.back, ), ios: IosBrowserOptions( presentationStyle: PresentationStyle.fullScreen, colorScheme: BrowserColorScheme.dark, ), ), ), ); ``` ```kotlin theme={null} val result = DodoCheckout.start( activity, CheckoutParams( checkoutUrl = checkoutUrl, returnUrl = "myapp://checkout/return", customization = BrowserCustomization( toolbarColor = Color.parseColor("#6366F1"), closeButtonStyle = BrowserCustomization.CloseButtonStyle.BACK, ) ) ) { event -> } ``` ```swift theme={null} let result = try await DodoCheckout.start( checkoutUrl: checkoutUrl, returnUrl: URL(string: "myapp://checkout/return")!, customization: BrowserCustomization( presentationStyle: .fullScreen, colorScheme: .dark ) ) ``` ## Best Practices * **Security**: Never ship an API key in your app. Create checkout sessions on your backend and pass only the resulting `checkout_url` to the client. * **Authority**: Treat `CheckoutResult.status` as a UI hint. Grant access only after your backend confirms the payment. * **User Experience**: Show a loading state while your backend creates the session, and handle `cancelled` as a normal outcome rather than an error. * **Testing**: Use test mode and test cards, and verify the return-URL round trip on a real device as well as a simulator. ## Troubleshooting ### Common Issues * **Callback never arrives**: The scheme in `returnUrl` must match the one you registered. On Android that is the `dodoCallbackScheme` manifest placeholder; on iOS and React Native it is the `Info.plist` URL type. * **Checkout returns to the browser instead of your app (iOS)**: You haven't forwarded the incoming URL. Call `DodoCheckout.handleOpenURL(url)` from `.onOpenURL`, `scene(_:openURLContexts:)`, or a React Native `Linking` listener. * **`PLATFORM_ERROR` on Android**: Most often a scheme mismatch. It can also appear if your `MainActivity` sets `android:taskAffinity=""` (the stock `flutter create` default), which lets some OEM builds lose the in-flight checkout. * **`ALREADY_IN_PROGRESS`**: A checkout is still open. Await or dismiss the previous one before starting another. * **Build fails with an unresolved placeholder**: You added the Android SDK but never set `manifestPlaceholders["dodoCallbackScheme"]`. * **Payment succeeded but access wasn't granted**: Expected if you're keying off the mobile result. Grant access from the `payment.succeeded` / `subscription.active` webhook instead. ## Additional Resources * [Payment Integration Guide](/developer-resources/integration-guide) * [Webhook Documentation](/developer-resources/webhooks/intents/webhook-events-guide) * [Testing Process](/miscellaneous/testing-process) * [Technical FAQs](/miscellaneous/faq) For questions or support, contact [support@dodopayments.com](mailto:support@dodopayments.com). # Next.js Adaptor Source: https://docs.dodopayments.com/developer-resources/nextjs-adaptor Learn how to integrate Dodo Payments with your Next.js App Router project using our NextJS Adaptor. Covers checkout, customer portal, webhooks, and secure environment setup. Integrate Dodo Payments checkout with static, dynamic, and session flows. Allow customers to manage subscriptions and details. Receive and process Dodo Payments webhook events. ## Installation Run the following command in your project root: ```bash theme={null} npm install @dodopayments/nextjs ``` Create a .env file in your project root: ```env theme={null} DODO_PAYMENTS_API_KEY=your-api-key DODO_PAYMENTS_WEBHOOK_KEY=your-webhook-secret DODO_PAYMENTS_RETURN_URL=https://yourdomain.com/checkout/success DODO_PAYMENTS_ENVIRONMENT="test_mode"or"live_mode" ``` Never commit your .env file or secrets to version control. ## Route Handler Examples All examples assume you are using the Next.js App Router. Use this handler to integrate Dodo Payments checkout into your Next.js app. Supports static (GET), dynamic (POST), and checkout session (POST) payment flows. ```typescript Next.js Route Handler expandable theme={null} // app/checkout/route.ts import { Checkout } from "@dodopayments/nextjs"; export const GET = Checkout({ bearerToken: process.env.DODO_PAYMENTS_API_KEY, returnUrl: process.env.DODO_PAYMENTS_RETURN_URL, environment: process.env.DODO_PAYMENTS_ENVIRONMENT, type: "static", // optional, defaults to 'static' }); export const POST = Checkout({ bearerToken: process.env.DODO_PAYMENTS_API_KEY, returnUrl: process.env.DODO_PAYMENTS_RETURN_URL, environment: process.env.DODO_PAYMENTS_ENVIRONMENT, type: "dynamic", // for dynamic checkout }); export const POST = Checkout({ bearerToken: process.env.DODO_PAYMENTS_API_KEY, returnUrl: process.env.DODO_PAYMENTS_RETURN_URL, environment: process.env.DODO_PAYMENTS_ENVIRONMENT, type: "session", // for checkout sessions }); ``` ```curl Static Checkout Curl example expandable theme={null} curl --request GET \ --url 'https://example.com/api/checkout?productId=pdt_fqJhl7pxKWiLhwQR042rh' \ --header 'User-Agent: insomnia/11.2.0' \ --cookie mode=test ``` ```curl Dynamic Checkout Curl example expandable theme={null} curl --request POST \ --url https://example.com/api/checkout \ --header 'Content-Type: application/json' \ --header 'User-Agent: insomnia/11.2.0' \ --cookie mode=test \ --data '{ "billing": { "city": "Texas", "country": "US", "state": "Texas", "street": "56, hhh", "zipcode": "560000" }, "customer": { "email": "test@example.com", "name": "test" }, "metadata": {}, "payment_link": true, "product_id": "pdt_QMDuvLkbVzCRWRQjLNcs", "quantity": 1, "billing_currency": "USD", "discount_codes": ["IKHZ23M9GQ"], "return_url": "https://example.com", "trial_period_days": 10 }' ``` ```curl Checkout Session Curl example expandable theme={null} curl --request POST \ --url https://example.com/api/checkout \ --header 'Content-Type: application/json' \ --header 'User-Agent: insomnia/11.2.0' \ --cookie mode=test \ --data '{ "product_cart": [ { "product_id": "pdt_QMDuvLkbVzCRWRQjLNcs", "quantity": 1 } ], "customer": { "email": "test@example.com", "name": "test" }, "return_url": "https://example.com/success" }' ``` Use this handler to allow customers to manage their subscriptions and details via the Dodo Payments customer portal. ```typescript Next.js Route Handler expandable theme={null} // app/customer-portal/route.ts import { CustomerPortal } from "@dodopayments/nextjs"; export const GET = CustomerPortal({ bearerToken: process.env.DODO_PAYMENTS_API_KEY, environment: process.env.DODO_PAYMENTS_ENVIRONMENT, }); ``` ```curl Customer Portal Curl example expandable theme={null} curl --request GET \ --url 'https://example.com/api/customer-portal?customer_id=cus_9VuW4K7O3GHwasENg31m&send_email=true' \ --header 'User-Agent: insomnia/11.2.0' \ --cookie mode=test ``` Use this handler to receive and process Dodo Payments webhook events securely in your Next.js app. ```typescript Next.js Route Handler expandable theme={null} // app/api/webhook/dodo-payments/route.ts import { Webhooks } from "@dodopayments/nextjs"; export const POST = Webhooks({ webhookKey: process.env.DODO_PAYMENTS_WEBHOOK_KEY, onPayload: async (payload) => { // handle the payload }, // ... other event handlers for granular control }); ``` ## Checkout Route Handler Dodo Payments supports three types of payment flows for integrating payments into your website, this adaptor supports all types of payment flows. * **Static Payment Links:** Instantly shareable URLs for quick, no-code payment collection. * **Dynamic Payment Links:** Programmatically generate payment links with custom details using the API or SDKs. * **Checkout Sessions:** Create secure, customizable checkout experiences with pre-configured product carts and customer details. ### Supported Query Parameters Product identifier (e.g., ?productId=pdt\_nZuwz45WAs64n3l07zpQR). Quantity of the product. Customer's full name. Customer's first name. Customer's last name. Customer's email address. Customer's country. Customer's address line. Customer's city. Customer's state/province. Customer's zip/postal code. Disable full name field. Disable first name field. Disable last name field. Disable email field. Disable country field. Disable address line field. Disable city field. Disable state field. Disable zip code field. Specify the payment currency (e.g., USD). Show currency selector. Fixes the amount charged, in major currency units (e.g., 12.5 for \$12.50). Pay What You Want products only, and ignored if below the product's minimum price. Show discount fields. Any query parameter starting with metadata\_ will be passed as metadata. If productId is missing, the handler returns a 400 response. Invalid query parameters also result in a 400 response. ### Response Format Static checkout returns a JSON response with the checkout URL: ```json theme={null} { "checkout_url": "https://checkout.dodopayments.com/..." } ``` * Send parameters as a JSON body in a POST request. * Supports both one-time and recurring payments. * For a complete list of supported POST body fields, refer to: * [Request body for a One Time Payment Product](https://docs.dodopayments.com/api-reference/payments/post-payments) * [Request body for a Subscription Product](https://docs.dodopayments.com/api-reference/subscriptions/post-subscriptions) ### Response Format Dynamic checkout returns a JSON response with the checkout URL: ```json theme={null} { "checkout_url": "https://checkout.dodopayments.com/..." } ``` Checkout sessions provide a more secure, hosted checkout experience that handles the complete payment flow for both one-time purchases and subscriptions with full customization control. Refer to [Checkout Sessions Integration Guide](https://docs.dodopayments.com/developer-resources/checkout-session) for more details and a complete list of supported fields. ### Response Format Checkout sessions return a JSON response with the checkout URL: ```json theme={null} { "checkout_url": "https://checkout.dodopayments.com/session/..." } ``` ## Customer Portal Route Handler The Customer Portal Route Handler enables you to seamlessly integrate the Dodo Payments customer portal into your Next.js application. ### Query Parameters The customer ID for the portal session (e.g., ?customer\_id=cus\_123). If set to true, sends an email to the customer with the portal link. Returns 400 if customer\_id is missing. ## Webhook Route Handler * **Method:** Only POST requests are supported. Other methods return 405. * **Signature Verification:** Verifies the webhook signature using webhookKey. Returns 401 if verification fails. * **Payload Validation:** Validated with Zod. Returns 400 for invalid payloads. * **Error Handling:** * 401: Invalid signature * 400: Invalid payload * 500: Internal error during verification * **Event Routing:** Calls the appropriate event handler based on the payload type. ### Supported Webhook Event Handlers ```typescript Typescript expandable theme={null} onPayload?: (payload: WebhookPayload) => Promise; onPaymentSucceeded?: (payload: WebhookPayload) => Promise; onPaymentFailed?: (payload: WebhookPayload) => Promise; onPaymentProcessing?: (payload: WebhookPayload) => Promise; onPaymentCancelled?: (payload: WebhookPayload) => Promise; onRefundSucceeded?: (payload: WebhookPayload) => Promise; onRefundFailed?: (payload: WebhookPayload) => Promise; onDisputeOpened?: (payload: WebhookPayload) => Promise; onDisputeExpired?: (payload: WebhookPayload) => Promise; onDisputeAccepted?: (payload: WebhookPayload) => Promise; onDisputeCancelled?: (payload: WebhookPayload) => Promise; onDisputeChallenged?: (payload: WebhookPayload) => Promise; onDisputeWon?: (payload: WebhookPayload) => Promise; onDisputeLost?: (payload: WebhookPayload) => Promise; onSubscriptionActive?: (payload: WebhookPayload) => Promise; onSubscriptionOnHold?: (payload: WebhookPayload) => Promise; onSubscriptionRenewed?: (payload: WebhookPayload) => Promise; onSubscriptionPlanChanged?: (payload: WebhookPayload) => Promise; onSubscriptionCancelled?: (payload: WebhookPayload) => Promise; onSubscriptionFailed?: (payload: WebhookPayload) => Promise; onSubscriptionExpired?: (payload: WebhookPayload) => Promise; onSubscriptionUpdated?: (payload: WebhookPayload) => Promise; onLicenseKeyCreated?: (payload: WebhookPayload) => Promise; onAbandonedCheckoutDetected?: (payload: WebhookPayload) => Promise; onAbandonedCheckoutRecovered?: (payload: WebhookPayload) => Promise; onDunningStarted?: (payload: WebhookPayload) => Promise; onDunningRecovered?: (payload: WebhookPayload) => Promise; onCreditAdded?: (payload: WebhookPayload) => Promise; onCreditDeducted?: (payload: WebhookPayload) => Promise; onCreditExpired?: (payload: WebhookPayload) => Promise; onCreditRolledOver?: (payload: WebhookPayload) => Promise; onCreditRolloverForfeited?: (payload: WebhookPayload) => Promise; onCreditOverageCharged?: (payload: WebhookPayload) => Promise; onCreditManualAdjustment?: (payload: WebhookPayload) => Promise; onCreditBalanceLow?: (payload: WebhookPayload) => Promise; ``` *** ## Prompt for LLM ``` You are an expert Next.js developer assistant. Your task is to guide a user through integrating the @dodopayments/nextjs adapter into their existing Next.js project. The @dodopayments/nextjs adapter provides route handlers for Dodo Payments' Checkout, Customer Portal, and Webhook functionalities, designed for the Next.js App Router. First, install the necessary packages. Use the package manager appropriate for your project (npm, yarn, or bun) based on the presence of lock files (e.g., package-lock.json for npm, yarn.lock for yarn, bun.lockb for bun): npm install @dodopayments/nextjs Here's how you should structure your response: Ask the user which functionalities they want to integrate. "Which parts of the @dodopayments/nextjs adapter would you like to integrate into your project? You can choose one or more of the following: Checkout Route Handler (for handling product checkouts) Customer Portal Route Handler (for managing customer subscriptions/details) Webhook Route Handler (for receiving Dodo Payments webhook events) All (integrate all three)" Based on the user's selection, provide detailed integration steps for each chosen functionality. If Checkout Route Handler is selected: Purpose: This handler manages different types of checkout flows. All checkout types (static, dynamic, and sessions) return JSON responses with checkout URLs for programmatic handling. File Creation: Create a new file at app/checkout/route.ts in your Next.js project. Code Snippet: // app/checkout/route.ts import { Checkout } from '@dodopayments/nextjs' export const GET = Checkout({ bearerToken: process.env.DODO_PAYMENTS_API_KEY!, returnUrl: process.env.DODO_PAYMENTS_RETURN_URL, environment: process.env.DODO_PAYMENTS_ENVIRONMENT, type: "static", }); export const POST = Checkout({ bearerToken: process.env.DODO_PAYMENTS_API_KEY!, returnUrl: process.env.DODO_PAYMENTS_RETURN_URL, environment: process.env.DODO_PAYMENTS_ENVIRONMENT, type: "session", // or "dynamic" for dynamic link }); Configuration & Usage: bearerToken: Your Dodo Payments API key. It's recommended to set this via the DODO_PAYMENTS_API_KEY environment variable. returnUrl: (Optional) The URL to redirect the user to after a successful checkout. environment: (Optional) Set to "test_mode" for testing, or omit/set to "live_mode" for production. type: (Optional) Set to "static" for GET/static checkout, "dynamic" for POST/dynamic checkout, or "session" for POST/checkout sessions. Static Checkout (GET) Query Parameters: productId (required): Product identifier (e.g., ?productId=pdt_nZuwz45WAs64n3l07zpQR) quantity (optional): Quantity of the product Customer Fields (optional): fullName, firstName, lastName, email, country, addressLine, city, state, zipCode Disable Flags (optional, set to true to disable): disableFullName, disableFirstName, disableLastName, disableEmail, disableCountry, disableAddressLine, disableCity, disableState, disableZipCode Advanced Controls (optional): paymentCurrency, showCurrencySelector, paymentAmount, showDiscounts Metadata (optional): Any query parameter starting with metadata_ (e.g., ?metadata_userId=abc123) Returns: {"checkout_url": "https://checkout.dodopayments.com/..."} Dynamic Checkout (POST) - Returns JSON with checkout_url: Parameters are sent as a JSON body. Supports both one-time and recurring payments. Returns: {"checkout_url": "https://checkout.dodopayments.com/..."}. For a complete list of supported POST body fields, refer to: Docs - One Time Payment Product: https://docs.dodopayments.com/api-reference/payments/post-payments Docs - Subscription Product: https://docs.dodopayments.com/api-reference/subscriptions/post-subscriptions Checkout Sessions (POST) - (Recommended) A more customizable checkout experience. Returns JSON with checkout_url: Parameters are sent as a JSON body. Supports both one-time and recurring payments. Returns: {"checkout_url": "https://checkout.dodopayments.com/session/..."}. For a complete list of supported fields, refer to: Checkout Sessions Integration Guide: https://docs.dodopayments.com/developer-resources/checkout-session Error Handling: If productId is missing or other parameters are invalid, the handler will return a 400 response. If Customer Portal Route Handler is selected: Purpose: This handler redirects authenticated users to their Dodo Payments customer portal. File Creation: Create a new file at app/customer-portal/route.ts in your Next.js project. Code Snippet: // app/customer-portal/route.ts import { CustomerPortal } from '@dodopayments/nextjs' export const GET = CustomerPortal({ bearerToken: process.env.DODO_PAYMENTS_API_KEY!, environment: "test_mode", }); Query Parameters: customer_id (required): The customer ID for the portal session (e.g., ?customer_id=cus_123) send_email (optional, boolean): If set to true, sends an email to the customer with the portal link. Returns 400 if customer_id is missing. If Webhook Route Handler is selected: Purpose: This handler processes incoming webhook events from Dodo Payments, allowing your application to react to events like successful payments, refunds, or subscription changes. File Creation: Create a new file at app/api/webhook/dodo-payments/route.ts in your Next.js project. Code Snippet: // app/api/webhook/dodo-payments/route.ts import { Webhooks } from '@dodopayments/nextjs' export const POST = Webhooks({ webhookKey: process.env.DODO_WEBHOOK_SECRET!, onPayload: async (payload) => { // handle the payload }, // ... other event handlers for granular control }); Handler Details: Method: Only POST requests are supported. Other methods return 405. Signature Verification: The handler verifies the webhook signature using the webhookKey and returns 401 if verification fails. Payload Validation: The payload is validated with Zod. Returns 400 for invalid payloads. Error Handling: 401: Invalid signature 400: Invalid payload 500: Internal error during verification Event Routing: Calls the appropriate event handler based on the payload type. Supported Webhook Event Handlers: onPayload?: (payload: WebhookPayload) => Promise onPaymentSucceeded?: (payload: WebhookPayload) => Promise onPaymentFailed?: (payload: WebhookPayload) => Promise onPaymentProcessing?: (payload: WebhookPayload) => Promise onPaymentCancelled?: (payload: WebhookPayload) => Promise onRefundSucceeded?: (payload: WebhookPayload) => Promise onRefundFailed?: (payload: WebhookPayload) => Promise onDisputeOpened?: (payload: WebhookPayload) => Promise onDisputeExpired?: (payload: WebhookPayload) => Promise onDisputeAccepted?: (payload: WebhookPayload) => Promise onDisputeCancelled?: (payload: WebhookPayload) => Promise onDisputeChallenged?: (payload: WebhookPayload) => Promise onDisputeWon?: (payload: WebhookPayload) => Promise onDisputeLost?: (payload: WebhookPayload) => Promise onSubscriptionActive?: (payload: WebhookPayload) => Promise onSubscriptionOnHold?: (payload: WebhookPayload) => Promise onSubscriptionRenewed?: (payload: WebhookPayload) => Promise onSubscriptionPlanChanged?: (payload: WebhookPayload) => Promise onSubscriptionCancelled?: (payload: WebhookPayload) => Promise onSubscriptionFailed?: (payload: WebhookPayload) => Promise onSubscriptionExpired?: (payload: WebhookPayload) => Promise onSubscriptionUpdated?: (payload: WebhookPayload) => Promise onLicenseKeyCreated?: (payload: WebhookPayload) => Promise onAbandonedCheckoutDetected?: (payload: WebhookPayload) => Promise onAbandonedCheckoutRecovered?: (payload: WebhookPayload) => Promise onDunningStarted?: (payload: WebhookPayload) => Promise onDunningRecovered?: (payload: WebhookPayload) => Promise onCreditAdded?: (payload: WebhookPayload) => Promise onCreditDeducted?: (payload: WebhookPayload) => Promise onCreditExpired?: (payload: WebhookPayload) => Promise onCreditRolledOver?: (payload: WebhookPayload) => Promise onCreditRolloverForfeited?: (payload: WebhookPayload) => Promise onCreditOverageCharged?: (payload: WebhookPayload) => Promise onCreditManualAdjustment?: (payload: WebhookPayload) => Promise onCreditBalanceLow?: (payload: WebhookPayload) => Promise Environment Variable Setup: To ensure the adapter functions correctly, you will need to manually set up the following environment variables in your Next.js project's deployment environment (e.g., Vercel, Netlify, AWS, etc.): DODO_PAYMENTS_API_KEY: Your Dodo Payments API Key (required for Checkout and Customer Portal). RETURN_URL: (Optional) The URL to redirect to after a successful checkout (for Checkout handler). DODO_WEBHOOK_SECRET: Your Dodo Payments Webhook Secret (required for Webhook handler). Example .env file: DODO_PAYMENTS_API_KEY=your-api-key DODO_PAYMENTS_WEBHOOK_KEY=your-webhook-secret DODO_PAYMENTS_ENVIRONMENT="test_mode" or "live_mode" DODO_PAYMENTS_RETURN_URL=your-return-url Usage in your code: bearerToken: process.env.DODO_PAYMENTS_API_KEY! webhookKey: process.env.DODO_WEBHOOK_SECRET! Important: Never commit sensitive environment variables directly into your version control. Use environment variables for all sensitive information. If the user needs assistance setting up environment variables for their specific deployment environment, ask them what platform they are using (e.g., Vercel, Netlify, AWS, etc.), and provide guidance. You can also add comments to their PR or chat depending on the context ``` # Nuxt Adaptor Source: https://docs.dodopayments.com/developer-resources/nuxt-adaptor Integrate Dodo Payments with your Nuxt project using the official Nuxt module. Covers checkout, customer portal, webhooks, and secure environment setup. Integrate Dodo Payments checkout into your Nuxt app using a server route. Allow customers to manage subscriptions and details via a Nuxt server route. Receive and process Dodo Payments webhook events securely in Nuxt. ## Overview This guide explains how to integrate Dodo Payments into your Nuxt application using the official Nuxt module. You'll learn how to set up checkout, customer portal, and webhook API routes, and how to securely manage environment variables. ## Installation Run the following command in your project root: ```bash theme={null} npm install @dodopayments/nuxt ``` Add @dodopayments/nuxt to your modules array and configure it: ```typescript nuxt.config.ts expandable theme={null} export default defineNuxtConfig({ modules: ["@dodopayments/nuxt"], devtools: { enabled: true }, compatibilityDate: "2025-02-25", runtimeConfig: { private: { bearerToken: process.env.NUXT_PRIVATE_BEARER_TOKEN, webhookKey: process.env.NUXT_PRIVATE_WEBHOOK_KEY, environment: process.env.NUXT_PRIVATE_ENVIRONMENT, returnUrl: process.env.NUXT_PRIVATE_RETURNURL }, } }); ``` Never commit your .env file or secrets to version control. ## API Route Handler Examples All Dodo Payments integrations in Nuxt are handled via server routes in the server/routes/api/ directory. Use this handler to integrate Dodo Payments checkout into your Nuxt app. Supports static (GET), dynamic (POST), and session (POST) payment flows. ```typescript server/routes/api/checkout.get.ts expandable theme={null} export default defineEventHandler((event) => { const { private: { bearerToken, environment, returnUrl }, } = useRuntimeConfig(); const handler = checkoutHandler({ bearerToken: bearerToken, environment: environment, returnUrl: returnUrl, }); return handler(event); }); ``` ```typescript server/routes/api/checkout.post.ts expandable theme={null} export default defineEventHandler((event) => { const { private: { bearerToken, environment, returnUrl }, } = useRuntimeConfig(); const handler = checkoutHandler({ bearerToken: bearerToken, environment: environment, returnUrl: returnUrl, type: "dynamic" }); return handler(event); }); ``` ```typescript server/routes/api/checkout.post.ts (Checkout Sessions) expandable theme={null} export default defineEventHandler((event) => { const { private: { bearerToken, environment, returnUrl }, } = useRuntimeConfig(); const handler = checkoutHandler({ bearerToken: bearerToken, environment: environment, returnUrl: returnUrl, type: "session" }); return handler(event); }); ``` If productId is missing or invalid, the handler returns a 400 response. ```curl Static Checkout Curl example expandable theme={null} curl --request GET \ --url 'https://example.com/api/checkout?productId=pdt_fqJhl7pxKWiLhwQR042rh' \ --header 'User-Agent: insomnia/11.2.0' \ --cookie mode=test ``` ```curl Dynamic Checkout Curl example expandable theme={null} curl --request POST \ --url https://example.com/api/checkout \ --header 'Content-Type: application/json' \ --header 'User-Agent: insomnia/11.2.0' \ --cookie mode=test \ --data '{ "billing": { "city": "Texas", "country": "US", "state": "Texas", "street": "56, hhh", "zipcode": "560000" }, "customer": { "email": "test@example.com", "name": "test" }, "metadata": {}, "payment_link": true, "product_id": "pdt_QMDuvLkbVzCRWRQjLNcs", "quantity": 1, "billing_currency": "USD", "discount_codes": ["IKHZ23M9GQ"], "return_url": "https://example.com", "trial_period_days": 10 }' ``` ```curl Checkout Session Curl example expandable theme={null} curl --request POST \ --url https://example.com/api/checkout \ --header 'Content-Type: application/json' \ --header 'User-Agent: insomnia/11.2.0' \ --cookie mode=test \ --data '{ "product_cart": [ { "product_id": "pdt_QMDuvLkbVzCRWRQjLNcs", "quantity": 1 } ], "customer": { "email": "test@example.com", "name": "test" }, "return_url": "https://example.com/success" }' ``` Create a GET route to allow customers to access their portal. Accepts customer\_id as a query parameter. ```typescript server/routes/api/customer-portal.get.ts expandable theme={null} export default defineEventHandler((event) => { const { private: { bearerToken, environment }, } = useRuntimeConfig(); const handler = customerPortalHandler({ bearerToken, environment: environment, }); return handler(event); }); ``` **Query Parameters:** * customer\_id (required): The customer ID for the portal session (e.g., ?customer\_id=cus\_123) * send\_email (optional, boolean): If true, sends an email to the customer with the portal link. Returns 400 if customer\_id is missing. ```curl Customer Portal Curl example expandable theme={null} curl --request GET \ --url 'https://example.com/api/customer-portal?customer_id=cus_9VuW4K7O3GHwasENg31m&send_email=true' \ --header 'User-Agent: insomnia/11.2.0' \ --cookie mode=test ``` Create a POST route to securely receive and process webhook events from Dodo Payments. ```typescript server/routes/api/webhook.post.ts expandable theme={null} export default defineEventHandler((event) => { const { private: { webhookKey }, } = useRuntimeConfig(); const handler = Webhooks({ webhookKey: webhookKey, onPayload: async (payload: any) => { // Handle webhook payload here }, // ...add other event handlers as needed }); return handler(event); }); ``` ## Checkout Route Handler Dodo Payments supports three types of payment flows for integrating payments into your website, this adaptor supports all types of payment flows. * **Static Payment Links:** Instantly shareable URLs for quick, no-code payment collection. * **Dynamic Payment Links:** Programmatically generate payment links with custom details using the API or SDKs. * **Checkout Sessions:** Create secure, customizable checkout experiences with pre-configured product carts and customer details. ### Supported Query Parameters Product identifier (e.g., ?productId=pdt\_nZuwz45WAs64n3l07zpQR). Quantity of the product. Customer's full name. Customer's first name. Customer's last name. Customer's email address. Customer's country. Customer's address line. Customer's city. Customer's state/province. Customer's zip/postal code. Disable full name field. Disable first name field. Disable last name field. Disable email field. Disable country field. Disable address line field. Disable city field. Disable state field. Disable zip code field. Specify the payment currency (e.g., USD). Show currency selector. Fixes the amount charged, in major currency units (e.g., 12.5 for \$12.50). Pay What You Want products only, and ignored if below the product's minimum price. Show discount fields. Any query parameter starting with metadata\_ will be passed as metadata. If productId is missing, the handler returns a 400 response. Invalid query parameters also result in a 400 response. ### Response Format Static checkout returns a JSON response with the checkout URL: ```json theme={null} { "checkout_url": "https://checkout.dodopayments.com/..." } ``` * Send parameters as a JSON body in a POST request. * Supports both one-time and recurring payments. * For a complete list of supported POST body fields, refer to: * [Request body for a One Time Payment Product](https://docs.dodopayments.com/api-reference/payments/post-payments) * [Request body for a Subscription Product](https://docs.dodopayments.com/api-reference/subscriptions/post-subscriptions) ### Response Format Dynamic checkout returns a JSON response with the checkout URL: ```json theme={null} { "checkout_url": "https://checkout.dodopayments.com/..." } ``` Checkout sessions provide a more secure, hosted checkout experience that handles the complete payment flow for both one-time purchases and subscriptions with full customization control. Refer to [Checkout Sessions Integration Guide](https://docs.dodopayments.com/developer-resources/checkout-session) for more details and a complete list of supported fields. ### Response Format Checkout sessions return a JSON response with the checkout URL: ```json theme={null} { "checkout_url": "https://checkout.dodopayments.com/session/..." } ``` ## Customer Portal Route Handler The Customer Portal Route Handler enables you to seamlessly integrate the Dodo Payments customer portal into your Nuxt application. ### Query Parameters The customer ID for the portal session (e.g., ?customer\_id=cus\_123). If set to true, sends an email to the customer with the portal link. Returns 400 if customer\_id is missing. ## Webhook Route Handler * **Method:** Only POST requests are supported. Other methods return 405. * **Signature Verification:** Verifies the webhook signature using webhookKey. Returns 401 if verification fails. * **Payload Validation:** Validated with Zod. Returns 400 for invalid payloads. * **Error Handling:** * 401: Invalid signature * 400: Invalid payload * 500: Internal error during verification * **Event Routing:** Calls the appropriate event handler based on the payload type. ### Supported Webhook Event Handlers ```typescript Typescript expandable theme={null} onPayload?: (payload: WebhookPayload) => Promise; onPaymentSucceeded?: (payload: WebhookPayload) => Promise; onPaymentFailed?: (payload: WebhookPayload) => Promise; onPaymentProcessing?: (payload: WebhookPayload) => Promise; onPaymentCancelled?: (payload: WebhookPayload) => Promise; onRefundSucceeded?: (payload: WebhookPayload) => Promise; onRefundFailed?: (payload: WebhookPayload) => Promise; onDisputeOpened?: (payload: WebhookPayload) => Promise; onDisputeExpired?: (payload: WebhookPayload) => Promise; onDisputeAccepted?: (payload: WebhookPayload) => Promise; onDisputeCancelled?: (payload: WebhookPayload) => Promise; onDisputeChallenged?: (payload: WebhookPayload) => Promise; onDisputeWon?: (payload: WebhookPayload) => Promise; onDisputeLost?: (payload: WebhookPayload) => Promise; onSubscriptionActive?: (payload: WebhookPayload) => Promise; onSubscriptionOnHold?: (payload: WebhookPayload) => Promise; onSubscriptionRenewed?: (payload: WebhookPayload) => Promise; onSubscriptionPlanChanged?: (payload: WebhookPayload) => Promise; onSubscriptionCancelled?: (payload: WebhookPayload) => Promise; onSubscriptionFailed?: (payload: WebhookPayload) => Promise; onSubscriptionExpired?: (payload: WebhookPayload) => Promise; onSubscriptionUpdated?: (payload: WebhookPayload) => Promise; onLicenseKeyCreated?: (payload: WebhookPayload) => Promise; onAbandonedCheckoutDetected?: (payload: WebhookPayload) => Promise; onAbandonedCheckoutRecovered?: (payload: WebhookPayload) => Promise; onDunningStarted?: (payload: WebhookPayload) => Promise; onDunningRecovered?: (payload: WebhookPayload) => Promise; onCreditAdded?: (payload: WebhookPayload) => Promise; onCreditDeducted?: (payload: WebhookPayload) => Promise; onCreditExpired?: (payload: WebhookPayload) => Promise; onCreditRolledOver?: (payload: WebhookPayload) => Promise; onCreditRolloverForfeited?: (payload: WebhookPayload) => Promise; onCreditOverageCharged?: (payload: WebhookPayload) => Promise; onCreditManualAdjustment?: (payload: WebhookPayload) => Promise; onCreditBalanceLow?: (payload: WebhookPayload) => Promise; ``` *** ## Prompt for LLM ``` You are an expert Nuxt developer assistant. Your task is to guide a user through integrating the @dodopayments/nuxt module into their existing Nuxt project. The @dodopayments/nuxt module provides API route handlers for Dodo Payments' Checkout, Customer Portal, and Webhook functionalities, designed for Nuxt 3 server routes. First, install the necessary package: npm install @dodopayments/nuxt Second, add the configuration to nuxt.config.ts export default defineNuxtConfig({ modules: ["@dodopayments/nuxt"], devtools: { enabled: true }, compatibilityDate: "2025-02-25", runtimeConfig: { private: { bearerToken: process.env.NUXT_PRIVATE_BEARER_TOKEN, webhookKey: process.env.NUXT_PRIVATE_BEARER_TOKEN, environment: process.env.NUXT_PRIVATE_ENVIRONMENT, returnUrl: process.env.NUXT_PRIVATE_RETURNURL }, } }); Here's how you should structure your response: Ask the user which functionalities they want to integrate. "Which parts of the @dodopayments/nuxt module would you like to integrate into your project? You can choose one or more of the following: Checkout API Route (for handling product checkouts) Customer Portal API Route (for managing customer subscriptions/details) Webhook API Route (for receiving Dodo Payments webhook events) All (integrate all three)" Based on the user's selection, provide detailed integration steps for each chosen functionality. If Checkout API Route is selected: Purpose: This route redirects users to the Dodo Payments checkout page. File Creation: Create a new file at server/routes/api/checkout.get.ts in your Nuxt project. Code Snippet: // server/routes/api/checkout.get.ts export default defineEventHandler((event) => { const { private: { bearerToken, environment, returnUrl }, } = useRuntimeConfig(); const handler = checkoutHandler({ bearerToken: bearerToken, environment: environment, returnUrl: returnUrl, }); return handler(event); }); Configuration & Usage: - bearerToken: Your Dodo Payments API key. Set via the NUXT_PRIVATE_BEARER_TOKEN environment variable. - returnUrl: (Optional) The URL to redirect the user to after a successful checkout. - environment: (Optional) Set to your environment (e.g., "test_mode" or "live_mode"). Static Checkout (GET) Query Parameters: - productId (required): Product identifier (e.g., ?productId=pdt_nZuwz45WAs64n3l07zpQR) - quantity (optional): Quantity of the product - Customer Fields (optional): fullName, firstName, lastName, email, country, addressLine, city, state, zipCode - Disable Flags (optional, set to true to disable): disableFullName, disableFirstName, disableLastName, disableEmail, disableCountry, disableAddressLine, disableCity, disableState, disableZipCode - Advanced Controls (optional): paymentCurrency, showCurrencySelector, paymentAmount, showDiscounts - Metadata (optional): Any query parameter starting with metadata_ (e.g., ?metadata_userId=abc123) Dynamic Checkout (POST): Parameters are sent as a JSON body. Supports both one-time and recurring payments. For a complete list of supported POST body fields, refer to: - Docs - One Time Payment Product: https://docs.dodopayments.com/api-reference/payments/post-payments - Docs - Subscription Product: https://docs.dodopayments.com/api-reference/subscriptions/post-subscriptions Checkout Sessions (POST) - (Recommended) A more customizable checkout experience. Returns JSON with checkout_url: Parameters are sent as a JSON body. Supports both one-time and recurring payments. Returns: {"checkout_url": "https://checkout.dodopayments.com/session/..."}. For a complete list of supported fields, refer to: Checkout Sessions Integration Guide: https://docs.dodopayments.com/developer-resources/checkout-session Error Handling: If productId is missing or other query parameters are invalid, the handler will return a 400 response. If Customer Portal API Route is selected: Purpose: This route allows customers to access their Dodo Payments customer portal. File Creation: Create a new file at server/routes/api/customer-portal.get.ts in your Nuxt project. Code Snippet: // server/routes/api/customer-portal.get.ts export default defineEventHandler((event) => { const { private: { bearerToken, environment }, } = useRuntimeConfig(); const handler = customerPortalHandler({ bearerToken, environment: environment, }); return handler(event); }); Query Parameters: - customer_id (required): The customer ID for the portal session (e.g., ?customer_id=cus_123) - send_email (optional, boolean): If set to true, sends an email to the customer with the portal link. - Returns 400 if customer_id is missing. If Webhook API Route is selected: Purpose: This route processes incoming webhook events from Dodo Payments, allowing your application to react to events like successful payments, refunds, or subscription changes. File Creation: Create a new file at server/routes/api/webhook.post.ts in your Nuxt project. Code Snippet: // server/routes/api/webhook.post.ts export default defineEventHandler((event) => { const { private: { webhookKey }, } = useRuntimeConfig(); const handler = Webhooks({ webhookKey: webhookKey, onPayload: async (payload) => { // handle the payload }, // ... other event handlers for granular control }); return handler(event); }); Handler Details: - Method: Only POST requests are supported. Other methods return 405. - Signature Verification: The handler verifies the webhook signature using webhookKey and returns 401 if verification fails. - Payload Validation: The payload is validated with Zod. Returns 400 for invalid payloads. Error Handling: - 401: Invalid signature - 400: Invalid payload - 500: Internal error during verification Event Routing: Calls the appropriate event handler based on the payload type. Supported event handlers include: - onPayload?: (payload: WebhookPayload) => Promise - onPaymentSucceeded?: (payload: WebhookPayload) => Promise - onPaymentFailed?: (payload: WebhookPayload) => Promise - onPaymentProcessing?: (payload: WebhookPayload) => Promise - onPaymentCancelled?: (payload: WebhookPayload) => Promise - onRefundSucceeded?: (payload: WebhookPayload) => Promise - onRefundFailed?: (payload: WebhookPayload) => Promise - onDisputeOpened?: (payload: WebhookPayload) => Promise - onDisputeExpired?: (payload: WebhookPayload) => Promise - onDisputeAccepted?: (payload: WebhookPayload) => Promise - onDisputeCancelled?: (payload: WebhookPayload) => Promise - onDisputeChallenged?: (payload: WebhookPayload) => Promise - onDisputeWon?: (payload: WebhookPayload) => Promise - onDisputeLost?: (payload: WebhookPayload) => Promise - onSubscriptionActive?: (payload: WebhookPayload) => Promise - onSubscriptionOnHold?: (payload: WebhookPayload) => Promise - onSubscriptionRenewed?: (payload: WebhookPayload) => Promise - onSubscriptionPlanChanged?: (payload: WebhookPayload) => Promise - onSubscriptionCancelled?: (payload: WebhookPayload) => Promise - onSubscriptionFailed?: (payload: WebhookPayload) => Promise - onSubscriptionExpired?: (payload: WebhookPayload) => Promise - onSubscriptionUpdated?: (payload: WebhookPayload) => Promise - onLicenseKeyCreated?: (payload: WebhookPayload) => Promise - onAbandonedCheckoutDetected?: (payload: WebhookPayload) => Promise - onAbandonedCheckoutRecovered?: (payload: WebhookPayload) => Promise - onDunningStarted?: (payload: WebhookPayload) => Promise - onDunningRecovered?: (payload: WebhookPayload) => Promise - onCreditAdded?: (payload: WebhookPayload) => Promise - onCreditDeducted?: (payload: WebhookPayload) => Promise - onCreditExpired?: (payload: WebhookPayload) => Promise - onCreditRolledOver?: (payload: WebhookPayload) => Promise - onCreditRolloverForfeited?: (payload: WebhookPayload) => Promise - onCreditOverageCharged?: (payload: WebhookPayload) => Promise - onCreditManualAdjustment?: (payload: WebhookPayload) => Promise - onCreditBalanceLow?: (payload: WebhookPayload) => Promise Environment Variable Setup: To ensure the module functions correctly, set up the following environment variables in your Nuxt project's deployment environment (e.g., Vercel, Netlify, AWS, etc.): - NUXT_PRIVATE_BEARER_TOKEN: Your Dodo Payments API Key (required for Checkout and Customer Portal). - NUXT_PRIVATE_WEBHOOK_KEY: Your Dodo Payments Webhook Secret (required for Webhook handler). - NUXT_PRIVATE_ENVIRONMENT: (Optional) Set to your environment (e.g., "test_mode" or "live_mode"). - NUXT_PRIVATE_RETURNURL: (Optional) The URL to redirect to after a successful checkout (for Checkout handler). Usage in your code: bearerToken: useRuntimeConfig().private.bearerToken webhookKey: useRuntimeConfig().private.webhookKey Important: Never commit sensitive environment variables directly into your version control. Use environment variables for all sensitive information. If the user needs assistance setting up environment variables for their specific deployment environment, ask them what platform they are using (e.g., Vercel, Netlify, AWS, etc.), and provide guidance. ``` # On-Demand Subscriptions Source: https://docs.dodopayments.com/developer-resources/ondemand-subscriptions Integrate on-demand subscriptions by authorizing mandates, creating variable charges, handling webhooks, and implementing safe retry policies. ## Overview On-demand subscriptions let you authorize a customer's payment method once and then charge variable amounts whenever you need, instead of on a fixed schedule. This feature is available for all accounts—no approval required. Use this guide to: * Create an on-demand subscription (authorize a mandate with optional initial price) * Trigger subsequent charges with custom amounts * Track outcomes using webhooks For a general subscription setup, see the [Subscription Integration Guide](/developer-resources/subscription-integration-guide). ## Prerequisites * Dodo Payments merchant account and API key * Webhook secret configured and an endpoint to receive events * A subscription product in your catalog This guide creates the on-demand subscription through a checkout session (`POST /checkouts`), which always returns a hosted `checkout_url`. Redirect the customer there to approve the mandate, and set `return_url` to where they should land afterward. ## How on-demand works 1. You create a subscription with the `on_demand` object to authorize a payment method and optionally collect an initial charge. 2. Later, you create charges against that subscription with custom amounts using the dedicated charge endpoint. 3. You listen to webhooks (e.g., `payment.succeeded`, `payment.failed`) to update your system. ## Create an on-demand subscription Endpoint: [POST /checkouts](/api-reference/checkout-sessions/create) Key request fields (body):\ Please find them in [Create Checkout Session](/api-reference/checkout-sessions/create) ### Create an on-demand subscription ```javascript theme={null} import DodoPayments from 'dodopayments'; const client = new DodoPayments({ bearerToken: process.env.DODO_PAYMENTS_API_KEY, environment: 'test_mode', // defaults to 'live_mode' }); async function main() { const subscription = await client.checkoutSessions.create({ product_cart: [{ product_id: 'pdt_123', quantity: 1 }], billing_address: { city: 'SF', country: 'US', state: 'CA', street: '1 Market St', zipcode: '94105' }, customer: { customer_id: 'cus_123' }, return_url: 'https://example.com/billing/success', subscription_data: { on_demand: { mandate_only: true // set false to collect an initial charge // product_price: 1000, // optional: charge $10.00 now if mandate_only is false // product_currency: 'USD', // product_description: 'Custom initial charge', // adaptive_currency_fees_inclusive: false, } } }); console.log(subscription.checkout_url); } main().catch(console.error); ``` ```python theme={null} import os from dodopayments import DodoPayments client = DodoPayments( bearer_token=os.environ.get('DODO_PAYMENTS_API_KEY'), environment="test_mode", # defaults to "live_mode" ) checkout = client.checkout_sessions.create( product_cart=[ {"product_id": "pdt_123", "quantity": 1} ], billing_address={ "city": "SF", "country": "US", "state": "CA", "street": "1 Market St", "zipcode": "94105", }, customer={ "customer_id": "cus_123", }, return_url="https://example.com/billing/success", subscription_data={ "on_demand":{ "mandate_only": True, # set False to collect an initial charge # "product_price": 1000, # optional: charge $10.00 now if mandate_only is false # "product_currency": "USD", # "product_description": "Custom initial charge", # "adaptive_currency_fees_inclusive": False, } }, ) print(checkout.checkout_url) ``` ```go theme={null} package main import ( "context" "fmt" "os" "github.com/dodopayments/dodopayments-go" "github.com/dodopayments/dodopayments-go/option" ) func main() { client := dodopayments.NewClient( option.WithBearerToken(os.Getenv("DODO_PAYMENTS_API_KEY")), option.WithEnvironmentTestMode(), // defaults to live mode ) checkout, err := client.CheckoutSessions.New(context.TODO(), dodopayments.CheckoutSessionNewParams{ CheckoutSessionRequest: dodopayments.CheckoutSessionRequestParam{ ProductCart: dodopayments.F([]dodopayments.ProductItemReqParam{ { ProductID: dodopayments.F("pdt_123"), Quantity: dodopayments.F(int64(1)), }, }), BillingAddress: dodopayments.F(dodopayments.CheckoutSessionBillingAddressParam{ City: dodopayments.F("SF"), Country: dodopayments.F(dodopayments.CountryCodeUs), State: dodopayments.F("CA"), Street: dodopayments.F("1 Market St"), Zipcode: dodopayments.F("94105"), }), Customer: dodopayments.F[dodopayments.CustomerRequestUnionParam]( dodopayments.AttachExistingCustomerParam{ CustomerID: dodopayments.F("cus_123"), }, ), ReturnURL: dodopayments.F("https://example.com/billing/success"), SubscriptionData: dodopayments.F(dodopayments.SubscriptionDataParam{ OnDemand: dodopayments.F(dodopayments.OnDemandSubscriptionParam{ MandateOnly: dodopayments.F(true), // set false to collect an initial charge // ProductPrice: dodopayments.F(int64(1000)), // optional: charge $10.00 now if mandate_only is false // ProductCurrency: dodopayments.F(dodopayments.CurrencyUsd), // ProductDescription: dodopayments.F("Custom initial charge"), }), }), }, }) if err != nil { panic(err) } fmt.Println(checkout.CheckoutURL) } ``` ```bash theme={null} curl -X POST "$DODO_API/checkouts" \ -H "Authorization: Bearer $DODO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "product_cart": [ { "product_id": "pdt_123", "quantity": 1 } ], "customer": { "customer_id": "cus_123" }, "billing_address": { "street": "1 Market St", "city": "SF", "state": "CA", "country": "US", "zipcode": "94105" }, "subscription_data": { "on_demand": { "mandate_only": true } }, "return_url": "https://example.com/billing/success" }' ``` ```json Success theme={null} { "session_id": "cks_123", "checkout_url": "https://test.checkout.dodopayments.com/session/cks123" } ``` ## Charge an on-demand subscription After the mandate is authorized, create charges as needed. Endpoint: [POST /subscriptions/\{subscription\_id}/charge](/api-reference/subscriptions/create-charge) Key request fields (body): Amount to charge (in the smallest currency unit). Example: to charge \$25.00, pass 2500. Optional currency override for the charge. Optional description override for this charge. If true, includes adaptive currency fees within product\_price. If false, fees are added on top. Specify how the customer's wallet balance is used to settle this charge. Additional metadata for the payment. If omitted, the subscription metadata is used. ```javascript theme={null} import DodoPayments from 'dodopayments'; const client = new DodoPayments({ bearerToken: process.env.DODO_PAYMENTS_API_KEY }); async function chargeNow(subscriptionId) { const res = await client.subscriptions.charge(subscriptionId, { product_price: 2500 }); console.log(res.payment_id); } chargeNow('sub_123').catch(console.error); ``` ```python theme={null} import os from dodopayments import DodoPayments client = DodoPayments(bearer_token=os.environ.get('DODO_PAYMENTS_API_KEY')) response = client.subscriptions.charge( subscription_id="sub_123", product_price=2500, ) print(response.payment_id) ``` ```go theme={null} package main import ( "context" "fmt" "github.com/dodopayments/dodopayments-go" "github.com/dodopayments/dodopayments-go/option" ) func main() { client := dodopayments.NewClient(option.WithBearerToken("YOUR_API_KEY")) res, err := client.Subscriptions.Charge(context.TODO(), "sub_123", dodopayments.SubscriptionChargeParams{ ProductPrice: dodopayments.F(int64(2500)), }) if err != nil { panic(err) } fmt.Println(res.PaymentID) } ``` ```bash theme={null} curl -X POST "$DODO_API/subscriptions/sub_123/charge" \ -H "Authorization: Bearer $DODO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "product_price": 2500, "product_description": "Extra usage for March" }' ``` ```json Success theme={null} { "payment_id": "pay_abc123" } ``` Charging a subscription that is not on-demand may fail. Ensure the subscription has `on_demand: true` in its details before charging. ## Handling failed charges When a charge against an on-demand subscription fails, you decide what happens next. Unlike scheduled subscriptions — where a failed renewal stops further automatic billing — **on-demand subscriptions remain chargeable after a failure**. You can call the charge endpoint again as part of your own retry logic. ### What happens on failure The `POST /subscriptions/{subscription_id}/charge` request either returns an error response or completes asynchronously and emits a `payment.failed` webhook with the decline reason. The subscription may move to the `on_hold` state and emit a `subscription.on_hold` webhook (see [Subscription States → On Hold](/features/subscription#on-hold-state)). This is a signal — not a lock. For on-demand subscriptions, `on_hold` does **not** prevent you from charging again. For on-demand flows, Dodo does **not** auto-retry. You can call `POST /subscriptions/{subscription_id}/charge` again at any time to retry. Apply the [safe retry policy](#payment-retries) below — use exponential backoff, skip hard declines, and avoid burst patterns — so retries are not flagged by our fraud and risk systems. If retries keep failing because the payment method itself is broken (expired card, closed account, etc.), use [`POST /subscriptions/{subscription_id}/update-payment-method`](/api-reference/subscriptions/update-payment-method) to collect a new one from the customer. On success, the subscription returns to `active` and `payment.succeeded` followed by `subscription.active` webhooks are emitted. **On-demand vs scheduled**: For scheduled subscriptions, Dodo runs its own renewal retries and dunning. For on-demand subscriptions, you own the retry policy because only you know when the next charge should occur (it's driven by your usage events, not a calendar). ### Webhook sequence on a failed on-demand charge | Order | Event | Meaning | | ----- | ---------------------- | ------------------------------------------------------------------------------------ | | 1 | `payment.failed` | The on-demand charge attempt did not succeed (includes the decline reason) | | 2 | `subscription.on_hold` | The subscription was placed on hold (informational; does not block further charges) | | 3\* | `payment.succeeded` | A subsequent charge — either your retry or after a payment method update — succeeded | | 4\* | `subscription.active` | The subscription returned to `active` after a successful charge | Events 3 and 4 only fire after a follow-up charge succeeds. ### Retry responsibility Dodo Payments **does not auto-retry failed on-demand charges**. You own the retry policy. Follow the safe retry guidelines below to avoid being flagged by our fraud detection systems as card testing. [Subscription Dunning](/features/recovery/subscription-dunning) — the built-in email recovery sequence — is scoped to failed *renewal* payments on scheduled subscriptions and customer-initiated cancellations. It is not designed for on-demand charge failures. Communicate with the customer directly (e.g., transactional email or in-app prompt) when you decide the payment method needs to be updated. ## Payment retries Our fraud detection system may block aggressive retry patterns (and can flag them as potential card testing). Follow a safe retry policy. Burst retry patterns can be flagged as fraudulent or suspected card testing by our risk systems and processors. Avoid clustered retries; follow the backoff schedule and time alignment guidance below. ### Principles for safe retry policies * **Backoff mechanism**: Use exponential backoff between retries. * **Retry limits**: Cap total retries (3–4 attempts max). * **Intelligent filtering**: Retry only on retryable failures (e.g., network/issuer errors, insufficient funds); never retry hard declines. * **Card testing prevention**: Do not retry failures like `DO_NOT_HONOR`, `STOLEN_CARD`, `LOST_CARD`, `PICKUP_CARD`, `FRAUDULENT`, `AUTHENTICATION_FAILURE`. * **Vary metadata (optional)**: If you maintain your own retry system, differentiate retries via metadata (e.g., `retry_attempt`). ### Suggested retry schedule (subscriptions) * **1st attempt**: Immediate when you create the charge * **2nd attempt**: After 3 days * **3rd attempt**: After 7 more days (10 days total) * **4th attempt (final)**: After another 7 days (17 days total) Final step: if still unpaid, mark the subscription as unpaid or cancel it, based on your policy. Notify the customer during the window to update their payment method. ### Avoid burst retries; align to authorization time * Anchor retries to the original authorization timestamp to avoid "burst" behavior across your portfolio. * Example: If the customer starts a trial or mandate at 1:10 pm today, schedule follow-up retries at 1:10 pm on subsequent days per your backoff (e.g., +3 days → 1:10 pm, +7 days → 1:10 pm). * Alternatively, if you store the last successful payment time `T`, schedule the next attempt at `T + X days` to preserve time-of-day alignment. Time-zone and DST: use a consistent time standard for scheduling and convert for display only to maintain intervals. ### Decline codes you should not retry * `STOLEN_CARD` * `DO_NOT_HONOR` * `FRAUDULENT` * `PICKUP_CARD` * `AUTHENTICATION_FAILURE` * `LOST_CARD` For a comprehensive list of decline reasons and whether they are user-correctable, see the [Transaction Failures](/api-reference/transaction-failures) documentation. Only retry on soft/temporary issues (e.g., `insufficient_funds`, `issuer_unavailable`, `processing_error`, network timeouts). If the same decline repeats, pause further retries. ### Implementation guidelines (no code) * Use a scheduler/queue that persists precise timestamps; compute next attempt at the exact time-of-day offset (e.g., `T + 3 days` at the same HH:MM). * Maintain and reference the last successful payment timestamp `T` to compute the next attempt; do not bunch multiple subscriptions at the same instant. * Always evaluate the last decline reason; stop retries for hard declines in the skip list above. * Cap concurrent retries per customer and per account to prevent accidental surges. * Communicate proactively: email/SMS the customer to update their payment method before the next scheduled attempt. * Use metadata only for observability (e.g., `retry_attempt`); never try to "evade" fraud/risk systems by rotating inconsequential fields. ## Cancellation On-demand subscriptions follow a different cancellation flow from scheduled subscriptions because there is no fixed billing cycle to anchor an immediate end date. ### Customer portal behavior When a customer cancels an on-demand subscription from the [Customer Portal](/features/customer-portal), the cancellation is **scheduled for the next billing date** by default. The **Cancel Now** option is intentionally not shown for on-demand subscriptions. The reason: on-demand subscriptions do not have predictable recurring renewal dates — the next charge time is driven entirely by your usage events. Scheduling cancellation at the next billing date keeps the mandate active until the period boundary so any in-flight usage can still be charged, then ends the subscription cleanly. After the customer confirms cancellation: * The subscription stays `active` and remains chargeable via `POST /subscriptions/{id}/charge` until the scheduled cancellation date. * `cancel_at_next_billing_date` is set to `true` on the subscription. * A `subscription.cancelled` webhook is emitted when the cancellation takes effect. If you need to end the subscription immediately (for example, in response to a refund or a support request), cancel it programmatically via the API instead of relying on the customer portal flow. ### Cancel programmatically You can cancel an on-demand subscription via the API at any time. You control whether the cancellation is immediate or scheduled. Endpoint: [PATCH /subscriptions/\{subscription\_id}](/api-reference/subscriptions/patch-subscriptions) Set the subscription `status` to `cancelled` to end it right away. The mandate is revoked and no further charges can be created. ```javascript theme={null} import DodoPayments from 'dodopayments'; const client = new DodoPayments({ bearerToken: process.env.DODO_PAYMENTS_API_KEY }); await client.subscriptions.update('sub_123', { status: 'cancelled', }); ``` ```bash cURL theme={null} curl -X PATCH "$DODO_API/subscriptions/sub_123" \ -H "Authorization: Bearer $DODO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "status": "cancelled" }' ``` Mirror the customer portal behavior — keep the subscription active until the scheduled cancellation date, then end it. ```javascript theme={null} import DodoPayments from 'dodopayments'; const client = new DodoPayments({ bearerToken: process.env.DODO_PAYMENTS_API_KEY }); await client.subscriptions.update('sub_123', { cancel_at_next_billing_date: true, }); ``` ```bash cURL theme={null} curl -X PATCH "$DODO_API/subscriptions/sub_123" \ -H "Authorization: Bearer $DODO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "cancel_at_next_billing_date": true }' ``` ### Webhooks on cancellation | Event | When it fires | | --------------------------- | -------------------------------------------------------------------------------- | | `subscription.cancelled` | Subscription is fully cancelled and no longer chargeable | | `subscription.plan_changed` | `cancel_at_next_billing_date` was toggled (scheduled cancellation set or undone) | To distinguish on-demand cancellations from scheduled-subscription cancellations in your handler, check the subscription's `on_demand` flag when processing the webhook. ## Track outcomes with webhooks Implement webhook handling to track the customer journey. See [Implementing Webhooks](/developer-resources/integration-guide#implementing-webhooks). * **subscription.active**: Mandate authorized and subscription activated * **subscription.failed**: Creation failed (e.g., mandate failure) * **subscription.on\_hold**: Subscription placed on hold (e.g., unpaid state) * **subscription.cancelled**: Subscription fully cancelled (see [Cancellation](#cancellation)) * **payment.succeeded**: Charge succeeded * **payment.failed**: Charge failed For on-demand flows, focus on `payment.succeeded` and `payment.failed` to reconcile usage-based charges. When `payment.failed` is followed by `subscription.on_hold`, see [Handling failed charges](#handling-failed-charges) to recover the subscription. ## Testing and next steps Use your test API key to create the subscription, then open the returned `checkout_url` and complete the mandate. Call the charge endpoint with a small `product_price` (e.g., `100`) and verify you receive `payment.succeeded`. Switch to your live API key once you have validated events and internal state updates. ## Troubleshooting * **422 Invalid Request**: Ensure `on_demand.mandate_only` is provided on creation and `product_price` is provided for charges. * **Currency errors**: If you override `product_currency`, confirm it's supported for your account and customer. * **No webhooks received**: Verify your webhook URL and signature secret configuration. # Android Source: https://docs.dodopayments.com/developer-resources/sdks/android Open Dodo Payments' hosted checkout from an Android app in a Chrome Custom Tab and get a typed result back in one call. This is the official Android checkout SDK (`com.dodopayments.api:checkout-android`), for opening Dodo's hosted checkout. It is distinct from the [backend Kotlin SDK](/developer-resources/sdks/kotlin), which calls the Dodo Payments API from your server. Create the `checkout_url` that this SDK opens Best practices for mobile checkout flows The Android SDK opens Dodo's hosted checkout in a Chrome Custom Tab using `androidx.browser.customtabs`. It contains zero networking code and holds no API key. You pass a `checkoutUrl` from your backend's checkout session, and the SDK returns a typed `CheckoutResult` when the user completes or abandons the flow. **Requirements:** `minSdk` 23, Kotlin, Java 17. ## Installation ```kotlin build.gradle.kts theme={null} dependencies { implementation("com.dodopayments.api:checkout-android:1.0.0") } ``` Set your callback scheme as a Gradle manifest placeholder. The library's own manifest already declares the redirect activity's intent filter using the `${dodoCallbackScheme}` token, so this one property is the entire setup cost — you add no manifest XML: ```kotlin build.gradle.kts theme={null} android { defaultConfig { manifestPlaceholders["dodoCallbackScheme"] = "myapp" } } ``` The value must match the scheme in `CheckoutParams.returnUrl` (e.g. `myapp://checkout/return`). If you omit the placeholder entirely, the build fails immediately with an unresolved-placeholder error rather than failing silently at checkout time. If you set it but it doesn't match `returnUrl`'s scheme, `DodoCheckout.start` throws `PLATFORM_ERROR` before presenting anything. ## Usage The SDK supports two invocation styles. Register the contract with `registerForActivityResult`, then launch it: ```kotlin theme={null} import com.dodopayments.checkout.CheckoutParams import com.dodopayments.checkout.CheckoutStatus import com.dodopayments.checkout.DodoCheckout private val checkoutLauncher = registerForActivityResult(DodoCheckout.contract()) { result -> when (result.status) { CheckoutStatus.SUCCEEDED -> showSuccess(result.paymentId) CheckoutStatus.FAILED -> showFailure() CheckoutStatus.CANCELLED -> dismiss() CheckoutStatus.PENDING -> showPending() CheckoutStatus.EXPIRED -> showExpired() } } checkoutLauncher.launch( CheckoutParams( checkoutUrl = checkoutUrl, // from your backend's checkout session returnUrl = "myapp://checkout/return" ) ) ``` Prefer this style. The result is delivered through Android's OS-managed `ActivityResultRegistry`, so it survives process death. Call `DodoCheckout.start` from a coroutine scope: ```kotlin theme={null} import com.dodopayments.checkout.CheckoutParams import com.dodopayments.checkout.CheckoutStatus import com.dodopayments.checkout.DodoCheckout lifecycleScope.launch { val result = DodoCheckout.start( activity = this@MyActivity, params = CheckoutParams( checkoutUrl = checkoutUrl, // from your backend's checkout session returnUrl = "myapp://checkout/return" ), onEvent = { event -> println(event.name) } // logging only ) when (result.status) { CheckoutStatus.SUCCEEDED -> showSuccess(result.paymentId) CheckoutStatus.FAILED -> showFailure() CheckoutStatus.CANCELLED -> dismiss() CheckoutStatus.PENDING -> showPending() CheckoutStatus.EXPIRED -> showExpired() } } ``` This style resolves an in-memory `CompletableDeferred`, so it does **not** survive process death. `onEvent` is available only here, not on the contract. ## What the Result Means The `status` field is a UI hint, not proof of payment. Always verify the payment on your backend using webhooks or the Get Payment Detail endpoint before granting access. One of `SUCCEEDED`, `FAILED`, `CANCELLED`, `PENDING`, `EXPIRED`. Set when the return URL included one. Display it in the UI, don't use it to grant access. See Verify the Payment below. Set for subscription checkouts. Set when the checkout includes license key products. Set when the checkout captures an email. Every query parameter from the return URL, verbatim. ## Verify the Payment Listen for payment events in real time Query the payment status on demand Grant access to the user only after one of these confirms the payment. Do not rely on the `CheckoutResult.status` alone. ## Appearance Customization Customize the Custom Tab's toolbar, buttons, and color scheme via `customization` on `CheckoutParams`. All fields are optional; omitting `customization` uses Android's default Custom Tab appearance. Toolbar background color, as an ARGB `Color` int. Navigation bar color. Divider color above the navigation bar. `DEFAULT` shows the system "X" icon; `BACK` draws a back arrow instead. Which side of the toolbar the close button appears on: `START` or `END`. Shows the toolbar's share icon. Shows the page title under the URL in the toolbar. Lets the toolbar auto-hide as the page scrolls. Shows "Bookmark this page" in the overflow menu. Shows "Download page" in the overflow menu. Forces light or dark appearance regardless of the device's system setting: `SYSTEM`, `LIGHT`, or `DARK`. ```kotlin theme={null} import com.dodopayments.checkout.BrowserCustomization checkoutLauncher.launch( CheckoutParams( checkoutUrl = checkoutUrl, returnUrl = "myapp://checkout/return", customization = BrowserCustomization( toolbarColor = Color.parseColor("#6366F1"), closeButtonStyle = BrowserCustomization.CloseButtonStyle.BACK, colorScheme = BrowserCustomization.ColorScheme.DARK, ) ) ) ``` ## Errors `DodoCheckout.start` throws `CheckoutError` only for misuse or a platform failure. Read the code from `CheckoutError.code`: * `INVALID_CHECKOUT_URL`: not a `checkout.dodopayments.com` session URL. * `INVALID_RETURN_URL`: not a valid absolute URL. * `ALREADY_IN_PROGRESS`: a checkout is already running. * `PLATFORM_ERROR`: unexpected platform failure, including a `returnUrl` whose scheme doesn't match your `dodoCallbackScheme` placeholder. A user cancelling or a declined payment is always a result (`CANCELLED` or `FAILED`), never a thrown error. With the launcher style, validation errors throw out of `launcher.launch(...)`. ## Abandoned Sessions If the app is killed or the user force-stops it during checkout, the SDK stores the session locally. On the next app launch, check for an abandoned session and reconcile it with your backend: ```kotlin theme={null} DodoCheckout.getAbandonedSession(context)?.let { abandoned -> // reconcile abandoned.sessionId with your backend, then: DodoCheckout.clearAbandonedSession(context) } ``` The `abandoned.createdAt` is an epoch timestamp in milliseconds. ## Related Best practices for mobile checkout flows Backend SDK for server-side operations # C# Source: https://docs.dodopayments.com/developer-resources/sdks/csharp Integrate Dodo Payments into your .NET applications with modern async/await support The C# SDK provides convenient access to the Dodo Payments REST API from applications written in C#. It features an async Task-based API with strong typing, automatic retries, and comprehensive error handling. ## Installation Install the package from [NuGet](https://www.nuget.org/packages/DodoPayments.Client): ```bash theme={null} dotnet add package DodoPayments.Client ``` The SDK requires .NET 8.0 or later. It works with ASP.NET Core, Console applications, and other .NET project types. ## Quick Start Initialize the client and create a checkout session: ```csharp theme={null} using System; using DodoPayments.Client; using DodoPayments.Client.Models.CheckoutSessions; // Configured using the DODO_PAYMENTS_API_KEY and DODO_PAYMENTS_BASE_URL environment variables DodoPaymentsClient client = new(); CheckoutSessionCreateParams parameters = new() { ProductCart = [ new() { ProductID = "product_id", Quantity = 1, }, ], }; var checkoutSessionResponse = await client.CheckoutSessions.Create(parameters); Console.WriteLine(checkoutSessionResponse.SessionId); ``` Always store your API keys securely using environment variables, user secrets, or Azure Key Vault. Never hardcode them in your source code or commit them to version control. ## Core Features Full async Task-based API for non-blocking operations Comprehensive type safety with nullable reference types Automatic retries with exponential backoff for transient errors Built-in exception hierarchy for precise error management ## Configuration ### Environment Variables ```bash .env theme={null} DODO_PAYMENTS_API_KEY=your_api_key_here ``` ```csharp theme={null} // Automatically reads from environment variables DodoPaymentsClient client = new(); ``` | Property | Environment variable | Required | Default value | | ------------- | --------------------------- | -------- | --------------------------------- | | `BearerToken` | `DODO_PAYMENTS_API_KEY` | true | - | | `WebhookKey` | `DODO_PAYMENTS_WEBHOOK_KEY` | false | - | | `BaseUrl` | `DODO_PAYMENTS_BASE_URL` | true | `"https://live.dodopayments.com"` | ### Manual Configuration ```csharp theme={null} DodoPaymentsClient client = new() { BearerToken = "My Bearer Token" }; ``` ### Environments Switch between live and test mode: ```csharp theme={null} using DodoPayments.Client.Core; DodoPaymentsClient client = new() { BaseUrl = EnvironmentUrl.TestMode }; ``` ### Retries The SDK automatically retries 2 times by default with exponential backoff. It retries on connection errors and status codes 408, 409, 429, and 5xx. ```csharp theme={null} // Custom retry count DodoPaymentsClient client = new() { MaxRetries = 3 }; ``` ### Timeouts Requests time out after 1 minute by default. ```csharp theme={null} DodoPaymentsClient client = new() { Timeout = TimeSpan.FromSeconds(30) }; ``` ### Per-Request Overrides Temporarily modify configuration for a single request using `WithOptions`: ```csharp theme={null} var response = await client .WithOptions(options => options with { Timeout = TimeSpan.FromSeconds(10), MaxRetries = 5, }) .CheckoutSessions.Create(parameters); ``` ## Common Operations ### Create a Checkout Session ```csharp theme={null} var parameters = new CheckoutSessionCreateParams { ProductCart = [ new() { ProductID = "pdt_123", Quantity = 1 } ], ReturnUrl = "https://yourdomain.com/return" }; var session = await client.CheckoutSessions.Create(parameters); Console.WriteLine($"Checkout URL: {session.CheckoutUrl}"); ``` ### Manage Customers ```csharp theme={null} // Create a customer var customer = await client.Customers.Create(new CustomerCreateParams { Email = "customer@example.com", Name = "John Doe" }); // Retrieve customer var retrieved = await client.Customers.Retrieve(new CustomerRetrieveParams { CustomerID = "cus_123" }); Console.WriteLine($"Customer: {retrieved.Name} ({retrieved.Email})"); ``` ### Handle Subscriptions `POST /subscriptions` (the SDK's `subscriptions.create` method) is **deprecated**. It still works for existing integrations, but new integrations should create subscriptions through a [Checkout Session](/developer-resources/checkout-session). ```csharp theme={null} using DodoPayments.Client.Models.Payments; using DodoPayments.Client.Models.Subscriptions; // Create a subscription var subscription = await client.Subscriptions.Create(new SubscriptionCreateParams { Billing = new BillingAddress { Country = "US", City = "San Francisco", State = "CA", Street = "1 Market St", Zipcode = "94105", }, Customer = new AttachExistingCustomer { CustomerID = "cus_123" }, ProductID = "pdt_456", Quantity = 1, }); // Charge an on-demand subscription // ProductPrice is in the lowest currency denomination (e.g., 2500 = $25.00 USD) var charge = await client.Subscriptions.Charge(new SubscriptionChargeParams { SubscriptionID = subscription.SubscriptionId, ProductPrice = 2500, }); ``` `Billing` requires at minimum the two-letter ISO `Country` code. Use `AttachExistingCustomer` to attach an existing customer, or `NewCustomer` to create one. `ProductPrice` is in the lowest currency denomination. ## Error Handling The SDK throws specific exceptions based on the HTTP status code. All 4xx errors inherit from `DodoPayments4xxException`. | Status | Exception | | ------ | ------------------------------------------- | | 400 | `DodoPaymentsBadRequestException` | | 401 | `DodoPaymentsUnauthorizedException` | | 403 | `DodoPaymentsForbiddenException` | | 404 | `DodoPaymentsNotFoundException` | | 422 | `DodoPaymentsUnprocessableEntityException` | | 429 | `DodoPaymentsRateLimitException` | | 5xx | `DodoPayments5xxException` | | others | `DodoPaymentsUnexpectedStatusCodeException` | Other exception types: * `DodoPaymentsIOException`: I/O networking errors * `DodoPaymentsInvalidDataException`: Failure to interpret parsed data * `DodoPaymentsException`: Base class for all exceptions ## Pagination ### Auto-Pagination Iterate through all results across all pages using the `Paginate` method, which returns an `IAsyncEnumerable`: ```csharp theme={null} var page = await client.Payments.List(parameters); await foreach (var item in page.Paginate()) { Console.WriteLine(item); } ``` ### Manual Pagination ```csharp theme={null} var page = await client.Payments.List(); while (true) { foreach (var item in page.Items) { Console.WriteLine(item); } if (!page.HasNext()) { break; } page = await page.Next(); } ``` ## ASP.NET Core Integration Register the client in your DI container: ```csharp Program.cs theme={null} using DodoPayments.Client; var builder = WebApplication.CreateBuilder(args); builder.Services.AddSingleton(sp => { var configuration = sp.GetRequiredService(); return new DodoPaymentsClient { BearerToken = configuration["DodoPayments:ApiKey"] }; }); var app = builder.Build(); app.Run(); ``` ```json appsettings.json theme={null} { "DodoPayments": { "ApiKey": "your_api_key_here" } } ``` For development, use [user secrets](https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets) instead of storing keys in `appsettings.json`: ```bash theme={null} dotnet user-secrets init dotnet user-secrets set "DodoPayments:ApiKey" "your_api_key_here" ``` ## Resources View package on NuGet Gallery View source code and contribute Complete API documentation Get help and connect with developers ## Support Need help with the C# SDK? * **Discord**: Join our [community server](https://discord.gg/bYqAp4ayYh) for real-time support * **Email**: Contact us at [support@dodopayments.com](mailto:support@dodopayments.com) * **GitHub**: Open an issue on the [repository](https://github.com/dodopayments/dodopayments-csharp) # Flutter Source: https://docs.dodopayments.com/developer-resources/sdks/flutter Open Dodo Payments' hosted checkout from Flutter in a system browser tab and get a typed result back in one call. This is the official Dodo Payments Flutter package (`dodopayments_checkout` on pub.dev). A separate, community-built package also exists, see [Community Projects](/community/projects). Create the checkout\_url this SDK opens, from your backend. See how this fits into the full mobile payment flow. `dodopayments_checkout` opens Dodo's hosted checkout in `SFSafariViewController` on iOS and a Chrome Custom Tab on Android — the same native cores used by the standalone [iOS](/developer-resources/sdks/ios) and [Android](/developer-resources/sdks/android) SDKs. All checkout logic lives in those native cores; the Dart layer passes the call through a typed [Pigeon](https://pub.dev/packages/pigeon) channel. It holds no API key and never calls the Dodo Payments API. Requires Flutter 3.44+ / Dart 3.12+, iOS 16+, and Android `minSdk` 23. ## Installation ```yaml pubspec.yaml theme={null} dependencies: dodopayments_checkout: ^1.0.0 ``` Add a URL type for your scheme in `ios/Runner/Info.plist`: ```xml ios/Runner/Info.plist theme={null} CFBundleURLTypes CFBundleURLName myapp CFBundleURLSchemes myapp ``` Then forward incoming URLs (e.g. via [`app_links`](https://pub.dev/packages/app_links)) into the SDK, because `SFSafariViewController` cannot catch its own return URL: ```dart theme={null} import 'package:dodopayments_checkout/dodopayments_checkout.dart'; DodoCheckout.instance.handleOpenURL(url); ``` It's safe to forward every URL here. `handleOpenURL` only acts on URLs matching your registered `returnUrl` and resolves `false` for anything else. Set your callback scheme as a Gradle manifest placeholder: ```kotlin android/app/build.gradle theme={null} android { defaultConfig { manifestPlaceholders["dodoCallbackScheme"] = "myapp" } } ``` If `MainActivity` sets `android:taskAffinity=""` (the stock `flutter create` default), remove it or give the SDK's activities the same affinity. Otherwise some OEM Android builds can lose the in-flight checkout and return `PLATFORM_ERROR`. ## Usage ```dart theme={null} import 'package:dodopayments_checkout/dodopayments_checkout.dart'; final result = await DodoCheckout.instance.start( CheckoutParams( checkoutUrl: Uri.parse(checkoutUrl), // from your backend's checkout session returnUrl: Uri.parse('myapp://checkout/return'), // scheme must be registered (see Setup) onEvent: (event) => print(event.type), // logging only ), ); switch (result.status) { case CheckoutStatus.succeeded: showSuccess(result.paymentId); case CheckoutStatus.failed: showFailure(); case CheckoutStatus.cancelled: dismiss(); case CheckoutStatus.pending: showPending(); case CheckoutStatus.expired: showExpired(); } ``` ## What the Result Means `result.status` is a UI hint, not proof of payment. Confirm every payment from your backend, via the `payment.succeeded` / `subscription.active` webhook. One of `succeeded`, `failed`, `cancelled`, `pending`, `expired`. Set when the return URL included one. Display it in the UI, don't use it to grant access. See Verify the Payment below. Set for subscription checkouts. Set when the checkout includes license key products. Set when the checkout captures an email. Every query parameter from the return URL, verbatim. ## Verify the Payment Dodo Payments calls your backend when a payment succeeds or a subscription activates. Look up `paymentId` with your secret key to check its status directly. Grant access after one of these confirms the payment, never from `result.status` alone. ## Appearance Customization Customize the checkout browser's toolbar, buttons, and color scheme via `customization` on `CheckoutParams`. Options are grouped by platform because Android's Custom Tab and iOS's `SFSafariViewController` expose different native controls. All fields are optional; omitting `customization` uses each platform's default appearance. Toolbar background color. Navigation bar color. Divider color above the navigation bar. `standard` shows the system "X" icon; `back` draws a back arrow instead. Which side of the toolbar the close button appears on. Shows the toolbar's share icon. Shows the page title under the URL in the toolbar. Lets the toolbar auto-hide as the page scrolls. Shows "Bookmark this page" in the overflow menu. Shows "Download page" in the overflow menu. Forces light or dark appearance regardless of the device's system setting. Label or icon for the dismiss button. `pageSheet` presents as a card with swipe-to-dismiss; `fullScreen` covers the whole screen. Lets the toolbar collapse on scroll. Only visible when `presentationStyle` is `fullScreen` — `pageSheet` keeps the bars pinned regardless of this setting. Forces light or dark appearance regardless of the device's system setting. ```dart theme={null} final result = await DodoCheckout.instance.start( CheckoutParams( checkoutUrl: Uri.parse(checkoutUrl), returnUrl: Uri.parse('myapp://checkout/return'), customization: BrowserCustomization( android: AndroidBrowserOptions( toolbarColor: Color(0xFF6366F1), closeButtonStyle: CloseButtonStyle.back, ), ios: IosBrowserOptions( presentationStyle: PresentationStyle.fullScreen, colorScheme: BrowserColorScheme.dark, ), ), ), ); ``` ## Errors `start` throws `CheckoutException` only for misuse or a platform failure. A cancelled or declined payment is always a result, never an exception. * `invalidCheckoutUrl` (`INVALID_CHECKOUT_URL`): not a `checkout.dodopayments.com` session URL. * `invalidReturnUrl` (`INVALID_RETURN_URL`): not a valid absolute URL. * `alreadyInProgress` (`ALREADY_IN_PROGRESS`): a checkout is already running. * `platformError` (`PLATFORM_ERROR`): unexpected platform failure. ## Abandoned Sessions If the app is killed mid-checkout, recover the session on next launch and reconcile it with your backend. ```dart theme={null} import 'package:dodopayments_checkout/dodopayments_checkout.dart'; final abandoned = await DodoCheckout.instance.getAbandonedSession(); if (abandoned != null) { // reconcile abandoned.sessionId with your backend, then: await DodoCheckout.instance.clearAbandonedSession(); } ``` ## Related The same contract for Android, iOS, and React Native. A separate, community-built Flutter package also exists. # Go Source: https://docs.dodopayments.com/developer-resources/sdks/go Integrate Dodo Payments into your Go applications with an idiomatic, performant SDK The Go SDK provides a clean and idiomatic Go interface for integrating Dodo Payments into your applications. It offers context support, strongly typed responses, middleware capabilities, and is safe for concurrent use. ## Installation Install the SDK using Go modules: ```bash theme={null} go get github.com/dodopayments/dodopayments-go ``` Or to pin to a specific version: ```bash theme={null} go get -u 'github.com/dodopayments/dodopayments-go@v1.97.3' ``` The SDK requires Go 1.22 or later versions, leveraging modern Go features for optimal performance. ## Quick Start Initialize the client and create your first checkout session: ```go theme={null} package main import ( "context" "fmt" "log" "github.com/dodopayments/dodopayments-go" "github.com/dodopayments/dodopayments-go/option" ) func main() { client := dodopayments.NewClient( option.WithBearerToken("My Bearer Token"), // defaults to os.LookupEnv("DODO_PAYMENTS_API_KEY") option.WithEnvironmentTestMode(), // defaults to option.WithEnvironmentLiveMode() ) checkoutSessionResponse, err := client.CheckoutSessions.New(context.TODO(), dodopayments.CheckoutSessionNewParams{ CheckoutSessionRequest: dodopayments.CheckoutSessionRequestParam{ ProductCart: dodopayments.F([]dodopayments.ProductItemReqParam{{ ProductID: dodopayments.F("product_id"), Quantity: dodopayments.F(int64(1)), }}), }, }) if err != nil { panic(err.Error()) } fmt.Printf("Session ID: %s\n", checkoutSessionResponse.SessionID) } ``` Always store your API keys securely using environment variables. Never hardcode them in your source code. ## Core Features Full support for `context.Context` for cancellation and timeouts Strongly typed requests and responses for compile-time safety Extensible middleware support for logging, metrics, and custom logic Thread-safe client designed for concurrent operations ## Configuration ### Context and Timeouts Leverage Go's context for timeouts and cancellation: ```go theme={null} import ( "context" "time" "github.com/dodopayments/dodopayments-go" "github.com/dodopayments/dodopayments-go/option" ) // Create a context with timeout ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() payment, err := client.Payments.New(ctx, dodopayments.PaymentNewParams{ Billing: dodopayments.F(dodopayments.BillingAddressParam{ Country: dodopayments.F(dodopayments.CountryCodeUs), City: dodopayments.F("San Francisco"), State: dodopayments.F("CA"), Street: dodopayments.F("1 Market St"), Zipcode: dodopayments.F("94105"), }), Customer: dodopayments.F[dodopayments.CustomerRequestUnionParam]( dodopayments.AttachExistingCustomerParam{ CustomerID: dodopayments.F("cus_123"), }, ), ProductCart: dodopayments.F([]dodopayments.OneTimeProductCartItemParam{{ ProductID: dodopayments.F("pdt_456"), Quantity: dodopayments.F(int64(1)), }}), }) if err != nil { if ctx.Err() == context.DeadlineExceeded { log.Println("Request timed out") } else { log.Fatal(err) } } ``` ### Retry Configuration Configure automatic retry behavior: ```go theme={null} // Configure default for all requests (default is 2) client := dodopayments.NewClient( option.WithMaxRetries(0), // disable retries ) // Override per-request client.CheckoutSessions.New( context.TODO(), dodopayments.CheckoutSessionNewParams{ CheckoutSessionRequest: dodopayments.CheckoutSessionRequestParam{ ProductCart: dodopayments.F([]dodopayments.ProductItemReqParam{{ ProductID: dodopayments.F("product_id"), Quantity: dodopayments.F(int64(0)), }}), }, }, option.WithMaxRetries(5), ) ``` ## Common Operations ### Create a Checkout Session Generate a checkout session: ```go theme={null} session, err := client.CheckoutSessions.New(ctx, dodopayments.CheckoutSessionNewParams{ CheckoutSessionRequest: dodopayments.CheckoutSessionRequestParam{ ProductCart: dodopayments.F([]dodopayments.ProductItemReqParam{{ ProductID: dodopayments.F("pdt_123"), Quantity: dodopayments.F(int64(1)), }}), ReturnURL: dodopayments.F("https://yourdomain.com/return"), }, }) if err != nil { log.Fatal(err) } fmt.Printf("Checkout URL: %s\n", session.CheckoutURL) ``` ### Manage Customers Create and retrieve customer information: ```go theme={null} // Create a customer customer, err := client.Customers.New(ctx, dodopayments.CustomerNewParams{ Email: dodopayments.F("customer@example.com"), Name: dodopayments.F("John Doe"), Metadata: dodopayments.F(map[string]string{ "user_id": "12345", }), }) if err != nil { log.Fatal(err) } // Retrieve customer customer, err = client.Customers.Get(ctx, "cus_123") if err != nil { log.Fatal(err) } fmt.Printf("Customer: %s (%s)\n", customer.Name, customer.Email) ``` ### Handle Subscriptions Create and manage recurring subscriptions: `POST /subscriptions` (the SDK's `subscriptions.create` method) is **deprecated**. It still works for existing integrations, but new integrations should create subscriptions through a [Checkout Session](/developer-resources/checkout-session). ```go theme={null} import "time" // Create a subscription subscription, err := client.Subscriptions.New(ctx, dodopayments.SubscriptionNewParams{ Billing: dodopayments.F(dodopayments.BillingAddressParam{ Country: dodopayments.F(dodopayments.CountryCodeUs), City: dodopayments.F("San Francisco"), State: dodopayments.F("CA"), Street: dodopayments.F("1 Market St"), Zipcode: dodopayments.F("94105"), }), Customer: dodopayments.F[dodopayments.CustomerRequestUnionParam]( dodopayments.AttachExistingCustomerParam{ CustomerID: dodopayments.F("cus_123"), }, ), ProductID: dodopayments.F("pdt_456"), Quantity: dodopayments.F(int64(1)), }) if err != nil { log.Fatal(err) } // Charge an on-demand subscription // ProductPrice is in the lowest currency denomination (e.g., 2500 = $25.00 USD) chargeResponse, err := client.Subscriptions.Charge(ctx, subscription.SubscriptionID, dodopayments.SubscriptionChargeParams{ ProductPrice: dodopayments.F(int64(2500)), }, ) if err != nil { log.Fatal(err) } // Get usage history (for metered subscriptions) usageHistory, err := client.Subscriptions.GetUsageHistory( ctx, subscription.SubscriptionID, dodopayments.SubscriptionGetUsageHistoryParams{ StartDate: dodopayments.F(time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)), EndDate: dodopayments.F(time.Date(2024, 3, 31, 23, 59, 59, 0, time.UTC)), }, ) if err != nil { log.Fatal(err) } ``` `Billing` requires at minimum the two-letter ISO `Country` code. `Customer` is a `CustomerRequestUnionParam` — pass `AttachExistingCustomerParam{CustomerID: ...}` for an existing customer or `NewCustomerParam{Email: ..., Name: ...}` for a new one. `ProductPrice` is in the lowest currency denomination. ## Usage-Based Billing ### Ingest Usage Events Track custom events: ```go theme={null} import "github.com/dodopayments/dodopayments-go" response, err := client.UsageEvents.Ingest(ctx, dodopayments.UsageEventIngestParams{ Events: dodopayments.F([]dodopayments.EventInputParam{{ EventID: dodopayments.F("api_call_12345"), CustomerID: dodopayments.F("cus_abc123"), EventName: dodopayments.F("api_request"), Timestamp: dodopayments.F(time.Date(2024, 1, 15, 10, 30, 0, 0, time.UTC)), }}), }) if err != nil { log.Fatal(err) } ``` ### List Usage Events ```go theme={null} // List events with filters params := dodopayments.UsageEventListParams{ CustomerID: dodopayments.F("cus_abc123"), EventName: dodopayments.F("api_request"), } events, err := client.UsageEvents.List(ctx, params) if err != nil { log.Fatal(err) } for _, event := range events.Items { fmt.Printf("Event %s: %s at %s\n", event.EventID, event.EventName, event.Timestamp) } ``` ## Error Handling When the API returns a non-success status code, the SDK returns an error of type `*dodopayments.Error`. It exposes the `StatusCode`, the underlying `*http.Request` and `*http.Response`, and the JSON of the error body. Use the `errors.As` pattern to inspect it, and branch on `StatusCode` to handle specific cases: ```go theme={null} payment, err := client.Payments.New(ctx, dodopayments.PaymentNewParams{ Billing: dodopayments.F(dodopayments.BillingAddressParam{ Country: dodopayments.F(dodopayments.CountryCodeUs), City: dodopayments.F("San Francisco"), State: dodopayments.F("CA"), Street: dodopayments.F("1 Market St"), Zipcode: dodopayments.F("94105"), }), Customer: dodopayments.F[dodopayments.CustomerRequestUnionParam]( dodopayments.AttachExistingCustomerParam{CustomerID: dodopayments.F("cus_123")}, ), ProductCart: dodopayments.F([]dodopayments.OneTimeProductCartItemParam{{ ProductID: dodopayments.F("pdt_456"), Quantity: dodopayments.F(int64(1)), }}), }) if err != nil { var apiErr *dodopayments.Error if errors.As(err, &apiErr) { fmt.Printf("Status Code: %d\n", apiErr.StatusCode) fmt.Println(string(apiErr.DumpResponse(true))) // Serialized HTTP response // Handle specific status codes switch apiErr.StatusCode { case 401: log.Println("Authentication failed") case 422: log.Println("Invalid request parameters") case 429: log.Println("Rate limit exceeded") default: log.Printf("API error: %s", apiErr.Error()) } } else { log.Fatal(err) } } ``` ## Middleware Add custom middleware for logging or metrics: ```go theme={null} func Logger(req *http.Request, next option.MiddlewareNext) (res *http.Response, err error) { // Before the request start := time.Now() log.Printf("Request: %s %s\n", req.Method, req.URL) // Forward the request to the next handler res, err = next(req) // After the request end := time.Now() log.Printf("Response: %d in %v\n", res.StatusCode, end.Sub(start)) return res, err } client := dodopayments.NewClient( option.WithMiddleware(Logger), ) ``` ## Concurrency The client is safe for concurrent use: ```go theme={null} package main import ( "context" "sync" "log" "github.com/dodopayments/dodopayments-go" ) func main() { client := dodopayments.NewClient() var wg sync.WaitGroup for i := 0; i < 10; i++ { wg.Add(1) go func(idx int) { defer wg.Done() payment, err := client.Payments.New(context.Background(), dodopayments.PaymentNewParams{ Billing: dodopayments.F(dodopayments.BillingAddressParam{ Country: dodopayments.F(dodopayments.CountryCodeUs), City: dodopayments.F("San Francisco"), State: dodopayments.F("CA"), Street: dodopayments.F("1 Market St"), Zipcode: dodopayments.F("94105"), }), Customer: dodopayments.F[dodopayments.CustomerRequestUnionParam]( dodopayments.AttachExistingCustomerParam{CustomerID: dodopayments.F("cus_123")}, ), ProductCart: dodopayments.F([]dodopayments.OneTimeProductCartItemParam{{ ProductID: dodopayments.F("pdt_456"), Quantity: dodopayments.F(int64(1)), }}), }) if err != nil { log.Printf("Failed to create payment %d: %v", idx, err) return } log.Printf("Created payment %d: %s", idx, payment.PaymentID) }(i) } wg.Wait() } ``` ## Resources View source code and contribute Complete API documentation Get help and connect with developers Report bugs or request features ## Support Need help with the Go SDK? * **Discord**: Join our [community server](https://discord.gg/bYqAp4ayYh) for real-time support * **Email**: Contact us at [support@dodopayments.com](mailto:support@dodopayments.com) * **GitHub**: Open an issue on the [repository](https://github.com/dodopayments/dodopayments-go) ## Contributing We welcome contributions! Check the [contributing guidelines](https://github.com/dodopayments/dodopayments-go/blob/main/CONTRIBUTING.md) to get started. # iOS Source: https://docs.dodopayments.com/developer-resources/sdks/ios Open Dodo Payments' hosted checkout from an iOS app in SFSafariViewController and get a typed result back in one call. This is the official Dodo Payments iOS checkout SDK for Swift. It opens Dodo's hosted checkout in a native browser view and returns a typed result. Create the checkout\_url this SDK opens, from your backend. See how this fits into the full mobile payment flow. The iOS SDK opens Dodo's hosted checkout in `SFSafariViewController`, holds no API key, and never calls the Dodo API directly. All checkout logic runs in the browser; the SDK simply manages the view lifecycle and captures the return URL. Requires iOS 16+, Swift 6. ## Installation In Xcode, go to **File → Add Package Dependencies** and enter: ``` https://github.com/dodopayments/dodopayments-mobile-sdk-ios ``` Select version 1.0.0 or later. Alternatively, add to your `Package.swift`: ```swift Package.swift theme={null} .package(url: "https://github.com/dodopayments/dodopayments-mobile-sdk-ios", from: "1.0.0") ``` Your app must register a URL scheme to receive the return URL from the checkout. Add this to your `Info.plist`: ```xml Info.plist theme={null} CFBundleURLTypes CFBundleURLName myapp CFBundleURLSchemes myapp ``` You can also add this via Xcode's **Info → URL Types** UI. ## Usage ```swift theme={null} import DodoCheckout let result = try await DodoCheckout.start( checkoutUrl: checkoutUrl, // from your backend's checkout session returnUrl: URL(string: "myapp://checkout/return")!, onEvent: { event in print(event.name) } // logging only ) switch result.status { case .succeeded: showSuccess(result.paymentId) case .failed: showFailure() case .cancelled: dismiss() case .pending: showPending() case .expired: showExpired() } ``` ## Forwarding the Return URL `SFSafariViewController` has no in-process way to catch its own return URL. Your app must forward incoming URLs into the SDK. ```swift theme={null} .onOpenURL { url in DodoCheckout.handleOpenURL(url) } ``` ```swift SceneDelegate.swift theme={null} func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { guard let url = URLContexts.first?.url else { return } DodoCheckout.handleOpenURL(url) } ``` It's safe to forward every URL here. `handleOpenURL` only acts on URLs matching your registered `returnUrl` and returns `false` for anything else. ## What the Result Means `result.status` is a UI hint, not proof of payment. Confirm every payment from your backend, via the `payment.succeeded` / `subscription.active` webhook. One of `succeeded`, `failed`, `cancelled`, `pending`, `expired`. Set when the return URL included one. Display it in the UI, don't use it to grant access. See Verify the Payment below. Set for subscription checkouts. Set when the checkout includes license key products. Set when the checkout captures an email. Every query parameter from the return URL, verbatim. ## Verify the Payment Dodo Payments calls your backend when a payment succeeds or a subscription activates. Look up `paymentId` with your secret key to check its status directly. Grant access after one of these confirms the payment, never from `result.status` alone. ## Appearance Customization Customize the sheet's dismiss button, presentation style, and color scheme via `customization` on `start(...)`. All fields are optional; omitting `customization` uses iOS's default `SFSafariViewController` appearance. Label or icon for the dismiss button: `done`, `close`, or `cancel`. `pageSheet` presents as a card with swipe-to-dismiss; `fullScreen` covers the whole screen. Lets the toolbar collapse on scroll. Only visible when `presentationStyle` is `fullScreen` — `pageSheet` keeps the bars pinned regardless of this setting. Forces light or dark appearance regardless of the device's system setting: `system`, `light`, or `dark`. ```swift theme={null} let result = try await DodoCheckout.start( checkoutUrl: checkoutUrl, returnUrl: URL(string: "myapp://checkout/return")!, customization: BrowserCustomization( dismissButtonStyle: .close, presentationStyle: .fullScreen, colorScheme: .dark ) ) ``` ## Errors `start` throws `CheckoutError` only for misuse or a platform failure. A cancelled or declined payment is always a result, never an exception. * `invalidCheckoutUrl` (`INVALID_CHECKOUT_URL`): not a `checkout.dodopayments.com` session URL. * `invalidReturnUrl` (`INVALID_RETURN_URL`): not a valid absolute URL. * `alreadyInProgress` (`ALREADY_IN_PROGRESS`): a checkout is already running. * `platformError` (`PLATFORM_ERROR`): unexpected platform failure. ## Abandoned Sessions If the app is killed mid-checkout, recover the session on next launch and reconcile it with your backend. ```swift theme={null} import DodoCheckout if let abandoned = DodoCheckout.getAbandonedSession() { // reconcile abandoned.sessionId with your backend, then: DodoCheckout.clearAbandonedSession() } ``` ## Related The same contract for Android, React Native, and Flutter. Wraps this same Swift core on iOS. # Java Source: https://docs.dodopayments.com/developer-resources/sdks/java Integrate Dodo Payments into your Java applications with a robust, type-safe SDK The Java SDK provides convenient and ergonomic access to the Dodo Payments REST API for applications written in Java. It utilizes Java-specific features like Optional, Stream, and CompletableFuture for modern Java development. ## Installation ### Maven Add the dependency to your `pom.xml`: ```xml pom.xml theme={null} com.dodopayments.api dodo-payments-java 1.97.1 ``` ### Gradle Add the dependency to your `build.gradle`: ```kotlin build.gradle.kts theme={null} implementation("com.dodopayments.api:dodo-payments-java:1.97.1") ``` Always use the latest SDK version to access the newest Dodo Payments features. Check [Maven Central](https://central.sonatype.com/artifact/com.dodopayments.api/dodo-payments-java) for the latest version. The SDK supports Java 8 and all later versions, including Java 11, 17, and 21. ## Quick Start Initialize the client and create a checkout session: ```java theme={null} import com.dodopayments.api.client.DodoPaymentsClient; import com.dodopayments.api.client.okhttp.DodoPaymentsOkHttpClient; import com.dodopayments.api.models.checkoutsessions.CheckoutSessionCreateParams; import com.dodopayments.api.models.checkoutsessions.CheckoutSessionRequest; import com.dodopayments.api.models.checkoutsessions.ProductItemReq; // Configure using environment variables (DODO_PAYMENTS_API_KEY, DODO_PAYMENTS_BASE_URL) // Or system properties (dodopayments.apiKey, dodopayments.baseUrl) DodoPaymentsClient client = DodoPaymentsOkHttpClient.fromEnv(); CheckoutSessionRequest params = CheckoutSessionRequest.builder() .addProductCart(ProductItemReq.builder() .productId("product_id") .quantity(1) .build()) .build(); CheckoutSessionResponse checkoutSessionResponse = client.checkoutSessions().create(params); System.out.println(checkoutSessionResponse.sessionId()); ``` Always store your API keys securely using environment variables, system properties, or a secure configuration management system. Never hardcode them in your source code. ## Core Features Strongly typed API with compile-time safety Safe for concurrent use in multi-threaded applications Intuitive builder pattern for constructing requests CompletableFuture support for asynchronous operations ## Configuration ### Environment Variables Configure using environment variables or system properties: ```bash .env theme={null} DODO_PAYMENTS_API_KEY=your_api_key_here DODO_PAYMENTS_BASE_URL=https://live.dodopayments.com ``` ```java theme={null} // Automatically reads from environment variables DodoPaymentsClient client = DodoPaymentsOkHttpClient.fromEnv(); ``` ### Manual Configuration Configure manually with all options: ```java theme={null} import java.time.Duration; DodoPaymentsClient client = DodoPaymentsOkHttpClient.builder() .bearerToken("your_api_key_here") .baseUrl("https://live.dodopayments.com") .maxRetries(4) .timeout(Duration.ofSeconds(30)) .responseValidation(true) .build(); ``` ### Test Mode Configure for the Test Mode environment: ```java theme={null} DodoPaymentsClient testClient = DodoPaymentsOkHttpClient.builder() .fromEnv() .testMode() .build(); ``` ## Common Operations ### Create a Checkout Session Generate a checkout session: ```java theme={null} CheckoutSessionRequest params = CheckoutSessionRequest.builder() .addProductCart(ProductItemReq.builder() .productId("pdt_123") .quantity(1) .build()) .returnUrl("https://yourdomain.com/return") .build(); CheckoutSessionResponse session = client.checkoutSessions().create(params); System.out.println("Checkout URL: " + session.checkoutUrl()); ``` ### Manage Customers Create and retrieve customer information: ```java theme={null} import com.dodopayments.api.models.customers.Customer; import com.dodopayments.api.models.customers.CustomerCreateParams; // Create a customer CustomerCreateParams createParams = CustomerCreateParams.builder() .email("customer@example.com") .name("John Doe") .putMetadata("user_id", "12345") .build(); Customer customer = client.customers().create(createParams); // Retrieve customer Customer retrieved = client.customers().retrieve("cus_123"); System.out.println("Customer: " + retrieved.name() + " (" + retrieved.email() + ")"); ``` ### Handle Subscriptions Create and manage recurring subscriptions: `POST /subscriptions` (the SDK's `subscriptions.create` method) is **deprecated**. It still works for existing integrations, but new integrations should create subscriptions through a [Checkout Session](/developer-resources/checkout-session). ```java theme={null} import com.dodopayments.api.models.payments.AttachExistingCustomer; import com.dodopayments.api.models.payments.BillingAddress; import com.dodopayments.api.models.misc.CountryCode; import com.dodopayments.api.models.subscriptions.SubscriptionChargeParams; import com.dodopayments.api.models.subscriptions.SubscriptionChargeResponse; import com.dodopayments.api.models.subscriptions.SubscriptionCreateParams; import com.dodopayments.api.models.subscriptions.SubscriptionCreateResponse; // Create a subscription SubscriptionCreateParams subscriptionParams = SubscriptionCreateParams.builder() .billing(BillingAddress.builder() .city("San Francisco") .country(CountryCode.US) .state("CA") .street("1 Market St") .zipcode("94105") .build()) .customer(AttachExistingCustomer.builder() .customerId("cus_123") .build()) .productId("pdt_456") .quantity(1) .paymentLink(true) .returnUrl("https://yourdomain.com/return") .build(); SubscriptionCreateResponse subscription = client.subscriptions().create(subscriptionParams); System.out.println("Subscription ID: " + subscription.subscriptionId()); // Charge an on-demand subscription // product_price is in the lowest currency denomination (e.g., 2500 = $25.00 USD) SubscriptionChargeParams chargeParams = SubscriptionChargeParams.builder() .subscriptionId(subscription.subscriptionId()) .productPrice(2500) .build(); SubscriptionChargeResponse chargeResponse = client.subscriptions().charge(chargeParams); System.out.println("Payment ID: " + chargeResponse.paymentId()); ``` `productPrice` is expressed in the lowest currency denomination (e.g., cents for USD, paise for INR). To charge \$25.00, pass `2500`. Charging via `subscriptions().charge(...)` is intended for [on-demand subscriptions](/developer-resources/ondemand-subscriptions). Standard scheduled subscriptions are billed automatically based on the product's pricing schedule. ## Usage-Based Billing ### Configure Meters Create and manage meters for tracking usage: ```java theme={null} import com.dodopayments.api.models.meters.*; // Create API calls meter MeterCreateParams apiMeterParams = MeterCreateParams.builder() .name("API Requests") .eventName("api_request") .aggregation(MeterAggregation.builder() .type(MeterAggregation.Type.COUNT) .build()) .measurementUnit("calls") .build(); Meter apiMeter = client.meters().create(apiMeterParams); System.out.println("Meter created: " + apiMeter.id()); // List all meters client.meters().list() .autoPager() .forEach(m -> System.out.println("Meter: " + m.name() + " - " + m.aggregation())); ``` ### Ingest Usage Events Track custom events: ```java theme={null} import com.dodopayments.api.models.usageevents.EventInput; import com.dodopayments.api.models.usageevents.*; import java.time.OffsetDateTime; // Ingest single event UsageEventIngestParams singleEventParams = UsageEventIngestParams.builder() .addEvent(EventInput.builder() .eventId("api_call_" + System.currentTimeMillis()) .customerId("cus_abc123") .eventName("api_request") .timestamp(OffsetDateTime.now()) .putMetadata("endpoint", "/api/v1/users") .putMetadata("method", "GET") .putMetadata("tokens_used", "150") .build()) .build(); UsageEventIngestResponse response = client.usageEvents().ingest(singleEventParams); System.out.println("Processed: " + response.ingestedCount()); ``` ### Batch Ingest Events Ingest multiple events efficiently (max 1000 per request): ```java theme={null} UsageEventIngestParams.Builder batchBuilder = UsageEventIngestParams.builder(); for (int i = 0; i < 100; i++) { batchBuilder.addEvent(EventInput.builder() .eventId("batch_event_" + i + "_" + System.currentTimeMillis()) .customerId("cus_abc123") .eventName("video_transcode") .timestamp(OffsetDateTime.now().minusSeconds(i)) .putMetadata("video_id", "video_" + i) .putMetadata("duration_seconds", String.valueOf(120 + i)) .build()); } UsageEventIngestResponse batchResponse = client.usageEvents().ingest(batchBuilder.build()); System.out.println("Batch processed: " + batchResponse.ingestedCount() + " events"); ``` ## Error Handling Comprehensive error handling for different scenarios: ```java theme={null} import com.dodopayments.api.errors.*; try { Payment payment = client.payments().retrieve("pay_invalid"); } catch (NotFoundException e) { System.err.println("Payment not found: " + e.getMessage()); } catch (UnauthorizedException e) { System.err.println("Authentication failed: " + e.getMessage()); } catch (PermissionDeniedException e) { System.err.println("Permission denied: " + e.getMessage()); } catch (BadRequestException e) { System.err.println("Invalid request: " + e.getMessage()); } catch (UnprocessableEntityException e) { System.err.println("Validation error: " + e.getMessage()); } catch (RateLimitException e) { System.err.println("Rate limit exceeded: " + e.getMessage()); // SDK automatically retries with backoff } catch (InternalServerException e) { System.err.println("Server error: " + e.getMessage()); } catch (DodoPaymentsServiceException e) { System.err.println("API error: " + e.statusCode() + " - " + e.getMessage()); } ``` The SDK automatically retries requests on connection errors, 408, 409, 429, and 5xx errors with exponential backoff. ## Async Operations Use CompletableFuture for asynchronous operations: ```java theme={null} import java.util.concurrent.CompletableFuture; CompletableFuture future = client.async() .checkoutSessions() .create(params); // Handle response asynchronously future.thenAccept(response -> { System.out.println("Session created: " + response.sessionId()); }).exceptionally(ex -> { System.err.println("Error: " + ex.getMessage()); return null; }); ``` ## Spring Boot Integration ### Configuration Class ```java theme={null} import com.dodopayments.api.client.DodoPaymentsClient; import com.dodopayments.api.client.okhttp.DodoPaymentsOkHttpClient; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class DodoPaymentsConfig { @Value("${dodo.api.key}") private String apiKey; @Value("${dodo.environment:test}") private String environment; @Bean public DodoPaymentsClient dodoPayments() { return DodoPaymentsOkHttpClient.builder() .bearerToken(apiKey) .baseUrl(environment.equals("live") ? "https://live.dodopayments.com" : "https://test.dodopayments.com") .build(); } } ``` ### Service Layer ```java theme={null} import com.dodopayments.api.client.DodoPaymentsClient; import com.dodopayments.api.models.checkoutsessions.*; import org.springframework.stereotype.Service; @Service public class PaymentService { private final DodoPaymentsClient client; public PaymentService(DodoPaymentsClient client) { this.client = client; } public CheckoutSessionResponse createCheckout(List items) { CheckoutSessionRequest params = CheckoutSessionRequest.builder() .productCart(items) .returnUrl("https://yourdomain.com/return") .build(); return client.checkoutSessions().create(params); } } ``` ## Resources View source code and contribute Complete API documentation Get help and connect with developers Report bugs or request features ## Support Need help with the Java SDK? * **Discord**: Join our [community server](https://discord.gg/bYqAp4ayYh) for real-time support * **Email**: Contact us at [support@dodopayments.com](mailto:support@dodopayments.com) * **GitHub**: Open an issue on the [repository](https://github.com/dodopayments/dodopayments-java) ## Contributing We welcome contributions! Check the [contributing guidelines](https://github.com/dodopayments/dodopayments-java/blob/main/CONTRIBUTING.md) to get started. # Kotlin Source: https://docs.dodopayments.com/developer-resources/sdks/kotlin Integrate Dodo Payments into your Kotlin applications with modern coroutines and null-safety The Kotlin SDK provides convenient access to the Dodo Payments REST API from applications written in Kotlin. It features nullable values, Sequence, suspend functions, and other Kotlin-specific features for ergonomic use. ## Installation ### Gradle (Kotlin DSL) Add the dependency to your `build.gradle.kts`: ```kotlin build.gradle.kts theme={null} implementation("com.dodopayments.api:dodo-payments-kotlin:1.97.1") ``` ### Maven Add the dependency to your `pom.xml`: ```xml pom.xml theme={null} com.dodopayments.api dodo-payments-kotlin 1.97.1 ``` Always use the latest SDK version to access the newest Dodo Payments features. Check [Maven Central](https://central.sonatype.com/artifact/com.dodopayments.api/dodo-payments-kotlin) for the latest version. The SDK requires Java 8 or later and is compatible with both JVM and Android platforms. ## Quick Start Initialize the client and create a checkout session: ```kotlin theme={null} import com.dodopayments.api.client.DodoPaymentsClient import com.dodopayments.api.client.okhttp.DodoPaymentsOkHttpClient import com.dodopayments.api.models.checkoutsessions.CheckoutSessionCreateParams import com.dodopayments.api.models.checkoutsessions.CheckoutSessionRequest import com.dodopayments.api.models.checkoutsessions.ProductItemReq // Configure using environment variables (DODO_PAYMENTS_API_KEY, DODO_PAYMENTS_BASE_URL) // Or system properties (dodopayments.apiKey, dodopayments.baseUrl) val client: DodoPaymentsClient = DodoPaymentsOkHttpClient.fromEnv() val params: CheckoutSessionRequest = CheckoutSessionRequest.builder() .addProductCart(ProductItemReq.builder() .productId("product_id") .quantity(1) .build()) .build() val checkoutSessionResponse: CheckoutSessionResponse = client.checkoutSessions().create(params) println(checkoutSessionResponse.sessionId()) ``` Always store your API keys securely using environment variables or encrypted configuration. Never commit them to version control. ## Core Features Full support for Kotlin coroutines for asynchronous operations Leverage Kotlin's null safety for robust error handling Idiomatic Kotlin extensions for enhanced functionality Type-safe data classes with copy and destructuring support ## Configuration ### From Environment Variables Initialize from environment variables or system properties: ```kotlin theme={null} val client: DodoPaymentsClient = DodoPaymentsOkHttpClient.fromEnv() ``` ### Manual Configuration Configure manually with all options: ```kotlin theme={null} import java.time.Duration val client = DodoPaymentsOkHttpClient.builder() .bearerToken("your_api_key_here") .baseUrl("https://live.dodopayments.com") .maxRetries(3) .timeout(Duration.ofSeconds(30)) .build() ``` ### Test Mode Configure for the Test Mode environment: ```kotlin theme={null} val testClient = DodoPaymentsOkHttpClient.builder() .fromEnv() .testMode() .build() ``` ### Timeouts and Retries Configure globally or per-request: ```kotlin theme={null} import com.dodopayments.api.core.RequestOptions // Global configuration val client = DodoPaymentsOkHttpClient.builder() .fromEnv() .timeout(Duration.ofSeconds(45)) .maxRetries(3) .build() // Per-request timeout override val product = client.products().retrieve( "pdt_123", RequestOptions.builder() .timeout(Duration.ofSeconds(10)) .build() ) ``` ## Common Operations ### Create a Checkout Session Generate a checkout session: ```kotlin theme={null} val params = CheckoutSessionRequest.builder() .addProductCart(ProductItemReq.builder() .productId("pdt_123") .quantity(1) .build()) .returnUrl("https://yourdomain.com/return") .build() val session = client.checkoutSessions().create(params) println("Checkout URL: ${session.checkoutUrl()}") ``` ### Create a Product Create products with detailed configuration: ```kotlin theme={null} import com.dodopayments.api.models.products.Price import com.dodopayments.api.models.products.Product import com.dodopayments.api.models.products.ProductCreateParams import com.dodopayments.api.models.misc.Currency import com.dodopayments.api.models.misc.TaxCategory import com.dodopayments.api.models.misc.TimeInterval val createParams = ProductCreateParams.builder() .name("Premium Subscription") .description("Monthly subscription with all features") .price( Price.RecurringPrice.builder() .currency(Currency.USD) .price(2999) // $29.99 in cents .discount(0L) .purchasingPowerParity(false) .paymentFrequencyCount(1) .paymentFrequencyInterval(TimeInterval.MONTH) .subscriptionPeriodCount(1) .subscriptionPeriodInterval(TimeInterval.MONTH) .build() ) .taxCategory(TaxCategory.DIGITAL_PRODUCTS) .build() val product: Product = client.products().create(createParams) println("Created product ID: ${product.productId()}") ``` ### Activate License Key Activate license keys for customers: ```kotlin theme={null} import com.dodopayments.api.models.licenses.LicenseActivateParams import com.dodopayments.api.models.licenses.LicenseActivateResponse val activateParams = LicenseActivateParams.builder() .licenseKey("XXXX-XXXX-XXXX-XXXX") .name("user-laptop-01") .build() try { val activationResult: LicenseActivateResponse = client.licenses() .activate(activateParams) println("License activated successfully") println("Instance ID: ${activationResult.id()}") println("License key ID: ${activationResult.licenseKeyId()}") } catch (e: UnprocessableEntityException) { println("License activation failed: ${e.message}") } ``` ### Handle Subscriptions Create and manage recurring subscriptions: `POST /subscriptions` (the SDK's `subscriptions.create` method) is **deprecated**. It still works for existing integrations, but new integrations should create subscriptions through a [Checkout Session](/developer-resources/checkout-session). ```kotlin theme={null} import com.dodopayments.api.models.payments.AttachExistingCustomer import com.dodopayments.api.models.payments.BillingAddress import com.dodopayments.api.models.misc.CountryCode import com.dodopayments.api.models.subscriptions.SubscriptionChargeParams import com.dodopayments.api.models.subscriptions.SubscriptionCreateParams // Create a subscription val subscriptionParams = SubscriptionCreateParams.builder() .billing(BillingAddress.builder() .city("San Francisco") .country(CountryCode.US) .state("CA") .street("1 Market St") .zipcode("94105") .build()) .customer(AttachExistingCustomer.builder() .customerId("cus_123") .build()) .productId("pdt_456") .quantity(1) .build() val subscription = client.subscriptions().create(subscriptionParams) println("Subscription ID: ${subscription.subscriptionId()}") // Charge an on-demand subscription // productPrice is in the lowest currency denomination (e.g., 2500 = $25.00 USD) val chargeParams = SubscriptionChargeParams.builder() .subscriptionId(subscription.subscriptionId()) .productPrice(2500) .build() val chargeResponse = client.subscriptions().charge(chargeParams) println("Payment ID: ${chargeResponse.paymentId()}") ``` `billing` requires at minimum a two-letter ISO `country` code. Use `AttachExistingCustomer` to attach an existing customer, or `NewCustomer` to create one. `productPrice` is expressed in the lowest currency denomination. ## Usage-Based Billing ### Record Usage Events Track usage for meters: ```kotlin theme={null} import com.dodopayments.api.models.usageevents.EventInput import com.dodopayments.api.models.usageevents.UsageEventIngestParams val usageParams = UsageEventIngestParams.builder() .addEvent(EventInput.builder() .customerId("cust_456") .eventId("event_123") .eventName("api_call") .build()) .build() client.usageEvents().ingest(usageParams) println("Usage event recorded") ``` ## Async Operations ### Async Client Use the async client for coroutine-based operations: ```kotlin theme={null} import com.dodopayments.api.client.DodoPaymentsClientAsync import com.dodopayments.api.client.okhttp.DodoPaymentsOkHttpClientAsync import kotlinx.coroutines.runBlocking val asyncClient: DodoPaymentsClientAsync = DodoPaymentsOkHttpClientAsync.fromEnv() runBlocking { val customer = asyncClient.customers().retrieve("cust_123") println("Customer email: ${customer.email()}") } ``` ## Error Handling Handle errors with Kotlin's exception handling: ```kotlin theme={null} import com.dodopayments.api.errors.* try { val payment = client.payments().create(params) println("Success: ${payment.paymentId()}") } catch (e: AuthenticationException) { println("Authentication failed: ${e.message}") } catch (e: InvalidRequestException) { println("Invalid request: ${e.message}") e.parameter?.let { println("Parameter: $it") } } catch (e: RateLimitException) { println("Rate limit exceeded, retry after: ${e.retryAfter}") } catch (e: DodoPaymentsServiceException) { println("API error: ${e.statusCode()} - ${e.message}") } ``` ### Functional Error Handling Use `Result` for functional error handling: ```kotlin theme={null} fun safeCreatePayment(client: DodoPaymentsClient): Result = runCatching { client.payments().create(params) } // Usage safeCreatePayment(client) .onSuccess { payment -> println("Created: ${payment.paymentId()}") } .onFailure { error -> println("Error: ${error.message}") } ``` Use Kotlin's `runCatching` for a more functional approach to error handling with Result types. ## Android Integration Use with Android applications: ```kotlin theme={null} import android.app.Application import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.dodopayments.api.client.DodoPaymentsClient import kotlinx.coroutines.launch class PaymentViewModel(application: Application) : ViewModel() { private val client = DodoPaymentsOkHttpClient.builder() .bearerToken(BuildConfig.DODO_API_KEY) .build() fun createCheckout(productId: String) { viewModelScope.launch { try { val session = client.async().checkoutSessions().create(params) // Open checkout URL in browser or WebView openUrl(session.checkoutUrl()) } catch (e: Exception) { handleError(e) } } } } ``` ## Response Validation Enable response validation: ```kotlin theme={null} import com.dodopayments.api.core.RequestOptions // Per-request validation val product = client.products().retrieve( "pdt_123", RequestOptions.builder() .responseValidation(true) .build() ) // Or validate explicitly val validatedProduct = product.validate() ``` ## Advanced Features ### Proxy Configuration Configure proxy settings: ```kotlin theme={null} import java.net.InetSocketAddress import java.net.Proxy val client = DodoPaymentsOkHttpClient.builder() .fromEnv() .proxy( Proxy( Proxy.Type.HTTP, InetSocketAddress("proxy.example.com", 8080) ) ) .build() ``` ### Temporary Configuration Modify client configuration temporarily: ```kotlin theme={null} val customClient = client.withOptions { it.baseUrl("https://example.com") it.maxRetries(5) } ``` ## Ktor Integration Integrate with Ktor server applications: ```kotlin theme={null} import io.ktor.server.application.* import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* fun Application.configureRouting() { val client = DodoPaymentsOkHttpClient.builder() .bearerToken(environment.config.property("dodo.apiKey").getString()) .build() routing { post("/create-checkout") { try { val request = call.receive() val session = client.checkoutSessions().create(params) call.respond(mapOf("checkout_url" to session.checkoutUrl())) } catch (e: DodoPaymentsServiceException) { call.respond(HttpStatusCode.BadRequest, mapOf("error" to e.message)) } } } } ``` ## Resources View source code and contribute Complete API documentation Get help and connect with developers Report bugs or request features ## Support Need help with the Kotlin SDK? * **Discord**: Join our [community server](https://discord.gg/bYqAp4ayYh) for real-time support * **Email**: Contact us at [support@dodopayments.com](mailto:support@dodopayments.com) * **GitHub**: Open an issue on the [repository](https://github.com/dodopayments/dodopayments-kotlin) ## Contributing We welcome contributions! Check the [contributing guidelines](https://github.com/dodopayments/dodopayments-kotlin/blob/main/CONTRIBUTING.md) to get started. # PHP Source: https://docs.dodopayments.com/developer-resources/sdks/php Integrate Dodo Payments into your PHP applications with a modern, PSR-4 compliant SDK The PHP SDK provides a robust and flexible way to integrate Dodo Payments into your PHP applications. Built following modern PHP standards with PSR-4 autoloading, it offers extensive test coverage and detailed documentation. ## Installation Install the SDK using Composer: ```bash theme={null} composer require "dodopayments/client 6.7.1" ``` The SDK requires PHP 8.1.0 or higher and Composer for dependency management. ## Quick Start Initialize the client and create a checkout session: ```php theme={null} checkoutSessions->create( productCart: [["productID" => "product_id", "quantity" => 1]] ); var_dump($checkoutSessionResponse->session_id); ``` Store your API keys securely using environment variables. Never expose them in your codebase or commit them to version control. ## Core Features Follows PHP Standards Recommendations for modern PHP development Built for PHP 8.1+ with type declarations and strict types Comprehensive test coverage for reliability and stability Clear exception types for different error scenarios ## Value Objects The SDK uses named parameters to specify optional arguments. You can initialize value objects using the static `with` constructor: ```php theme={null} withCustomerID("customer_id"); ``` ## Configuration ### Retry Configuration Certain errors are automatically retried 2 times by default with a short exponential backoff. The following errors trigger automatic retries: * Connection errors (network connectivity problems) * 408 Request Timeout * 409 Conflict * 429 Rate Limit * 500+ Internal errors * Timeouts Configure retry behavior globally or per-request: ```php theme={null} 0]); // Or, configure per-request $result = $client->checkoutSessions->create( productCart: [["productID" => "product_id", "quantity" => 1]], requestOptions: ['maxRetries' => 5], ); ``` ## Common Operations ### Create a Checkout Session Generate a checkout session: ```php theme={null} $session = $client->checkoutSessions->create( productCart: [ ["productID" => "pdt_123", "quantity" => 1] ], returnURL: "https://yourdomain.com/return" ); header('Location: ' . $session->checkout_url); ``` ### Manage Customers Create and retrieve customer information: ```php theme={null} // Create a customer $customer = $client->customers->create( email: "customer@example.com", name: "John Doe", metadata: [ "user_id" => "12345" ] ); // Retrieve customer $customer = $client->customers->retrieve("cus_123"); echo "Customer: {$customer->name} ({$customer->email})"; ``` ### Handle Subscriptions Create and manage recurring subscriptions: `POST /subscriptions` (the SDK's `subscriptions.create` method) is **deprecated**. It still works for existing integrations, but new integrations should create subscriptions through a [Checkout Session](/developer-resources/checkout-session). ```php theme={null} use Dodopayments\Customers\AttachExistingCustomer; use Dodopayments\Payments\BillingAddress; // Create a subscription $subscription = $client->subscriptions->create( billing: BillingAddress::with( country: 'US', city: 'San Francisco', state: 'CA', street: '1 Market St', zipcode: '94105', ), customer: AttachExistingCustomer::with(customerID: 'cus_123'), productID: 'pdt_456', quantity: 1, ); // Charge an on-demand subscription // productPrice is in the lowest currency denomination (e.g., 2500 = $25.00 USD) $charge = $client->subscriptions->charge( $subscription->subscription_id, productPrice: 2500, ); ``` `billing` requires at minimum the two-letter ISO `country` code. Pass `AttachExistingCustomer::with(customerID: '...')` to attach an existing customer, or `NewCustomer::with(email: '...', name: '...')` to create one. `productPrice` is in the lowest currency denomination. ## Pagination Work with paginated list responses: ```php theme={null} $page = $client->payments->list(); var_dump($page); // Fetch items from the current page foreach ($page->getItems() as $item) { var_dump($item->brand_id); } // Auto-paginate: fetch items from all pages foreach ($page->pagingEachItem() as $item) { var_dump($item->brand_id); } ``` ## Error Handling When the library cannot connect to the API or receives a non-success status code (4xx or 5xx), a subclass of `APIException` is thrown: ```php theme={null} checkoutSessions->create( productCart: [["productID" => "product_id", "quantity" => 1]] ); } catch (APIConnectionException $e) { echo "The server could not be reached", PHP_EOL; var_dump($e->getPrevious()); } catch (RateLimitException $_) { echo "A 429 status code was received; we should back off a bit.", PHP_EOL; } catch (APIStatusException $e) { echo "Another non-200-range status code was received", PHP_EOL; echo $e->getMessage(); } ``` ### Error Types | Cause | Error Type | | ---------------- | ------------------------------ | | HTTP 400 | `BadRequestException` | | HTTP 401 | `AuthenticationException` | | HTTP 403 | `PermissionDeniedException` | | HTTP 404 | `NotFoundException` | | HTTP 409 | `ConflictException` | | HTTP 422 | `UnprocessableEntityException` | | HTTP 429 | `RateLimitException` | | HTTP >= 500 | `InternalServerException` | | Other HTTP error | `APIStatusException` | | Timeout | `APITimeoutException` | | Network error | `APIConnectionException` | Always wrap API calls in try-catch blocks to handle potential errors gracefully and provide meaningful feedback to users. ## Advanced Usage ### Undocumented Endpoints Make requests to undocumented endpoints: ```php theme={null} request( method: "post", path: '/undocumented/endpoint', query: ['dog' => 'woof'], headers: ['useful-header' => 'interesting-value'], body: ['hello' => 'world'] ); ``` ### Undocumented Parameters Send undocumented parameters to any endpoint or read undocumented response properties: ```php theme={null} checkoutSessions->create( productCart: [["productID" => "product_id", "quantity" => 1]], requestOptions: [ 'extraQueryParams' => ["my_query_parameter" => "value"], 'extraBodyParams' => ["my_body_parameter" => "value"], 'extraHeaders' => ["my-header" => "value"], ], ); ``` The `extra*` parameters with the same name override documented parameters. ## Framework Integration ### Laravel Create a service for Laravel applications: ```php theme={null} client = new Client( bearerToken: config('services.dodo.api_key') ); } public function createCheckout(array $items) { return $this->client->checkoutSessions->create( productCart: $items, returnURL: route('checkout.return') ); } } ``` Add configuration in `config/services.php`: ```php theme={null} 'dodo' => [ 'api_key' => env('DODO_API_KEY'), 'environment' => env('DODO_ENVIRONMENT', 'test_mode') ], ``` ### Symfony Create a service in Symfony: ```php theme={null} client = new Client(bearerToken: $apiKey); } public function createPayment(string $productId, string $customerId, array $billing): object { return $this->client->payments->create( billing: $billing, customer: ['customerID' => $customerId], productCart: [['productID' => $productId, 'quantity' => 1]], ); } } ``` Register in `config/services.yaml`: ```yaml theme={null} services: App\Service\DodoPaymentService: arguments: $apiKey: "%env(DODO_API_KEY)%" ``` ## Resources View source code and contribute Complete API documentation Get help and connect with developers Report bugs or request features ## Support Need help with the PHP SDK? * **Discord**: Join our [community server](https://discord.gg/bYqAp4ayYh) for real-time support * **Email**: Contact us at [support@dodopayments.com](mailto:support@dodopayments.com) * **GitHub**: Open an issue on the [repository](https://github.com/dodopayments/dodopayments-php) ## Contributing We welcome contributions! Check the [contributing guidelines](https://github.com/dodopayments/dodopayments-php/blob/main/CONTRIBUTING.md) to get started. # Python Source: https://docs.dodopayments.com/developer-resources/sdks/python Integrate Dodo Payments into your Python applications with a Pythonic interface and modern async/await support The Python SDK offers a Pythonic interface to the Dodo Payments API, providing both synchronous and asynchronous clients with type definitions for requests and responses. It supports Python 3.9+ and includes comprehensive test coverage. ## Installation Install the SDK using pip: ```bash theme={null} pip install dodopayments ``` For enhanced async performance with aiohttp: ```bash theme={null} pip install dodopayments[aiohttp] ``` The SDK requires Python 3.9 or higher. We recommend using the latest stable version of Python for the best experience and security updates. ## Quick Start ### Synchronous Client ```python theme={null} import os from dodopayments import DodoPayments client = DodoPayments( bearer_token=os.environ.get("DODO_PAYMENTS_API_KEY"), # This is the default and can be omitted environment="test_mode", # defaults to "live_mode" ) checkout_session_response = client.checkout_sessions.create( product_cart=[ { "product_id": "product_id", "quantity": 1 } ], ) print(checkout_session_response.session_id) ``` ### Asynchronous Client ```python theme={null} import os import asyncio from dodopayments import AsyncDodoPayments client = AsyncDodoPayments( bearer_token=os.environ.get("DODO_PAYMENTS_API_KEY"), environment="test_mode", ) async def main() -> None: checkout_session_response = await client.checkout_sessions.create( product_cart=[ { "product_id": "product_id", "quantity": 1, } ], ) print(checkout_session_response.session_id) asyncio.run(main()) ``` Always store your API keys securely using environment variables. Never commit them to version control. ## Core Features Clean, idiomatic Python code that follows PEP 8 guidelines and Python conventions Full support for asynchronous operations with asyncio and optional aiohttp integration Complete type hints for better IDE support and type checking with mypy Automatic pagination for list responses with simple iteration ## Configuration ### Environment Variables Configure using environment variables: ```bash .env theme={null} DODO_PAYMENTS_API_KEY=your_api_key_here ``` ### Timeouts Configure request timeouts globally or per-request: ```python theme={null} import httpx from dodopayments import DodoPayments # Configure default for all requests (default is 1 minute) client = DodoPayments( timeout=20.0, # 20 seconds ) # More granular control client = DodoPayments( timeout=httpx.Timeout(60.0, read=5.0, write=10.0, connect=2.0), ) # Override per-request client.with_options(timeout=5.0).checkout_sessions.create( product_cart=[ { "product_id": "product_id", "quantity": 1, } ], ) ``` ### Retries Configure automatic retry behavior: ```python theme={null} from dodopayments import DodoPayments # Configure default for all requests (default is 2) client = DodoPayments( max_retries=0, # disable retries ) # Override per-request client.with_options(max_retries=5).checkout_sessions.create( product_cart=[ { "product_id": "product_id", "quantity": 1, } ], ) ``` ## Common Operations ### Create a Checkout Session Generate a checkout session: ```python theme={null} session = client.checkout_sessions.create( product_cart=[ { "product_id": "pdt_123", "quantity": 1 } ], return_url="https://yourdomain.com/return" ) print(f"Checkout URL: {session.checkout_url}") ``` ### Manage Customers Create and retrieve customer information: ```python theme={null} # Create a customer customer = client.customers.create( email="customer@example.com", name="John Doe", metadata={ "user_id": "12345" } ) # Retrieve customer customer = client.customers.retrieve("cus_123") print(f"Customer: {customer.name} ({customer.email})") ``` ### Handle Subscriptions Create and manage recurring subscriptions: `POST /subscriptions` (the SDK's `subscriptions.create` method) is **deprecated**. It still works for existing integrations, but new integrations should create subscriptions through a [Checkout Session](/developer-resources/checkout-session). ```python theme={null} # Create a subscription subscription = client.subscriptions.create( billing={ "country": "US", "city": "San Francisco", "state": "CA", "street": "1 Market St", "zipcode": "94105", }, customer={"customer_id": "cus_123"}, # or {"email": "...", "name": "..."} for a new customer product_id="pdt_456", quantity=1, ) # Charge an on-demand subscription # product_price is in the lowest currency denomination (e.g., 2500 = $25.00 USD) charge_response = client.subscriptions.charge( subscription_id=subscription.subscription_id, product_price=2500, ) # Retrieve usage history (for metered subscriptions) usage_history = client.subscriptions.retrieve_usage_history( subscription_id=subscription.subscription_id, start_date="2024-01-01T00:00:00Z", ) ``` `billing` requires at minimum the two-letter ISO country code. `customer` accepts either `{"customer_id": ...}` to attach an existing customer or `{"email": ..., "name": ...}` to create a new one. `product_price` is in the lowest currency denomination. ## Usage-Based Billing ### Ingest Usage Events Track custom events for usage-based billing: ```python theme={null} response = client.usage_events.ingest( events=[ { "event_id": "api_call_12345", "customer_id": "cus_abc123", "event_name": "api_request", "timestamp": "2024-01-15T10:30:00Z", "metadata": { "endpoint": "/api/v1/users", "method": "GET", "tokens_used": "150" } } ] ) ``` ### List and Retrieve Events ```python theme={null} # Get a specific event event = client.usage_events.retrieve("api_call_12345") # List events with filtering events = client.usage_events.list( customer_id="cus_abc123", event_name="api_request", page_size=20 ) for event in events.items: print(f"Event: {event.event_id} at {event.timestamp}") ``` ## Pagination ### Auto-Pagination Iterate through all items automatically: ```python theme={null} from dodopayments import DodoPayments client = DodoPayments() all_payments = [] # Automatically fetches more pages as needed for payment in client.payments.list(): all_payments.append(payment) print(all_payments) ``` ### Async Pagination ```python theme={null} import asyncio from dodopayments import AsyncDodoPayments client = AsyncDodoPayments() async def main() -> None: all_payments = [] # Iterate through items across all pages async for payment in client.payments.list(): all_payments.append(payment) print(all_payments) asyncio.run(main()) ``` ### Manual Pagination For more control over pagination: ```python theme={null} # Access items from current page first_page = client.payments.list() for payment in first_page.items: print(payment.brand_id) # Check for more pages if first_page.has_next_page(): next_page = first_page.get_next_page() print(f"Fetched {len(next_page.items)} more items") ``` ## HTTP Client Configuration Customize the underlying `httpx` client: ```python theme={null} import httpx from dodopayments import DodoPayments, DefaultHttpxClient client = DodoPayments( base_url="http://my.test.server.example.com:8083", http_client=DefaultHttpxClient( proxy="http://my.test.proxy.example.com", transport=httpx.HTTPTransport(local_address="0.0.0.0"), ), ) ``` ## Async with aiohttp Use aiohttp for enhanced async performance: ```python theme={null} import asyncio from dodopayments import DefaultAioHttpClient from dodopayments import AsyncDodoPayments async def main() -> None: async with AsyncDodoPayments( bearer_token="My Bearer Token", http_client=DefaultAioHttpClient(), ) as client: checkout_session_response = await client.checkout_sessions.create( product_cart=[ { "product_id": "product_id", "quantity": 1, } ], ) print(checkout_session_response.session_id) asyncio.run(main()) ``` ## Logging Enable logging by setting the environment variable: ```bash theme={null} export DODO_PAYMENTS_LOG=info ``` Or for debug-level logging: ```bash theme={null} export DODO_PAYMENTS_LOG=debug ``` ## Framework Integration ### FastAPI ```python theme={null} from fastapi import FastAPI, HTTPException from dodopayments import AsyncDodoPayments from pydantic import BaseModel import os app = FastAPI() dodo = AsyncDodoPayments(bearer_token=os.getenv("DODO_API_KEY")) class CheckoutRequest(BaseModel): product_id: str quantity: int @app.post("/create-checkout") async def create_checkout(request: CheckoutRequest): try: session = await dodo.checkout_sessions.create( product_cart=[{ "product_id": request.product_id, "quantity": request.quantity }], return_url="https://yourdomain.com/return" ) return {"checkout_url": session.checkout_url} except Exception as e: raise HTTPException(status_code=400, detail=str(e)) ``` ### Django ```python theme={null} from django.http import JsonResponse from django.views.decorators.http import require_POST from django.views.decorators.csrf import csrf_exempt from dodopayments import DodoPayments import os import json client = DodoPayments(bearer_token=os.getenv("DODO_PAYMENTS_API_KEY")) @csrf_exempt @require_POST def create_checkout(request): try: data = json.loads(request.body) session = client.checkout_sessions.create( product_cart=[{ "product_id": data.get("product_id"), "quantity": data.get("quantity", 1) }], return_url="https://yourdomain.com/return" ) return JsonResponse({ "status": "success", "checkout_url": session.checkout_url, "session_id": session.session_id }) except Exception as e: return JsonResponse({ "status": "error", "message": str(e) }, status=400) ``` ## Resources View source code and contribute Complete API documentation Get help and connect with developers Report bugs or request features ## Support Need help with the Python SDK? * **Discord**: Join our [community server](https://discord.gg/bYqAp4ayYh) for real-time support * **Email**: Contact us at [support@dodopayments.com](mailto:support@dodopayments.com) * **GitHub**: Open an issue on the [repository](https://github.com/dodopayments/dodopayments-python) ## Contributing We welcome contributions! Check the [contributing guidelines](https://github.com/dodopayments/dodopayments-python/blob/main/CONTRIBUTING.md) to get started. # React Native Source: https://docs.dodopayments.com/developer-resources/sdks/react-native Open Dodo Payments' hosted checkout from a React Native app in a system browser tab and get a typed result back in one call. This is the official Dodo Payments React Native checkout SDK, `@dodopayments/react-native-checkout`. It opens Dodo's hosted checkout in a native browser view and returns a typed result. Note: an older, unrelated package named `dodopayments-react-native-sdk` (unscoped) exists with a completely different API. This page documents the current official scoped package only. Create the checkout\_url this SDK opens, from your backend. See how this fits into the full mobile payment flow. The React Native SDK is a thin Turbo Module wrapper over the same native Swift and Kotlin cores. It opens `SFSafariViewController` on iOS and a Chrome Custom Tab on Android, holds no API key, and never calls the Dodo API directly. All checkout logic runs in the browser; the SDK simply manages the view lifecycle and captures the return URL. This SDK requires **New Architecture only**, React Native 0.76+, iOS 16+, and Android `minSdk` 24. ## Installation The package is autolinked and pulls `com.dodopayments.api:checkout-android` from Maven. ```sh theme={null} npm i @dodopayments/react-native-checkout ``` No additional setup needed; the native dependency is resolved automatically. ```sh theme={null} npm i @dodopayments/react-native-checkout cd ios && pod install ``` The Swift core is bundled in the package and installed via CocoaPods. Development builds only (not Expo Go). ```sh theme={null} npm i @dodopayments/react-native-checkout ``` Then configure your `app.json` (see Register a Callback URL Scheme below). Your app must register a URL scheme to receive the return URL from the checkout. In `android/app/build.gradle`: ```kotlin android/app/build.gradle theme={null} android { defaultConfig { manifestPlaceholders["dodoCallbackScheme"] = "myapp" } } ``` Replace `"myapp"` with your app's scheme. In `ios/YourApp/Info.plist`: ```xml Info.plist theme={null} CFBundleURLTypes CFBundleURLName myapp CFBundleURLSchemes myapp ``` You can also add this via Xcode's **Info → URL Types** UI. The package's own config plugin registers the scheme for both platforms at `prebuild`. Pass it a `scheme` option in `app.json`: ```json app.json theme={null} { "expo": { "scheme": "myapp", "plugins": [ [ "@dodopayments/react-native-checkout", { "scheme": "myappcheckout" } ] ] } } ``` `scheme` must match what you pass as `returnUrl` (e.g. `myappcheckout://return`), and must differ from `expo.scheme`. Expo already registers that one on `MainActivity`, and reusing it can route the checkout return to the wrong screen. Rebuild the native project after editing `app.json`: ```sh theme={null} npx expo prebuild --clean ``` This works with development builds only, not Expo Go. If `android/app/build.gradle` is hand-maintained and has no standard `defaultConfig { }` block for the plugin to find, add the placeholder yourself instead. See the Android tab above. ## Usage ```typescript theme={null} import { Linking } from 'react-native'; import { DodoCheckout } from '@dodopayments/react-native-checkout'; // Required for iOS's return-URL handling. Linking.addEventListener('url', ({ url }) => DodoCheckout.handleOpenURL(url)); const result = await DodoCheckout.start({ checkoutUrl, // from your backend's checkout session returnUrl: 'myapp://checkout/return', // scheme must be registered (see Installation) onEvent: (e) => console.log(e.type), // logging only }); switch (result.status) { case 'succeeded': showSuccess(result.paymentId); break; case 'failed': showFailure(); break; case 'cancelled': dismiss(); break; case 'pending': showPending(); break; case 'expired': showExpired(); break; } ``` ## Forwarding the Return URL The `Linking` listener is required for iOS's return-URL handling. On Android, `handleOpenURL` is a no-op that resolves `false` because the Android core handles its redirect natively. It's safe to register the listener unconditionally on both platforms. ```typescript theme={null} import { Linking } from 'react-native'; import { DodoCheckout } from '@dodopayments/react-native-checkout'; Linking.addEventListener('url', ({ url }) => { DodoCheckout.handleOpenURL(url); }); ``` ## What the Result Means `result.status` is a UI hint, not proof of payment. Confirm every payment from your backend, via the `payment.succeeded` / `subscription.active` webhook. One of `succeeded`, `failed`, `cancelled`, `pending`, `expired`. Set when the return URL included one. Display it in the UI, don't use it to grant access. See Verify the Payment below. Set for subscription checkouts. Set when the checkout includes license key products. Set when the checkout captures an email. Every query parameter from the return URL, verbatim. ## Verify the Payment Dodo Payments calls your backend when a payment succeeds or a subscription activates. Look up `paymentId` with your secret key to check its status directly. Grant access after one of these confirms the payment, never from `result.status` alone. ## Appearance Customization Customize the checkout browser's toolbar, buttons, and color scheme via `customization` on `start(...)`. Options are grouped by platform because Android's Custom Tab and iOS's `SFSafariViewController` expose different native controls. All fields are optional; omitting `customization` uses each platform's default appearance. Toolbar background color. Navigation bar color. Divider color above the navigation bar. `default` shows the system "X" icon; `back` draws a back arrow instead. Which side of the toolbar the close button appears on. Shows the toolbar's share icon. Shows the page title under the URL in the toolbar. Lets the toolbar auto-hide as the page scrolls. Shows "Bookmark this page" in the overflow menu. Shows "Download page" in the overflow menu. Forces light or dark appearance regardless of the device's system setting. Label or icon for the dismiss button. `pageSheet` presents as a card with swipe-to-dismiss; `fullScreen` covers the whole screen. Lets the toolbar collapse on scroll. Only visible when `presentationStyle` is `fullScreen` — `pageSheet` keeps the bars pinned regardless of this setting. Forces light or dark appearance regardless of the device's system setting. ```typescript theme={null} const result = await DodoCheckout.start({ checkoutUrl, returnUrl: 'myapp://checkout/return', customization: { android: { toolbarColor: '#6366F1', closeButtonStyle: 'back' }, ios: { presentationStyle: 'fullScreen', colorScheme: 'dark' }, }, }); ``` ## Errors `start` rejects with a `CheckoutError` only for misuse or a platform failure. A cancelled or declined payment is always a result, never an exception. * `INVALID_CHECKOUT_URL`: not a `checkout.dodopayments.com` session URL. * `INVALID_RETURN_URL`: not a valid absolute URL. * `ALREADY_IN_PROGRESS`: a checkout is already running. * `PLATFORM_ERROR`: unexpected platform failure. ## Abandoned Sessions If the app or the JS bundle is killed mid-checkout, the promise is lost but the native layer keeps the session. Recover it on next mount and reconcile it with your backend. ```typescript theme={null} import { DodoCheckout } from '@dodopayments/react-native-checkout'; const abandoned = await DodoCheckout.getAbandonedSession(); if (abandoned) { // reconcile abandoned.sessionId with your backend, then: await DodoCheckout.clearAbandonedSession(); } ``` ## Related The same contract for Android, iOS, and Flutter. A complete Expo example with checkout integration. # Ruby Source: https://docs.dodopayments.com/developer-resources/sdks/ruby Integrate Dodo Payments into your Ruby applications with an elegant, Ruby-native SDK The Ruby SDK provides a simple and intuitive way to integrate Dodo Payments into your Ruby applications. It follows Ruby conventions and best practices, offering comprehensive error handling, pagination, and middleware support. ## Installation Add the gem to your Gemfile: ```ruby Gemfile theme={null} gem "dodopayments", "~> 2.9" ``` Always use the latest SDK version to access the newest Dodo Payments features. Run `bundle update dodopayments` regularly to stay up to date. Then run: ```bash theme={null} bundle install ``` The SDK supports Ruby 3.2.0 and later versions, with comprehensive types, error handling, and retry mechanisms. ## Quick Start Initialize the client and create a checkout session: ```ruby theme={null} require "bundler/setup" require "dodopayments" dodo_payments = Dodopayments::Client.new( bearer_token: ENV["DODO_PAYMENTS_API_KEY"], # This is the default and can be omitted environment: "test_mode" # defaults to "live_mode" ) checkout_session_response = dodo_payments.checkout_sessions.create( product_cart: [{product_id: "product_id", quantity: 1}] ) puts(checkout_session_response.session_id) ``` Store your API keys securely using environment variables. Never commit them to version control or expose them in your code. ## Core Features Follows Ruby naming conventions and idiomatic patterns Clean, readable API that feels natural to Ruby developers Built-in auto-paging iterators for list responses Optional Sorbet types for enhanced type safety ## Configuration ### Timeout Configuration Configure request timeouts: ```ruby theme={null} # Configure default for all requests (default is 60 seconds) dodo_payments = Dodopayments::Client.new( timeout: nil # disable timeout ) # Or, configure per-request dodo_payments.checkout_sessions.create( product_cart: [{product_id: "product_id", quantity: 1}], request_options: {timeout: 5} ) ``` ### Retry Configuration Configure automatic retry behavior: ```ruby theme={null} # Configure default for all requests (default is 2) dodo_payments = Dodopayments::Client.new( max_retries: 0 # disable retries ) # Or, configure per-request dodo_payments.checkout_sessions.create( product_cart: [{product_id: "product_id", quantity: 1}], request_options: {max_retries: 5} ) ``` ## Common Operations ### Create a Checkout Session Generate a checkout session: ```ruby theme={null} session = dodo_payments.checkout_sessions.create( product_cart: [ { product_id: "pdt_123", quantity: 1 } ], return_url: "https://yourdomain.com/return" ) # Redirect to checkout redirect_to session.checkout_url ``` ### Manage Customers Create and retrieve customer information: ```ruby theme={null} # Create a customer customer = dodo_payments.customers.create( email: "customer@example.com", name: "John Doe", metadata: { user_id: "12345" } ) # Retrieve customer customer = dodo_payments.customers.retrieve("cus_123") puts "Customer: #{customer.name} (#{customer.email})" ``` ### Handle Subscriptions Create and manage recurring subscriptions: `POST /subscriptions` (the SDK's `subscriptions.create` method) is **deprecated**. It still works for existing integrations, but new integrations should create subscriptions through a [Checkout Session](/developer-resources/checkout-session). ```ruby theme={null} # Create a subscription subscription = dodo_payments.subscriptions.create( billing: { country: "US", city: "San Francisco", state: "CA", street: "1 Market St", zipcode: "94105" }, customer: { customer_id: "cus_123" }, # or { email: "...", name: "..." } for a new customer product_id: "pdt_456", quantity: 1 ) # Charge an on-demand subscription # product_price is in the lowest currency denomination (e.g., 2500 = $25.00 USD) charge = dodo_payments.subscriptions.charge( subscription.subscription_id, product_price: 2500 ) # Update subscription metadata updated = dodo_payments.subscriptions.update( subscription.subscription_id, metadata: { plan_type: "premium" } ) ``` `billing` requires at minimum the two-letter ISO `country` code. `customer` accepts either `{ customer_id: "..." }` to attach an existing customer or `{ email: "...", name: "..." }` to create a new one. `product_price` is in the lowest currency denomination. ## Pagination ### Auto-Pagination Automatically iterate through all pages: ```ruby theme={null} page = dodo_payments.payments.list # Fetch single item from page payment = page.items[0] puts(payment.brand_id) # Automatically fetches more pages as needed page.auto_paging_each do |payment| puts(payment.brand_id) end ``` ### Manual Pagination For more control over pagination: ```ruby theme={null} page = dodo_payments.payments.list if page.next_page? new_page = page.next_page puts(new_page.items[0].brand_id) end ``` ## Error Handling Handle various Dodo Payments API errors: ```ruby theme={null} begin checkout_session = dodo_payments.checkout_sessions.create( product_cart: [{product_id: "product_id", quantity: 1}] ) rescue Dodopayments::Errors::APIConnectionError => e puts("The server could not be reached") puts(e.cause) # an underlying Exception, likely raised within `net/http` rescue Dodopayments::Errors::RateLimitError => e puts("A 429 status code was received; we should back off a bit.") rescue Dodopayments::Errors::APIStatusError => e puts("Another non-200-range status code was received") puts(e.status) end ``` Implement retry logic with exponential backoff for rate limit errors to ensure your application handles high-volume scenarios gracefully. ## Type Safety with Sorbet Use Sorbet for type-safe request parameters: ```ruby theme={null} # Type-safe using Sorbet RBI definitions dodo_payments.checkout_sessions.create( product_cart: [ Dodopayments::ProductItemReq.new( product_id: "product_id", quantity: 1 ) ] ) # Hashes work, but are not typesafe dodo_payments.checkout_sessions.create( product_cart: [{product_id: "product_id", quantity: 1}] ) # You can also splat a full Params class params = Dodopayments::CheckoutSessionCreateParams.new( product_cart: [ Dodopayments::ProductItemReq.new( product_id: "product_id", quantity: 1 ) ] ) dodo_payments.checkout_sessions.create(**params) ``` ## Advanced Usage ### Undocumented Endpoints Make requests to undocumented endpoints: ```ruby theme={null} response = dodo_payments.request( method: :post, path: '/undocumented/endpoint', query: {"dog": "woof"}, headers: {"useful-header": "interesting-value"}, body: {"hello": "world"} ) ``` ### Undocumented Parameters Send undocumented parameters: ```ruby theme={null} checkout_session_response = dodo_payments.checkout_sessions.create( product_cart: [{product_id: "product_id", quantity: 1}], request_options: { extra_query: {my_query_parameter: value}, extra_body: {my_body_parameter: value}, extra_headers: {"my-header": value} } ) # Access undocumented response properties puts(checkout_session_response[:my_undocumented_property]) ``` ## Rails Integration ### Create an Initializer Create `config/initializers/dodo_payments.rb`: ```ruby theme={null} require "dodopayments" DODO_CLIENT = Dodopayments::Client.new( bearer_token: Rails.application.credentials.dodo_api_key, environment: Rails.env.production? ? "live_mode" : "test_mode" ) ``` ### Service Object Pattern Create a payment service: ```ruby theme={null} # app/services/payment_service.rb class PaymentService def initialize @client = DODO_CLIENT end def create_checkout(items) @client.checkout_sessions.create( product_cart: items, return_url: Rails.application.routes.url_helpers.checkout_return_url ) end def process_payment(product_id:, customer_id:, billing:) @client.payments.create( billing: billing, customer: { customer_id: customer_id }, product_cart: [{ product_id: product_id, quantity: 1 }] ) end end ``` ### Controller Integration Use in your Rails controllers: ```ruby theme={null} # app/controllers/checkouts_controller.rb class CheckoutsController < ApplicationController def create service = PaymentService.new session = service.create_checkout(checkout_params[:items]) redirect_to session.checkout_url, allow_other_host: true rescue Dodopayments::Errors::APIError => e flash[:error] = "Payment error: #{e.message}" redirect_to cart_path end private def checkout_params params.require(:checkout).permit(items: [:product_id, :quantity]) end end ``` ## Sinatra Integration Use with Sinatra applications: ```ruby theme={null} require "sinatra" require "dodopayments" configure do set :dodo_client, Dodopayments::Client.new( bearer_token: ENV["DODO_API_KEY"] ) end post "/create-checkout" do content_type :json begin session = settings.dodo_client.checkout_sessions.create( product_cart: JSON.parse(request.body.read)["items"], return_url: "#{request.base_url}/return" ) { checkout_url: session.checkout_url }.to_json rescue Dodopayments::Errors::APIError => e status 400 { error: e.message }.to_json end end ``` ## Resources View source code and contribute Complete API documentation Get help and connect with developers Report bugs or request features ## Support Need help with the Ruby SDK? * **Discord**: Join our [community server](https://discord.gg/bYqAp4ayYh) for real-time support * **Email**: Contact us at [support@dodopayments.com](mailto:support@dodopayments.com) * **GitHub**: Open an issue on the [repository](https://github.com/dodopayments/dodopayments-ruby) ## Contributing We welcome contributions! Check the [contributing guidelines](https://github.com/dodopayments/dodopayments-ruby/blob/main/CONTRIBUTING.md) to get started. # Rust Source: https://docs.dodopayments.com/developer-resources/sdks/rust Integrate Dodo Payments into your Rust applications with an async-first, strongly typed SDK built on Tokio and reqwest The Rust SDK provides convenient, async-first access to the Dodo Payments REST API from applications written in Rust. It offers strongly typed requests and responses, built-in pagination helpers, and configurable timeouts and environments. ## Installation Add the SDK to your project with Cargo: ```bash theme={null} cargo add dodopayments ``` Or add it to your `Cargo.toml` manually: ```toml theme={null} [dependencies] dodopayments = "1.106.0" tokio = { version = "1", features = ["full"] } serde_json = "1" futures = "0.3" # required for streaming paginated results ``` The SDK requires Rust 1.75 or later, leveraging modern async features for optimal performance. ## Quick Start The client reads your API key from the `DODO_PAYMENTS_API_KEY` environment variable by default. Initialize the client and create your first checkout session: ```rust theme={null} use dodopayments::Client; #[tokio::main] async fn main() -> dodopayments::Result<()> { let client = Client::from_env()?; let result = client .checkout_sessions() .create() .body(dodopayments::models::CheckoutSessionsCreateParams { product_cart: Some(vec![dodopayments::models::ProductItemReq { product_id: "product_id".to_string(), quantity: 1, addons: None, amount: None, credit_entitlements: None, }]), ..Default::default() }) .await?; println!("{result:?}"); Ok(()) } ``` Always store your API keys securely using environment variables. Never hardcode them in your source code. ## Core Features Built on Tokio and reqwest with `async`/`await` support throughout Strongly typed requests and responses for compile-time safety Stream every item across pages or advance one page at a time Configure environments, timeouts, and base URLs per client ## Configuration ### Environment Variables By default, `Client::from_env()` reads your API key from the `DODO_PAYMENTS_API_KEY` environment variable and uses the default base URL unless you set `DODO_PAYMENTS_BASE_URL`: ```bash theme={null} export DODO_PAYMENTS_API_KEY="your_api_key" export DODO_PAYMENTS_BASE_URL="https://test.dodopayments.com" # optional ``` You can also configure the client explicitly. `Client::new` returns a `Result`, so unwrap it with `?` inside a function that returns `dodopayments::Result`: ```rust theme={null} use dodopayments::{Client, ClientConfig}; #[tokio::main] async fn main() -> dodopayments::Result<()> { let client = Client::new( ClientConfig::new("https://live.dodopayments.com").with_api_key("My API Key"), )?; println!("{}", client.base_url()); Ok(()) } ``` ### Environments | Name | Base URL | | ----------- | ------------------------------- | | `live_mode` | `https://live.dodopayments.com` | | `test_mode` | `https://test.dodopayments.com` | The default base URL is `https://live.dodopayments.com`. Select another environment with the `Environment` enum instead of hard-coding URLs: ```rust theme={null} use dodopayments::{Client, ClientConfig, Environment}; let client = Client::new( ClientConfig::from_environment(Environment::TestMode).with_api_key("My API Key"), )?; ``` To keep reading the API key from `DODO_PAYMENTS_API_KEY` via `from_env()` while targeting a non-default environment, override it on the config: ```rust theme={null} use dodopayments::{Client, ClientConfig, Environment}; let client = Client::new(ClientConfig::from_env()?.with_environment(Environment::TestMode))?; ``` ### Timeouts The default request timeout is 30 seconds. Override it per client: ```rust theme={null} use std::time::Duration; use dodopayments::{Client, ClientConfig}; let client = Client::new( ClientConfig::new("https://live.dodopayments.com") .with_api_key("My API Key") .with_timeout(Duration::from_secs(60)), )?; ``` ## Common Operations ### Create a Checkout Session Generate a checkout session: ```rust theme={null} let session = client .checkout_sessions() .create() .body(dodopayments::models::CheckoutSessionsCreateParams { product_cart: Some(vec![dodopayments::models::ProductItemReq { product_id: "pdt_123".to_string(), quantity: 1, addons: None, amount: None, credit_entitlements: None, }]), return_url: Some("https://yourdomain.com/return".to_string()), ..Default::default() }) .await?; println!("{session:?}"); ``` ### Manage Customers Create and retrieve customer information: ```rust theme={null} // Create a customer let customer = client .customers() .create() .body(dodopayments::models::CustomerCreateParams { email: "customer@example.com".to_string(), name: "John Doe".to_string(), ..Default::default() }) .await?; // Retrieve a customer let customer = client .customers() .retrieve() .customer_id("cus_123") .await?; println!("{customer:?}"); ``` ### Handle Subscriptions Create and manage recurring subscriptions: `POST /subscriptions` (the SDK's `subscriptions.create` method) is **deprecated**. It still works for existing integrations, but new integrations should create subscriptions through a [Checkout Session](/developer-resources/checkout-session). ```rust theme={null} let subscription = client .subscriptions() .create() .body(dodopayments::models::SubscriptionCreateParams { billing: dodopayments::models::BillingAddress { country: "US".to_string(), city: "San Francisco".to_string(), state: "CA".to_string(), street: "1 Market St".to_string(), zipcode: "94105".to_string(), }, customer: dodopayments::models::CustomerRequest::AttachExisting( dodopayments::models::AttachExistingCustomer { customer_id: "cus_123".to_string(), }, ), product_id: "pdt_456".to_string(), quantity: 1, ..Default::default() }) .await?; println!("{subscription:?}"); ``` `billing` requires at minimum the two-letter ISO `country` code. `customer` is a `CustomerRequest` enum — pass `AttachExisting` for an existing customer or `New` for a new one. Amount fields such as `product_price` are in the lowest currency denomination (e.g., `2500` = \$25.00 USD). ## Usage-Based Billing ### Ingest Usage Events Track custom events: ```rust theme={null} let response = client .usage_events() .ingest() .body(dodopayments::models::UsageEventsIngestParams { events: Some(vec![dodopayments::models::EventInput { customer_id: "cus_abc123".to_string(), event_id: "api_call_12345".to_string(), event_name: "api_request".to_string(), metadata: None, timestamp: None, }]), ..Default::default() }) .await?; println!("{response:?}"); ``` ### List Usage Events ```rust theme={null} let events = client .usage_events() .list() .query(serde_json::json!({ "customer_id": "cus_abc123", "event_name": "api_request", })) .await?; for event in &events.items { println!("{event:?}"); } ``` ## Pagination List endpoints return a typed page whose `items` field holds the current page of results. Stream every item across all pages with `into_stream`: ```rust theme={null} use futures::StreamExt; let mut items = Box::pin( client .payments() .list() .query(serde_json::json!({})) .await? .into_stream(), ); while let Some(item) = items.next().await { let item = item?; println!("{item:?}"); } ``` Or advance one page at a time with `get_next_page`: ```rust theme={null} let mut page = client .payments() .list() .query(serde_json::json!({})) .await?; loop { for item in &page.items { println!("{item:?}"); } match page.get_next_page().await? { Some(next) => page = next, None => break, } } ``` ## Error Handling Every method returns a `dodopayments::Result`. Failures are represented by the `dodopayments::Error` enum. Match on it to handle API errors distinctly from transport errors: ```rust theme={null} let result = client .checkout_sessions() .create() .body(dodopayments::models::CheckoutSessionsCreateParams { product_cart: Some(vec![dodopayments::models::ProductItemReq { product_id: "product_id".to_string(), quantity: 1, addons: None, amount: None, credit_entitlements: None, }]), ..Default::default() }) .await; match result { Ok(value) => println!("{value:?}"), Err(dodopayments::Error::Api { status, message }) => { eprintln!("API returned {status}: {message}"); } Err(err) => eprintln!("request failed: {err}"), } ``` ## Undocumented Endpoints To call an endpoint not yet exposed as a typed method, use the low-level `request` builder, which applies authentication and the base URL: ```rust theme={null} let response = client .request(reqwest::Method::GET, "/some/path") .send() .await?; ``` ## Resources View source code and contribute View the published crate and versions Complete API documentation Get help and connect with developers ## Support Need help with the Rust SDK? * **Discord**: Join our [community server](https://discord.gg/bYqAp4ayYh) for real-time support * **Email**: Contact us at [support@dodopayments.com](mailto:support@dodopayments.com) * **GitHub**: Open an issue on the [repository](https://github.com/dodopayments/dodopayments-rust/issues) ## Contributing We welcome contributions! Check the [contributing guidelines](https://github.com/dodopayments/dodopayments-rust/blob/main/CONTRIBUTING.md) to get started. # TypeScript Source: https://docs.dodopayments.com/developer-resources/sdks/typescript Integrate Dodo Payments into your TypeScript and Node.js applications with type safety and modern async/await support The TypeScript SDK provides convenient server-side access to the Dodo Payments REST API for TypeScript and JavaScript applications. It features comprehensive type definitions, error handling, retries, timeouts, and auto-pagination for seamless payment processing. ## Installation Install the SDK using your package manager of choice: ```bash npm theme={null} npm install dodopayments ``` ```bash yarn theme={null} yarn add dodopayments ``` ```bash pnpm theme={null} pnpm add dodopayments ``` ## Quick Start Initialize the client with your API key and start processing payments: ```javascript theme={null} import DodoPayments from 'dodopayments'; const client = new DodoPayments({ bearerToken: process.env['DODO_PAYMENTS_API_KEY'], // This is the default and can be omitted environment: 'test_mode', // defaults to 'live_mode' }); const checkoutSessionResponse = await client.checkoutSessions.create({ product_cart: [{ product_id: 'product_id', quantity: 1 }], }); console.log(checkoutSessionResponse.session_id); ``` Always store your API keys securely using environment variables. Never commit them to version control or expose them in client-side code. ## Core Features Full TypeScript support with comprehensive type definitions for all API endpoints Automatic pagination for list responses makes working with large datasets effortless Built-in error types with detailed messages for different failure scenarios Configurable automatic retries with exponential backoff for transient errors ## Configuration ### Environment Variables Set environment variables for secure configuration: ```bash .env theme={null} DODO_PAYMENTS_API_KEY=your_api_key_here ``` ### Timeout Configuration Configure request timeouts globally or per-request: ```typescript theme={null} // Configure default timeout for all requests (default is 1 minute) const client = new DodoPayments({ timeout: 20 * 1000, // 20 seconds }); // Override per-request await client.checkoutSessions.create( { product_cart: [{ product_id: 'product_id', quantity: 1 }] }, { timeout: 5 * 1000 }, ); ``` ### Retry Configuration Configure automatic retry behavior: ```javascript theme={null} // Configure default for all requests (default is 2 retries) const client = new DodoPayments({ maxRetries: 0, // disable retries }); // Override per-request await client.checkoutSessions.create( { product_cart: [{ product_id: 'product_id', quantity: 1 }] }, { maxRetries: 5 }, ); ``` The SDK automatically retries requests that fail due to network errors or server issues (5xx responses) with exponential backoff. ## Common Operations ### Create a Checkout Session Generate a checkout session for collecting payment information: ```typescript theme={null} const session = await client.checkoutSessions.create({ product_cart: [ { product_id: 'pdt_123', quantity: 1 } ], return_url: 'https://yourdomain.com/return' }); console.log('Redirect to:', session.checkout_url); ``` ### Manage Customers Create and retrieve customer information: ```typescript theme={null} // Create a customer const customer = await client.customers.create({ email: 'customer@example.com', name: 'John Doe', metadata: { user_id: '12345' } }); // Retrieve customer const retrieved = await client.customers.retrieve('cus_123'); console.log(`Customer: ${retrieved.name} (${retrieved.email})`); ``` ### Handle Subscriptions Create and manage recurring subscriptions: `POST /subscriptions` (the SDK's `subscriptions.create` method) is **deprecated**. It still works for existing integrations, but new integrations should create subscriptions through a [Checkout Session](/developer-resources/checkout-session). ```typescript theme={null} // Create a subscription const subscription = await client.subscriptions.create({ billing: { country: 'US', city: 'San Francisco', state: 'CA', street: '1 Market St', zipcode: '94105', }, customer: { customer_id: 'cus_123', // or pass { email, name } to create a new customer }, product_id: 'pdt_456', quantity: 1, }); // Charge an on-demand subscription // product_price is in the lowest currency denomination (e.g., 2500 = $25.00 USD) const chargeResponse = await client.subscriptions.charge(subscription.subscription_id, { product_price: 2500, }); // Retrieve subscription usage history (for metered subscriptions) const usageHistory = await client.subscriptions.retrieveUsageHistory(subscription.subscription_id, { start_date: '2024-01-01T00:00:00Z', end_date: '2024-03-31T23:59:59Z', }); ``` `billing` requires at minimum the two-letter ISO country code. `customer` is a union of `{ customer_id }` (to attach an existing customer) or `{ email, name? }` (to create a new one). `product_price` is expressed in the lowest currency denomination. ## Usage-Based Billing ### Ingest Usage Events Track custom events for usage-based billing: ```typescript theme={null} await client.usageEvents.ingest({ events: [ { event_id: 'api_call_12345', customer_id: 'cus_abc123', event_name: 'api_request', timestamp: '2024-01-15T10:30:00Z', metadata: { endpoint: '/api/v1/users', method: 'GET', tokens_used: '150' } } ] }); ``` Events must have unique `event_id` values for idempotency. Duplicate IDs within the same request are rejected, and subsequent requests with existing IDs are ignored. ### Retrieve Usage Events Fetch detailed information about usage events: ```typescript theme={null} // Get a specific event const event = await client.usageEvents.retrieve('api_call_12345'); // List events with filtering const events = await client.usageEvents.list({ customer_id: 'cus_abc123', event_name: 'api_request', start: '2024-01-14T10:30:00Z', end: '2024-01-15T10:30:00Z' }); ``` ## Proxy Configuration Configure proxy settings for different runtimes: ### Node.js (using undici) ```typescript theme={null} import DodoPayments from 'dodopayments'; import * as undici from 'undici'; const proxyAgent = new undici.ProxyAgent('http://localhost:8888'); const client = new DodoPayments({ fetchOptions: { dispatcher: proxyAgent, }, }); ``` ### Bun ```typescript theme={null} import DodoPayments from 'dodopayments'; const client = new DodoPayments({ fetchOptions: { proxy: 'http://localhost:8888', }, }); ``` ### Deno ```typescript theme={null} import DodoPayments from 'npm:dodopayments'; const httpClient = Deno.createHttpClient({ proxy: { url: 'http://localhost:8888' } }); const client = new DodoPayments({ fetchOptions: { client: httpClient, }, }); ``` ## Logging Control log verbosity using environment variables or client options: ```typescript theme={null} // Via client option const client = new DodoPayments({ logLevel: 'debug', // Show all log messages }); ``` ```bash theme={null} # Via environment variable export DODO_PAYMENTS_LOG=debug ``` **Available log levels:** * `'debug'` - Show debug messages, info, warnings, and errors * `'info'` - Show info messages, warnings, and errors * `'warn'` - Show warnings and errors (default) * `'error'` - Show only errors * `'off'` - Disable all logging At the debug level, all HTTP requests and responses are logged, including headers and bodies. Some authentication headers are redacted, but sensitive data in bodies may still be visible. ## Migration from Node.js SDK If you're upgrading from the legacy Node.js SDK, the TypeScript SDK offers improved type safety and features: Learn how to migrate from the Node.js SDK to the TypeScript SDK ## Auto-Pagination List methods in the DodoPayments API are paginated. You can use the `for await … of` syntax to iterate through items across all pages: ```typescript theme={null} async function fetchAllPayments() { const allPayments = []; // Automatically fetches more pages as needed. for await (const paymentListResponse of client.payments.list()) { allPayments.push(paymentListResponse); } return allPayments; } ``` Alternatively, you can request a single page at a time: ```typescript theme={null} let page = await client.payments.list(); for (const paymentListResponse of page.items) { console.log(paymentListResponse); } // Convenience methods are provided for manually paginating: while (page.hasNextPage()) { page = await page.getNextPage(); // ... } ``` ## Requirements The following runtimes are supported: * Web browsers (Up-to-date Chrome, Firefox, Safari, Edge, and more) * Node.js 20 LTS or later ([non-EOL](https://endoflife.date/nodejs)) versions * Deno v1.28.0 or higher * Bun 1.0 or later * Cloudflare Workers * Vercel Edge Runtime * Jest 28 or greater with the `"node"` environment * Nitro v2.6 or greater TypeScript >= 4.9 is supported. ## Resources View source code and contribute Complete API documentation Get help and connect with developers Report bugs or request features ## Support Need help with the TypeScript SDK? * **Discord**: Join our [community server](https://discord.gg/bYqAp4ayYh) for real-time support * **Email**: Contact us at [support@dodopayments.com](mailto:support@dodopayments.com) * **GitHub**: Open an issue on the [repository](https://github.com/dodopayments/dodopayments-typescript) ## Contributing We welcome contributions! Check the [contributing guidelines](https://github.com/dodopayments/dodopayments-typescript/blob/main/CONTRIBUTING.md) to get started. # Tanstack Adaptor Source: https://docs.dodopayments.com/developer-resources/tanstack-adaptor Learn how to integrate Dodo Payments with your Tanstack App Router project using our Tanstack Adaptor. Covers checkout, customer portal, webhooks, and secure environment setup. Integrate Dodo Payments checkout into your Tanstack app. Allow customers to manage subscriptions and details. Receive and process Dodo Payments webhook events. ## Installation Run the following command in your project root: ```bash theme={null} npm install @dodopayments/tanstack ``` Create a .env file in your project root: ```env expandable theme={null} DODO_PAYMENTS_API_KEY=your-api-key DODO_PAYMENTS_WEBHOOK_KEY=your-webhook-secret DODO_PAYMENTS_RETURN_URL=your-return-url DODO_PAYMENTS_ENVIRONMENT="test_mode" or "live_mode" ``` Never commit your .env file or secrets to version control. ## Route Handler Examples All examples assume you are using the Tanstack App Router. Use this handler to integrate Dodo Payments checkout into your Tanstack app. Supports static (GET), dynamic (POST), and session (POST) payment flows. ```typescript Tanstack Route Handler expandable theme={null} // src/routes/api/checkout.ts import { Checkout } from "@dodopayments/tanstack"; import { createServerFileRoute } from "@tanstack/react-start/server"; export const ServerRoute = createServerFileRoute("/api/checkout") .methods({ GET: async ({ request }) => { return Checkout({ bearerToken: process.env.DODO_PAYMENTS_API_KEY, returnUrl: process.env.DODO_PAYMENTS_RETURN_URL, environment: process.env.DODO_PAYMENTS_ENVIRONMENT, type: "static", // optional, defaults to 'static' })(request) }, POST: async ({ request }) => { return Checkout({ bearerToken: process.env.DODO_PAYMENTS_API_KEY, returnUrl: process.env.DODO_PAYMENTS_RETURN_URL, environment: process.env.DODO_PAYMENTS_ENVIRONMENT, type: "session", // or "dynamic" for dynamic link })(request) } }) ``` ```curl Static Checkout Curl example expandable theme={null} curl --request GET \ --url 'https://example.com/api/checkout?productId=pdt_fqJhl7pxKWiLhwQR042rh' \ --header 'User-Agent: insomnia/11.2.0' \ --cookie mode=test ``` ```curl Dynamic Checkout Curl example expandable theme={null} curl --request POST \ --url https://example.com/api/checkout \ --header 'Content-Type: application/json' \ --header 'User-Agent: insomnia/11.2.0' \ --cookie mode=test \ --data '{ "billing": { "city": "Texas", "country": "US", "state": "Texas", "street": "56, hhh", "zipcode": "560000" }, "customer": { "email": "test@example.com", "name": "test" }, "metadata": {}, "payment_link": true, "product_id": "pdt_QMDuvLkbVzCRWRQjLNcs", "quantity": 1, "billing_currency": "USD", "discount_codes": ["IKHZ23M9GQ"], "return_url": "https://example.com", "trial_period_days": 10 }' ``` ```curl Checkout Session Curl example expandable theme={null} curl --request POST \ --url https://example.com/api/checkout \ --header 'Content-Type: application/json' \ --header 'User-Agent: insomnia/11.2.0' \ --cookie mode=test \ --data '{ "product_cart": [ { "product_id": "pdt_QMDuvLkbVzCRWRQjLNcs", "quantity": 1 } ], "customer": { "email": "test@example.com", "name": "test" }, "return_url": "https://example.com/success" }' ``` Use this handler to allow customers to manage their subscriptions and details via the Dodo Payments customer portal. ```typescript Tanstack Route Handler expandable theme={null} // src/routes/api/customer-portal.ts import { CustomerPortal } from "@dodopayments/tanstack"; import { createServerFileRoute } from "@tanstack/react-start/server"; export const ServerRoute = createServerFileRoute('/api/customer-portal') .methods({ GET: async ({ request }) => { return CustomerPortal({ bearerToken: process.env.DODO_PAYMENTS_API_KEY, environment: process.env.DODO_PAYMENTS_ENVIRONMENT, })(request) } }) ``` ```curl Customer Portal Curl example expandable theme={null} curl --request GET \ --url 'https://example.com/api/customer-portal?customer_id=cus_9VuW4K7O3GHwasENg31m&send_email=true' \ --header 'User-Agent: insomnia/11.2.0' \ --cookie mode=test ``` Use this handler to receive and process Dodo Payments webhook events securely in your Tanstack app. ```typescript Tanstack Route Handler expandable theme={null} // src/routes/api/webhook.ts import { Webhooks } from "@dodopayments/tanstack"; import { createServerFileRoute } from "@tanstack/react-start/server"; export const ServerRoute = createServerFileRoute('/api/webhook') .methods({ POST: async ({ request }) => { return Webhooks({ webhookKey: process.env.DODO_PAYMENTS_WEBHOOK_KEY, onPayload: async (payload) => { console.log(payload) } })(request) } }) ``` ## Checkout Route Handler Dodo Payments supports three types of payment flows for integrating payments into your website, this adaptor supports all types of payment flows. * **Static Payment Links:** Instantly shareable URLs for quick, no-code payment collection. * **Dynamic Payment Links:** Programmatically generate payment links with custom details using the API or SDKs. * **Checkout Sessions:** Create secure, customizable checkout experiences with pre-configured product carts and customer details. ### Supported Query Parameters Product identifier (e.g., ?productId=pdt\_nZuwz45WAs64n3l07zpQR). Quantity of the product. Customer's full name. Customer's first name. Customer's last name. Customer's email address. Customer's country. Customer's address line. Customer's city. Customer's state/province. Customer's zip/postal code. Disable full name field. Disable first name field. Disable last name field. Disable email field. Disable country field. Disable address line field. Disable city field. Disable state field. Disable zip code field. Specify the payment currency (e.g., USD). Show currency selector. Fixes the amount charged, in major currency units (e.g., 12.5 for \$12.50). Pay What You Want products only, and ignored if below the product's minimum price. Show discount fields. Any query parameter starting with metadata\_ will be passed as metadata. If productId is missing, the handler returns a 400 response. Invalid query parameters also result in a 400 response. ### Response Format Static checkout returns a JSON response with the checkout URL: ```json theme={null} { "checkout_url": "https://checkout.dodopayments.com/..." } ``` * Send parameters as a JSON body in a POST request. * Supports both one-time and recurring payments. * For a complete list of supported POST body fields, refer to: * [Request body for a One Time Payment Product](https://docs.dodopayments.com/api-reference/payments/post-payments) * [Request body for a Subscription Product](https://docs.dodopayments.com/api-reference/subscriptions/post-subscriptions) ### Response Format Dynamic checkout returns a JSON response with the checkout URL: ```json theme={null} { "checkout_url": "https://checkout.dodopayments.com/..." } ``` Checkout sessions provide a more secure, hosted checkout experience that handles the complete payment flow for both one-time purchases and subscriptions with full customization control. Refer to [Checkout Sessions Integration Guide](https://docs.dodopayments.com/developer-resources/checkout-session) for more details and a complete list of supported fields. ### Response Format Checkout sessions return a JSON response with the checkout URL: ```json theme={null} { "checkout_url": "https://checkout.dodopayments.com/session/..." } ``` ## Customer Portal Route Handler The Customer Portal Route Handler enables you to seamlessly integrate the Dodo Payments customer portal into your Tanstack application. ### Query Parameters The customer ID for the portal session (e.g., ?customer\_id=cus\_123). If set to true, sends an email to the customer with the portal link. Returns 400 if customer\_id is missing. ## Webhook Route Handler * **Method:** Only POST requests are supported. Other methods return 405. * **Signature Verification:** Verifies the webhook signature using webhookKey. Returns 401 if verification fails. * **Payload Validation:** Validated with Zod. Returns 400 for invalid payloads. * **Error Handling:** * 401: Invalid signature * 400: Invalid payload * 500: Internal error during verification * **Event Routing:** Calls the appropriate event handler based on the payload type. ### Supported Webhook Event Handlers ```typescript Typescript expandable theme={null} onPayload?: (payload: WebhookPayload) => Promise; onPaymentSucceeded?: (payload: WebhookPayload) => Promise; onPaymentFailed?: (payload: WebhookPayload) => Promise; onPaymentProcessing?: (payload: WebhookPayload) => Promise; onPaymentCancelled?: (payload: WebhookPayload) => Promise; onRefundSucceeded?: (payload: WebhookPayload) => Promise; onRefundFailed?: (payload: WebhookPayload) => Promise; onDisputeOpened?: (payload: WebhookPayload) => Promise; onDisputeExpired?: (payload: WebhookPayload) => Promise; onDisputeAccepted?: (payload: WebhookPayload) => Promise; onDisputeCancelled?: (payload: WebhookPayload) => Promise; onDisputeChallenged?: (payload: WebhookPayload) => Promise; onDisputeWon?: (payload: WebhookPayload) => Promise; onDisputeLost?: (payload: WebhookPayload) => Promise; onSubscriptionActive?: (payload: WebhookPayload) => Promise; onSubscriptionOnHold?: (payload: WebhookPayload) => Promise; onSubscriptionRenewed?: (payload: WebhookPayload) => Promise; onSubscriptionPlanChanged?: (payload: WebhookPayload) => Promise; onSubscriptionCancelled?: (payload: WebhookPayload) => Promise; onSubscriptionFailed?: (payload: WebhookPayload) => Promise; onSubscriptionExpired?: (payload: WebhookPayload) => Promise; onSubscriptionUpdated?: (payload: WebhookPayload) => Promise; onLicenseKeyCreated?: (payload: WebhookPayload) => Promise; onAbandonedCheckoutDetected?: (payload: WebhookPayload) => Promise; onAbandonedCheckoutRecovered?: (payload: WebhookPayload) => Promise; onDunningStarted?: (payload: WebhookPayload) => Promise; onDunningRecovered?: (payload: WebhookPayload) => Promise; onCreditAdded?: (payload: WebhookPayload) => Promise; onCreditDeducted?: (payload: WebhookPayload) => Promise; onCreditExpired?: (payload: WebhookPayload) => Promise; onCreditRolledOver?: (payload: WebhookPayload) => Promise; onCreditRolloverForfeited?: (payload: WebhookPayload) => Promise; onCreditOverageCharged?: (payload: WebhookPayload) => Promise; onCreditManualAdjustment?: (payload: WebhookPayload) => Promise; onCreditBalanceLow?: (payload: WebhookPayload) => Promise; ``` *** ## Prompt for LLM ``` You are an expert Tanstack developer assistant. Your task is to guide a user through integrating the @dodopayments/tanstack adapter into their existing Tanstack project. The @dodopayments/tanstack adapter provides route handlers for Dodo Payments' Checkout, Customer Portal, and Webhook functionalities, designed for the Tanstack App Router. First, install the necessary packages. Use the package manager appropriate for your project (npm, yarn, or bun) based on the presence of lock files (e.g., package-lock.json for npm, yarn.lock for yarn, bun.lockb for bun): npm install @dodopayments/tanstack Here's how you should structure your response: Ask the user which functionalities they want to integrate. "Which parts of the @dodopayments/tanstack adapter would you like to integrate into your project? You can choose one or more of the following: Checkout Route Handler (for handling product checkouts) Customer Portal Route Handler (for managing customer subscriptions/details) Webhook Route Handler (for receiving Dodo Payments webhook events) All (integrate all three)" Based on the user's selection, provide detailed integration steps for each chosen functionality. If Checkout Route Handler is selected: Purpose: This handler redirects users to the Dodo Payments checkout page. File Creation: Create a new file at app/checkout/route.ts in your Tanstack project. Code Snippet: // src/routes/api/checkout.ts import { Checkout } from "@dodopayments/tanstack"; import { createServerFileRoute } from "@tanstack/react-start/server"; export const ServerRoute = createServerFileRoute("/api/checkout") .methods({ GET: async ({ request }) => { return Checkout({ bearerToken: process.env.DODO_PAYMENTS_API_KEY, returnUrl: process.env.DODO_PAYMENTS_RETURN_URL, environment: process.env.DODO_PAYMENTS_ENVIRONMENT, type: "static", // optional, defaults to 'static' })(request) }, POST: async ({ request }) => { return Checkout({ bearerToken: process.env.DODO_PAYMENTS_API_KEY, returnUrl: process.env.DODO_PAYMENTS_RETURN_URL, environment: process.env.DODO_PAYMENTS_ENVIRONMENT, type: "session", // or "dynamic" for dynamic link })(request) } }) Configuration & Usage: bearerToken: Your Dodo Payments API key. It's recommended to set this via the DODO_PAYMENTS_API_KEY environment variable. returnUrl: (Optional) The URL to redirect the user to after a successful checkout. environment: (Optional) Set to "test_mode" for testing, or omit/set to "live_mode" for production. type: (Optional) Set to "static" for GET/static checkout, "dynamic" for POST/dynamic checkout, or "session" for POST/checkout sessions. Static Checkout (GET) Query Parameters: productId (required): Product identifier (e.g., ?productId=pdt_nZuwz45WAs64n3l07zpQR) quantity (optional): Quantity of the product Customer Fields (optional): fullName, firstName, lastName, email, country, addressLine, city, state, zipCode Disable Flags (optional, set to true to disable): disableFullName, disableFirstName, disableLastName, disableEmail, disableCountry, disableAddressLine, disableCity, disableState, disableZipCode Advanced Controls (optional): paymentCurrency, showCurrencySelector, paymentAmount, showDiscounts Metadata (optional): Any query parameter starting with metadata_ (e.g., ?metadata_userId=abc123) Returns: {"checkout_url": "https://checkout.dodopayments.com/..."} Dynamic Checkout (POST) - Returns JSON with checkout_url: Parameters are sent as a JSON body. Supports both one-time and recurring payments. Returns: {"checkout_url": "https://checkout.dodopayments.com/..."}. For a complete list of supported POST body fields, refer to: Docs - One Time Payment Product: https://docs.dodopayments.com/api-reference/payments/post-payments Docs - Subscription Product: https://docs.dodopayments.com/api-reference/subscriptions/post-subscriptions Checkout Sessions (POST) - (Recommended) A more customizable checkout experience. Returns JSON with checkout_url: Parameters are sent as a JSON body. Supports both one-time and recurring payments. Returns: {"checkout_url": "https://checkout.dodopayments.com/session/..."}. For a complete list of supported fields, refer to: Checkout Sessions Integration Guide: https://docs.dodopayments.com/developer-resources/checkout-session Error Handling: If productId is missing or other query parameters are invalid, the handler will return a 400 response. If Customer Portal Route Handler is selected: Purpose: This handler redirects authenticated users to their Dodo Payments customer portal. File Creation: Create a new file at app/customer-portal/route.ts in your Tanstack project. Code Snippet: // src/routes/api/customer-portal.ts import { CustomerPortal } from "@dodopayments/tanstack"; import { createServerFileRoute } from "@tanstack/react-start/server"; export const ServerRoute = createServerFileRoute('/api/customer-portal') .methods({ GET: async ({ request }) => { return CustomerPortal({ bearerToken: process.env.DODO_PAYMENTS_API_KEY, environment: process.env.DODO_PAYMENTS_ENVIRONMENT, })(request) } }) Query Parameters: customer_id (required): The customer ID for the portal session (e.g., ?customer_id=cus_123) send_email (optional, boolean): If set to true, sends an email to the customer with the portal link. Returns 400 if customer_id is missing. If Webhook Route Handler is selected: Purpose: This handler processes incoming webhook events from Dodo Payments, allowing your application to react to events like successful payments, refunds, or subscription changes. File Creation: Create a new file at app/api/webhook/dodo-payments/route.ts in your Tanstack project. Code Snippet: // src/routes/api/webhook.ts import { Webhooks } from "@dodopayments/tanstack"; import { createServerFileRoute } from "@tanstack/react-start/server"; export const ServerRoute = createServerFileRoute('/api/webhook') .methods({ POST: async ({ request }) => { return Webhooks({ webhookKey: process.env.DODO_PAYMENTS_WEBHOOK_KEY, onPayload: async (payload) => { console.log(payload) } })(request) } }) Handler Details: Method: Only POST requests are supported. Other methods return 405. Signature Verification: The handler verifies the webhook signature using the webhookKey and returns 401 if verification fails. Payload Validation: The payload is validated with Zod. Returns 400 for invalid payloads. Error Handling: 401: Invalid signature 400: Invalid payload 500: Internal error during verification Event Routing: Calls the appropriate event handler based on the payload type. Supported Webhook Event Handlers: onPayload?: (payload: WebhookPayload) => Promise onPaymentSucceeded?: (payload: WebhookPayload) => Promise onPaymentFailed?: (payload: WebhookPayload) => Promise onPaymentProcessing?: (payload: WebhookPayload) => Promise onPaymentCancelled?: (payload: WebhookPayload) => Promise onRefundSucceeded?: (payload: WebhookPayload) => Promise onRefundFailed?: (payload: WebhookPayload) => Promise onDisputeOpened?: (payload: WebhookPayload) => Promise onDisputeExpired?: (payload: WebhookPayload) => Promise onDisputeAccepted?: (payload: WebhookPayload) => Promise onDisputeCancelled?: (payload: WebhookPayload) => Promise onDisputeChallenged?: (payload: WebhookPayload) => Promise onDisputeWon?: (payload: WebhookPayload) => Promise onDisputeLost?: (payload: WebhookPayload) => Promise onSubscriptionActive?: (payload: WebhookPayload) => Promise onSubscriptionOnHold?: (payload: WebhookPayload) => Promise onSubscriptionRenewed?: (payload: WebhookPayload) => Promise onSubscriptionPlanChanged?: (payload: WebhookPayload) => Promise onSubscriptionCancelled?: (payload: WebhookPayload) => Promise onSubscriptionFailed?: (payload: WebhookPayload) => Promise onSubscriptionExpired?: (payload: WebhookPayload) => Promise onSubscriptionUpdated?: (payload: WebhookPayload) => Promise onLicenseKeyCreated?: (payload: WebhookPayload) => Promise onAbandonedCheckoutDetected?: (payload: WebhookPayload) => Promise onAbandonedCheckoutRecovered?: (payload: WebhookPayload) => Promise onDunningStarted?: (payload: WebhookPayload) => Promise onDunningRecovered?: (payload: WebhookPayload) => Promise onCreditAdded?: (payload: WebhookPayload) => Promise onCreditDeducted?: (payload: WebhookPayload) => Promise onCreditExpired?: (payload: WebhookPayload) => Promise onCreditRolledOver?: (payload: WebhookPayload) => Promise onCreditRolloverForfeited?: (payload: WebhookPayload) => Promise onCreditOverageCharged?: (payload: WebhookPayload) => Promise onCreditManualAdjustment?: (payload: WebhookPayload) => Promise onCreditBalanceLow?: (payload: WebhookPayload) => Promise Environment Variable Setup: To ensure the adapter functions correctly, you will need to manually set up the following environment variables in your Tanstack project's deployment environment (e.g., Vercel, Netlify, AWS, etc.): DODO_PAYMENTS_API_KEY: Your Dodo Payments API Key (required for Checkout and Customer Portal). RETURN_URL: (Optional) The URL to redirect to after a successful checkout (for Checkout handler). DODO_PAYMENTS_WEBHOOK_SECRET: Your Dodo Payments Webhook Secret (required for Webhook handler). Example .env file: DODO_PAYMENTS_API_KEY=your-api-key DODO_PAYMENTS_WEBHOOK_KEY=your-webhook-secret DODO_PAYMENTS_RETURN_URL=your-return-url DODO_PAYMENTS_ENVIRONMENT="test_mode" or "live_mode" Usage in your code: bearerToken: process.env.DODO_PAYMENTS_API_KEY webhookKey: process.env.DODO_PAYMENTS_WEBHOOK_KEY Important: Never commit sensitive environment variables directly into your version control. Use environment variables for all sensitive information. If the user needs assistance setting up environment variables for their specific deployment environment, ask them what platform they are using (e.g., Vercel, Netlify, AWS, etc.), and provide guidance. You can also add comments to their PR or chat depending on the context ``` # Balances Source: https://docs.dodopayments.com/features/account-summary-payout-wallet Track your financial activity across multi-currency wallets with comprehensive transaction ledgers, including payments, refunds, fees, and payouts in USD, GBP, and EUR. ## Introduction The **Balances** section provides merchants with a comprehensive overview of their financial activity across all currencies. It includes every transaction type - payments, refunds, fees, taxes, and payouts - ensuring full visibility into both earnings and deductions. ## Key Details Balances dashboard showing wallet overview and transaction ledger ## Wallet Structure Each business account maintains **three native wallets** based on supported payout currencies: | Wallet | Currency Code | | ---------- | ------------- | | USD Wallet | USD | | GBP Wallet | GBP | | EUR Wallet | EUR | ### Wallet Logic * Merchant sales are **credited to the appropriate wallet** based on the **customer's payment currency** and **geographic location**. * This system optimizes for **payment processor efficiency**, **reduced transaction fees** and **lower currency conversion charges**. ## Balance Ledger Each wallet has an **independent ledger** recording all related financial activity - payments, refunds, fees, and payouts - in that specific wallet's currency. A **Consolidated Ledger** combines all wallet data, offering a unified view of the merchant's entire financial history. ### Filters and Navigation * **Currency Filter:** Allows merchants to view transactions by specific wallet (USD, GBP, EUR). * **All Transactions View:** Provides an aggregated view across all wallets with unified sorting, filtering, and search capabilities. ### Transaction Types The following transaction types are recorded in the Balance ledger: | Transaction Type | Symbol | Description | | -------------------- | ---------- | ------------------------------------------------- | | **Payment** | Credit (+) | Successful customer payment | | **Refund** | Debit (-) | Returned amount to the customer | | **Transaction Fees** | Debit (-) | Processing fees applied to payments | | **Refund Fees** | Debit (-) | Fees deducted for processing refunds | | **Sales Tax** | Debit (-) | Tax amount charged on payments | | **Sales Tax Refund** | Credit (+) | Refunded portion of sales tax on refunds | | **Payout** | Debit (-) | Amount transferred to the merchant's bank account | | **Payout Fees** | Debit (-) | Transaction fees deducted from payouts | #### Additional Fields * **Transaction Amount:** Total value associated with the transaction. * **Transaction Timestamp:** Date and time of transaction completion. * **Transaction ID:** Unique identifier for tracking and support purposes. ## Total Balance The **Total Balance** section shows a **consolidated indicative amount** of all wallet balances - **USD, GBP, and EUR** - after conversion to USD. **Indicative Display:** The displayed amount is **for reference only**. Final payout amounts may vary based on the actual exchange rate at the time of transfer. ## Conclusion The **Balances** aims to deliver **transparency, accuracy, and flexibility** in managing multi-currency funds. By maintaining separate native wallets, providing currency-based filters, and enabling indicative consolidated views, Dodo Payments ensures merchants always have a clear understanding of their earnings and payout readiness. For any discrepancies or clarifications, please reach out to **[support@dodopayments.com](mailto:support@dodopayments.com)** with your **Transaction ID** and issue details. # Adaptive Currency Source: https://docs.dodopayments.com/features/adaptive-currency Display prices in each customer's local currency to reduce friction and improve conversion. Automatic detection, conversion, and same‑currency refunds. Adaptive Currency lets you show localized prices and accept payments in customers' local currencies. This reduces friction at checkout and builds trust. ## What Is Adaptive Currency? Adaptive Currency displays product prices in the customer's local currency instead of only your base currency. When enabled, checkout defaults to the detected local currency for supported countries, with the option to switch back to your base currency. Your base currency is whatever you priced the product or add-on in — it can be any currency Dodo Payments can charge, not just USD. Adaptive Currency converts that base price at live exchange rates. To set your own fixed price per currency or country instead, see [Localized Pricing](/features/localized-pricing). ## Key Benefits * **Localized payment experience**: Prices appear in local currency by default. * **More payment methods**: Unlocks payment methods available for the local currency. * **Same‑currency refunds**: Refund the customer in the currency they paid with. ## Enable Adaptive Currency Log in to your Merchant Dashboard and go to Settings → Business. Enable the Adaptive Pricing toggle. You can disable it at any time. Changes apply only to future transactions. Save your settings. Verify a test checkout shows prices in your local currency when supported. Adaptive Currency ## Customer Experience 1. **Detection**: The system detects the customer's country based on the billing address. 2. **Currency selection**: If the country is supported, prices show in the local currency by default. Customers can switch back to your base currency. 3. **Payment methods**: Localized payment methods appear where applicable. 4. **Checkout**: Payment is completed in the selected currency. ## Conversion and Fees Adaptive Currency charges the customer in their local currency using the latest exchange rates. * **You pay**: 0% (default) or 2–4% (when fees inclusive is enabled) * **Your customers pay**: 2–4% based on order value (default) or 0% (when fees inclusive is enabled) Tiered fees applied at checkout: * 4% for orders under \$500 * 3% for 500 to \$1,500 * 2% for over \$1,500 ### Choose who bears the fee By default, adaptive currency fees are added on top of your displayed price and borne by the customer. With the **Fees Inclusive** setting, you can absorb the fee yourself: the customer sees the same local-currency price they always would, and the fee is deducted from your settlement. Go to **Settings → Business** and enable the Adaptive Pricing toggle if you haven't already. Once Adaptive Pricing is on, enable the **Fees Inclusive** sub-toggle in the same section. | Mode | Customer sees | Merchant settles | | ------------------- | ----------------------------- | ----------------------------- | | Exclusive (default) | Local price + 2–4% fee on top | Full base price | | Inclusive | Local price (unchanged) | Base price minus the 2–4% fee | You can also override the merchant default per request by passing `adaptive_currency_fees_inclusive` (boolean) on a one-time payment, an on-demand subscription (`on_demand.adaptive_currency_fees_inclusive`), a subscription charge, or a plan change. It is not accepted on checkout sessions: ```typescript theme={null} const payment = await client.payments.create({ product_cart: [{ product_id: 'pdt_abc', quantity: 1 }], customer: { customer_id: 'cus_123' }, billing: { country: 'US' }, adaptive_currency_fees_inclusive: true, // override the business setting for this payment return_url: 'https://yoursite.com/return' }); ``` INR → INR transactions are always treated as inclusive regardless of the business setting or per-request override. When a [Localized Pricing](/features/localized-pricing) rule matches, the transaction is always treated as inclusive, regardless of your **Fees Inclusive** setting or any per-request override. The customer pays exactly the localized amount you set, and the conversion fee is deducted from your settlement. ## Supported Currencies | Currency Code | Currency Name | Countries | Minimum Amount | | ------------- | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | | AED | UAE Dirham | United Arab Emirates | 2.00 AED | | ALL | Albanian Lek | Albania | 50.00 ALL | | AMD | Armenian Dram | Armenia | 500.00 AMD | | AUD | Australian Dollar | Australia, Nauru | 0.50 AUD | | AWG | Aruban Florin | Aruba | 2.00 AWG | | AZN | Azerbaijani Manat | Azerbaijan | 2.00 AZN | | BAM | Bosnia-Herzegovina Convertible Mark | Bosnia and Herzegovina | 2.00 BAM | | BDT | Bangladeshi Taka | Bangladesh | 100.00 BDT | | BMD | Bermudian Dollar | Bermuda | 1.00 BMD | | BND | Brunei Dollar | Brunei | 1.00 BND | | BOB | Bolivian Boliviano | Bolivia | 5.00 BOB | | BRL | Brazilian Real | Brazil | 0.50 BRL | | BSD | Bahamian Dollar | Bahamas | 1.00 BSD | | BWP | Botswanan Pula | Botswana | 15.00 BWP | | BZD | Belize Dollar | Belize | 2.00 BZD | | CAD | Canadian Dollar | Canada | 0.50 CAD | | CHF | Swiss Franc | Switzerland, Liechtenstein | 0.50 CHF | | CLP | Chilean Peso | Chile | 1000.00 CLP | | CNY | Chinese Yuan | China | 4.00 CNY | | CRC | Costa Rican Colón | Costa Rica | 500.00 CRC | | CZK | Czech Koruna | Czech Republic | 15.00 CZK | | DKK | Danish Krone | Denmark, Greenland | 2.50 DKK | | DOP | Dominican Peso | Dominican Republic | 100.00 DOP | | EGP | Egyptian Pound | Egypt | 50.00 EGP | | ETB | Ethiopian Birr | Ethiopia | 100.00 ETB | | EUR | Euro | Austria, Belgium, Cyprus, Estonia, Finland, France, Germany, Greece, Ireland, Italy, Latvia, Lithuania, Luxembourg, Malta, Netherlands, Portugal, Slovakia, Slovenia, Spain, Andorra, Monaco, Croatia, San Marino, Montenegro | 0.50 EUR | | FJD | Fijian Dollar | Fiji | 2.00 FJD | | GBP | British Pound | United Kingdom | 0.30 GBP | | GEL | Georgian Lari | Georgia | 3.00 GEL | | GMD | Gambian Dalasi | Gambia | 100.00 GMD | | GTQ | Guatemalan Quetzal | Guatemala | 10.00 GTQ | | GYD | Guyanese Dollar | Guyana | 200.00 GYD | | HKD | Hong Kong Dollar | Hong Kong | 4.00 HKD | | HNL | Honduran Lempira | Honduras | 25.00 HNL | | HUF | Hungarian Forint | Hungary | 175.00 HUF | | IDR | Indonesian Rupiah | Indonesia | 8500.00 IDR | | ILS | Israeli New Shekel | Israel | 3.00 ILS | | INR | Indian Rupee | India | 5.00 INR | | JPY | Japanese Yen | Japan | 50 JPY | | KRW | South Korean Won | South Korea | 50 KRW | | KZT | Kazakhstani Tenge | Kazakhstan | 500.00 KZT | | LKR | Sri Lankan Rupee | Sri Lanka | 300.00 LKR | | LRD | Liberian Dollar | Liberia | 200.00 LRD | | LSL | Lesotho Loti | Lesotho | 20.00 LSL | | MAD | Moroccan Dirham | Morocco | 10.00 MAD | | MKD | Macedonian Denar | North Macedonia | 50.00 MKD | | MOP | Macanese Pataca | Macau | 10.00 MOP | | MUR | Mauritian Rupee | Mauritius | 50.00 MUR | | MVR | Maldivian Rufiyaa | Maldives | 15.00 MVR | | MWK | Malawian Kwacha | Malawi | 2000.00 MWK | | MXN | Mexican Peso | Mexico | 10.00 MXN | | MYR | Malaysian Ringgit | Malaysia | 4.00 MYR | | NGN | Nigerian Naira | Nigeria | 2000.00 NGN | | NOK | Norwegian Krone | Norway | 3.00 NOK | | NPR | Nepalese Rupee | Nepal | 150.00 NPR | | NZD | New Zealand Dollar | New Zealand | 1.00 NZD | | PEN | Peruvian Sol | Peru | 3.00 PEN | | PGK | Papua New Guinean Kina | Papua New Guinea | 4.00 PGK | | PHP | Philippine Peso | Philippines | 50.00 PHP | | PLN | Polish Zloty | Poland | 2.00 PLN | | PYG | Paraguayan Guaraní | Paraguay | 4000 PYG | | QAR | Qatari Rial | Qatar | 3.00 QAR | | RON | Romanian Leu | Romania | 2.00 RON | | RSD | Serbian Dinar | Serbia | 60.00 RSD | | SAR | Saudi Riyal | Saudi Arabia | 2.00 SAR | | SBD | Solomon Islands Dollar | Solomon Islands | 10.00 SBD | | SCR | Seychellois Rupee | Seychelles | 15.00 SCR | | SEK | Swedish Krona | Sweden | 3.00 SEK | | SGD | Singapore Dollar | Singapore | 0.50 SGD | | SZL | Swazi Lilangeni | Eswatini | 20.00 SZL | | THB | Thai Baht | Thailand | 25.00 THB | | TOP | Tongan Paʻanga | Tonga | 2.00 TOP | | TRY | Turkish Lira | Turkey | 20.00 TRY | | TWD | New Taiwan Dollar | Taiwan | 20.00 TWD | | TZS | Tanzanian Shilling | Tanzania | 3000.00 TZS | | UYU | Uruguayan Peso | Uruguay | 50.00 UYU | | VND | Vietnamese Dong | Vietnam | 12000 VND | | WST | Samoan Tala | Samoa | 2.00 WST | | XAF | Central African CFA Franc | Cameroon, Central African Republic, Chad, Republic of the Congo, Equatorial Guinea, Gabon | 300 XAF | | XOF | West African CFA Franc | Benin, Burkina Faso, Côte d'Ivoire, Guinea-Bissau, Mali, Niger, Senegal, Togo | 300 XOF | | ZAR | South African Rand | South Africa | 20.00 ZAR | | ZMW | Zambian Kwacha | Zambia | 30.00 ZMW | ## Refunds and Adjustments Dodo Payments issues refunds in the currency the customer originally paid, using the latest exchange rate. The amount in your base currency remains fixed on your dashboard, invoices, and in the refund. This means the customer may receive more or less than the original local‑currency amount depending on FX changes. Adaptive Currency fees which are generally the FX fees are not refunded. **Example refund** 1. You sell a product for 100 USD with Adaptive Currency enabled. 2. A Canadian customer sees 137 CAD at an exchange rate of 1.37 CAD per 1 USD and completes the purchase. 3. We process the payment, converting 137 CAD to 100 USD for your settlement. 4. Later, the exchange rate changes to 1.40 CAD per 1 USD and you issue a full refund. 5. We deduct 100 USD and refund the customer 140 CAD. ## Invoices and Taxation * Invoices show only the settlement currency amount. * Taxes and platform fees are calculated on the settlement currency amount. * Example: a \$10 sale converted to 36 AED still reflects as \$10 in the Dashboard and invoices. All amounts are rounded according to Dodo Payments' internal rounding logic. ## Integration Examples **Checkout Sessions with billing currency** Pass `billing_currency` to explicitly set the billing currency for the session. When Adaptive Pricing is disabled, `billing_currency` is ignored. ```typescript theme={null} const session = await client.checkoutSessions.create({ product_cart: [ { product_id: 'pdt_one_time_or_subscription', quantity: 1 } ], billing_currency: 'AED', return_url: 'https://example.com/return' }); ``` Configure Billing Currency using our Checkout Session API. # Add-ons for Subscriptions Source: https://docs.dodopayments.com/features/addons Enhance your subscription products with flexible add-ons for seat-based billing, feature upgrades, and creative pricing models Add-ons are additional products that can be attached to your main subscription products, enabling flexible pricing models and enhanced customer experiences. Whether you need seat-based billing, feature upgrades, or custom pricing structures, add-ons give you the power to create sophisticated subscription offerings. Create add-ons for additional team seats, user licenses, or capacity upgrades with per-seat pricing. Extend usage limits, API calls, or data allowances with flexible add-on pricing. ## What Are Add-ons? Add-ons are supplementary products that customers can purchase alongside their main subscription. They're perfect for: * **Seat-based billing**: Additional team members, user licenses, or concurrent users * **Feature upgrades**: Premium features, advanced analytics, or priority support * **Usage extensions**: Extra storage, API calls, or bandwidth allowances * **Service add-ons**: Professional services, training, or consultation hours Add-ons attached to subscription products in the dashboard ## Key Benefits * **Flexible Pricing Models**: Offer base plans with optional add-ons to create sophisticated pricing structures. You can address diverse customer segments with upgrades that grow as your customers' needs change. * **Revenue Optimization**: Boost your average revenue per user (ARPU) by presenting relevant add-ons. This enables natural upsell opportunities as customers add features over time. * **Simplified Management**: Manage all pricing components from one dashboard. Add-ons are automatically included in both checkout sessions and subscription management. * **Customer Choice**: Allow customers to customize their subscriptions by selecting only the add-ons they need, which enhances satisfaction and reduces churn. ## Creating Add-ons Add-ons are created as separate products in your Dodo Payments dashboard and then attached to your main subscription products. This separation allows you to: * Reuse add-ons across multiple subscription products * Manage pricing independently * Track add-on performance separately * Update add-ons without affecting base subscriptions Creating add-ons in the dashboard interface ### Add-on Configuration When creating add-ons, you can configure: * **Pricing**: Set the add-on amount (`price`) in the smallest currency unit. The add-on is billed on the parent subscription's cycle * **Currency**: Set the base price in any currency Dodo Payments can charge. The searchable currency selector pins **USD, GBP, EUR, and INR** to the top; customers outside your base currency are billed through [Adaptive Currency](/features/adaptive-currency) * **Quantity**: Customers choose a quantity when the add-on is attached to a subscription * **Availability**: Control which subscription products can use the add-on * **Tax settings**: Configure the appropriate `tax_category` ### Getting Started Ready to implement add-ons in your subscription business? Here's how to get started: Identify the additional features, services, or capacity that would benefit your customers as add-ons. Consider: * What do customers frequently request? * What features could be monetized separately? * What would create natural upgrade paths? Use the Dodo Payments dashboard or API to create your first add-on product. Follow our step-by-step guide to create add-ons in the dashboard. Connect your add-ons to the appropriate subscription products where they should be available. Create test checkout sessions with different add-on combinations to ensure everything works correctly. Track add-on adoption rates and revenue impact to optimize your pricing strategy. ## API Management Dodo Payments provides a comprehensive API for managing add-ons programmatically: Use the `POST /addons` endpoint to create new add-ons with custom pricing, descriptions, and configuration options. View the complete API documentation for creating add-ons. Modify existing add-ons using the `PATCH /addons/{id}` endpoint to update pricing, descriptions, or availability. Learn how to update add-on details programmatically. Use `GET /addons` to list all add-ons or `GET /addons/{id}` to retrieve specific add-on details. Access the complete listing and retrieval API documentation. Update add-on images using the `PUT /addons/{id}/images` endpoint for better product presentation. Learn how to manage add-on images via API. ## Common Use Cases * **Seat-Based Billing**: Additional team members, user licenses, or concurrent users * **Feature Upgrades**: Premium features, advanced analytics, or priority support * **Usage Extensions**: Extra storage, API calls, or bandwidth allowances * **Service Add-ons**: Professional services, training, or consultation hours ## Integration Examples ### Checkout Sessions with Add-ons When creating checkout sessions, you can include add-ons with custom quantities: ```typescript theme={null} const session = await client.checkoutSessions.create({ product_cart: [ { product_id: 'your_subscription_id', quantity: 1, addons: [ { addon_id: 'your_addon_id', quantity: 3 // 3 additional seats } ] } ], // ... other checkout options }); ``` To let customers add or remove add-ons themselves on the checkout page, set the [`allow_editing_addons`](/developer-resources/checkout-session#optional-fields) feature flag to `true` when creating the session. It defaults to `false` and applies only to subscription products. ```typescript theme={null} const session = await client.checkoutSessions.create({ product_cart: [{ product_id: 'your_subscription_id', quantity: 1 }], feature_flags: { allow_editing_addons: true // customers can edit add-ons at checkout }, }); ``` ### Plan Changes with Add-ons Modify existing subscriptions to add, remove, or update add-ons: ```typescript theme={null} // Add add-ons to existing subscription await client.subscriptions.changePlan('sub_123', { product_id: 'pdt_new', quantity: 1, proration_billing_mode: 'difference_immediately', addons: [ { addon_id: 'addon_123', quantity: 2 } ] }); // Remove all existing add-ons await client.subscriptions.changePlan('sub_123', { product_id: 'pdt_new', quantity: 1, proration_billing_mode: 'difference_immediately', addons: [] // Empty array removes all existing add-ons }); ``` ### Dynamic Pricing Calculate total costs dynamically based on add-on selections: ```typescript theme={null} function calculateTotalCost(basePrice: number, addons: AddonSelection[]) { const addonTotal = addons.reduce((sum, addon) => sum + (addon.price * addon.quantity), 0 ); return basePrice + addonTotal; } ``` ## Best Practices * **Start simple**: Launch with 2-3 core add-ons and expand options based on customer feedback and usage. * **Maintain pricing clarity**: Clearly communicate add-on pricing and value, so customers understand what they're getting for the extra cost. * **Test thoroughly**: Validate add-on combinations to ensure pricing calculations remain accurate and checkout flows function smoothly. ### Design Considerations * **Clear Value Proposition**: Each add-on should have a clear benefit that customers can easily understand * **Logical Grouping**: Group related add-ons together in your checkout flow * **Flexible Quantities**: Allow customers to adjust quantities of add-ons as needed * **Transparent Pricing**: Show total costs clearly throughout the checkout process Add-ons are a powerful way to create flexible, scalable pricing models that grow with your customers. Start with simple use cases and expand as you learn what works best for your business and customers. # Affiliates Source: https://docs.dodopayments.com/features/affiliates Launch and manage your affiliate program while processing transactions through Dodo Payments. ## Introduction This guide walks you through how to track affiliate referrals, handle commission events, and grow your revenue with trusted affiliate partnerships via Dodo Payments. We offer integrations with leading affiliate platforms including **Affonso**, **Dub Partners**, and **Rekomi** to help you manage and track your affiliate programs seamlessly. ## Key Features No spreadsheets or manual tracking. Referrals are automatically attributed and logged when a purchase is made. Both you and your affiliates can monitor performance and leads from dedicated dashboards. Use tracking scripts, metadata fields, and webhook events to capture referral data across signups and transactions. All sensitive credentials and webhook data are securely exchanged and stored using best-in-class encryption. Launch your affiliate program in minutes—no code changes required beyond copy-pasting script snippets and API tokens. Plug affiliate tracking directly into your checkout flows using our integrations with Affonso, Dub Partners, and Rekomi, leading affiliate marketing platforms. ## Integration Options Dodo Payments integrates with leading affiliate management platforms: * **[Affonso](https://affonso.io)**: A comprehensive affiliate management platform with automated commission tracking, real-time reporting, and dedicated affiliate dashboards. * **[Dub Partners](https://dub.co)**: Use Dub's link management and conversion tracking to power your affiliate program with detailed attribution analytics. * **[Rekomi](https://rekomi.com)**: An affiliate tracking and management platform with a one-paste Dodo Payments connection that also handles affiliate payouts and tax forms for you. Choose the platform that best fits your needs, or use both for different use cases. ## Affonso Integration We have partnered with Affonso, a leading affiliate management platform to power your sales. Follow the steps below to connect your Dodo Payments account with Affonso and begin tracking affiliate-driven sales.