Forms
Overview

Forms

Gordon CRM includes a headless form submission API that lets you capture leads from any website or application. The API uses a keyless pipeline for browser-based submissions and API key authentication for server-to-server integrations.

How It Works

Each form in Gordon CRM has a unique submission endpoint:

POST https://app.gordoncrm.com/api/forms/\{FORM_ID\}

The API accepts both JSON (application/json) and form-encoded (application/x-www-form-urlencoded) request bodies. Every submission passes through a multi-step security pipeline before creating or updating a contact.

Form Settings

Each form has the following configurable properties:

FieldTypeDescription
nametextDisplay name for the form (e.g. "Contact Us", "Newsletter Signup").
allowed_domainstext[]List of hostnames allowed to submit via browser. Empty = open mode.
api_keytextSecret key for server-to-server authentication.
success_redirect_urltextOptional URL to redirect the user after submission (HTML embed forms).

Accepted Submission Fields

FieldRequiredDescription
emailThe contact's email address. Used as the unique identifier per workspace.
first_nameContact first name.
last_nameContact last name.
phoneContact phone number.
is_subscribedMarketing consent flag. Accepts true, "true", or "on". See Subscription Consent.
opt_in_sourceFreeform label for consent origin (defaults to the form ID if omitted).
notes / messageIf present, automatically attached as a contact note. See Contact Notes.
website_urlHoneypot field. Must be included in browser forms but left empty. See Honeypot.

Security Pipeline

Every submission passes through these checks in order. If any check fails, the request is rejected before reaching the next step.

StepCheckFailure Response
1Form lookup — Verify the form ID exists404 Form not found
2Rate limiting — IP-based throttle (5 requests per 60 seconds per form+IP)429 Too many requests
3Authentication — API key header OR domain whitelist (see below)403 Origin not allowed
4Body parsing — JSON or form-encoded400 Could not parse request body
5Honeypot — If website_url is populated, silently drop (return 200 to fool bots)Silent drop
6Email validationemail field must be present and non-empty400 email is required
7Double opt-in — Context-aware verification for existing contactsSee Double Opt-In

Authentication

The API supports two authentication methods depending on where the submission originates:

Browser Submissions (Domain Whitelist)

For websites and SPAs, the API validates the request's Origin or Referer header against the form's allowed_domains list. No API key is needed.

  • Exact matchyourwebsite.com matches requests from yourwebsite.com
  • Wildcard match*.yourwebsite.com matches www.yourwebsite.com, blog.yourwebsite.com, etc.

Open Mode: If the allowed_domains list is empty, the form accepts submissions from any origin. This is convenient for development and testing, but should be locked down before going live.

Server-to-Server (API Key)

For backend services, Zapier, Make.com, and mobile apps, include the form's secret API key in the x-api-key header. This bypasses the domain whitelist check entirely.

Security: Never expose the API key in client-side code. It is strictly for backend use.

See the HTML Embed, React / SPA, and Server / API Key integration guides for implementation examples.

Honeypot Spam Protection

Every browser-facing form should include a hidden website_url input field. This field is invisible to real users but gets filled out by automated spam bots:

<div style="position:absolute;left:-9999px" aria-hidden="true">
  <input type="text" name="website_url" tabindex="-1" autocomplete="off" />
</div>

If the API receives a non-empty website_url value, it silently drops the submission and returns 200 OK to fool the bot. The bot's IP is still recorded by the rate limiter.

Do not remove this field from browser forms, or your form will be vulnerable to spam. Server-to-server submissions (with API key) do not need this field.

Rate Limiting

The API enforces IP-based rate limiting per form:

ParameterValue
Window60 seconds
Max requests per IP per form5
Over-limit response429 Too many requests

Rate limit state is stored in a form_rate_limits table and managed by a Postgres RPC function. On RPC errors, the system fails open to avoid blocking legitimate submissions.

Context-Aware Double Opt-In

When a form submission is received, the API checks whether the email already exists as a contact in the workspace. The behavior differs based on this check:

New Contact (Email Not Found)

The submission is processed immediately:

  1. A new contact is created (or upserted) with the submitted data.
  2. The submission is logged in form_submissions.
  3. If notes or message is present, a contact note is attached.
  4. Automations are fired (see Automation Integration).
  5. The user is redirected to the success URL (or receives a JSON 200 response).

Existing Contact (Email Already Exists)

The submission is staged for verification:

  1. The payload is stored in pending_form_submissions with a cryptographic token (24-hour expiry).
  2. A verification email is sent to the contact's email address with a "Verify Submission" button.
  3. The user sees a success response immediately (to prevent email enumeration).
  4. When the contact clicks the verification link (/api/forms/verify?token=...), the staged submission is processed using the same flow as new contacts.

Why? This prevents impersonation attacks where someone submits a form using another person's email address to overwrite their contact data (name, phone, etc.).

If the verification link expires (after 24 hours) or has already been used, the user sees a friendly error page.

Subscription Consent

Forms can collect explicit marketing consent for email compliance (CAN-SPAM, GDPR). When the is_subscribed field is present and truthy (true, "true", or "on"):

  • is_subscribed is set to true on the contact record.
  • opt_in_timestamp is recorded with the current time.
  • opt_in_source is set to the provided value (or defaults to the form ID).
  • opt_in_ip is recorded from the submitter's IP address.
  • Any prior unsubscribed_at timestamp is cleared.
  • Non-complaint suppressions (bounce, unsubscribe, manual) are cleared.

Complaint suppressions are never cleared. If a contact previously filed a spam complaint, re-subscribing via a form will not remove the complaint suppression. This is a deliberate compliance safeguard.

Contact Notes from Submissions

If a form submission includes a notes or message field, the content is automatically attached as a note on the contact record. The note is formatted as:

Message added via '{form name}' form submission:

{content of the notes/message field}

This allows contact forms, inquiry forms, and similar use cases to capture freeform messages directly into the CRM timeline without any additional configuration.

Automation Integration

Form submissions fire the form_submission trigger in the Automations engine. This allows you to create rules like:

  • When "Contact Form" is submitted → Add tag "New Lead"
  • When "Demo Request" is submitted → Create task "Follow up with demo request"
  • When any form is submitted → Enroll in "Welcome" campaign

The trigger passes the form ID and the resulting contact ID to the automation engine. See Automations → Actions for the full list of available actions.

Success Responses

After successful processing, the API responds based on the form's success_redirect_url setting:

SettingResponse
success_redirect_url is set302 Redirect to the configured URL
success_redirect_url is empty200 OK with JSON \{ "success": true, "contact_id": "..." \}

For HTML embed forms, the redirect is the expected behavior. For SPA integrations, the JSON response allows inline success handling without a page navigation.

CORS

The API returns dynamic CORS headers based on the form's allowed_domains configuration:

  • Restricted mode (domains configured) — Access-Control-Allow-Origin echoes back the request origin only if it matches the whitelist.
  • Open mode (no domains configured) — Access-Control-Allow-Origin echoes back any origin.
  • The OPTIONS preflight handler returns the same dynamic headers.

This is relevant for React / SPA integrations where the browser enforces CORS policies on fetch() requests.

Automation Relationships

Each form's detail page includes an inline automation relationships card that shows all automation rules referencing this form. Relationships are rendered as natural-language sentences — for example, "When this form is submitted, it adds the New Lead tag to the contact."

See Automations → Dependency Tracking for details on the dependency tracking system.

Deleting Forms

Deleting a form permanently removes the form, all its submissions, and any automation rules triggered by this form.

If automation rules reference the form, the delete confirmation dialog warns:

"This form is used in N automation rule(s). Deleting it will also remove those rules and all submissions. Continue?"

Integration Guides

Choose your integration method based on your website or application:

  • HTML Embed — Squarespace, Wix, WordPress, Webflow, or any static HTML site.
  • React / SPA — React, Vue, Next.js, or any application using fetch().
  • Server / API Key — Zapier, Make.com, backend services, or mobile apps.