Forms
Build hosted lead-capture forms with a typed field library, conditional logic, spam protection, consent capture, and submission actions that create leads or update contacts.
Forms
Pact forms are hosted, embeddable lead-capture pages. You design the fields,
publish to a slug, and every submission is stored, analyzed, and routed to a
CRM action — a new lead or a contact update — with consent captured inline.
Admin CRUD lives at /v1/forms; the public render and submit endpoints live at
/public/forms. Backing tables are tenant_forms and form_submissions
(alembic 0117).
The field library
A form definition is a list of typed fields. Supported types
(_VALID_FIELD_TYPES): text, email, phone, textarea, select,
multi_select, checkbox, radio, number, date, file, signature,
payment, rating, hidden, and consent. Each field can be required,
half- or full-width, carry options, and map to a tenant custom field on
the target entity via custom_field_id.
Two capabilities worth calling out:
- Conditional logic — a field's
show_ifpredicate shows it only when another field equals a given value. - Consent fields carry
consent_textand record a consent grant on submit (see below).
Create, update, delete
GET /v1/forms → list (paginated)
POST /v1/forms → create
GET /v1/forms/{form_id} → get one
PATCH /v1/forms/{form_id} → update name/slug/definition/status/action/settings
DELETE /v1/forms/{form_id} → soft delete
A form has a status (draft, published, archived), a URL slug
(lowercase, ^[a-z0-9_-]+$), and a submission_action — create_lead,
update_contact, or none. Reads are open to any authenticated member; writes
require member-or-higher, consistent with other CRM objects. Tenant isolation is
derived from the auth context.
Start from a template
GET /v1/forms/templates → list the built-in catalog
POST /v1/forms/templates/clone → clone a template into a new form
Pact ships a catalog of ready-made templates in core/form_templates.py —
including lead capture, demo request, support ticket, NPS, event and webinar
registration, free-trial signup, quote request, job application, and a
GDPR data-request form — each pre-wired with sensible fields, a consent field,
and a suggested slug.
Theme and behavior
The form definition carries a success_message, an optional redirect_url
(scheme-validated to block javascript:/data: XSS), notification emails
(notify_emails), and a theme object (primary/accent/background colors, logo,
header text, font) that layers per-form overrides on top of tenant branding. The
logo_url and redirect_url are both validated to allow only http(s).
Spam protection and consent (public submit)
The public submit path (/public/forms, api/routes/public_forms.py) applies
several safeguards before a submission is accepted:
- CAPTCHA — Google reCAPTCHA (
g_recaptcha_response) and Cloudflare Turnstile are both verified server-side when configured; secrets come from the tenant credential store. - Honeypot field and IP rate-limiting.
- Consent capture — when a
consentfield is present, the submission records a consent grant throughcore.consent.consent_service, tying the lead to a verifiable opt-in. - UTM + source —
utm_paramsandsource_urlare captured for attribution.
Submission action dispatch
After validation, the submission is dispatched per the form's submission_action:
create_lead— creates a lead vialead_service, carrying the captured UTM and source data, ready for an AE to convert.update_contact— matches an existing contact (e.g. by email) and updates it.none— stores the submission only.
Custom hosting domains
Public forms can be served from your own domain. Register and DNS-verify a
form-hosting domain at /v1/form-domains (backed by
tenant_form_domains); Pact resolves the tenant from the request
host at render time.