Outgrow builder forms7 min read
Framer forms: when to add a form backend, and how
Framer forms already send to email, Sheets or a webhook; add Ikiform as the webhook for stored, filtered submissions, team alerts and more destinations.
Framer's native forms send each submission to email, Google Sheets or a webhook, with spam protection built in, so for one inbox or one sheet you may not need anything else. Add Ikiform as the webhook destination when you want every submission stored with a spam status, validated against your fields, emailed to the team and forwarded to several signed webhooks. Because Framer posts from its servers, either loosen the form's origin checks or put a small relay in between that verifies Framer's signature.
What's different on Framer
Framer is the one builder in this guide without a limit to escape. Its forms can send to email, Google Sheets or any webhook, and Framer has built-in spam protection that flags or blocks suspect submissions. Its webhooks are signed: each request carries a Framer-Signature header, an HMAC-SHA256 of the body and the submission id, and a Framer-Webhook-Submission-Id. Framer retries up to five times when your endpoint doesn't answer 2xx, and doesn't follow redirects.
So be honest about why you'd add a form backend. Here's what each side covers:
| You want | Framer alone | With Ikiform as the webhook |
|---|---|---|
| Submissions in one inbox or one sheet | Yes | Yes, plus the dashboard |
| A searchable record with spam marked, and CSV export | Your inbox or sheet is the record | Yes |
| Server-side checks on types, lengths and options | No | Yes, with a field definition |
| Alerts to several people, with Reply going to the submitter | Email destination | Up to 10 recipients, with reply-to |
| Several tools fed from one form, each retried | One webhook per destination you add | Up to 3 signed endpoints, retried with a delivery log |
| The same backend across Framer, Webflow and code sites | No | Yes |
If the first row is all you need, stay with Framer's destinations. The rest of this page is for when it isn't.
The catch: Framer posts from its servers
A webhook destination means Framer's servers send the post, not the visitor's browser. Three things change on the Ikiform side:
- No origin. Server posts carry no
Originheader, so with Allowed domains set, every post is treated as coming from the wrong site. - Shared IP addresses. Ikiform counts rate limits per IP address, and every post comes from a few Framer addresses instead of your visitors'.
- No honeypot. Framer sends the inputs you built and nothing else, and its form can't run Ikiform's human check.
You can accept that, or fix it with a relay. Both paths are below.
- Direct: a visitor submits the Framer form, Framer's servers send it as JSON with no Origin header, and Ikiform accepts it with Allowed domains empty and a Relaxed rate limit.
- Relay: Framer's servers sign every post with your secret and send it to a Worker you deploy.
- The Worker recomputes Framer's signature. A post without a valid signature gets a 401 and goes no further.
- A valid post is forwarded to Ikiform with your secret key and Framer's submission id as the idempotency key, so a Framer retry never stores a second copy.
Option 1: Ikiform as the webhook
- In Framer, give each input a lowercase name:
name,work_email,company,team_size. Framer uses the names as JSON keys. - Select the form, and next to Send To choose Add… > Webhook. Paste your endpoint. Leave the secret empty; Ikiform doesn't check
Framer-Signature. - In the Ikiform form's settings, leave Allowed domains empty, leave Spam check off, and set Rate limit to Relaxed.
- Publish. Framer only sends webhooks from the published site.
https://ikiform.com/f/frm_xxxxxxxxxxxxxxxxIkiform answers 201 once the submission is stored, which Framer counts as delivered. A 422 validation_failed fails the same way on all five retries, so fix the input names rather than wait. Spam scoring still runs on every post: links, spam phrases, disposable addresses and repeats.
Option 2: a signed relay
Deploy a Cloudflare Worker that checks Framer's signature and forwards valid posts with your secret key. Then the Ikiform form can list your Framer domain under Allowed domains with Reject, so anything that isn't a browser on your site or your Worker is refused.
interface Env {
FRAMER_SECRET: string;
IKIFORM_SECRET_KEY: string;
}
const ENDPOINT = "https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx";
const encoder = new TextEncoder();
async function signatureFor(secret: string, body: ArrayBuffer, submissionId: string) {
const key = await crypto.subtle.importKey(
"raw",
encoder.encode(secret),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"],
);
const id = encoder.encode(submissionId);
const message = new Uint8Array(body.byteLength + id.byteLength);
message.set(new Uint8Array(body), 0);
message.set(id, body.byteLength);
const digest = new Uint8Array(await crypto.subtle.sign("HMAC", key, message));
return `sha256=${[...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
}
function sameText(a: string, b: string) {
const left = encoder.encode(a);
const right = encoder.encode(b);
return left.byteLength === right.byteLength && crypto.subtle.timingSafeEqual(left, right);
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
if (request.method !== "POST") return new Response(null, { status: 405 });
const signature = request.headers.get("framer-signature") ?? "";
const submissionId = request.headers.get("framer-webhook-submission-id") ?? "";
const body = await request.arrayBuffer();
if (!submissionId || !sameText(signature, await signatureFor(env.FRAMER_SECRET, body, submissionId))) {
return new Response("Bad signature", { status: 401 });
}
const response = await fetch(ENDPOINT, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${env.IKIFORM_SECRET_KEY}`,
"Idempotency-Key": submissionId,
},
body,
});
if (response.ok) return new Response(null, { status: 204 });
return new Response(await response.text(), { status: response.status });
},
};Deploy it
npx wrangler secret put FRAMER_SECRET
npx wrangler secret put IKIFORM_SECRET_KEY
npx wrangler deployFRAMER_SECRETis the secret you set on Framer's webhook destination. Framer requires at least 32 characters and won't show it again, so generate it, store it, and paste it in both places.IKIFORM_SECRET_KEYis a secret API key from the Ikiform dashboard. Never put it in Framer.- The signature is
sha256=plus the hex HMAC of the raw body followed by the submission id. Read the body once as bytes and use those same bytes for the check and the forward. Idempotency-Keymakes a Framer retry replay Ikiform's first answer instead of storing a duplicate. Keys are kept for 24 hours.- The Worker passes Ikiform's status through. Framer retries any answer that isn't
2xx, up to five times, so a429or5xxgets another chance, while a422fails the same way each time until you fix the input names.
Then point Framer's webhook at the Worker's URL instead of the endpoint, with the secret filled in.
IKIFORM_SECRET_KEY in server-only environment variables.Option 3: skip Framer's form
A code component renders a real form and posts from the visitor's browser, so origin checks, the honeypot and per-visitor rate limits work as on any site. You lose Framer's visual form editing for that form. The Framer docs guide has the component.
Check it works on Framer
- Publish the Framer site and submit the form there, not in the editor preview.
- Open Submissions in the dashboard. The new submission is there with every field you sent.
- Check the field names match your input names exactly, with no Framer-added keys.
- For the relay: send a request to the Worker with a made-up
Framer-Signatureand confirm it answers401. - For the relay: in the submission's details, the origin is empty and the status is received, not spam.
- Check the inbox of each notification recipient. Each gets an email with the fields, and Reply goes to the submitter when the form has an
emailfield. - If you added a webhook, open the endpoint in Webhooks. The delivery shows
succeededwith your server's answer.
Gotchas
| Problem | What to do |
|---|---|
Every webhook post is under spam with origin_mismatch | Allowed domains is set while Framer posts directly. Clear it, or put the relay in between. |
The Worker answers 401 to real posts | The secret differs between Framer and the Worker, or something read the body as text and changed it. Paste the secret again and hash the raw bytes. |
429 rate_limited during a launch | All posts share Framer's or Cloudflare's addresses. Choose Relaxed, or a Custom limit. |
422 validation_failed | The post doesn't match the form's field definition. error.fields names each field and the reason. Match the input name to the field id. |
403 challenge_required | The form's spam check is set to Ikiform and a fetch post had no token. Get one with challenge(formId) from @ikiform/sdk before each submit, or turn the check off. |
Questions
Not always. Framer sends submissions to email, Google Sheets or a webhook on its own. Add Ikiform when you want a stored, filtered record, server-side validation, team alerts with reply-to, or several retried webhooks from one form.
No. Ikiform authenticates posts by origin or by your secret API key, not by Framer's header. To check Framer-Signature, put a small relay between Framer and Ikiform that verifies it and forwards with your secret key.
Most often because Allowed domains is set. Framer's webhook posts come from its servers with no Origin header, so they don't match any domain. Clear the list, or use a relay that posts with your secret key.
They can when posting directly, if a delivery is retried after Ikiform stored it. The relay sends Framer's submission id as an idempotency key, so a retry replays the first answer instead.
Not yet. A post that contains a file is refused with 422 files_not_supported. Ask for a link to the file instead, such as a shared drive URL.
Stuck on a step, or want a guide for your situation? Email hello@ikiform.com.