Skip to content

Cloudflare Workers

Learn how to handle form submissions in Cloudflare Workers and Pages using the Forminit SDK.

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.


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 deploy or git push

Before integrating Forminit with Cloudflare Workers:

  1. Create a Forminit account at forminit.com
  2. Create a form in your dashboard and copy the Form ID
  3. Create an API key from Account > API Tokens
  4. Install Wrangler (Cloudflare’s CLI tool): npm install -g wrangler

npm create cloudflare@latest forminit-worker
cd forminit-worker

Select “Hello World” worker when prompted.

npm install forminit
wrangler secret put FORMINIT_API_KEY

Enter your Forminit API key when prompted. This keeps your key secure and out of your code.

In your Forminit dashboard, go to Form Settings and set authentication mode to Protected. This requires the API key for submissions.

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 }
      );
    }
  },
};
wrangler deploy

Your worker is now live at https://forminit-worker.<your-subdomain>.workers.dev


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);
  }
}
<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>

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 }
      );
    }
  },
};

{
  "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"
    }
  }
}
FieldTypeDescription
successbooleanAlways true on success
redirectUrlstringThank you page URL
submission.hashIdstringUnique submission identifier
submission.datestringTimestamp (YYYY-MM-DD HH:mm:ss)
submission.blocksobjectAll submitted field values
{
  "success": false,
  "error": "FI_SCHEMA_FORMAT_EMAIL",
  "code": 400,
  "message": "Invalid email format for field: 'contact'. Please enter a valid email address."
}

Error CodeStatusDescription
FORM_NOT_FOUND404Form ID doesn’t exist
FORM_DISABLED403Form is disabled
MISSING_API_KEY401API key not provided
EMPTY_SUBMISSION400No fields submitted
FI_SCHEMA_FORMAT_EMAIL400Invalid email format
FI_RULES_PHONE_INVALID400Invalid phone format
TOO_MANY_REQUESTS429Rate limit exceeded

Forminit uses a block-based system to structure form data. Each submission contains an array of blocks representing different field types.

Quick reference:

Block TypeDescription
senderSubmitter info (email, name, phone) - required
textFree-form text
numberNumeric value
emailAdditional email (not sender’s)
phonePhone in E.164 format
selectSingle or multi-select
ratingRating 1-5
dateISO 8601 date
fileFile upload
countryISO country code

For complete documentation on all block types, validation rules, and examples, see the Form Blocks Reference.


  1. Store API keys as secrets - Use wrangler secret put instead of environment variables in wrangler.toml

  2. Validate input - Check that required fields exist before forwarding to Forminit

  3. Use CORS appropriately - Restrict Access-Control-Allow-Origin to your domain in production:

    'Access-Control-Allow-Origin': 'https://yourdomain.com'
  4. Forward client IP - Always include user info via setUserInfo() for accurate geolocation and analytics

  5. Handle errors gracefully - Never expose internal errors to clients


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

If you’re hosting your frontend on Cloudflare Pages, you have two options:

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();