Signup Forms

Embed forms on your website to collect contacts. Submissions feed into the same Contacts API, with optional double opt-in confirmation and a contact.subscribed webhook event.

How it works

  1. Create a form in the dashboard at /forms and customize fields, button colour, and double opt-in behaviour.
  2. Copy one of two embed snippets (plain HTML <form> or a JS widget) and paste it on your site.
  3. When a visitor submits, the public endpoint /api/forms/{id}/submit validates input and either upserts the contact directly (DOI off) or sends a confirmation email (DOI on).
  4. On confirmation (or direct add) we fire the contact.subscribed webhook event so your downstream automations can pick up.

Form Object

json
{
  "object": "signup_form",
  "id": "clx_form123",
  "name": "Newsletter signup",
  "fields": ["email", "first_name"],
  "button_text": "Subscribe",
  "button_color": "#10b981",
  "success_message": "Thanks for subscribing!",
  "double_opt_in": true,
  "redirect_url": null,
  "captcha_enabled": false,
  "captcha_provider": "turnstile",
  "turnstile_site_key": null,
  "turnstile_secret_configured": false,
  "block_disposable_emails": true,
  "active": true,
  "created_at": "2026-05-01T12:00:00.000Z",
  "updated_at": "2026-05-01T12:00:00.000Z"
}
POST/v1/forms

Create a new signup form. Requires the form:write scope.

Request Body

json
{
  "name": "Newsletter signup",
  "fields": ["email", "first_name"],
  "buttonText": "Subscribe",
  "buttonColor": "#10b981",
  "doubleOptIn": true,
  "captchaEnabled": false,
  "blockDisposableEmails": true
}

Response

json
{
  "object": "signup_form",
  "id": "clx_form123",
  "name": "Newsletter signup",
  ...
}
GET/v1/forms

List all signup forms for the current account. Requires the form:read scope.

Response

json
{
  "object": "list",
  "has_more": false,
  "data": [ /* array of form objects */ ]
}
PATCH/v1/forms/{id}

Update a form. Any field that's omitted is left unchanged. Requires form:write.

Request Body

json
{
  "buttonColor": "#f97316",
  "active": false
}

Response

json
{
  "object": "signup_form",
  "id": "clx_form123",
  "active": false,
  ...
}
DELETE/v1/forms/{id}

Delete a signup form. Embedded snippets pointing at this form will stop working immediately.

Response

json
{
  "object": "signup_form",
  "id": "clx_form123",
  "deleted": true
}

Embedding on your site

The dashboard generates two snippets per form. Pick whichever fits your site.

Plain HTML form (no JavaScript)

Submits as a regular <form> POST. The browser navigates to a thanks page (or your redirect_url) after the request.

html
<form action="https://smtpfa.st/api/forms/clx_form123/submit" method="POST">
  <input type="email" name="email" placeholder="[email protected]" required />
  <input type="text" name="first_name" placeholder="First name" />
  <input type="text" name="_hp" tabindex="-1" autocomplete="off" style="position:absolute;left:-9999px"/>
  <button type="submit">Subscribe</button>
</form>

JavaScript widget (recommended)

A small async script renders the form into the placeholder div, posts via fetch, and shows the success message inline. Style overrides come from your form config so dashboard edits propagate without re-pasting the snippet.

html
<div data-smtpfast-form="clx_form123"></div>
<script src="https://smtpfa.st/api/forms/clx_form123/embed.js" async></script>

Public submission endpoint

The submission endpoint is intentionally anonymous (no API key) so it can be called from unprivileged web pages. Authorization is implicit in the form ID.

POST/api/forms/{id}/submit

Public form submission. Accepts both application/json and application/x-www-form-urlencoded bodies. CORS is open. Rate-limited to 10 requests / minute / IP / form. The honeypot field (_hp), disposable-email filter, per-email confirmation throttle, and optional Turnstile verification reduce spam.

Request Body

json
{
  "email": "[email protected]",
  "first_name": "Steve"
}

Response

json
{
  "success": true,
  "message": "Almost done! We sent a confirmation link to [email protected]."
}

Double opt-in flow

When double_opt_in is true (recommended), submissions are stashed in a pending table for up to 7 days while we email a confirmation link. The contact only appears in your account — and the contact.subscribed webhook only fires — after the recipient clicks through.

  • Confirmation links are signed with HMAC-SHA256, so tampered tokens are rejected without a DB lookup.
  • Re-submitting the same email replaces the pending row — only the most recent confirmation link works.
  • Re-clicking a successful confirmation link is idempotent; it won't create a duplicate contact.
  • Confirmation emails are sent from a SMTPfast-managed sender so forms work even before you've verified a domain.

contact.subscribed webhook

Subscribe to this event on any of your webhooks to receive notifications when a contact joins via a form (or via direct API upsert from the public submit endpoint).

json
{
  "type": "contact.subscribed",
  "data": {
    "contact_id": "clx_contact123",
    "email": "[email protected]",
    "first_name": "Steve",
    "last_name": null,
    "form_id": "clx_form123",
    "double_opt_in": true
  },
  "timestamp": "2026-05-01T12:00:00.000Z"
}

Bot protection & rate limits

  • Honeypot: Forms include a hidden _hp field. Real users don't fill it; bots usually do. Tripped submissions return a normal-looking success without storing anything.
  • Rate limit: 10 requests per minute per (form, IP). Excess returns HTTP 429.
  • Confirmation throttle: Double opt-in emails are capped per (form, email) to slow confirmation-mail abuse.
  • Cloudflare Turnstile: Enable CAPTCHA in the form builder by adding your Turnstile site key and secret key. Tokens are validated server-side before contacts or pending confirmations are created.
  • Disposable email filter: Common temporary inbox domains are silently ignored by default.
  • Suppression list: Submissions for addresses already on the form-owner's suppression list (previously bounced or complained) are silently dropped.
  • Confirmation expiry: Pending DOI tokens are valid for 7 days. After that the submitter must resubmit.