How to add OpenAI text completion to a Webflow form with a Cloud App

Learn how to build a Webflow OpenAI form that processes text using the Responses API.

How to add OpenAI text completion to a Webflow form with a Cloud App

Colin Lateano
Developer Evangelist
View author profile
Colin Lateano
Developer Evangelist
View author profile
Table of contents

You can connect a Webflow form to the OpenAI API in one round trip. The trick is a Webflow Cloud App Route Handler that processes the API call on the server side, so your key never reaches the browser, and the response arrives without a redirect.

Calling the OpenAI API directly from Webflow's page-level custom code exposes your key in the browser. A Webflow Cloud App Route Handler solves this: the form submits, the handler calls OpenAI with your key stored in a server-side environment variable, and the completion renders on the page in a single round trip.

In this guide, we'll walk through the full setup: installing the OpenAI SDK in the Cloud App, building the Route Handler that handles the API call, scaffolding the form in the Webflow Designer, and writing the custom JavaScript that connects everything.

What do you need to add OpenAI text completion to a Webflow form?

You need a Webflow Cloud App, an OpenAI API key, a form in the Webflow Designer, and a small block of custom JavaScript.

Here are the requirements:

  1. A Webflow site on a paid plan (required for Webflow Cloud)
  2. A Webflow Cloud App already scaffolded (run webflow auth login && webflow cloud init if you haven't done this yet)
  3. An OpenAI account with an API key
  4. Node.js 20.0.0 or higher and npm installed

The Cloud App side adds one Route Handler and one environment variable. The Webflow site side adds a form and about 25 lines of custom code.

One thing I want to address upfront: never call the OpenAI API directly from Webflow's page-level custom code. That exposes your API key in the browser, where anyone can find it in the network inspector and use it to drain your account.

5 steps to add OpenAI text completion to a Webflow form

These five steps split cleanly into two workstreams: server-side work in the Cloud App (Steps 1-3) and front-end work in Webflow (Steps 4 and 5).

The form lives in your Webflow site. The OpenAI logic lives in the Cloud App Route Handler. Custom JavaScript on the Webflow page bridges the two: it intercepts the form submit, calls the Route Handler, and writes the completion back into the page.

Here's the sequence to set it up.

1. Install the OpenAI SDK in the Cloud App

The openainpm package is the only new dependency. It gives you a typed client for the Responses API that runs natively in Cloudflare Workers.

In your Cloud App project root:

npm install openai

The openai package is the official TypeScript SDK. Versions 4 and above use the Web Fetch API for HTTP requests rather than Node.js built-ins, which means they run in V8 isolates, the runtime Cloudflare Workers uses. No polyfills, no compatibility flags needed.

After installation, verify that the package appeared in package.json under dependencies. The version should be ^4 or higher.

2. Add the OpenAI API key to your environment

The API key goes in two places:

  • .env.local for local development
  • Webflow Cloud's environment panel for production

The key must stay server-side only. No NEXT_PUBLIC_ prefix, ever.

Add to .env.local in your Cloud App project:

OPENAI_API_KEY=sk-proj-your_key_here

OPENAI_API_KEY has no NEXT_PUBLIC_ prefix. It stays server-side only and is never sent to the browser. If you accidentally prefix it with NEXT_PUBLIC_, the key will appear in your production JavaScript bundle.

Before deploying, add the same key to your Webflow Cloud environment. Go to your Webflow site settings, navigate to Webflow Cloud, open your Cloud environment, and add OPENAI_API_KEY under Environment Variables.

3. Build the OpenAI completion Route Handler

This is the entire backend. The Route Handler receives a prompt from the Webflow page, calls the OpenAI Responses API with your server-side key, and returns the generated text as JSON. The Webflow custom code never touches the API key.

A word on abuse risk: Once this endpoint is deployed, it will be publicly accessible. Anyone who finds the URL can call it, supply a prompt, and charge tokens to your OpenAI account.

Two mitigations are built into the code below:

  • First, the system prompt is hard-coded on the server side; callers can't override the model's instructions from the client.
  • Second, the handler checks the request referrer and rejects calls that don't originate from your domain.

Neither protection is bulletproof (referrer headers can be spoofed), but together they raise the bar significantly. If you're running a high-traffic form, add Cloudflare rate limiting on top.

Create app/api/complete/route.ts in your Cloud App:

// app/api/complete/route.ts
export const runtime = 'edge'

import OpenAI from 'openai'

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
})

// Hard-code all system prompts here. Never accept instructions from the client.
// If you have multiple forms, add an entry per form ID and use a hidden field
// in your Webflow form to pass the matching key (see Step 5).
const SYSTEM_PROMPTS: Record<string, string> = {
  default: 'You are a helpful assistant.',
  // product: 'You write punchy, benefit-first product copy in 50 words or fewer.',
  // bio: 'You write first-person professional bios for LinkedIn.',
}

const ALLOWED_ORIGIN = process.env.ALLOWED_ORIGIN // e.g. 'https://your-site.webflow.io'

export async function POST(request: Request) {
  // Referrer check — rejects requests that don't come from your domain.
  // Not foolproof, but filters opportunistic abuse.
  const referrer = request.headers.get('referer') ?? ''
  if (ALLOWED_ORIGIN && !referrer.startsWith(ALLOWED_ORIGIN)) {
    return Response.json({ error: 'Forbidden' }, { status: 403 })
  }

  const { prompt, formId } = await request.json()

  if (!prompt || typeof prompt !== 'string' || prompt.trim().length === 0) {
    return Response.json({ error: 'Prompt is required' }, { status: 400 })
  }

  // formId selects a pre-approved system prompt. Unknown IDs fall back to default.
  const systemPrompt = SYSTEM_PROMPTS[formId] ?? SYSTEM_PROMPTS.default

  const response = await openai.responses.create({
    model: 'gpt-4o',
    instructions: systemPrompt,
    input: prompt.trim(),
  })

  return Response.json({ text: response.output_text })
}

Add ALLOWED_ORIGIN as an environment variable in Webflow Cloud alongside OPENAI_API_KEY — set it to your published site's domain (e.g. https://your-site.webflow.io).

A few things worth noting in this code:

  • export const runtime = 'edge' tells Next.js to run this handler in Cloudflare Workers rather than a Node.js server.
  • SYSTEM_PROMPTS is a server-side map. System instructions are never accepted from the client; a caller can choose which pre-approved prompt to use via formId, but cannot supply the instruction text itself. Add one entry per form on your site.
  • ALLOWED_ORIGIN gates requests by referrer. Set it in your Cloud environment to your published domain. Remove it only during local testing.
  • openai.responses.create() is OpenAI's current recommended API for text generation. OpenAI recommends the Responses API over Chat Completions for all new projects.
  • output_text aggregates all text output from the model into a single string.

For a simple text completion task, that's everything you need.

Testing the Route Handler locally before touching Webflow

Before wiring up the Webflow form, confirm the Route Handler works on its own. Run this from your terminal while your Cloud App dev server is running:

curl -X POST http://localhost:3000/app/api/complete \
  -H "Content-Type: application/json" \
  -H "Referer: https://your-site.webflow.io" \
  -d '{"prompt": "Write a 50-word product description for noise-cancelling headphones.", "formId": "product"}'

The Referer header is required locally if ALLOWED_ORIGIN is set. Pass the formId key that matches one of your SYSTEM_PROMPTS entries, or omit it to use the default prompt.

At this point, the AI layer is complete and independently testable.

4. Build the OpenAI prompt form in Webflow Designer

The form needs four elements:

  • A textarea for the prompt
  • A submit button
  • A result container div
  • A loading indicator

The element IDs you set here are what the custom JavaScript targets. Get them right, and the wiring in Step 5 is trivial.

Add a Text Area element and set its ID to ai-prompt in the element settings. This is where users type their request.

Add a Div Block with the ID ai-result and set its Display to None by default. This is where the generated text will appear. Inside it, add a Text Block with the ID ai-output for the actual content, and a second Text Block with the ID ai-loading that reads "Generating...". You'll show and hide both via JavaScript.

The form's Submit Button can stay as-is. The JavaScript will intercept the submit event before Webflow's own form processor handles it.

If you have more than one AI-powered form on your site, add a Hidden Field element inside each form block. Set its ID to form-id and its default value to a string that matches the corresponding key in your SYSTEM_PROMPTS map (e.g. product, bio).

The JavaScript in Step 5 will read this field and send it as formId. This is how you run multiple forms with different behaviors from a single Route Handler without making the system prompt configurable by the caller.

No changes to the form's action URL or method are needed in the Designer. The interception happens at the JavaScript level.

5. Add the Webflow custom code to call the Route Handler

This script intercepts the form submit, calls the Cloud App Route Handler with the prompt, and writes the OpenAI response into the result container. It goes in your Webflow page settings under Footer code (Before </body> tag):

<script>
const form = document.querySelector('[data-name="your-form-name"]');
const promptField = document.getElementById('ai-prompt');
const formIdField = document.getElementById('form-id'); // optional: hidden field
const resultContainer = document.getElementById('ai-result');
const outputText = document.getElementById('ai-output');
const loadingText = document.getElementById('ai-loading');

if (form) {
  form.addEventListener('submit', async (event) => {
    event.preventDefault();

    const prompt = promptField.value.trim();
    if (!prompt) return;

    // Read formId from a hidden field if present, otherwise send nothing (Route Handler defaults to 'default').
    const formId = formIdField ? formIdField.value : 'default';

    resultContainer.style.display = 'block';
    loadingText.style.display = 'block';
    outputText.textContent = '';
    resultContainer.scrollIntoView({ behavior: 'smooth' });

    try {
      const response = await fetch('/app/api/complete', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ prompt, formId }),
      });

      if (!response.ok) {
        throw new Error(`Request failed: ${response.status}`);
      }

      const data = await response.json();
      outputText.textContent = data.text;
    } catch (error) {
      outputText.textContent = 'Something went wrong. Please try again.';
      console.error('Completion error:', error);
    } finally {
      loadingText.style.display = 'none';
    }
  });
}
</script>

Replace YOUR_CLOUD_APP_DOMAIN.webflow.io with your actual domain if the Cloud App is on a separate hostname, or use a relative /app/api/complete path if the Cloud App is mounted on the same domain.

The data-name selector on the form block targets the Webflow form by its form name attribute, which you set in the Form Block settings in the Designer. Using data-name is more reliable than targeting by class, since Webflow sometimes regenerates class names. Replace "your-form-name" with whatever name you've given the form.

event.preventDefault() is doing the critical work here. Without it, clicking Submit triggers Webflow's native form submission, which sends the data to its form-processing endpoint and redirects to the success state. Your fetch call never fires. With it, the entire form submit is handed to your async function instead.

The system prompt is not defined in this script. It lives in the Route Handler's SYSTEM_PROMPTS map on the server side, where it can't be overridden by a caller. The formId value this script sends selects which approved prompt applies, nothing more.

To add a new behavior, add an entry to SYSTEM_PROMPTS in the Route Handler and set the matching formId value in the corresponding Webflow form's hidden field.

Deploy when ready:

webflow auth login
webflow cloud deploy

Or push to the connected GitHub branch to trigger an automatic deployment.

The form is live. Type a prompt, click Submit, and the completion appears on the page without a redirect or reload.

What causes the OpenAI form integration to fail on Webflow?

Almost every failure falls into one of four buckets: wrong fetch path, broken form event interception, missing environment variable, or an empty model response.

Here's how to diagnose each one.

Thefetchcall is hitting the wrong path

If your Cloud App's basePath in next.config.ts is set to /app, the Route Handler is at /app/api/complete on your domain. If the basePath is something else, update the fetch URL to match. Open the browser's network inspector while submitting. The failed request shows the exact URL being hit and the HTTP status code.

event.preventDefault()is not running

If the page redirects to Webflow's success state on submit, the script either isn't loading (check the Custom Code panel for syntax errors) or the form selector isn't matching. Confirm the data-name attribute in the script matches the Form Name in the Webflow Designer's form settings exactly, including capitalization and spaces.

OPENAI_API_KEYnot set in the Cloud environment

The Route Handler returns a 500, and the browser console shows an OpenAI authentication error. Confirm that the key is present in Webflow Cloud → Environment Variables, then redeploy. Environment variables only take effect after a fresh deployment. They are not picked up mid-session.

The model returns an emptyoutput_text

This is rare but happens when the prompt triggers OpenAI's content policy and the model refuses to respond. The output_text property returns an empty string in this case rather than an error. Add a check: if (!data.text) { outputText.textContent = 'No response generated. Try rephrasing your prompt.'; }.

The referrer check is blocking your own form

If the Route Handler returns 403 on a live form but curl tests pass locally, the ALLOWED_ORIGIN environment variable doesn't match your published domain exactly. Check for a trailing slash mismatch (https://your-site.webflow.io vs https://your-site.webflow.io/) or a staging subdomain that differs from production.

Update ALLOWED_ORIGIN in Webflow Cloud to match the exact origin the browser sends, then redeploy.

Where to take your Webflow Cloud and AI models integration next

You now have a Webflow form that calls the OpenAI server-side API and renders the result on the page without a redirect or reload. From here, the most common extension is streaming. Rather than waiting for the full completion before displaying anything, the response appears word by word.

Explore Webflow + ChatGPT for streaming responses and multi-turn conversation history in your Webflow forms.

Explore Webflow + Claude for long-context analysis, document processing, and reasoning-heavy completions.

Explore Webflow + Gemini for multimodal inputs that combine image and text prompts in a single form.

Frequently asked questions

Can I use gpt-4o or gpt-4o-mini instead of gpt-5.5?

Yes. Swap the model string in the Route Handler: model: 'gpt-5.4-mini' for lower cost and faster latency, or model: 'gpt-5.4-nano' for the fastest and most affordable option on simple tasks. I use gpt-5.5 as the default in this guide because it's OpenAI's current flagship model. The full OpenAI model list shows all available options, along with context window and pricing details. If your use case is simple (bio generator, short copy, bullet points), gpt-5.4-mini is often enough and significantly cheaper per token.

Does this work for Webflow sites without a Cloud App?

Not with a server-side API key, which is the safe approach. Without a Cloud App, you'd need an external backend to proxy the OpenAI call. Some teams use a separate Next.js app or a Cloudflare Worker standalone (not through Webflow Cloud). A Webflow Cloud App is the cleanest path if you're already on the Webflow platform. There's no separate hosting, no CORS configuration, and the Route Handler deploys alongside your Webflow site.

Why use the Responses API instead of Chat Completions?

OpenAI's text generation documentation now recommends the Responses API over Chat Completions for all new projects. The Responses API has a simpler interface (a single input parameter instead of a messages array), better support for reasoning models, and an output_text convenience property that automatically aggregates all text output. Chat Completions still work, but the Responses API is where OpenAI is focusing on new features.


Last Updated
May 22, 2026
Category

Related articles

How to add a Calendly popup modal to Webflow and keep visitors on-site
How to add a Calendly popup modal to Webflow and keep visitors on-site

How to add a Calendly popup modal to Webflow and keep visitors on-site

How to add a Calendly popup modal to Webflow and keep visitors on-site

Development
By
Colin Lateano
,
,
Read article
How to send Webflow form submissions to HubSpot via Zapier without losing data
How to send Webflow form submissions to HubSpot via Zapier without losing data

How to send Webflow form submissions to HubSpot via Zapier without losing data

How to send Webflow form submissions to HubSpot via Zapier without losing data

Development
By
Colin Lateano
,
,
Read article
How to customize Webflow form notification emails for every form on your site
How to customize Webflow form notification emails for every form on your site

How to customize Webflow form notification emails for every form on your site

How to customize Webflow form notification emails for every form on your site

Development
By
Colin Lateano
,
,
Read article
How to replace Webflow's default order email with a fully custom SendGrid template
How to replace Webflow's default order email with a fully custom SendGrid template

How to replace Webflow's default order email with a fully custom SendGrid template

How to replace Webflow's default order email with a fully custom SendGrid template

Development
By
Colin Lateano
,
,
Read article

verifone logomonday.com logospotify logoted logogreenhouse logoclear logocheckout.com logosoundcloud logoreddit logothe new york times logoideo logoupwork logodiscord logo
verifone logomonday.com logospotify logoted logogreenhouse logoclear logocheckout.com logosoundcloud logoreddit logothe new york times logoideo logoupwork logodiscord logo

Get started for free

Try Webflow for as long as you like with our free Starter plan. Purchase a paid Site plan to publish, host, and unlock additional features.

Get started — it’s free
Watch demo

Try Webflow for as long as you like with our free Starter plan. Purchase a paid Site plan to publish, host, and unlock additional features.