> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dodopayments.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Cloudflare Workers

> DodoPayments वेबहुक्स को Cloudflare Workers पर तैनात करें

<Card title="GitHub Repository" icon="github" href="https://github.com/dodopayments/cloud-functions/tree/main/cloudflare">
  पूर्ण स्रोत कोड और सेटअप गाइड
</Card>

## त्वरित सेटअप

### 1. पूर्वापेक्षाएँ

* [Cloudflare खाता](https://dash.cloudflare.com/sign-up)
* [Neon डेटाबेस](https://neon.com) खाता
* [डैशबोर्ड](https://app.dodopayments.com/) से DodoPayments API कुंजी

### 2. निर्भरताएँ स्थापित करें

```bash theme={null}
npm install -g wrangler
wrangler login
git clone https://github.com/dodopayments/cloud-functions.git
cd cloud-functions/cloudflare
npm install
```

### 3. डेटाबेस सेटअप

1. [Neon](https://neon.com) के लिए साइन अप करें
2. एक नया प्रोजेक्ट बनाएं
3. SQL एडिटर खोलें
4. [`schema.sql`](#database-schema) की सामग्री कॉपी करके पेस्ट करें
5. क्वेरी चलाएं
6. Neon → Connection Details से अपना कनेक्शन स्ट्रिंग प्राप्त करें

### 4. प्रारंभिक रहस्यों को कॉन्फ़िगर करें

```bash theme={null}
# Set your Neon database URL
wrangler secret put DATABASE_URL

# Set your API key
wrangler secret put DODO_PAYMENTS_API_KEY
```

> **Note:** जब आपके पास आपका वेबहुक URL हो, तो तैनाती के बाद `DODO_PAYMENTS_WEBHOOK_KEY` सेट किया जाएगा।

### 5. wrangler.toml को अपडेट करें

`wrangler.toml` को संपादित करें और अपना वर्कर नाम सेट करें:

```toml theme={null}
name = "my-dodo-webhook"
```

### 6. तैनात करें

```bash theme={null}
npm run deploy
```

### 7. अपना वेबहुक URL प्राप्त करें

आपका वेबहुक URL है:

```
https://[worker-name].[your-subdomain].workers.dev
```

### 8. DodoPayments डैशबोर्ड में वेबहुक पंजीकृत करें

1. [DodoPayments Dashboard](https://app.dodopayments.com) → Developer → Webhooks पर जाएं
2. एक नया वेबहुक एंडपॉइंट बनाएं
3. अपने वेबहुक URL को एंडपॉइंट के रूप में कॉन्फ़िगर करें
4. इन सब्सक्रिप्शन इवेंट्स को सक्षम करें:
   * `subscription.active`
   * `subscription.cancelled`
   * `subscription.renewed`
5. **Signing Secret** को कॉपी करें

### 9. वेबहुक कुंजी सेट करें और पुनः तैनात करें

```bash theme={null}
wrangler secret put DODO_PAYMENTS_WEBHOOK_KEY
npm run deploy
```

## यह क्या करता है

सदस्यता घटनाओं को संसाधित करता है और उन्हें PostgreSQL में संग्रहीत करता है:

* **subscription.active** - ग्राहक और सदस्यता रिकॉर्ड बनाता/अपडेट करता है
* **subscription.cancelled** - सदस्यता को रद्द के रूप में चिह्नित करता है
* **subscription.renewed** - अगली बिलिंग तिथि को अपडेट करता है

## प्रमुख विशेषताएँ

✅ **Signature verification** - dodopayments लाइब्रेरी का उपयोग करके\
✅ **Idempotency** - वेबहुक ID के साथ डुप्लिकेट प्रोसेसिंग को रोकता है\
✅ **Event logging** - `webhook_events` तालिका में पूर्ण ऑडिट ट्रेल\
✅ **Error handling** - लॉग किया गया और पुनः प्रयास योग्य

> **Note:** यह कार्यान्वयन तीन मूल सब्सक्रिप्शन इवेंट्स (`subscription.active`, `subscription.cancelled`, `subscription.renewed`) को न्यूनतम फ़ील्ड्स के साथ हैंडल करने का प्रदर्शन करता है। आप इसे अपनी आवश्यकताओं के अनुसार अतिरिक्त इवेंट प्रकारों और फ़ील्ड्स का समर्थन करने के लिए आसानी से विस्तारित कर सकते हैं।

## कॉन्फ़िगरेशन फ़ाइलें

<CodeGroup>
  ```json package.json theme={null}
  {
    "name": "dodo-webhook-cloudflare",
    "version": "1.0.0",
    "type": "module",
    "description": "DodoPayments Webhook Handler for Cloudflare Workers",
    "main": "worker.ts",
    "scripts": {
      "dev": "wrangler dev",
      "deploy": "wrangler deploy",
      "tail": "wrangler tail"
    },
    "dependencies": {
      "@neondatabase/serverless": "^1.0.2",
      "dodopayments": "^2.4.1"
    },
    "devDependencies": {
      "@cloudflare/workers-types": "^4.0.0",
      "typescript": "^5.9.3",
      "wrangler": "^4.43.0"
    }
  }
  ```

  ```toml wrangler.toml expandable theme={null}
  # Cloudflare Workers Configuration
  name = "dodo-webhook"
  main = "worker.ts"
  compatibility_date = "2025-10-20"

  [observability]
  [observability.logs]
  enabled = true
  head_sampling_rate = 1
  invocation_logs = true
  persist = true
  ```

  ```json tsconfig.json theme={null}
  {
    "compilerOptions": {
      "target": "ES2022",
      "module": "ES2022",
      "lib": ["ES2022"],
      "moduleResolution": "bundler",
      "types": ["@cloudflare/workers-types"],
      "esModuleInterop": true,
      "strict": true,
      "skipLibCheck": true,
      "resolveJsonModule": true,
      "allowSyntheticDefaultImports": true,
      "forceConsistentCasingInFileNames": true,
      "isolatedModules": true
    },
    "include": ["*.ts"],
    "exclude": ["node_modules"]
  }
  ```
</CodeGroup>

## डेटाबेस स्कीमा

<CodeGroup>
  ```sql schema.sql expandable theme={null}
  -- DodoPayments Webhook Database Schema
  -- Compatible with PostgreSQL (Supabase, Neon, etc.)

  -- Enable UUID extension (if not already enabled)
  CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

  -- Customers table
  CREATE TABLE IF NOT EXISTS customers (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    email TEXT NOT NULL,
    name TEXT NOT NULL,
    dodo_customer_id TEXT UNIQUE NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
  );

  -- Subscriptions table
  CREATE TABLE IF NOT EXISTS subscriptions (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    customer_id UUID NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
    dodo_subscription_id TEXT UNIQUE NOT NULL,
    product_id TEXT NOT NULL,
    status TEXT NOT NULL CHECK (status IN ('pending', 'active', 'on_hold', 'cancelled', 'failed', 'expired')),
    billing_interval TEXT NOT NULL CHECK (billing_interval IN ('day', 'week', 'month', 'year')),
    amount INTEGER NOT NULL,
    currency TEXT NOT NULL,
    next_billing_date TIMESTAMP WITH TIME ZONE NOT NULL,
    cancelled_at TIMESTAMP WITH TIME ZONE,
    created_at TIMESTAMP WITH TIME ZONE NOT NULL,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
  );

  -- Webhook events log
  CREATE TABLE IF NOT EXISTS webhook_events (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    webhook_id TEXT UNIQUE,
    event_type TEXT NOT NULL,
    data JSONB NOT NULL,
    processed BOOLEAN DEFAULT FALSE,
    error_message TEXT,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    processed_at TIMESTAMP WITH TIME ZONE,
    attempts INTEGER DEFAULT 0
  );

  -- Indexes for better query performance
  CREATE INDEX IF NOT EXISTS idx_customers_email ON customers(email);
  CREATE INDEX IF NOT EXISTS idx_customers_dodo_id ON customers(dodo_customer_id);
  CREATE INDEX IF NOT EXISTS idx_subscriptions_dodo_id ON subscriptions(dodo_subscription_id);
  CREATE INDEX IF NOT EXISTS idx_subscriptions_customer_id ON subscriptions(customer_id);
  CREATE INDEX IF NOT EXISTS idx_subscriptions_status ON subscriptions(status);
  CREATE INDEX IF NOT EXISTS idx_webhook_events_processed ON webhook_events(processed, created_at);
  CREATE INDEX IF NOT EXISTS idx_webhook_events_type ON webhook_events(event_type);
  CREATE INDEX IF NOT EXISTS idx_webhook_events_created_at ON webhook_events(created_at DESC);
  CREATE INDEX IF NOT EXISTS idx_webhook_events_webhook_id ON webhook_events(webhook_id);

  -- Function to automatically update updated_at timestamp
  CREATE OR REPLACE FUNCTION update_updated_at_column()
  RETURNS TRIGGER AS $$
  BEGIN
    NEW.updated_at = NOW();
    RETURN NEW;
  END;
  $$ LANGUAGE plpgsql;

  -- Triggers to automatically update updated_at
  CREATE TRIGGER update_customers_updated_at
    BEFORE UPDATE ON customers
    FOR EACH ROW
    EXECUTE FUNCTION update_updated_at_column();

  CREATE TRIGGER update_subscriptions_updated_at
    BEFORE UPDATE ON subscriptions
    FOR EACH ROW
    EXECUTE FUNCTION update_updated_at_column();

  -- Comments for documentation
  COMMENT ON TABLE customers IS 'Stores customer information from DodoPayments';
  COMMENT ON TABLE subscriptions IS 'Stores subscription data from DodoPayments';
  COMMENT ON TABLE webhook_events IS 'Logs all incoming webhook events for audit and retry purposes';

  COMMENT ON COLUMN customers.dodo_customer_id IS 'Unique customer ID from DodoPayments';
  COMMENT ON COLUMN subscriptions.dodo_subscription_id IS 'Unique subscription ID from DodoPayments';
  COMMENT ON COLUMN subscriptions.amount IS 'Amount in smallest currency unit (e.g., cents)';
  COMMENT ON COLUMN subscriptions.currency IS 'Currency used for the subscription payments (e.g., USD, EUR, INR)';
  COMMENT ON COLUMN webhook_events.attempts IS 'Number of processing attempts for failed webhooks';
  COMMENT ON COLUMN webhook_events.data IS 'Full webhook payload as JSON';
  ```
</CodeGroup>

**बनाई गई तालिकाएँ:**

* **customers** - ईमेल, नाम, dodo\_customer\_id
* **subscriptions** - स्थिति, राशि, अगली\_billing\_date, ग्राहकों से जुड़ा
* **webhook\_events** - idempotency के लिए webhook\_id के साथ घटना लॉग

## कार्यान्वयन कोड

<CodeGroup>
  ```typescript worker.ts expandable theme={null}
  import { DodoPayments } from 'dodopayments';
  import { neon, NeonQueryFunction } from '@neondatabase/serverless';

  interface Env {
    DATABASE_URL: string;
    DODO_PAYMENTS_API_KEY: string;
    DODO_PAYMENTS_WEBHOOK_KEY: string;
  }

  interface WebhookPayload {
    business_id: string;
    type: string;
    timestamp: string;
    data: {
      payload_type:
        | "Payment"
        | "Subscription"
        | "Refund"
        | "Dispute"
        | "LicenseKey"
        | "CreditLedgerEntry"
        | "CreditBalanceLow"
        | "AbandonedCheckout"
        | "DunningAttempt"
        | "EntitlementGrant";
      subscription_id: string;
      customer: {
        customer_id: string;
        email: string;
        name: string;
      };
      product_id: string;
      status: string;
      recurring_pre_tax_amount: number;
      payment_frequency_interval: string;
      created_at: string;
      next_billing_date: string;
      cancelled_at?: string | null;
      currency: string;
    };
  }

  const corsHeaders = {
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type, webhook-id, webhook-signature, webhook-timestamp',
    'Access-Control-Allow-Methods': 'POST, OPTIONS',
  };

  async function handleSubscriptionEvent(sql: NeonQueryFunction<false, false>, payload: WebhookPayload, status: string) {
    if (!payload.data.customer.customer_id || !payload.data.subscription_id) {
      throw new Error('Missing required fields: customer_id or subscription_id');
    }

    console.log('🔄 Processing subscription event:', JSON.stringify(payload, null, 2));

    const customer = payload.data.customer;

    // Upsert customer (create if doesn't exist, otherwise use existing)
    const customerResult = await sql`
      INSERT INTO customers (email, name, dodo_customer_id, created_at)
      VALUES (${customer.email}, ${customer.name}, ${customer.customer_id}, ${new Date().toISOString()})
      ON CONFLICT (dodo_customer_id) 
      DO UPDATE SET 
        email = EXCLUDED.email,
        name = EXCLUDED.name,
        updated_at = ${new Date().toISOString()}
      RETURNING id
    `;

    const customerId = customerResult[0].id;
    console.log(`✅ Customer upserted with ID: ${customerId}`);

    // Upsert subscription
    await sql`
      INSERT INTO subscriptions (
        customer_id, dodo_subscription_id, product_id, status, 
        billing_interval, amount, currency, created_at, next_billing_date, cancelled_at, updated_at
      )
      VALUES (
        ${customerId}, ${payload.data.subscription_id},
        ${payload.data.product_id}, ${status},
        ${payload.data.payment_frequency_interval.toLowerCase()}, ${payload.data.recurring_pre_tax_amount},
        ${payload.data.currency}, ${payload.data.created_at}, ${payload.data.next_billing_date},
        ${payload.data.cancelled_at ?? null}, ${new Date().toISOString()}
      )
      ON CONFLICT (dodo_subscription_id) 
      DO UPDATE SET 
        customer_id = EXCLUDED.customer_id,
        product_id = EXCLUDED.product_id,
        status = EXCLUDED.status,
        billing_interval = EXCLUDED.billing_interval,
        amount = EXCLUDED.amount,
        currency = EXCLUDED.currency,
        created_at = EXCLUDED.created_at,
        next_billing_date = EXCLUDED.next_billing_date,
        cancelled_at = EXCLUDED.cancelled_at,
        updated_at = EXCLUDED.updated_at
    `;

    console.log(`✅ Subscription upserted with ${status} status`)
  }

  export default {
    async fetch(request: Request, env: Env): Promise<Response> {
      // Handle CORS preflight
      if (request.method === 'OPTIONS') {
        return new Response('ok', { headers: corsHeaders });
      }

      if (request.method !== 'POST') {
        return new Response(JSON.stringify({ error: 'Method not allowed' }), {
          status: 405,
          headers: { ...corsHeaders, 'Content-Type': 'application/json' }
        });
      }

      try {
        const rawBody = await request.text();
        console.log('📨 Webhook received');

        // Verify required environment variables
        if (!env.DODO_PAYMENTS_API_KEY) {
          console.error('❌ DODO_PAYMENTS_API_KEY is not configured');
          return new Response(
            JSON.stringify({ error: 'API key not configured' }),
            { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
          );
        }

        if (!env.DODO_PAYMENTS_WEBHOOK_KEY) {
          console.error('❌ DODO_PAYMENTS_WEBHOOK_KEY is not configured');
          return new Response(
            JSON.stringify({ error: 'Webhook verification key not configured' }),
            { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
          );
        }

        if (!env.DATABASE_URL) {
          console.error('❌ DATABASE_URL is not configured');
          return new Response(
            JSON.stringify({ error: 'Database URL not configured' }),
            { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
          );
        }

        // Verify webhook signature (required for security)
        const webhookHeaders = {
          'webhook-id': request.headers.get('webhook-id') || '',
          'webhook-signature': request.headers.get('webhook-signature') || '',
          'webhook-timestamp': request.headers.get('webhook-timestamp') || '',
        };

        try {
          const dodoPaymentsClient = new DodoPayments({
            bearerToken: env.DODO_PAYMENTS_API_KEY,
            webhookKey: env.DODO_PAYMENTS_WEBHOOK_KEY,
          });
          const unwrappedWebhook = dodoPaymentsClient.webhooks.unwrap(rawBody, { headers: webhookHeaders });
          console.log('Unwrapped webhook:', unwrappedWebhook);
          console.log('✅ Webhook signature verified');
        } catch (error) {
          console.error('❌ Webhook verification failed:', error);
          return new Response(
            JSON.stringify({ error: 'Webhook verification failed' }),
            { status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
          );
        }

        // Initialize Neon client
        const sql = neon(env.DATABASE_URL);

        const payload: WebhookPayload = JSON.parse(rawBody);
        const eventType = payload.type;
        const eventData = payload.data;
        const webhookId = request.headers.get('webhook-id') || '';

        console.log(`📋 Webhook payload:`, JSON.stringify(payload, null, 2));

        // Check for duplicate webhook-id (idempotency)
        if (webhookId) {
          const existingEvent = await sql`
            SELECT id FROM webhook_events WHERE webhook_id = ${webhookId}
          `;

          if (existingEvent.length > 0) {
            console.log(`⚠️ Webhook ${webhookId} already processed, skipping (idempotency)`);
            return new Response(
              JSON.stringify({ success: true, message: 'Webhook already processed' }),
              { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
            );
          }
        }

        // Log webhook event with webhook_id for idempotency
        const logResult = await sql`
          INSERT INTO webhook_events (webhook_id, event_type, data, processed, created_at)
          VALUES (${webhookId || null}, ${eventType}, ${JSON.stringify(eventData)}, ${false}, ${new Date().toISOString()})
          RETURNING id
        `;

        const loggedEventId = logResult[0].id;
        console.log('📝 Webhook event logged with ID:', loggedEventId);

        console.log(`🔄 Processing: ${eventType} (${eventData.payload_type || 'unknown payload type'})`);

        try {
          switch (eventType) {
            case 'subscription.active':
              await handleSubscriptionEvent(sql, payload, 'active');
              break;
            case 'subscription.cancelled':
              await handleSubscriptionEvent(sql, payload, 'cancelled');
              break;
            case 'subscription.renewed':
              console.log('🔄 Subscription renewed - keeping active status and updating billing date');
              await handleSubscriptionEvent(sql, payload, 'active');
              break;
            default:
              console.log(`ℹ️ Event ${eventType} logged but not processed (no handler available)`);
          }

          await sql`
            UPDATE webhook_events 
            SET processed = ${true}, processed_at = ${new Date().toISOString()}
            WHERE id = ${loggedEventId}
          `;

          console.log('✅ Webhook marked as processed');
        } catch (processingError) {
          console.error('❌ Error processing webhook event:', processingError);

          await sql`
            UPDATE webhook_events 
            SET processed = ${false}, 
                error_message = ${processingError instanceof Error ? processingError.message : 'Unknown error'},
                processed_at = ${new Date().toISOString()}
            WHERE id = ${loggedEventId}
          `;

          throw processingError;
        }

        console.log('✅ Webhook processed successfully');

        return new Response(
          JSON.stringify({
            success: true,
            event_type: eventType,
            event_id: loggedEventId
          }),
          { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
        );

      } catch (error) {
        console.error('❌ Webhook processing failed:', error);
        return new Response(
          JSON.stringify({
            error: 'Webhook processing failed',
            details: error instanceof Error ? error.message : 'Unknown error'
          }),
          { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
        );
      }
    }
  };
  ```
</CodeGroup>

## यह कैसे काम करता है

वेबहुक हैंडलर:

1. **Verifies the signature** - HMAC-SHA256 का उपयोग करके सुनिश्चित करता है कि अनुरोध DodoPayments से है
2. **Checks for duplicates** - एक ही इवेंट को दो बार प्रोसेस होने से रोकने के लिए वेबहुक ID का उपयोग करता है
3. **Logs the event** - ऑडिट ट्रेल के लिए कच्चे वेबहुक को `webhook_events` तालिका में स्टोर करता है
4. **Processes the event** - ग्राहक और सब्सक्रिप्शन को बनाता है या अपडेट करता है
5. **Handles errors** - विफलताओं को लॉग करता है और पुनः प्रयास के लिए इवेंट को अपरप्रोसेस्ड के रूप में चिह्नित करता है

## परीक्षण

**स्थानीय विकास:**

```bash theme={null}
npm run dev  # Available at http://localhost:8787
```

**DodoPayments डैशबोर्ड में कॉन्फ़िगर करें:**

1. डेवलपर्स → वेबहुक्स पर जाएं
2. अपने कार्यकर्ता URL के साथ एंडपॉइंट जोड़ें
3. सक्षम करें: subscription.active, subscription.cancelled, subscription.renewed

## सामान्य समस्याएँ

| समस्या                  | समाधान                                                            |
| ----------------------- | ----------------------------------------------------------------- |
| सत्यापन विफल            | यह सुनिश्चित करें कि वेबहुक कुंजी DodoPayments डैशबोर्ड से सही है |
| डेटाबेस कनेक्शन त्रुटि  | Neon कनेक्शन स्ट्रिंग सत्यापित करें                               |
| वर्कर तैनात नहीं हो रहा | `npm install` चलाएं, `wrangler.toml` सिंटैक्स जांचें              |

## संसाधन

* [Cloudflare Workers Docs](https://developers.cloudflare.com/workers/)
* [Neon Docs](https://neon.com/docs/)
* [Webhook Events Guide](/developer-resources/webhooks/intents/webhook-events-guide)
* [GitHub Repo](https://github.com/dodopayments/cloud-functions/tree/main/cloudflare)
