דוגמה — שרת ביניים לטופס
getlead נועד להיקרא ישירות מהדפדפן, וברוב דפי הנחיתה זו התשובה הנכונה. יש בדיוק שלושה מצבים שבהם צריך חוליה באמצע: CAPTCHA (ה‑secret שלו לא יכול לשבת בדפדפן), לוגיקה או נתונים שאסור לחשוף (מפות Pointer, שדות מחושבים, כללי ניתוב), ודרישה לרישום או לוולידציה משלכם לפני שהפנייה נכנסת ל‑CRM.
העמוד הזה הוא המימוש המלא של החוליה הזו — בחמש שפות, עם כל מסלולי הכישלון סגורים.
מתי צריך שרת ביניים ומתי לא
Section titled “מתי צריך שרת ביניים ומתי לא”| המצב | ישירות מהדפדפן | דרך שרת ביניים |
|---|---|---|
| טופס יצירת קשר פשוט | ✔ עדיף — פחות חלקים נעים | מיותר |
| מלכודת דבש בלבד | ✔ מספיק | מיותר |
| reCAPTCHA / Turnstile | ✘ ה‑secret ידלוף | ✔ חובה |
| מיפוי Pointer שאסור לחשוף | ✘ המפה בקוד המקור של הדף | ✔ |
| שדה מחושב (ניקוד ליד, ניתוב לצוות) | ✘ | ✔ |
| רישום הפניות אצלכם בנוסף ל‑CRM | ✘ | ✔ |
| הגבלת קצב לפי IP | ✘ אין לדפדפן שליטה | ✔ |
| טופס שכבר קיים בתוסף WordPress | תלוי בתוסף — ראו הטמעה בפועל | ✔ נפוץ מאוד |
החוזה שהשרת שלכם מחזיר לדפדפן
Section titled “החוזה שהשרת שלכם מחזיר לדפדפן”הפיתוי הוא להעביר את התשובה של getlead כמו שהיא. אל תעשו את זה: קוד הסטטוס של המערכת מספר על המערכת, והדפדפן צריך לדעת מה להגיד למשתמש. הגדירו חוזה משלכם — קטן, קבוע, ובעל שדה code שאפשר לתרגם בממשק:
{ "ok": true, "leadId": "xK9mP2qRsT" }{ "ok": false, "code": "VALIDATION", "message": "נא למלא טלפון או אימייל תקינים" }מה קרה מול getlead |
מה השרת שלכם מחזיר | ה‑code |
מה המשתמש רואה | מה אתם עושים |
|---|---|---|---|---|
200 + success: true |
200 |
— | עמוד תודה | שולחים אירוע המרה |
400 + error של שדה חסר/לא תקין (למשל missing "PhoneNumber" or "Email") |
400 |
VALIDATION |
“נא למלא טלפון או אימייל תקינים” | כלום — זו טעות של המשתמש |
400 + AcceptWebLeads is Disabled |
503 |
DISABLED |
“אירעה תקלה, נסו שוב מאוחר יותר” | התראה לצוות. זו תקלת תצורה, לא של המשתמש |
5xx |
502 |
UPSTREAM |
“אירעה שגיאה, נסו שוב” | לוג + ניטור |
| timeout / שגיאת רשת | 504 |
TIMEOUT |
“אין חיבור, נסו שוב” | לוג + ניטור |
| מלכודת הדבש מלאה | 200 מזויף |
— | עמוד תודה | לא שולחים כלום ל‑CRM |
| CAPTCHA נכשל | 400 |
CAPTCHA |
“נא לאשר שאינכם רובוט” | ספירה — ריבוי כשלונות = תקיפה |
צד הדפדפן — טופס עם מלכודת דבש
Section titled “צד הדפדפן — טופס עם מלכודת דבש”החלק הזה זהה בכל השפות: הדפדפן מדבר רק עם השרת שלכם, ולכן אין בו לא Application Id ולא מפות Pointer.
<form id="lead-form" novalidate> <input name="fullName" placeholder="שם מלא" /> <input name="phone" type="tel" placeholder="טלפון" required /> <input name="email" type="email" placeholder="אימייל" /> <input name="city" placeholder="עיר" />
<!-- מלכודת דבש: בוט ימלא, אדם לא יראה. אל תקראו לשדה "honeypot". --> <input name="company_website" tabindex="-1" autocomplete="off" style="position:absolute;left:-9999px" aria-hidden="true" />
<!-- ווידג'ט Turnstile — ה-sitekey ציבורי; ה-secret יושב בשרת בלבד --> <div class="cf-turnstile" data-sitekey="0x0000000000000000000000"></div>
<button type="submit">שליחה</button> <p class="form-msg" role="status" aria-live="polite"></p></form>הכללים שהופכים את זה לטופס שעובד גם ביום רע: כפתור שננעל בזמן השליחה (כדי שלחיצה כפולה לא תיצור שני לידים), הודעה ב‑aria-live כדי שקורא מסך ישמע את השגיאה, ואירוע ההמרה ל‑Analytics רק אחרי הצלחה.
השרת שלכם — בחמש שפות
Section titled “השרת שלכם — בחמש שפות”<?phpdeclare(strict_types=1);header('Content-Type: application/json; charset=utf-8');
$APP_ID = getenv('MB_APP_ID'); // מזהה ציבורי — בקונפיג לנוחות בלבד$TS_SECRET = getenv('TURNSTILE_SECRET'); // סוד — ממנהל סודות או ממשתנה סביבה$ENDPOINT = "https://api.mbapps.co.il/functions/{$APP_ID}/getlead";
/** תשובה אחידה לדפדפן, ויציאה. */function reply(int $status, array $body): never{ http_response_code($status); echo json_encode($body, JSON_UNESCAPED_UNICODE); exit;}
$input = json_decode(file_get_contents('php://input') ?: '[]', true) ?? [];
// 1) מלכודת דבש — בוט. מחזירים "הצלחה" ולא שולחים כלום.if (!empty($input['company_website'])) { reply(200, ['ok' => true]);}
// 2) אימות טוקן ה-CAPTCHA — לפני כל דבר אחר$verify = json_decode((string) file_get_contents( 'https://challenges.cloudflare.com/turnstile/v0/siteverify', false, stream_context_create(['http' => [ 'method' => 'POST', 'header' => "Content-Type: application/json\r\n", 'content' => json_encode(['secret' => $TS_SECRET, 'response' => $input['token'] ?? '']), 'timeout' => 5, 'ignore_errors' => true, ]])), true);
if (empty($verify['success'])) { reply(400, ['ok' => false, 'code' => 'CAPTCHA', 'message' => 'נא לאשר שאינכם רובוט']);}
// 3) רשימת היתר — רק שדות שאנחנו מכירים עוברים הלאה$data = array_filter([ 'Name' => $input['fullName'] ?? null, 'PhoneNumber' => $input['phone'] ?? null, 'Email' => $input['email'] ?? null, // עיר כטקסט חופשי הולכת ל-CityText; השדה City הוא Pointer ל-CityList 'CityText' => $input['city'] ?? null, // Pointer — objectId חשוף בן 10 תווים. המפה נשארת בשרת: 'LeadSourceId' => ['facebook' => 'vOZLqOWnQf', 'google' => 'fHG8Rgy8Hr'][$input['source'] ?? ''] ?? null,], static fn ($v) => $v !== null && $v !== '');
if (empty($data['PhoneNumber']) && empty($data['Email'])) { reply(400, ['ok' => false, 'code' => 'VALIDATION', 'message' => 'נא למלא טלפון או אימייל']);}
// 4) העברה ל-getlead$ch = curl_init($ENDPOINT);curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, CURLOPT_HTTPHEADER => ['Content-Type: application/json', "X-Parse-Application-Id: {$APP_ID}"], CURLOPT_POSTFIELDS => json_encode($data, JSON_UNESCAPED_UNICODE),]);$body = curl_exec($ch);$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);$netErr = curl_error($ch);curl_close($ch);
// 5) מיפוי לחוזה שלנוif ($body === false) { error_log("getlead network error: {$netErr}"); // בלי פרטי המשתמש reply(504, ['ok' => false, 'code' => 'TIMEOUT', 'message' => 'אין חיבור, נסו שוב']);}if ($status === 200) { $json = json_decode($body, true) ?: []; $result = $json['result'] ?? []; // getlead עוטפת את ההצלחה ב-result if (!empty($result['success'])) reply(200, ['ok' => true, 'leadId' => $result['leadId'] ?? '']); error_log('getlead 200 without success flag'); reply(502, ['ok' => false, 'code' => 'UPSTREAM', 'message' => 'אירעה שגיאה, נסו שוב']);}if ($status === 400) { // כל שגיאה עסקית חוזרת כ-400 — מסתעפים על הגוף $err = json_decode($body, true)['error'] ?? ''; if (str_contains($err, 'Disabled')) { error_log('ALERT: AcceptWebLeads is disabled — every web lead is being dropped'); reply(503, ['ok' => false, 'code' => 'DISABLED', 'message' => 'אירעה תקלה, נסו שוב מאוחר יותר']); } if (str_contains($err, 'missing')) { reply(400, ['ok' => false, 'code' => 'VALIDATION', 'message' => 'נא למלא טלפון או אימייל תקינים']); } error_log("getlead 400: {$err}"); // למשל "User function not found" — תקלת שרת/תצורה, לא של המשתמש reply(502, ['ok' => false, 'code' => 'UPSTREAM', 'message' => 'אירעה שגיאה, נסו שוב']);}error_log("getlead {$status}: {$body}");reply(502, ['ok' => false, 'code' => 'UPSTREAM', 'message' => 'אירעה שגיאה, נסו שוב']);"""Flask — נקודת קצה שמאמתת CAPTCHA ומעבירה ל-getlead."""import loggingimport os
import requestsfrom flask import Flask, jsonify, request
app = Flask(__name__)log = logging.getLogger("lead-proxy")
APP_ID = os.environ["MB_APP_ID"] # מזהה ציבוריTS_SECRET = os.environ["TURNSTILE_SECRET"] # סוד — ממנהל סודותENDPOINT = f"https://api.mbapps.co.il/functions/{APP_ID}/getlead"HEADERS = {"Content-Type": "application/json", "X-Parse-Application-Id": APP_ID}
# מפת ה-Pointer נשארת בשרת ולא מגיעה לדפדפןSOURCE_MAP = {"facebook": "vOZLqOWnQf", "google": "fHG8Rgy8Hr", "site": "yOTdl4L99Q"}
def verify_captcha(token: str) -> bool: try: r = requests.post( "https://challenges.cloudflare.com/turnstile/v0/siteverify", json={"secret": TS_SECRET, "response": token}, timeout=5, ) return bool(r.json().get("success")) except requests.RequestException: return False
@app.post("/api/lead")def lead(): form = request.get_json(silent=True) or {}
# 1) מלכודת דבש — בוט. 200 מזויף, בלי לשלוח כלום. if form.get("company_website"): return jsonify(ok=True)
# 2) CAPTCHA if not verify_captcha(form.get("token", "")): return jsonify(ok=False, code="CAPTCHA", message="נא לאשר שאינכם רובוט"), 400
# 3) רשימת היתר data = { "Name": form.get("fullName"), "PhoneNumber": form.get("phone"), "Email": form.get("email"), "CityText": form.get("city"), # טקסט חופשי; City עצמו הוא Pointer ל-CityList "LeadSourceId": SOURCE_MAP.get(form.get("source")), # objectId חשוף, או None } data = {k: v for k, v in data.items() if v}
if not data.get("PhoneNumber") and not data.get("Email"): return jsonify(ok=False, code="VALIDATION", message="נא למלא טלפון או אימייל"), 400
# 4) העברה try: r = requests.post(ENDPOINT, json=data, headers=HEADERS, timeout=10) except requests.RequestException as exc: log.error("getlead network error: %s", exc) # בלי פרטי המשתמש return jsonify(ok=False, code="TIMEOUT", message="אין חיבור, נסו שוב"), 504
# 5) מיפוי לחוזה שלנו if r.status_code == 200: body = r.json() result = body.get("result", {}) # getlead עוטפת את ההצלחה ב-result if result.get("success"): return jsonify(ok=True, leadId=result["leadId"]) log.error("getlead 200 without success flag: %s", r.text[:200]) return jsonify(ok=False, code="UPSTREAM", message="אירעה שגיאה, נסו שוב"), 502 if r.status_code == 400: # כל שגיאה עסקית חוזרת כ-400 — מסתעפים על הגוף err = (r.json().get("error") if r.headers.get("content-type", "").startswith("application/json") else "") or "" if "Disabled" in err: log.critical("ALERT: AcceptWebLeads is disabled — every web lead is being dropped") return jsonify(ok=False, code="DISABLED", message="אירעה תקלה, נסו שוב מאוחר יותר"), 503 if "missing" in err: return jsonify(ok=False, code="VALIDATION", message="נא למלא טלפון או אימייל תקינים"), 400 log.error("getlead 400: %s", err) # למשל "User function not found" — תקלת שרת/תצורה return jsonify(ok=False, code="UPSTREAM", message="אירעה שגיאה, נסו שוב"), 502
log.error("getlead %s: %s", r.status_code, r.text[:200]) return jsonify(ok=False, code="UPSTREAM", message="אירעה שגיאה, נסו שוב"), 502// Express — נקודת קצה שמאמתת CAPTCHA ומעבירה ל-getleadimport express from 'express';
const app = express();app.use(express.json({ limit: '16kb' })); // גוף גדול מזה אינו טופס
const APP_ID = process.env.MB_APP_ID; // מזהה ציבוריconst TS_SECRET = process.env.TURNSTILE_SECRET; // סוד — ממנהל סודותconst ENDPOINT = `https://api.mbapps.co.il/functions/${APP_ID}/getlead`;
// מפת ה-Pointer נשארת בשרת ולא מגיעה לדפדפןconst SOURCE_MAP = { facebook: 'vOZLqOWnQf', google: 'fHG8Rgy8Hr', site: 'yOTdl4L99Q' };
async function verifyCaptcha(token) { try { const r = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ secret: TS_SECRET, response: token }), signal: AbortSignal.timeout(5_000), }); return (await r.json()).success === true; } catch { return false; }}
app.post('/api/lead', async (req, res) => { const { fullName, phone, email, city, source, token, company_website: honeypot } = req.body ?? {};
// 1) מלכודת דבש — בוט. 200 מזויף, בלי לשלוח כלום. if (honeypot) return res.json({ ok: true });
// 2) CAPTCHA if (!(await verifyCaptcha(token))) { return res.status(400).json({ ok: false, code: 'CAPTCHA', message: 'נא לאשר שאינכם רובוט' }); }
// 3) רשימת היתר const data = Object.fromEntries( Object.entries({ Name: fullName, PhoneNumber: phone, Email: email, CityText: city, // טקסט חופשי; City עצמו הוא Pointer ל-CityList LeadSourceId: SOURCE_MAP[source], // objectId חשוף, או undefined }).filter(([, v]) => v), );
if (!data.PhoneNumber && !data.Email) { return res.status(400).json({ ok: false, code: 'VALIDATION', message: 'נא למלא טלפון או אימייל' }); }
// 4) העברה let upstream; try { upstream = await fetch(ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Parse-Application-Id': APP_ID }, body: JSON.stringify(data), signal: AbortSignal.timeout(10_000), }); } catch (err) { console.error('getlead network error:', err.name); // בלי פרטי המשתמש return res.status(504).json({ ok: false, code: 'TIMEOUT', message: 'אין חיבור, נסו שוב' }); }
// 5) מיפוי לחוזה שלנו if (upstream.ok) { const body = await upstream.json().catch(() => ({})); // getlead עוטפת את ההצלחה ב-result if (body.result?.success) return res.json({ ok: true, leadId: body.result.leadId }); console.error('getlead 200 without success flag'); return res.status(502).json({ ok: false, code: 'UPSTREAM', message: 'אירעה שגיאה, נסו שוב' }); } if (upstream.status === 400) { // כל שגיאה עסקית חוזרת כ-400 — מסתעפים על הגוף const { error = '' } = await upstream.json().catch(() => ({})); if (error.includes('Disabled')) { console.error('ALERT: AcceptWebLeads is disabled — every web lead is being dropped'); return res.status(503).json({ ok: false, code: 'DISABLED', message: 'אירעה תקלה, נסו שוב מאוחר יותר' }); } if (error.includes('missing')) { return res.status(400).json({ ok: false, code: 'VALIDATION', message: 'נא למלא טלפון או אימייל תקינים' }); } console.error('getlead 400:', error); // למשל "User function not found" — תקלת שרת/תצורה return res.status(502).json({ ok: false, code: 'UPSTREAM', message: 'אירעה שגיאה, נסו שוב' }); } console.error('getlead', upstream.status, (await upstream.text()).slice(0, 200)); return res.status(502).json({ ok: false, code: 'UPSTREAM', message: 'אירעה שגיאה, נסו שוב' });});
app.listen(3000);// ASP.NET Core minimal API — נקודת קצה שמאמתת CAPTCHA ומעבירה ל-getleadusing System.Net.Http.Json;using System.Text.Json;
var builder = WebApplication.CreateBuilder(args);builder.Services.AddHttpClient();var app = builder.Build();
var appId = builder.Configuration["MB_APP_ID"]!; // מזהה ציבוריvar tsSecret = builder.Configuration["TURNSTILE_SECRET"]!; // סוד — ממנהל סודותvar endpoint = $"https://api.mbapps.co.il/functions/{appId}/getlead";
// מפת ה-Pointer נשארת בשרת ולא מגיעה לדפדפןvar sourceMap = new Dictionary<string, string>{ ["facebook"] = "vOZLqOWnQf", ["google"] = "fHG8Rgy8Hr", ["site"] = "yOTdl4L99Q",};
app.MapPost("/api/lead", async (LeadRequest form, IHttpClientFactory factory, ILogger<Program> log, CancellationToken ct) =>{ var http = factory.CreateClient(); http.Timeout = TimeSpan.FromSeconds(10);
// 1) מלכודת דבש — בוט. 200 מזויף, בלי לשלוח כלום. if (!string.IsNullOrEmpty(form.CompanyWebsite)) return Results.Ok(new { ok = true });
// 2) CAPTCHA var verify = await http.PostAsJsonAsync( "https://challenges.cloudflare.com/turnstile/v0/siteverify", new { secret = tsSecret, response = form.Token ?? "" }, ct); var verifyBody = await verify.Content.ReadFromJsonAsync<JsonElement>(cancellationToken: ct); if (!verifyBody.TryGetProperty("success", out var okProp) || !okProp.GetBoolean()) return Results.Json(new { ok = false, code = "CAPTCHA", message = "נא לאשר שאינכם רובוט" }, statusCode: 400);
// 3) רשימת היתר var data = new Dictionary<string, object?> { ["Name"] = form.FullName, ["PhoneNumber"] = form.Phone, // CityText ולא City: City הוא Pointer ל-CityList ["Email"] = form.Email, ["CityText"] = form.City, }; if (form.Source is not null && sourceMap.TryGetValue(form.Source, out var srcId)) data["LeadSourceId"] = srcId; // objectId חשוף foreach (var k in data.Where(kv => kv.Value is null or "").Select(kv => kv.Key).ToList()) data.Remove(k);
if (!data.ContainsKey("PhoneNumber") && !data.ContainsKey("Email")) return Results.Json(new { ok = false, code = "VALIDATION", message = "נא למלא טלפון או אימייל" }, statusCode: 400);
// 4) העברה using var req = new HttpRequestMessage(HttpMethod.Post, endpoint) { Content = JsonContent.Create(data), }; req.Headers.Add("X-Parse-Application-Id", appId);
HttpResponseMessage res; try { res = await http.SendAsync(req, ct); } catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException) { log.LogError("getlead network error: {Kind}", ex.GetType().Name); // בלי פרטי המשתמש return Results.Json(new { ok = false, code = "TIMEOUT", message = "אין חיבור, נסו שוב" }, statusCode: 504); }
// 5) מיפוי לחוזה שלנו if (res.IsSuccessStatusCode) { var body = await res.Content.ReadFromJsonAsync<JsonElement>(cancellationToken: ct); // getlead עוטפת את ההצלחה ב-result if (body.TryGetProperty("result", out var resObj) && resObj.TryGetProperty("success", out var s) && s.GetBoolean()) return Results.Ok(new { ok = true, leadId = resObj.GetProperty("leadId").GetString() });
log.LogError("getlead 200 without success flag"); return Results.Json(new { ok = false, code = "UPSTREAM", message = "אירעה שגיאה, נסו שוב" }, statusCode: 502); }
if ((int)res.StatusCode == 400) // כל שגיאה עסקית חוזרת כ-400 — מסתעפים על הגוף { var errBody = await res.Content.ReadFromJsonAsync<JsonElement>(cancellationToken: ct); var err = errBody.TryGetProperty("error", out var e) ? e.GetString() ?? "" : ""; if (err.Contains("Disabled")) return LogDisabled(log); if (err.Contains("missing")) return Results.Json(new { ok = false, code = "VALIDATION", message = "נא למלא טלפון או אימייל תקינים" }, statusCode: 400); log.LogError("getlead 400: {Error}", err); // למשל "User function not found" — תקלת שרת/תצורה } return Results.Json(new { ok = false, code = "UPSTREAM", message = "אירעה שגיאה, נסו שוב" }, statusCode: 502);});
static IResult LogDisabled(ILogger log){ log.LogCritical("ALERT: AcceptWebLeads is disabled — every web lead is being dropped"); return Results.Json(new { ok = false, code = "DISABLED", message = "אירעה תקלה, נסו שוב מאוחר יותר" }, statusCode: 503);}
app.Run();
public record LeadRequest(string? FullName, string? Phone, string? Email, string? City, string? Source, string? Token, string? CompanyWebsite);// הצד שרץ בדף — מדבר רק עם השרת שלכםconst form = document.getElementById('lead-form');const msg = form.querySelector('.form-msg');const btn = form.querySelector('button');
const MESSAGES = { VALIDATION: 'נא למלא טלפון או אימייל תקינים', CAPTCHA: 'נא לאשר שאינכם רובוט', DISABLED: 'אירעה תקלה, נסו שוב מאוחר יותר', UPSTREAM: 'אירעה שגיאה, נסו שוב', TIMEOUT: 'אין חיבור, נסו שוב',};
form.addEventListener('submit', async (e) => { e.preventDefault(); btn.disabled = true; // נעילה — לחיצה כפולה = שני לידים msg.textContent = '';
const f = Object.fromEntries(new FormData(form));
try { const res = await fetch('/api/lead', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ fullName: f.fullName, phone: f.phone, email: f.email, city: f.city, source: f.source, company_website: f.company_website, // מלכודת הדבש נשלחת כמו שהיא token: f['cf-turnstile-response'], // הווידג'ט מזריק את השדה הזה }), });
const body = await res.json().catch(() => ({}));
if (res.ok && body.ok) { window.dataLayer?.push({ event: 'form_submit', lead_id: body.leadId }); window.location.href = '/thank-you/'; // אירוע ההמרה — רק אחרי הצלחה return; } msg.textContent = MESSAGES[body.code] ?? MESSAGES.UPSTREAM; } catch { msg.textContent = MESSAGES.TIMEOUT; } finally { btn.disabled = false; }});אין לכם שרת? נקודת קצה serverless עושה את אותה עבודה. זו אותה לוגיקה בדיוק, בעטיפה של Cloudflare Pages Functions:
export async function onRequestPost({ request, env }) { const body = await request.json(); if (body.company_website) return Response.json({ ok: true });
const verify = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ secret: env.TURNSTILE_SECRET, response: body.token }), }).then((r) => r.json());
if (!verify.success) { return Response.json({ ok: false, code: 'CAPTCHA' }, { status: 400 }); }
const data = { Name: body.fullName, PhoneNumber: body.phone, Email: body.email }; Object.keys(data).forEach((k) => { if (!data[k]) delete data[k]; });
const upstream = await fetch( `https://api.mbapps.co.il/functions/${env.MB_APP_ID}/getlead`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Parse-Application-Id': env.MB_APP_ID }, body: JSON.stringify(data), }, );
const result = await upstream.json().catch(() => ({})); // getlead עוטפת את ההצלחה ב-result if (upstream.ok) return Response.json({ ok: true, leadId: result.result?.leadId }); if (upstream.status === 400) { // 400 לכל שגיאה עסקית — מסתעפים על הגוף if (String(result.error).includes('Disabled')) return Response.json({ ok: false, code: 'DISABLED' }, { status: 503 }); if (String(result.error).includes('missing')) return Response.json({ ok: false, code: 'VALIDATION' }, { status: 400 }); } return Response.json({ ok: false, code: 'UPSTREAM' }, { status: 502 });}אימות ה‑CAPTCHA — למה דווקא לפני הכול
Section titled “אימות ה‑CAPTCHA — למה דווקא לפני הכול”הטוקן שהווידג’ט מייצר בדפדפן הוא חד‑פעמי, והאימות מולו דורש secret. שני כללים:
- מאמתים לפני שמעבירים. אם תעבירו קודם ל‑
getleadותאמתו אחר כך, כבר יצרתם ליד — והבוט הצליח. - ה‑secret לא נכנס לקוד. משתנה סביבה או מנהל סודות, לפי מה שנהוג אצלכם.
נקודות הקצה של שכבת הטפסים אינן מאמתות reCAPTCHA, Turnstile או מלכודות דבש בעצמן. אין שם שדה טוקן, ושליחת אחד כזה פשוט נזרקת בשקט כמו כל מפתח לא מוכר (ב‑web2table: token ו‑company_website בגוף → 200, ולא נוצר שדה בסכימה). כל האנטי‑ספאם הוא באחריותכם — ראו מודל האבטחה.
שבע מלכודות של שרת ביניים
Section titled “שבע מלכודות של שרת ביניים”| המלכודת | מה קורה | מה עושים |
|---|---|---|
העברת הגוף כמו שהוא — fetch(ENDPOINT, { body: req.body }) |
כל מפתח שהתוקף שולח מגיע לפונקציה. מפתחות לא מוכרים אמנם נזרקים, אבל שדות אמיתיים שלא התכוונתם לחשוף (למשל LeadStatusId) כן ייקלטו |
רשימת היתר מפורשת, כמו בכל הדוגמאות כאן |
| בלי timeout | בקשה תלויה תופסת worker; מספיק שהמערכת תאט כדי שהשרת שלכם ייתקע | timeout על הקריאה ל‑CAPTCHA וגם על הקריאה ל‑getlead |
| PII בלוגים | טלפונים ואימיילים של לידים נכתבים ללוג ונשמרים שנה | לוגים בלי גוף הבקשה. מזהה בקשה, קוד סטטוס, וזהו |
| ניסיון חוזר על 400 | הבקשה תיכשל שוב בדיוק באותה צורה | חזרו רק על שגיאות רשת ו‑5xx, עם השהיה. ראו מגבלות |
| «Disabled» בשקט | כל הלידים נעלמים ואף אחד לא יודע | התראה תפעולית, לא רק לוג |
| CORS על הנקודה שלכם | הטופס יושב בדומיין אחר מהשרת והדפדפן חוסם | הגישו את הטופס מאותו דומיין, או החזירו Access-Control-Allow-Origin מדויק (לא * על נקודה שמקבלת נתונים) |
| 200 מזויף למלכודת דבש | קל לשכוח, והבוט לומד שהשדה מסגיר אותו | תמיד להחזיר בדיוק את אותה תשובה כמו הצלחה |
מה לבדוק אחרי הפריסה
Section titled “מה לבדוק אחרי הפריסה”- שליחה תקינה יוצרת ליד ב‑
AccountsעםIsAccount: false, והשדות שנשלחו אכן מלאים - שדה Pointer (למשל
LeadSourceId) אינו ריק — אם הוא ריק, הערך לא היה objectId בן 10 תווים - שליחה בלי טלפון ובלי אימייל מחזירה
400עםcode: "VALIDATION"ולא נוצר ליד - מילוי מלכודת הדבש מחזיר
200ולא נוצר ליד - טוקן CAPTCHA שגוי מחזיר
400עםcode: "CAPTCHA" - כיבוי זמני של הדגל (
400 … is Disabledמהשרת) מחזיר מהשרת שלכם503ומייצר התראה (בדקו את זה פעם אחת בכוונה) - הלוגים אינם מכילים טלפונים ואימיילים
- אירוע ההמרה ב‑Analytics נורה רק במסלול ההצלחה
- getlead — התיעוד המלא של נקודת הקצה, השדות והדגל
- מודל האבטחה — למה זו הדלת הציבורית היחידה, ומה CAPTCHA כן ולא עושה
- דגלי Config — שגיאת «Disabled», ולמה שורה כפולה הופכת אותה לבלתי צפויה
- אבחון תקלות בטפסים · שגיאות ופתרון תקלות
- הטמעה בפועל — WordPress, Wix ו‑HTML
- דוגמאות קוד — הקטלוג המלא