Universal Lead Capture
Technical instructions for integrating external websites with the Velo Dashboard. Optimized for Server-to-Server communication.
The Golden Rule: Server-Side Only
For production environments, DO NOT use client-side fetch. Use Next.js Server Actions or a dedicated backend proxy. This prevents CORS issues and keeps your Organization Keys hidden from public view.
Integration Overview
To capture leads from any website and route them to the correct Velo tenant workspace, you need to make an HTTP POST request to our centralized Edge Function.
The "Missing Passport" Issue (Critical)
Supabase Edge Functions require a Passport (JWT in the Authorization header) by default.
1. The Passport
The Authorization header. Proves you are a logged-in user.
2. The Org Key
The x-org-key header. Proves which Velo tenant you belong to.
Since lead forms are public, visitors don't have a passport. Velo Admins must set verify_jwt: false in the Supabase function configuration to allow "Passport-free" submissions.
Request Headers
| Header | Required | Value |
|---|---|---|
| Content-Type | Yes | application/json |
| x-org-key | Yes | The Public Lead Key (e.g., org_abc123...). This identifies your organization and is found in Settings > Security. |
Request Body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
| name | String | No | Full name of the prospect. |
| phone | String | Yes* | Contact number. (*Required if email is not provided) |
| String | Yes* | Email address. (*Required if phone is not provided) | |
| message | String | No | The inquiry or context text. |
| source | String | No | Origin of the lead (Default: website). |
| _honeypot | String | No | Security Layer: Must remain empty to pass spam check. |
| _ts | Number | No | Security Layer: Current timestamp (ms) to detect fast bots. |
| cf-turnstile-response | String | No | Security Layer: The token from Cloudflare Turnstile. |
Implementation Examples
1. Next.js Server Action (Recommended)
The most secure method. Hides your keys and bypasses CORS.
// src/app/actions/leads.ts
"use server"
export async function submitLeadAction(formData: any) {
// Layer 3: Timing Check (Optional local check)
const loadTime = parseInt(formData._ts || "0");
if (Date.now() - loadTime < 1000) return { success: false, error: "Spam detected" };
// Layer 3: Honeypot
if (formData._honeypot) return { success: true }; // Silent ignore
try {
const response = await fetch('https://fryqsyakqtnvprcyryvr.supabase.co/functions/v1/capture-lead', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-org-key': 'org_abc123...' // Keep this secret on the server!
},
body: JSON.stringify({
...formData,
source: 'main_website'
})
});
return await response.json();
} catch (error) {
return { success: false, error: "Connection failed" };
}
}2. AI IDE Agent Prompt (Cursor/Windsurf)
Use this prompt to have your AI agent build a secure, server-side form for you:
Success Response
{
"success": true,
"leadId": "uuid-of-the-saved-lead"
}Error Response
{
"error": "Error message details"
}Troubleshooting & Analogies
The Story: Post Box vs. Guard
"Imagine you built a Public Post Box (the API) for anyone to drop letters. But the building owner (Supabase) put a Security Guard at the door asking for an Employee ID (the Auth Header). Since regular people don't have Employee IDs, they are turned away before they even reach the box."
The Fix:
Set verify_jwt: false to remove the guard.
UNAUTHORIZED_NO_AUTH_HEADER
The guard is still there. Check your Supabase function settings.
SPAM_DETECTED
You submitted the form too fast (under 1s). Human check failed.
CAPTCHA_FAILED
Cloudflare Turnstile token is invalid or expired.
4-Layer Security Fortress
Velo uses a multi-layered defense to protect your leads:
- Prefix Validation: Keys must start with
org_to be processed. - Domain Locking: Only requests from authorized domains are processed.
- Anti-Spam: Honeypot and timing checks (>1s) block automated crawlers.
- Bot Prevention: Native Turnstile/reCAPTCHA support on the backend.
