Cloudflare Workers
Learn how to handle form submissions in Cloudflare Workers and Pages using the Forminit SDK.
What is Forminit?
Section titled “What is Forminit?”Forminit is a form backend service that handles everything you need for web forms: submissions, storage, validation, and notifications. Instead of setting up databases, building validation logic, and configuring email services, Forminit provides all of this through a simple API.
Your Cloudflare Worker acts as a secure proxy, keeping your API key hidden while forwarding form submissions to Forminit.
Why Use Forminit with Cloudflare?
Section titled “Why Use Forminit with Cloudflare?”Benefits for your forms:
- No database required - Submissions are stored securely in Forminit’s infrastructure
- Built-in email notifications - Get notified instantly when forms are submitted
- Automatic validation - Email, phone, URL, and country fields are validated server-side
- File upload handling - Accept files up to 25 MB without configuring storage
- Spam protection - Integrates with reCAPTCHA, hCaptcha, and honeypot
- UTM tracking - Automatically capture marketing attribution data
- Dashboard access - View, search, and export submissions from a web interface
Benefits of Cloudflare Workers & Pages:
- Global edge deployment - Low latency responses worldwide
- No cold starts - Instant execution, no waiting for containers to spin up
- Cost effective - Generous free tier with 100,000 requests/day
- Simple deployment - Deploy with
wrangler deployor git push
Prerequisites
Section titled “Prerequisites”Before integrating Forminit with Cloudflare Workers:
- Create a Forminit account at forminit.com
- Create a form in your dashboard and copy the Form ID
- Create an API key from Account > API Tokens
- Install Wrangler (Cloudflare’s CLI tool):
npm install -g wrangler
Quick Start
Section titled “Quick Start”1. Create a New Worker Project
Section titled “1. Create a New Worker Project”npm create cloudflare@latest forminit-worker
cd forminit-worker
Select “Hello World” worker when prompted.
2. Install the Forminit SDK
Section titled “2. Install the Forminit SDK”npm install forminit
3. Set Your API Key as a Secret
Section titled “3. Set Your API Key as a Secret”wrangler secret put FORMINIT_API_KEY
Enter your Forminit API key when prompted. This keeps your key secure and out of your code.
4. Set Authentication Mode to Protected
Section titled “4. Set Authentication Mode to Protected”In your Forminit dashboard, go to Form Settings and set authentication mode to Protected. This requires the API key for submissions.
5. Create the Worker
Section titled “5. Create the Worker”Replace the contents of src/index.ts:
import { Forminit } from 'forminit';
export interface Env {
FORMINIT_API_KEY: string;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// CORS headers for browser requests
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
};
// Handle preflight requests
if (request.method === 'OPTIONS') {
return new Response(null, { headers: corsHeaders });
}
// Only allow POST requests
if (request.method !== 'POST') {
return new Response('Method not allowed', { status: 405 });
}
try {
const body = await request.json() as { formId: string; blocks: unknown[] };
const { formId, blocks } = body;
if (!formId) {
return Response.json(
{ success: false, error: 'MISSING_FORM_ID', message: 'Form ID is required' },
{ status: 400, headers: corsHeaders }
);
}
// Initialize the Forminit SDK
const forminit = new Forminit({
apiKey: env.FORMINIT_API_KEY,
});
// Set user info for accurate geolocation and analytics
forminit.setUserInfo({
ip: request.headers.get('cf-connecting-ip') || undefined,
userAgent: request.headers.get('user-agent') || undefined,
referer: request.headers.get('referer') || undefined,
});
// Submit to Forminit
const { data, redirectUrl, error } = await forminit.submit(formId, { blocks });
if (error) {
return Response.json(
{ success: false, error: error.error, message: error.message, code: error.code },
{ status: error.code || 400, headers: corsHeaders }
);
}
return Response.json(
{ success: true, submission: data, redirectUrl },
{ status: 200, headers: corsHeaders }
);
} catch (err) {
return Response.json(
{ success: false, error: 'WORKER_ERROR', message: 'Failed to process submission' },
{ status: 500, headers: corsHeaders }
);
}
},
};
6. Deploy
Section titled “6. Deploy”wrangler deploy
Your worker is now live at https://forminit-worker.<your-subdomain>.workers.dev
Sending Submissions from Your Frontend
Section titled “Sending Submissions from Your Frontend”Basic Example
Section titled “Basic Example”async function submitForm(formData) {
const response = await fetch('https://forminit-worker.your-subdomain.workers.dev', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
formId: 'YOUR_FORM_ID',
blocks: [
{
type: 'sender',
properties: {
email: formData.email,
firstName: formData.firstName,
lastName: formData.lastName,
},
},
{
type: 'text',
name: 'message',
value: formData.message,
},
],
}),
});
const result = await response.json();
if (result.success) {
console.log('Submission ID:', result.submission.hashId);
} else {
console.error('Error:', result.message);
}
}
HTML Form Example
Section titled “HTML Form Example”<form id="contact-form">
<input type="text" id="firstName" placeholder="First name" required />
<input type="text" id="lastName" placeholder="Last name" required />
<input type="email" id="email" placeholder="Email" required />
<textarea id="message" placeholder="Message" required></textarea>
<button type="submit">Send</button>
</form>
<script>
document.getElementById('contact-form').addEventListener('submit', async (e) => {
e.preventDefault();
const response = await fetch('https://your-worker.workers.dev', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
formId: 'YOUR_FORM_ID',
blocks: [
{
type: 'sender',
properties: {
email: document.getElementById('email').value,
firstName: document.getElementById('firstName').value,
lastName: document.getElementById('lastName').value,
},
},
{
type: 'text',
name: 'message',
value: document.getElementById('message').value,
},
],
}),
});
const result = await response.json();
if (result.success) {
alert('Message sent!');
e.target.reset();
} else {
alert('Error: ' + result.message);
}
});
</script>
Handling File Uploads
Section titled “Handling File Uploads”For file uploads, you need to handle multipart/form-data. Here’s an extended worker:
import { Forminit } from 'forminit';
export interface Env {
FORMINIT_API_KEY: string;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
};
if (request.method === 'OPTIONS') {
return new Response(null, { headers: corsHeaders });
}
if (request.method !== 'POST') {
return new Response('Method not allowed', { status: 405 });
}
try {
const contentType = request.headers.get('content-type') || '';
const forminit = new Forminit({
apiKey: env.FORMINIT_API_KEY,
});
forminit.setUserInfo({
ip: request.headers.get('cf-connecting-ip') || undefined,
userAgent: request.headers.get('user-agent') || undefined,
referer: request.headers.get('referer') || undefined,
});
let formId: string;
let payload: FormData | { blocks: unknown[] };
if (contentType.includes('multipart/form-data')) {
// Handle file uploads with FormData
const formData = await request.formData();
formId = formData.get('formId') as string;
formData.delete('formId');
payload = formData;
} else {
// Handle JSON submissions
const body = await request.json() as { formId: string; blocks: unknown[] };
formId = body.formId;
payload = { blocks: body.blocks };
}
if (!formId) {
return Response.json(
{ success: false, error: 'MISSING_FORM_ID', message: 'Form ID is required' },
{ status: 400, headers: corsHeaders }
);
}
const { data, redirectUrl, error } = await forminit.submit(formId, payload);
if (error) {
return Response.json(
{ success: false, error: error.error, message: error.message, code: error.code },
{ status: error.code || 400, headers: corsHeaders }
);
}
return Response.json(
{ success: true, submission: data, redirectUrl },
{ status: 200, headers: corsHeaders }
);
} catch (err) {
return Response.json(
{ success: false, error: 'WORKER_ERROR', message: 'Failed to process submission' },
{ status: 500, headers: corsHeaders }
);
}
},
};
Response Structure
Section titled “Response Structure”Success Response
Section titled “Success Response”{
"success": true,
"redirectUrl": "https://forminit.com/thank-you",
"submission": {
"hashId": "7LMIBoYY74JOCp1k",
"date": "2026-01-17 14:30:00",
"blocks": {
"sender": {
"firstName": "John",
"lastName": "Doe",
"email": "[email protected]"
},
"message": "Hello world"
}
}
}
| Field | Type | Description |
|---|---|---|
success | boolean | Always true on success |
redirectUrl | string | Thank you page URL |
submission.hashId | string | Unique submission identifier |
submission.date | string | Timestamp (YYYY-MM-DD HH:mm:ss) |
submission.blocks | object | All submitted field values |
Error Response
Section titled “Error Response”{
"success": false,
"error": "FI_SCHEMA_FORMAT_EMAIL",
"code": 400,
"message": "Invalid email format for field: 'contact'. Please enter a valid email address."
}
Common Error Codes
Section titled “Common Error Codes”| Error Code | Status | Description |
|---|---|---|
FORM_NOT_FOUND | 404 | Form ID doesn’t exist |
FORM_DISABLED | 403 | Form is disabled |
MISSING_API_KEY | 401 | API key not provided |
EMPTY_SUBMISSION | 400 | No fields submitted |
FI_SCHEMA_FORMAT_EMAIL | 400 | Invalid email format |
FI_RULES_PHONE_INVALID | 400 | Invalid phone format |
TOO_MANY_REQUESTS | 429 | Rate limit exceeded |
Form Blocks Reference
Section titled “Form Blocks Reference”Forminit uses a block-based system to structure form data. Each submission contains an array of blocks representing different field types.
Quick reference:
| Block Type | Description |
|---|---|
sender | Submitter info (email, name, phone) - required |
text | Free-form text |
number | Numeric value |
email | Additional email (not sender’s) |
phone | Phone in E.164 format |
select | Single or multi-select |
rating | Rating 1-5 |
date | ISO 8601 date |
file | File upload |
country | ISO country code |
For complete documentation on all block types, validation rules, and examples, see the Form Blocks Reference.
Security Best Practices
Section titled “Security Best Practices”-
Store API keys as secrets - Use
wrangler secret putinstead of environment variables inwrangler.toml -
Validate input - Check that required fields exist before forwarding to Forminit
-
Use CORS appropriately - Restrict
Access-Control-Allow-Originto your domain in production:'Access-Control-Allow-Origin': 'https://yourdomain.com' -
Forward client IP - Always include user info via
setUserInfo()for accurate geolocation and analytics -
Handle errors gracefully - Never expose internal errors to clients
Environment Variables
Section titled “Environment Variables”Add these to your wrangler.toml for non-sensitive configuration:
name = "forminit-worker"
main = "src/index.ts"
compatibility_date = "2024-01-01"
[vars]
ALLOWED_ORIGIN = "https://yourdomain.com"
For your API key, always use secrets:
wrangler secret put FORMINIT_API_KEY
Using Cloudflare Pages
Section titled “Using Cloudflare Pages”If you’re hosting your frontend on Cloudflare Pages, you have two options:
Option 1: Public Mode (No Server Code)
Section titled “Option 1: Public Mode (No Server Code)”For simple forms that don’t need API key protection, you can use Forminit directly from your frontend with the CDN SDK.
1. Set Authentication Mode to Public
In your Forminit dashboard, go to Form Settings and set authentication mode to Public.
2. Add the SDK to Your HTML
<script src="https://forminit.com/sdk/v1/forminit.js"></script>
3. Submit Forms Directly
<form id="contact-form">
<input type="text" name="fi-sender-firstName" placeholder="First name" required />
<input type="text" name="fi-sender-lastName" placeholder="Last name" required />
<input type="email" name="fi-sender-email" placeholder="Email" required />
<textarea name="fi-text-message" placeholder="Message" required></textarea>
<button type="submit">Send</button>
</form>
<p id="form-result"></p>
<script>
const forminit = new Forminit();
const FORM_ID = 'YOUR_FORM_ID';
document.getElementById('contact-form').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const { data, error } = await forminit.submit(FORM_ID, formData);
if (error) {
document.getElementById('form-result').textContent = error.message;
return;
}
document.getElementById('form-result').textContent = 'Message sent!';
e.target.reset();
});
</script>
This approach is simpler and works great for basic contact forms.
Option 2: Pages Functions (Protected Mode)
Section titled “Option 2: Pages Functions (Protected Mode)”If you need to keep your API key secure, use Pages Functions. These are Workers that run alongside your Pages site.
1. Create the Function
Create functions/api/forminit.ts in your Pages project:
import { Forminit } from 'forminit';
interface Env {
FORMINIT_API_KEY: string;
}
export const onRequestPost: PagesFunction<Env> = async (context) => {
const { request, env } = context;
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
};
try {
const body = await request.json() as { formId: string; blocks: unknown[] };
const { formId, blocks } = body;
if (!formId) {
return Response.json(
{ success: false, error: 'MISSING_FORM_ID', message: 'Form ID is required' },
{ status: 400, headers: corsHeaders }
);
}
const forminit = new Forminit({
apiKey: env.FORMINIT_API_KEY,
});
forminit.setUserInfo({
ip: request.headers.get('cf-connecting-ip') || undefined,
userAgent: request.headers.get('user-agent') || undefined,
referer: request.headers.get('referer') || undefined,
});
const { data, redirectUrl, error } = await forminit.submit(formId, { blocks });
if (error) {
return Response.json(
{ success: false, error: error.error, message: error.message, code: error.code },
{ status: error.code || 400, headers: corsHeaders }
);
}
return Response.json(
{ success: true, submission: data, redirectUrl },
{ status: 200, headers: corsHeaders }
);
} catch (err) {
return Response.json(
{ success: false, error: 'WORKER_ERROR', message: 'Failed to process submission' },
{ status: 500, headers: corsHeaders }
);
}
};
export const onRequestOptions: PagesFunction = async () => {
return new Response(null, {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
},
});
};
2. Install the SDK
npm install forminit
3. Add Your API Key
In the Cloudflare dashboard, go to your Pages project > Settings > Environment variables and add:
- Variable name:
FORMINIT_API_KEY - Value: Your Forminit API key
- Select “Encrypt” to keep it secure
4. Submit from Your Frontend
const response = await fetch('/api/forminit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
formId: 'YOUR_FORM_ID',
blocks: [
{
type: 'sender',
properties: {
email: '[email protected]',
firstName: 'John',
lastName: 'Doe',
},
},
{
type: 'text',
name: 'message',
value: 'Hello world',
},
],
}),
});
const result = await response.json();
Related Documentation
Section titled “Related Documentation”- Form Blocks Reference - Complete reference for all block types
- File Uploads - Detailed file upload guide
- Submit Form API - Full REST API documentation
- Node.js Integration - Node.js setup guide