> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://typeform.ferndocs.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://typeform.ferndocs.com/_mcp/server.

# RTBF Custom Script

> Build custom scripts and automation to handle Right to Be Forgotten requests at scale across multiple forms and systems.

This guide provides advanced patterns and complete scripts for automating Right to Be Forgotten (RTBF) compliance across your Typeform infrastructure and integrated systems.

## Complete RTBF automation script

Here's a production-ready Node.js script for handling RTBF requests:

```javascript
const fetch = require('node-fetch');

class RTBFManager {
  constructor(config) {
    this.apiToken = config.apiToken;
    this.baseUrl = 'https://api.typeform.com';
    this.auditLog = config.auditLog || [];
  }

  /**
   * Main RTBF handler
   */
  async processRTBFRequest(params) {
    const {
      identifier,
      identifierType = 'email',
      formIds = [],
      reason = 'RTBF request',
      requesterEmail,
    } = params;

    const startTime = new Date();
    const deletionRecord = {
      id: this.generateDeletionId(),
      timestamp: startTime,
      identifier,
      identifierType,
      reason,
      requesterEmail,
      status: 'in_progress',
      deletedResponses: [],
      errors: [],
    };

    try {
      // Step 1: Find all matching responses
      console.log(
        `Searching for responses matching ${identifierType}: ${identifier}`
      );
      const matchingResponses = await this.findMatchingResponses(
        identifier,
        identifierType,
        formIds
      );

      if (matchingResponses.length === 0) {
        deletionRecord.status = 'no_data_found';
        this.auditLog.push(deletionRecord);
        return {
          success: true,
          message: 'No data found for the provided identifier',
          deletionRecord,
        };
      }

      console.log(`Found ${matchingResponses.length} matching responses`);

      // Step 2: Group by form for efficient deletion
      const responsesByForm = this.groupResponsesByForm(matchingResponses);

      // Step 3: Delete from Typeform
      for (const [formId, responses] of Object.entries(responsesByForm)) {
        try {
          await this.deleteResponses(
            formId,
            responses.map((r) => r.token)
          );
          deletionRecord.deletedResponses.push({
            formId,
            count: responses.length,
            tokens: responses.map((r) => r.token),
          });
          console.log(
            `Deleted ${responses.length} responses from form ${formId}`
          );
        } catch (error) {
          deletionRecord.errors.push({
            formId,
            error: error.message,
          });
          console.error(`Failed to delete from form ${formId}:`, error);
        }
      }

      // Step 4: Delete from integrated systems
      await this.deleteFromIntegratedSystems(identifier, deletionRecord);

      // Step 5: Finalize
      deletionRecord.status =
        deletionRecord.errors.length > 0 ? 'partial' : 'completed';
      deletionRecord.completedAt = new Date();
      deletionRecord.duration = deletionRecord.completedAt - startTime;

      this.auditLog.push(deletionRecord);

      // Step 6: Send notification
      if (requesterEmail) {
        await this.sendDeletionConfirmation(requesterEmail, deletionRecord);
      }

      return {
        success: true,
        deletionRecord,
      };
    } catch (error) {
      deletionRecord.status = 'failed';
      deletionRecord.error = error.message;
      deletionRecord.completedAt = new Date();
      this.auditLog.push(deletionRecord);

      console.error('RTBF process failed:', error);
      return {
        success: false,
        error: error.message,
        deletionRecord,
      };
    }
  }

  /**
   * Find all responses matching the identifier
   */
  async findMatchingResponses(identifier, identifierType, formIds) {
    const allMatches = [];

    // If no specific forms provided, get all forms
    if (formIds.length === 0) {
      const forms = await this.listAllForms();
      formIds = forms.map((f) => f.id);
    }

    for (const formId of formIds) {
      try {
        const responses = await this.fetchAllResponses(formId);
        const matches = responses.filter((response) =>
          this.responseMatchesIdentifier(response, identifier, identifierType)
        );

        allMatches.push(
          ...matches.map((r) => ({
            ...r,
            formId,
          }))
        );
      } catch (error) {
        console.error(`Error fetching responses for form ${formId}:`, error);
      }
    }

    return allMatches;
  }

  /**
   * Check if a response matches the identifier
   */
  responseMatchesIdentifier(response, identifier, identifierType) {
    // Check hidden fields
    if (response.hidden && response.hidden[identifierType] === identifier) {
      return true;
    }

    // Check answers
    for (const answer of response.answers || []) {
      if (identifierType === 'email' && answer.type === 'email') {
        if (answer.email.toLowerCase() === identifier.toLowerCase()) {
          return true;
        }
      }

      if (identifierType === 'phone' && answer.type === 'phone_number') {
        if (
          this.normalizePhone(answer.phone_number) ===
          this.normalizePhone(identifier)
        ) {
          return true;
        }
      }

      // Custom field matching
      if (answer.field?.ref === identifierType && answer.text === identifier) {
        return true;
      }
    }

    return false;
  }

  /**
   * Fetch all responses for a form (handles pagination)
   */
  async fetchAllResponses(formId) {
    const allResponses = [];
    let hasMore = true;
    let lastToken = null;

    while (hasMore) {
      const url = new URL(`${this.baseUrl}/forms/${formId}/responses`);
      url.searchParams.append('page_size', '1000');
      if (lastToken) {
        url.searchParams.append('before', lastToken);
      }

      const response = await fetch(url.toString(), {
        headers: {
          Authorization: `Bearer ${this.apiToken}`,
        },
      });

      if (!response.ok) {
        throw new Error(`Failed to fetch responses: ${response.statusText}`);
      }

      const data = await response.json();
      allResponses.push(...data.items);

      hasMore = data.items.length === 1000;
      if (hasMore) {
        lastToken = data.items[data.items.length - 1].token;
      }
    }

    return allResponses;
  }

  /**
   * Delete responses from Typeform
   */
  async deleteResponses(formId, tokens) {
    if (tokens.length === 0) return;

    // Typeform API has a limit on URL length, so batch if necessary
    const batchSize = 50;
    for (let i = 0; i < tokens.length; i += batchSize) {
      const batch = tokens.slice(i, i + batchSize);
      const tokenString = batch.join(',');

      const url = `${this.baseUrl}/forms/${formId}/responses?included_tokens=${tokenString}`;

      const response = await fetch(url, {
        method: 'DELETE',
        headers: {
          Authorization: `Bearer ${this.apiToken}`,
        },
      });

      if (response.status !== 204) {
        const error = await response.text();
        throw new Error(`Deletion failed: ${error}`);
      }

      // Rate limiting: wait between batches
      if (i + batchSize < tokens.length) {
        await this.sleep(1000);
      }
    }
  }

  /**
   * Delete from integrated systems
   */
  async deleteFromIntegratedSystems(identifier, deletionRecord) {
    const integrations = [];

    // Add your integrations here

    // Example: Delete from database
    try {
      await this.deleteFromDatabase(identifier);
      integrations.push({ system: 'database', status: 'success' });
    } catch (error) {
      integrations.push({
        system: 'database',
        status: 'failed',
        error: error.message,
      });
    }

    // Example: Delete from CRM
    try {
      await this.deleteFromCRM(identifier);
      integrations.push({ system: 'crm', status: 'success' });
    } catch (error) {
      integrations.push({
        system: 'crm',
        status: 'failed',
        error: error.message,
      });
    }

    // Example: Delete from email marketing
    try {
      await this.deleteFromEmailMarketing(identifier);
      integrations.push({ system: 'email_marketing', status: 'success' });
    } catch (error) {
      integrations.push({
        system: 'email_marketing',
        status: 'failed',
        error: error.message,
      });
    }

    deletionRecord.integrations = integrations;
  }

  /**
   * Integration placeholders - implement based on your systems
   */
  async deleteFromDatabase(identifier) {
    // Implement your database deletion logic
    console.log(`Deleting from database: ${identifier}`);
  }

  async deleteFromCRM(identifier) {
    // Implement your CRM deletion logic
    console.log(`Deleting from CRM: ${identifier}`);
  }

  async deleteFromEmailMarketing(identifier) {
    // Implement your email marketing deletion logic
    console.log(`Deleting from email marketing: ${identifier}`);
  }

  /**
   * Utility functions
   */
  async listAllForms() {
    const response = await fetch(`${this.baseUrl}/forms`, {
      headers: {
        Authorization: `Bearer ${this.apiToken}`,
      },
    });

    if (!response.ok) {
      throw new Error('Failed to list forms');
    }

    const data = await response.json();
    return data.items || [];
  }

  groupResponsesByForm(responses) {
    return responses.reduce((groups, response) => {
      const formId = response.formId;
      if (!groups[formId]) {
        groups[formId] = [];
      }
      groups[formId].push(response);
      return groups;
    }, {});
  }

  normalizePhone(phone) {
    return phone.replace(/[^0-9]/g, '');
  }

  generateDeletionId() {
    return `del_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  }

  async sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }

  async sendDeletionConfirmation(email, deletionRecord) {
    // Implement your email notification logic
    console.log(`Sending deletion confirmation to ${email}`);
    console.log(
      `Deleted ${deletionRecord.deletedResponses.length} response groups`
    );
  }

  /**
   * Get audit log
   */
  getAuditLog() {
    return this.auditLog;
  }

  /**
   * Export audit log
   */
  exportAuditLog(format = 'json') {
    if (format === 'json') {
      return JSON.stringify(this.auditLog, null, 2);
    }
    if (format === 'csv') {
      // Implement CSV export
      return this.auditLogToCSV();
    }
    throw new Error('Unsupported export format');
  }

  auditLogToCSV() {
    const headers = [
      'ID',
      'Timestamp',
      'Identifier',
      'Status',
      'Responses Deleted',
      'Errors',
    ];
    const rows = this.auditLog.map((record) => [
      record.id,
      record.timestamp.toISOString(),
      record.identifier,
      record.status,
      record.deletedResponses.length,
      record.errors.length,
    ]);

    return [headers, ...rows].map((row) => row.join(',')).join('\n');
  }
}

// Usage example
async function main() {
  const rtbfManager = new RTBFManager({
    apiToken: process.env.TYPEFORM_API_TOKEN,
  });

  // Process a single RTBF request
  const result = await rtbfManager.processRTBFRequest({
    identifier: 'user@example.com',
    identifierType: 'email',
    formIds: [], // Empty array searches all forms
    reason: 'GDPR deletion request',
    requesterEmail: 'user@example.com',
  });

  console.log('RTBF Result:', result);

  // Export audit log
  const auditLog = rtbfManager.exportAuditLog('json');
  console.log('Audit Log:', auditLog);
}

// Export for use in other scripts
module.exports = { RTBFManager };

// Run if executed directly
if (require.main === module) {
  main().catch(console.error);
}
```

## Environment setup

Create a `.env` file:

```bash
TYPEFORM_API_TOKEN=your_token_here
DATABASE_URL=postgresql://localhost:5432/mydb
CRM_API_KEY=your_crm_key
EMAIL_API_KEY=your_email_key
```

## Package dependencies

`package.json`:

```json
{
  "name": "typeform-rtbf-automation",
  "version": "1.0.0",
  "dependencies": {
    "node-fetch": "^2.6.1",
    "dotenv": "^16.0.0"
  }
}
```

## Running the script

```bash
# Install dependencies
npm install

# Run the script
node rtbf-script.js
```

## API endpoint setup

Create an endpoint in your application:

```javascript
const express = require('express');
const { RTBFManager } = require('./rtbf-script');

const app = express();
app.use(express.json());

const rtbfManager = new RTBFManager({
  apiToken: process.env.TYPEFORM_API_TOKEN,
});

app.post('/api/rtbf/request', async (req, res) => {
  try {
    const { email } = req.body;

    // Verify the requester
    if (!(await verifyRequester(email))) {
      return res.status(401).json({ error: 'Unauthorized' });
    }

    const result = await rtbfManager.processRTBFRequest({
      identifier: email,
      identifierType: 'email',
      requesterEmail: email,
    });

    res.json(result);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.listen(3000, () => {
  console.log('RTBF API listening on port 3000');
});
```

## Scheduled cleanup

Automate data retention with cron:

```javascript
const cron = require('node-cron');

// Run daily at midnight
cron.schedule('0 0 * * *', async () => {
  console.log('Running automated data retention cleanup');

  const cutoffDate = new Date();
  cutoffDate.setDate(cutoffDate.getDate() - 365); // 1 year retention

  // Delete old responses
  await enforceRetentionPolicy(cutoffDate);
});
```

## Testing

```javascript
const { RTBFManager } = require('./rtbf-script');

describe('RTBF Manager', () => {
  let rtbfManager;

  beforeEach(() => {
    rtbfManager = new RTBFManager({
      apiToken: process.env.TEST_API_TOKEN,
    });
  });

  it('should find matching responses', async () => {
    const result = await rtbfManager.findMatchingResponses(
      'test@example.com',
      'email',
      ['test_form_id']
    );

    expect(result).toBeInstanceOf(Array);
  });

  it('should delete responses successfully', async () => {
    const result = await rtbfManager.processRTBFRequest({
      identifier: 'test@example.com',
      identifierType: 'email',
      formIds: ['test_form_id'],
    });

    expect(result.success).toBe(true);
  });
});
```

## Best practices

1. **Batch operations**: Process deletions in batches to avoid rate limits
2. **Comprehensive search**: Search across all forms and systems
3. **Audit everything**: Maintain detailed logs of all deletions
4. **Verify identity**: Always authenticate deletion requests
5. **Handle errors**: Implement robust error handling and retry logic
6. **Test thoroughly**: Test with sandbox data before production use

## Next steps

* Review [Right to Be Forgotten basics](/responses-api/right-to-be-forgotten)
* Learn about [webhooks](/webhooks-api/walkthroughs) for real-time processing
* See the [API Reference](/api-reference) for complete documentation