Skip to navigation

RTBF Custom Script

Advanced automation for Right to Be Forgotten compliance

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:

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:

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:

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

Running the script

# Install dependencies
npm install
# Run the script
node rtbf-script.js

API endpoint setup

Create an endpoint in your application:

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:

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

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