Webflow Data Handling
Overview
Handle sensitive data correctly when working with the Webflow Data API v2. Covers
PII in form submissions, ecommerce customer data, CMS content classification,
GDPR/CCPA compliance patterns, and data retention policies.
Prerequisites
- Understanding of GDPR/CCPA requirements
- Webflow API token with
forms:read, ecommerce:read scopes
- Database for audit logging
- Scheduled job infrastructure for data cleanup
Webflow Data Classification
| Source |
Data Type |
PII Risk |
Handling |
| Form submissions |
Email, name, phone, message |
High |
Encrypt at rest, redact in logs |
| Ecommerce orders |
Name, email, address, payment |
High |
Never log, minimal retention |
| CMS items |
Blog posts, team bios, products |
Low-Medium |
May contain names/photos |
| Site analytics |
Page views, sessions |
Low |
Aggregate when possible |
| API tokens |
Access credentials |
Critical |
Never log, rotate quarterly |
Instructions
Step 1: PII Detection in Form Submissions
const PII_PATTERNS = [
{ type: "email", regex: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g },
{ type: "phone", regex: /\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/g },
{ type: "ssn", regex: /\b\d{3}-\d{2}-\d{4}\b/g },
{ type: "credit_card", regex: /\b\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b/g },
];
function detectPII(text: string): Array<{ type: string; found: boolean }> {
return PII_PATTERNS.map(p => ({
type: p.type,
found: p.regex.test(text),
})).filter(r => r.found);
}
// Scan form submissions for PII before logging
async function processFormSubmission(formId: string) {
const { formSubmissions } = await webflow.forms.listSubmissions(formId);
for (const sub of formSubmissions || []) {
const rawData = JSON.stringify(sub.formData);
const piiFindings = detectPII(rawData);
if (piiFindings.length > 0) {
console.warn(`PII detected in submission ${sub.id}: ${piiFindings.map(f => f.type).join(", ")}`);
// Log redacted version only
console.log("Form data:", redactPII(sub.formData || {}));
}
}
}
Step 2: PII Redaction
function redactPII(data: Record<string, any>): Record<string, any> {
const sensitiveFields = new Set([
"email", "phone", "telephone", "mobile", "ssn",
"password", "credit-card", "card-number", "address",
"full-name", "first-name", "last-name",
]);
const redacted: Record<string, any> = {};
for (const [key, value] of Object.entries(data)) {
const normalizedKey = key.toLowerCase().replace(/[\s_]/g, "-");
if (sensitiveFields.has(normalizedKey)) {
redacted[key] = "[REDACTED]";
} else if (typeof value === "string") {
// Redact inline PII patterns
let cleaned = value;
for (const pattern of PII_PATTERNS) {
cleaned = cleaned.replace(pattern.regex, `[${pattern.type.toUpperCase()}_REDACTED]`);
}
redacted[key] = cleaned;
} else {
redacted[key] = value;
}
}
return redacted;
}
// Usage in logging
async function logFormData(formData: Record<string, any>) {
console.log("Form submission (redacted):", redactPII(formData));
}
Step 3: Ecommerce Data Handling
// Order data contains high-sensitivity PII
async function processOrder(siteId: string, orderId: string) {
const order = await webflow.orders.get(siteId, orderId);
// NEVER log full customer info
const safeOrderLog = {
orderId: order.orderId,
status: order.status,
itemCount: order.purchasedItems?.length,
totalCents: order.customerPaid?.value,
// Redact customer info
customer: {
hasEmail: !!order.customerInfo?.email,
hasAddress: !!order.shippingAddress,
// Never: order.customerInfo?.email
// Never: order.shippingAddress?.addressLine1
},
createdAt: order.acceptedOn,
};
console.log("Order processed:", safeOrderLog);
}
Step 4: GDPR — Data Subject Access Request (DSAR)
interface DataExport {
source: string;
exportedAt: string;
requestedBy: string;
data: {
formSubmissions: Array<{ formName: string; submittedAt: string; data: Record<string, any> }>;
orders: Array<{ orderId: string; status: string; total: number; items: string[] }>;
};
}
async function exportUserData(siteId: string, userEmail: string): Promise<DataExport> {
const exportData: DataExport = {
source: "Webflow",
exportedAt: new Date().toISOString(),
requestedBy: userEmail,
data: { formSubmissions: [], orders: [] },
};
// 1. Find form submissions by email
const { forms } = await webflow.forms.list(siteId);
for (const form of forms || []) {
const { formSubmissions } = await webflow.forms.listSubmissions(form.id!);
for (const sub of formSubmissions || []) {
const formData = sub.formData || {};
// Check all fields for matching email
const hasEmail = Object.values(formData).some(
v => typeof v === "string" && v.toLowerCase() === userEmail.toLowerCase()
);
if (hasEmail) {
exportData.data.formSubmissions.push({
formName: form.displayName!,
submittedAt: sub.submittedAt!,
data: formData,
});
}
}
}
// 2. Find orders by email
const { orders } = await webflow.orders.list(siteId);
for (const order of orders || []) {
if (order.customerInfo?.email?.toLowerCase() === userEmail.toLowerCase()) {
exportData.data.orders.push({
orderId: order.orderId!,
status: order.status!,
total: (order.customerPaid?.value || 0) / 100,
items: order.purchasedItems?.map(i => i.productName || "Unknown") || [],
});
}
}
return exportData;
}
Step 5: GDPR — Right to Deletion
async function deleteUserData(
siteId: string,
userEmail: string
): Promise<{ deleted: string[]; retained: string[] }> {
const result = { deleted: [] as string[], retained: [] as string[] };
// Note: Webflow API does not currently support deleting form submissions
// via API. You must delete them through the Webflow dashboard.
// However, you can delete your local copies:
// 1. Delete local form submission copies
await db.formSubmissions.deleteMany({ email: userEmail, source: "webflow" });
result.deleted.push("Local form submission copies");
// 2. Delete local order copies (keep anonymized for accounting)
await db.orders.updateMany(
{ email: userEmail, source: "webflow" },
{ $set: { email: "[DELETED]", name: "[DELETED]", address: "[DELETED]" } }
);
result.retained.push("Anonymized order records (legal requirement)");
// 3. Audit log (required — never delete audit logs)
await db.auditLog.insertOne({
action: "GDPR_DELETION",
email: userEmail,
service: "webflow",
timestamp: new Date(),
deletedSources: result.deleted,
retainedSources: result.retained,
});
result.retained.push("Audit log entry");
return result;
}
Step 6: Data Retention Policy
| Data Type |
Retention |
Reason |
Auto-Cleanup |
| Form submissions |
90 days |
Business need |
Yes |
| Order records |
7 years |
Tax/accounting |
No |
| API call logs |
30 days |
Debugging |
Yes |
| Error logs |
90 days |
Root cause analysis |
Yes |
| Audit logs |
7 years |
Compliance |
No |
| Cached CMS content |
24 hours |
Performance |
Yes (TTL) |
async function cleanupExpiredData() {
const now = new Date();
// Delete form submissions older than 90 days
const formCutoff = new Date(now);
formCutoff.setDate(formCutoff.getDate() - 90);
await db.formSubmissions.deleteMany({
source: "webflow",
createdAt: { $lt: formCutoff },
type: { $nin: ["audit", "compliance"] },
});
// Delete API logs older than 30 days
const logCutoff = new Date(now);
logCutoff.setDate(logCutoff.getDate() - 30);
await db.apiLogs.deleteMany({
service: "webflow",
createdAt: { $lt: logCutoff },
});
console.log("Data cleanup completed");
}
// Schedule daily at 3 AM
// cron: "0 3 * * *"
Output
- PII detection for form submissions and order data
- Redaction layer for logging sensitive Webflow data
- GDPR DSAR export (forms + orders by email)
- Right to deletion with audit trail
- Data retention policy with automated cleanup
Error Handling
| Issue |
Cause |
Solution |
| PII in logs |
Missing redaction wrapper |
Wrap all logging with redactPII() |
| DSAR incomplete |
Not scanning all forms |
Iterate all forms in site |
| Deletion failed |
No API for form deletion |
Delete via Webflow dashboard |
| Audit gap |
Missing log entries |
Ensure audit logging in all deletion paths |
Resources
Next Steps
For enterprise access control, see webflow-enterprise-rbac.