Skip to content

Developer guide · Signup forms

Build your own signup experience.

Keep your own HTML, styling, and website framework. Connect your custom form to a published smartest.email form through its public signup endpoint.

This guide covers

Public form configuration and signups from any website origin

Consent, validation, responses, and rate limits

Form creation and publication with an account access token

This is the signup form API guide, not a platform-wide OpenAPI specification. API keys are not available in the current release.

01 / Get started

Use the URL from your form.

Create a form in Signup forms, provide a required email field and a required consent checkbox, then publish it. The Install tab shows the exact URL for your form.

Base URL

https://api.smartest.email/api/v1/public/forms/{FORM_ID}

All endpoints on this page use https://api.smartest.email in production. Replace FORM_ID with your form’s UUID. Local Docker uses http://localhost:8080 instead. The Install tab shows the exact URL generated for your form.

1

Create

Set your fields and a clear consent label.

2

Publish

New forms start as drafts; public routes return 404 until published.

3

Integrate

Use the embed snippet or call the two public endpoints below.

02 / Your website

Use your own HTML form.

Direct submission is supported. Create and publish a form in smartest.email to define its fields and consent wording, then use its form ID in your existing website form. You can choose the embedded form type without installing the widget.

Your website can submit with JavaScript fetch(), or your own server can forward the signup as JSON. The public endpoint needs no API key, access token, or smartest.email embed script.

POST
https://api.smartest.email/api/v1/public/forms/{FORM_ID}/submit

Replace FORM_ID with the UUID of your published form. Send Content-Type: application/json with the payload below.

JSON request · after the visitor gives consent
{
  "data": {
    "email": "visitor@example.com"
  },
  "consent": true
}

Set consent from the visitor’s explicit choice, and display the form’s consent wording and privacy notice. Keep consent at the top level; place configured contact fields inside data. Include every required field in your published form.

A plain HTML form action needs an adapter

The endpoint accepts JSON only. A normal <form method="post" action="…"> sends URL-encoded or multipart data and receives 415 JSON body required. Use the browser JavaScript example below, or post to your own server and have it send JSON to smartest.email.

Fetching the public configuration keeps your labels and validation aligned with the published form. It is not a prerequisite for each submission. Your integration handles its own success message or redirect; this endpoint always responds with JSON.

03 / Account access

Create and publish a form.

The dashboard is the simplest route. If you manage forms programmatically, these account endpoints require an access token from login and the listed role permissions. Keep that token on a trusted server, never in a public website bundle.

POST
https://api.smartest.email/api/v1/forms/

Create a draft. Requires form:create and an available form slot on the account plan; returns 201 with the form ID.

PATCH
https://api.smartest.email/api/v1/forms/{FORM_ID}

Publish with {"isActive":true}, or unpublish with false. Requires form:edit.

GET
https://api.smartest.email/api/v1/forms/{FORM_ID}/embed-code

Get the exact HTML mount element and script URL. Requires form:view; returns {html,script}.

GET
https://api.smartest.email/api/v1/forms/{FORM_ID}/submissions?page=1&pageSize=10

List accepted signups with recorded consent. Requires form:view and contact:view; returns paginated contact summaries.

Send Authorization: Bearer YOUR_ACCESS_TOKEN and Content-Type: application/json for JSON requests. Form creation uses the limits currently assigned to your workspace; the pricing preview does not change those limits.

POST /api/v1/forms/ · request body
{
  "name": "Newsletter",
  "type": "embedded",
  "privacyUrl": "https://example.com/privacy",
  "fields": [
    { "name": "email", "type": "email", "label": "Email address", "required": true },
    { "name": "consent", "type": "checkbox", "label": "I agree to receive email updates.", "required": true }
  ],
  "styling": {},
  "successAction": {}
}
PATCH /api/v1/forms/{FORM_ID} · request body
{ "isActive": true }

Supported form types are embedded, popup, and landing. Every published form must have a required email field of type email and a required consent field of type checkbox. Use an HTTPS privacy URL when you provide one; local HTTP links are accepted only for localhost.

04 / Public configuration

Render the fields you published.

This public endpoint needs no access token. Fetch it when displaying a form, then use the returned field names, consent label, and privacy URL in your custom UI.

GET
https://api.smartest.email/api/v1/public/forms/{FORM_ID}

Returns 200 with safe rendering data for an active form. Every successful fetch increments its view count; avoid polling.

GET
https://api.smartest.email/api/v1/public/forms/{FORM_ID}/embed.js

Ready-made inline or popup widget script. Use the exact script URL from Install if you do not need a custom UI.

GET
https://api.smartest.email/api/v1/public/forms/{FORM_ID}/landing

Hosted signup page for an active form.

GET /api/v1/public/forms/{FORM_ID} · example response
{
  "id": "FORM_ID",
  "name": "Newsletter",
  "organizationName": "Example Studio",
  "type": "embedded",
  "fields": [
    { "name": "email", "type": "email", "label": "Email address", "required": true },
    { "name": "consent", "type": "checkbox", "label": "I agree to receive email updates.", "required": true }
  ],
  "styling": {},
  "successAction": {},
  "privacyUrl": "https://example.com/privacy"
}

The response can include additional configured fields, choices, and validation rules. successAction describes behavior for the ready-made widget; direct submission still returns the generic JSON response below. The script and hosted page return 404 for draft, unpublished, invalid, or unknown forms.

05 / Public submission

Send consent with the data.

Post JSON after the visitor explicitly chooses to receive marketing email. The consent flag belongs at the top level; do not put consent inside data.

POST
https://api.smartest.email/api/v1/public/forms/{FORM_ID}/submit

No access token. Send Content-Type: application/json and {data:{email:...},consent:true}. Returns the same generic success for new and existing addresses.

HTML + browser JavaScript · email and consent form
<form id="signup-form">
  <fieldset id="signup-fields" disabled>
    <legend>Subscribe to email updates</legend>
    <label id="email-label" for="signup-email">Email address</label>
    <input id="signup-email" type="email" autocomplete="email" required>
    <label for="signup-consent">
      <input id="signup-consent" type="checkbox" required>
      <span id="consent-label"></span>
    </label>
    <a id="privacy-link" href="/privacy">Privacy notice</a>
    <button id="signup-submit" type="submit">Subscribe</button>
  </fieldset>
  <p id="signup-status" role="status" aria-live="polite"></p>
</form>
<script type="module">
  const endpoint = "https://api.smartest.email/api/v1/public/forms/FORM_ID";
  const form = document.querySelector("#signup-form");
  const fields = document.querySelector("#signup-fields");
  const button = document.querySelector("#signup-submit");
  const status = document.querySelector("#signup-status");
  let ready = false;
  let submitting = false;

  form.addEventListener("submit", async event => {
    event.preventDefault();
    if (!ready || submitting || !form.reportValidity()) return;
    submitting = true;
    button.disabled = true;
    try {
      const response = await fetch(endpoint + "/submit", {
        method: "POST",
        credentials: "omit",
        headers: { "Content-Type": "application/json", "Accept": "application/json" },
        body: JSON.stringify({
          data: { email: document.querySelector("#signup-email").value.trim() },
          consent: document.querySelector("#signup-consent").checked
        })
      });
      const result = await response.json();
      status.textContent = result.message || "Please try again.";
      if (response.ok) form.reset();
    } catch {
      status.textContent = "Please try again.";
    } finally {
      submitting = false;
      button.disabled = false;
    }
  });

  try {
    const response = await fetch(endpoint, { credentials: "omit" });
    if (!response.ok) throw new Error("Signup form unavailable");
    const config = await response.json();
    document.querySelector("#email-label").textContent =
      config.fields.find(field => field.name === "email").label;
    document.querySelector("#consent-label").textContent =
      config.fields.find(field => field.name === "consent").label;
    if (config.privacyUrl) {
      document.querySelector("#privacy-link").href = config.privacyUrl;
    }
    ready = true;
    fields.disabled = false;
  } catch (error) {
    status.textContent = error.message;
  }
</script>

Replace FORM_ID with your form’s UUID, and set the fallback /privacy link to your own notice if the form has no configured privacyUrl. This example handles the two required fields; render and send any additional required fields from your published configuration.

Accepted response

JSON
{"success":true,"message":"Thank you for subscribing."}

The response intentionally does not say whether an address already exists. Existing subscribed and unsubscribed contacts keep their current state; only a new accepted address adds a subscriber and consent record.

Only send names present in the published fields array. Required fields must be supplied. For extra fields, send checkbox values as booleans, number values as numeric strings, dates as YYYY-MM-DD, and select or radio values exactly as listed in options. The API validates the data again on the server.

The optional top-level website property is a spam honeypot. Omit it or leave it empty for a normal submission. A nonempty value returns the generic success response without adding a contact; it is not a field for the visitor’s website URL.

06 / Operations

Handle failures and traffic limits.

Public form endpoints allow cross-origin GET, POST, and OPTIONS requests from external websites with Content-Type and Accept headers. Use credentials: omit in browser requests; no account cookies, Authorization header, or API key is needed.

StatusMeaning
200Configuration or accepted submit; a duplicate address gets the same submit response.
400 / 415Malformed JSON / request is not application/json.
404Unknown, draft, unpublished, or invalid form.
409The account has reached its contact limit.
413 / 422Body exceeds 16 KiB / consent or field data is invalid.
429Rate limit reached; browser code can use retry_after in the JSON body. Server integrations can also read Retry-After.

Errors use {"error":true,"message":"..."}; rate-limit errors also include retry_after in seconds. Form requests share the public API limit of 30 requests per IP per minute, with a further 10 submissions per IP per minute. Limits are enforced per running server process. A restrictive site Content Security Policy must allow https://api.smartest.email in connect-src; the ready-made embed also needs it in script-src.

When your own server forwards submissions, they share that server’s outgoing IP for rate limiting. The public endpoint does not authenticate the supplied Origin header; the recorded origin is only context for the consent record.

Consent and delivery

A new accepted signup creates a subscribed contact and saves the form’s consent label, time, and supplied request origin. Your custom UI must display the form’s consent text and privacy notice before the visitor submits. The recorded request origin is not proof of identity. This endpoint uses single opt-in; it does not send a confirmation email or verify ownership of the address.

AI writing API

Open AI writing in the email builder to draft email copy, suggest subject lines or rewrite text in any supported language. Only the brief or text you submit is sent to LLMHub. Review the result, then copy it, insert a text block or replace the original selected text. Nothing is saved or sent automatically.

AI writing has its own server switch and requires workspace access and permission to create or edit campaigns or templates. Allowances reset monthly in UTC. Each request dispatched to LLMHub counts toward the allowance, including failed or incomplete results; invalid requests rejected before dispatch do not count. The API key stays on the server.

Read the AI writing API contract

Interface and email languages

Your profile language controls the website, dashboard and account emails. Templates and campaigns store their own content language. Changing the interface language never translates or changes saved email content.

A new campaign can inherit its template’s language, then keeps its own saved choice. AI output language starts from the template or campaign language; a different AI output choice applies only to that request. Existing emails without a recorded language remain unset until you choose one.

Choosing a content language controls email language metadata, text direction and new starter copy. It does not translate existing text. Read the content language API contract for save, inheritance and compatibility rules.

Read the content language API contract

Automation API (preview)

Automations are available only in a local preview. They are not enabled on the production service.

Create signup or manual journeys, enroll existing subscribers, and review recorded step history.

Branch on whether a contact has a tag, is missing a tag, or matches a text field. Use first name, last name, phone, source, or a custom text field with equals, does not equal, contains, is empty, or is not empty.

Text comparisons are case-sensitive and preserve spaces. Missing or null custom fields count as empty; numbers, booleans, lists, and objects always take the No path. Each condition is checked when the contact reaches it, and its recorded decision stays unchanged on retries.

An A/B split sends each enrollment down one of two paths using your chosen percentage. Set path A to a whole percentage from 1 to 99; path B receives the remainder. Assignment is approximate, not an exact quota. Each enrollment keeps its recorded path across retries. Reports show A and B counts; the split does not choose a winning path automatically.

A journey can contain one Yes/No condition or one A/B split, with two distinct forward paths and at most 20 steps. Splits do not replace the required tag, email or field update action.

Wait until a contact has a selected tag, for up to 30 days. The condition is checked immediately and then about every 30 seconds. The timeout continues while a journey is paused; an expired wait exits the enrollment without running its later steps.

Update a contact's first name, last name, phone number, or a custom field with literal text. An empty value clears the field. Email addresses, subscription consent, and source cannot be changed by this action.

Updates accept up to 500 characters and preserve spaces and line breaks. Custom field keys use 1–100 characters without leading or trailing spaces. Only the selected custom field is replaced with text. Retries do not repeat committed updates, and execution history does not expose field values.

Manual enrollment accepts up to 100 contacts per request. Only active manual journeys accept enrollments, and each contact can enter once.

Enrolling contacts starts this journey. Its emails and other actions may run immediately.

Filter enrollments by status or email address. Stop an active enrollment with a recorded reason to prevent its future steps. Earlier activity and subscription consent stay unchanged, and the contact cannot enter the same journey again.

Stopping requires permission to view and activate journeys and view contacts. If an email is already being sent, wait for its result before trying again. Retrying a successful stop preserves its original reason and time.

Read the automation OpenAPI contract