-
Notifications
You must be signed in to change notification settings - Fork 0
Website Lead Intake
Capture form submissions from your public marketing site as landing leads in GDX: each successful POST creates a row in the leads triage list, rings the in-app notification bell for every user, and writes an audit entry. This page covers wiring a website to the intake endpoint end to end.
The flow:
your website form
└── your site's server-side handler (holds the API key)
└── POST /api/v1/landing-leads (X-API-Key header)
├── landing_leads row → Leads triage list
├── broadcast notification → topbar bell, all users
└── audit entry → landing_lead_created
The endpoint authenticates with an API key carrying the landing_leads:write
scope. Mint one from the CLI, inside the app container:
python -m gdx_dispatch.tools.create_api_key \
--tenant <slug-or-uuid> \
--name "example.com lead form" \
--scopes landing_leads:writeOptional: --expires-days N (default: never expires), --dry-run.
The full key (gdx_live_…) is shown once — store it in your site's
secret store immediately. Only a SHA-256 hash and the 16-char prefix are
kept; there is no way to recover a lost key, only to revoke and re-mint.
Keys can also be minted, listed, and revoked over the API
(POST/GET /api/developer/keys, DELETE /api/developer/keys/<id>).
Revocation is immediate and permanent. (The old /developer portal UI page
was removed with the rest of the multi-tenant control plane in v1.38.0 —
the key API and CLI are the supported paths.)
The API key must never reach the browser. Have your site's form POST to a server-side handler on your site, and let that handler forward to GDX with the key. A static site with client-side JS would expose the key to every visitor — if you have no server side at all, put a tiny relay (worker, function, etc.) in front.
The forward call:
curl -X POST https://your-gdx-host/api/v1/landing-leads \
-H "X-API-Key: $GDX_LEAD_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Jane Doe",
"phone": "555-555-0100",
"email": "jane@example.com",
"message": "Broken spring, door stuck halfway.",
"source": "website",
"utm_source": "google",
"utm_campaign": "spring-repair"
}'Success → 201 with {"data": {"id": "<uuid>", "status": "new"}}.
Make your relay loud on failure. If GDX returns a non-2xx (revoked key, network trouble), a relay that still shows the visitor a "Thanks!" page will silently discard real customers. Log the failure where you'll see it, and ideally fall back to something durable (an email to yourself, a queue) so no lead is ever lost while the primary path is down.
All fields are optional strings at the API level — enforce your own required-ness in your site form. Server-side column limits:
| Field | Limit | Notes |
|---|---|---|
name |
200 | |
email |
254 | |
phone |
30 | over-limit values are rejected, not truncated |
message |
unlimited | text |
source |
100 | defaults to website; use it to distinguish forms (repair-form, …) |
referrer |
500 | |
utm_campaign / utm_source / utm_medium
|
200 each | shown in the triage list |
cf_turnstile_token |
— | Turnstile response token, if enabled (below) |
website |
— | honeypot — never render this as a visible field |
Responses:
| Status | Meaning |
|---|---|
201 |
Lead created (data.id, data.status) |
400 |
Turnstile challenge failed (error: challenge_failed + codes) |
401 |
Missing, unknown, expired, or revoked API key |
403 |
Key lacks the landing_leads:write scope |
429 |
Rate limit: 60 requests/minute per key |
Three independent layers:
-
Honeypot — the
websitefield. Include it in your form as a hidden input; humans leave it empty, bots fill it. A tripped honeypot returns a fake201(with a synthetic id) and stores nothing, so bots learn nothing. -
Cloudflare Turnstile (recommended) — create a Turnstile site in the
Cloudflare dashboard, render the widget on your form, and pass the response
token as
cf_turnstile_token. On the GDX side setTURNSTILE_SECRET(and optionallyTURNSTILE_HOSTNAMEto pin the expected hostname). Unset secret = fail-open: tokens are not checked, which is the intended dev/preview behavior — production should set the secret, otherwise expect cold-outreach and crypto spam to arrive as real-looking leads. - Rate limit — 60 requests/minute per key, enforced via Redis.
Submit a test through your real form, then check:
- Leads triage list — the submission appears with source and UTM tags.
- Notification bell — every user gets a "New lead" notification.
- On failure, the audit log and your relay's logs are the places to look.
-
401on every request — the key was revoked, expired, or theapi_keystable no longer holds it (e.g. after a database restore). Re-mint and update your site's secret. This failure mode is silent from the visitor's point of view — see "make your relay loud" above. -
403 Scope … required— the key exists but wasn't minted withlanding_leads:write. -
400 challenge_failed— the Turnstile token was missing/invalid whileTURNSTILE_SECRETis set; check the widget renders and the token field name matches. -
500on long values —phone(30) and other limited columns reject over-length input; validate lengths in your form.
- Sales & Estimating — where landing leads land and the lead pipeline they feed.
- Integrations — the rest of the public REST API and outbound webhooks.
-
Configuration Reference —
TURNSTILE_*and friends.