πŸš€

Getting Started

FramerSend lets you connect Framer forms to any CRM, email tool, or database β€” without exposing API keys in your client code. Here's how it works in 3 steps:

1

Purchase a connector

Browse our connectors catalog and buy the one you need ($19 one-time). You'll receive instant access after payment.

2

Set up in the dashboard

Sign in to your FramerSend dashboard. Add a Connection (paste your CRM credentials), then create a Form and map your fields.

3

Install the Framer override

Copy the generated Form ID and paste it into the FramerSend code override in your Framer project. That's it β€” submissions will flow to your CRM automatically.

πŸ“¦

Connect to Framer

FramerSend offers two ways to connect your forms. We highly recommend Method 1 for a completely No-Code experience.

Method 1: Smart Component (Recommended)

1

Create a Code Component

In your Framer project, open the Assets panel (left sidebar). Click the + icon next to "Code" and select "New component".

2

Paste the Code

Name it FramerSendWrapper, replace all default code with the following, then save:

FramerSendWrapper.tsx
// @ts-nocheck
import * as React from "react"
import { addPropertyControls, ControlType, RenderTarget } from "framer"

const API_URL = "https://framersend.vercel.app/api/submit"

export default function FramerSendWrapper(props) {
    const { formId, scrollId, style } = props
    const isCanvas = RenderTarget.current() === RenderTarget.canvas
    const markerRef = React.useRef<HTMLDivElement>(null)
    const [modalConfig, setModalConfig] = React.useState<{ type: 'success' | 'error', title: string, message: string, actionUrl?: string, actionLabel?: string } | null>(null)

    React.useEffect(() => {
        if (isCanvas || !formId || !markerRef.current) return

        // Find form by Scroll Section ID (id attribute), or fall back to first form
        let form = null
        if (scrollId) {
            const section = document.getElementById(scrollId)
            if (section) form = section.querySelector("form")
            // In case the ID was put directly on the form
            if (!form && section && section.tagName === "FORM") form = section
        }
        if (!form) {
            form = document.querySelector("form")
        }

        if (!form) return

        const handleSubmit = async (e) => {
            // We DO NOT prevent default or stop propagation!
            // We let Framer do its native behavior (Redirect, Send Email, Tracking).
            // We just quietly send data in the background and only show errors.

            const formData = new FormData(form)
            const data = Object.fromEntries(formData.entries())

            try {
                const res = await fetch(`${API_URL}/${formId}`, {
                    method: "POST",
                    headers: { "Content-Type": "application/json" },
                    body: JSON.stringify(data),
                    keepalive: true, // Ensures request finishes even if Framer redirects
                })

                if (!res.ok) {
                    try {
                        const errData = await res.json()
                        const isDomainError = errData.error === 'Domain not activated' || (errData.message && errData.message.includes('not authorized for this FramerSend connection'));
                        setModalConfig({ 
                            type: 'error', 
                            title: isDomainError ? 'Domain Not Activated' : 'Submission Failed', 
                            message: errData.message || errData.error || "Please try again later.",
                            actionUrl: isDomainError ? 'https://framersend.vercel.app/dashboard/licenses' : undefined,
                            actionLabel: isDomainError ? 'Activate Domain' : undefined
                        })
                    } catch (e) {
                        setModalConfig({ type: 'error', title: 'Error', message: "Submission failed. Please try again." })
                    }
                }
                // If success, we do NOTHING. Framer will show its own success state or do its own redirect.
            } catch {
                setModalConfig({ type: 'error', title: 'Network Error', message: "Please check your connection and try again." })
            }
        }

        form.addEventListener("submit", handleSubmit)

        return () => {
            form.removeEventListener("submit", handleSubmit)
        }
    }, [formId, isCanvas])

    // On Canvas: show a small visual indicator
    if (isCanvas) {
        const label = !formId
            ? "Set Form ID"
            : scrollId
                ? `⚑ ${scrollId}`
                : "⚑ FramerSend"
        return (
            <div
                ref={markerRef}
                style={{
                    ...style,
                    width: 150,
                    height: 36,
                    background: formId
                        ? "linear-gradient(135deg, #8b5cf6, #6d28d9)"
                        : "linear-gradient(135deg, #f59e0b, #d97706)",
                    display: "flex",
                    alignItems: "center",
                    justifyContent: "center",
                    gap: 6,
                    borderRadius: 20,
                    color: "#fff",
                    fontFamily: "Inter, system-ui, sans-serif",
                    fontSize: 11,
                    fontWeight: 700,
                    letterSpacing: "0.02em",
                    boxShadow: "0 2px 12px rgba(139, 92, 246, 0.3)",
                }}
            >
                <svg
                    width="14"
                    height="14"
                    viewBox="0 0 24 24"
                    fill="none"
                    stroke="currentColor"
                    strokeWidth="2.5"
                    strokeLinecap="round"
                    strokeLinejoin="round"
                >
                    <path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" />
                </svg>
                {label}
            </div>
        )
    }

    // In production: completely invisible, zero size + modal portal
    return (
        <>
            <div
                ref={markerRef}
                style={{
                    position: "absolute",
                    width: 0,
                    height: 0,
                    overflow: "hidden",
                    opacity: 0,
                    pointerEvents: "none",
                }}
            />
            {modalConfig && !isCanvas && (
                <div 
                    style={{
                        position: "fixed",
                        top: 0, left: 0, right: 0, bottom: 0,
                        backgroundColor: "rgba(0,0,0,0.5)",
                        backdropFilter: "blur(4px)",
                        WebkitBackdropFilter: "blur(4px)",
                        zIndex: 2147483647,
                        display: "flex",
                        alignItems: "center",
                        justifyContent: "center",
                        padding: 20,
                        fontFamily: "Inter, -apple-system, system-ui, sans-serif"
                    }}
                    onClick={() => setModalConfig(null)}
                >
                    <div 
                        style={{
                            background: "#fff",
                            borderRadius: 16,
                            padding: "32px 24px",
                            width: "100%",
                            maxWidth: 400,
                            boxShadow: "0 20px 40px rgba(0,0,0,0.2)",
                            textAlign: "center",
                            transform: "scale(1)",
                            animation: "fs-pop 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275)"
                        }}
                        onClick={e => e.stopPropagation()}
                    >
                        <div style={{
                            width: 64, height: 64, borderRadius: "50%",
                            background: modalConfig.type === 'success' ? '#ecfdf5' : '#fef2f2',
                            color: modalConfig.type === 'success' ? '#10b981' : '#ef4444',
                            display: 'flex', alignItems: 'center', justifyContent: 'center',
                            margin: '0 auto 20px'
                        }}>
                            {modalConfig.type === 'success' ? (
                                <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>
                            ) : (
                                <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg>
                            )}
                        </div>
                        <h3 style={{ margin: "0 0 12px", fontSize: 20, fontWeight: 700, color: "#111827" }}>{modalConfig.title}</h3>
                        <p style={{ margin: "0 0 24px", fontSize: 15, color: "#4b5563", lineHeight: 1.5 }}>{modalConfig.message}</p>
                        <div style={{ display: 'flex', gap: '10px', flexDirection: 'column' }}>
                            {modalConfig.actionUrl && (
                                <button 
                                    onClick={() => window.open(modalConfig.actionUrl, '_blank')}
                                    style={{
                                        width: "100%", padding: "12px 0",
                                        background: '#6d28d9',
                                        color: "#fff", border: "none", borderRadius: 8,
                                        fontSize: 15, fontWeight: 600, cursor: "pointer",
                                        transition: "opacity 0.2s"
                                    }}
                                    onMouseOver={e => e.currentTarget.style.opacity = '0.9'}
                                    onMouseOut={e => e.currentTarget.style.opacity = '1'}
                                >
                                    {modalConfig.actionLabel}
                                </button>
                            )}
                            <button 
                                onClick={() => setModalConfig(null)}
                                style={{
                                    width: "100%", padding: "12px 0",
                                    background: modalConfig.actionUrl ? '#f3f4f6' : (modalConfig.type === 'success' ? '#10b981' : '#ef4444'),
                                    color: modalConfig.actionUrl ? '#374151' : "#fff", border: "none", borderRadius: 8,
                                    fontSize: 15, fontWeight: 600, cursor: "pointer",
                                    transition: "opacity 0.2s"
                                }}
                                onMouseOver={e => e.currentTarget.style.opacity = '0.9'}
                                onMouseOut={e => e.currentTarget.style.opacity = '1'}
                            >
                                {modalConfig.actionUrl ? 'Dismiss' : (modalConfig.type === 'success' ? 'Continue' : 'Try Again')}
                            </button>
                        </div>
                    </div>
                    <style>{`
                        @keyframes fs-pop {
                            0% { transform: scale(0.9); opacity: 0; }
                            100% { transform: scale(1); opacity: 1; }
                        }
                    `}</style>
                </div>
            )}
        </>
    )
}

addPropertyControls(FramerSendWrapper, {
    formId: {
        type: ControlType.String,
        title: "Form ID",
        placeholder: "Paste your Form ID",
    },
    scrollId: {
        type: ControlType.String,
        title: "Scroll ID",
        placeholder: "e.g. my-form",
    },
})
3

Place it on your page

Drag the FramerSendWrapper from Assets onto your page β€” place it anywhere near your form (above, below, or beside). It auto-detects the form on your page.

4

Paste your Form ID

Click the FramerSendWrapper pill on the canvas. In the right panel, paste your Form ID from the FramerSend dashboard. The pill turns purple to confirm.

That's it! The component is invisible on your live site. It automatically captures form submissions and sends data to FramerSend. Publish your site and test!

πŸ”—

Connect a CRM

After purchasing a connector, head to your dashboard to set up the connection. Each CRM requires different credentials β€” here's a guide for the most popular ones.

πŸ“§

Klaviyo

Required: Private API Key + List ID

Where to find: Settings β†’ API Keys β†’ Create Private Key

πŸ’

Mailchimp

Required: API Key + Server Prefix + Audience ID

Where to find: Account β†’ Extras β†’ API Keys

πŸ”Ά

HubSpot

Required: Private App Access Token

Where to find: Settings β†’ Integrations β†’ Private Apps β†’ Create

πŸ“Š

Google Sheets

Required: Refresh Token + Spreadsheet ID

Where to find: OAuth2 flow β€” contact support for help

πŸ’¬

Slack

Required: Incoming Webhook URL

Where to find: Slack Apps β†’ Incoming Webhooks β†’ Add

βš™οΈ

Zapier

Required: Webhook URL

Where to find: Create Zap β†’ Webhooks by Zapier β†’ Catch Hook

πŸ’‘ Tip: For all other connectors, the credential fields are labeled with exact instructions on where to find each value in your CRM settings.

πŸ—ΊοΈ

Field Mapping

Field mapping tells FramerSend how to translate your Framer form field names into CRM property names. For example:

Framer Form Field→Klaviyo Property
email→$email
name→$first_name
phone→$phone_number
company→$organization

In the dashboard, each form has a Field Mapping section with dropdown menus pre-populated with the CRM's accepted properties. Simply select the matching CRM field for each Framer form field.

βš™οΈ

API Reference

If you want to integrate FramerSend manually (without the Framer override), you can call our API directly.

POST/api/submit/{formId}

Submit form data to be routed to the connected CRM.

Headers

headers
Content-Type: application/json
Origin: https://your-framer-site.com

Request Body

json
{
  "email": "user@example.com",
  "name": "Jane Doe",
  "phone": "+1234567890",
  "message": "I'd like to learn more"
}

Response (200 OK)

json
{
  "success": true,
  "id": "sub_abc123"
}

Error Response (429)

json
{
  "error": "Rate limit exceeded",
  "retryAfter": 60
}

Rate Limits: 10 requests per minute per IP. The X-RateLimit-Remaining header shows remaining requests.

❓

FAQ & Troubleshooting

My form submissions aren't showing up in my CRM

Check the following: (1) Your Form ID is correct in the override code, (2) Your CRM credentials are valid in the dashboard, (3) Your field mapping includes at least the email field. Check the Submissions tab in your dashboard for error logs.

Can I use FramerSend with a custom domain?

Yes! FramerSend works with any domain. Just make sure your Framer site's domain is set as the allowed origin in your form settings.

What happens if a submission fails?

Failed submissions are logged in your dashboard with the error message. You can retry them manually or fix the issue and resubmit.

Is there a submission limit?

Each connector includes 500 submissions per month. The All-in-One Bundle includes unlimited submissions.

Can I connect multiple CRMs to one form?

Currently each form connects to one CRM. To send to multiple CRMs, create separate forms with different Form IDs.

How do I update my CRM credentials?

Go to Dashboard β†’ Connections β†’ click your connection β†’ Update Credentials. Your existing forms will automatically use the new credentials.

Do you support Framer's native form element?

Yes! The code override works with Framer's built-in form component. Apply the withFramerSend override to your form element.

Is my data secure?

Absolutely. All credentials are encrypted at rest. Your API keys never appear in client-side code β€” only a public Form ID is exposed. We use HTTPS for all data transmission.

Still need help?

Reach out and we'll get back to you within 24 hours.

Contact Support β†’