FormBox.ai docs

Professional integration docs for agent-run form ops.

FormBox gives each website secure Hooks, ordered Inbox submissions, clean notifications, file handling, AI review, reports, and an API/MCP surface agents can operate without turning the dashboard into a maze.

Start here

  1. Create a Site.
  2. Create a Hook under that Site.
  3. Copy the Hook URL and secret.
  4. Wire the website form.
  5. Send a safe test and verify Inbox + Activity.

Quickstart

5-minute setup

  1. Step 1

    Add a Site with the website domain.

  2. Step 2

    Create a Hook for the form, like Quote Request or Contact.

  3. Step 3

    Set notification emails, CC emails, and allowed origins if needed.

  4. Step 4

    Copy the live Hook URL and intake secret from the Hook page.

  5. Step 5

    Wire HTML, fetch, or a same-origin server proxy.

  6. Step 6

    Submit one safe test, then confirm the item appears in Inbox with delivery Activity.

Use this exact Hook URL shape

Post submissions to https://hooks.formbox.ai/f/{hookId}/{slug}. Expected success response: { "ok": true, "inboxItemId": "...", "fileCount": 0 }.

Model

Core concepts

Site

A website/domain in FormBox. Sites own hooks, timezone overrides, and scoped dashboard views.

Hook

A secure intake URL for one form or workflow. Hooks store notification routing, allowed origins, status, and an intake secret.

Inbox

The operational queue of submissions. Each item keeps readable fields, raw payload JSON, metadata, files, delivery activity, and AI review state.

Reports

Filtered submission totals, charts by day/site/hook, failed-delivery counts, and CSV export.

Settings

Workspace defaults: timezone, email templates, AI review, users, API keys, and delivery ops.

API / MCP

REST/OpenAPI is the source of truth. The local MCP server wraps the same API for agent workflows.

Product boundary

FormBox is not a form builder. It is the form intake layer: secure Hooks, stored Inbox items, notification routing, files, metadata, reports, AI review, and API/MCP operations.

Integration

Choose the right integration path

Example

HTML form

Best fit for static sites and normal contact or quote forms. This example includes the Hook secret, honeypot, timing field, field order hint, Residential/Commercial conditional company field, and common field normalization.

HTML
<form action="https://hooks.formbox.ai/f/{hookId}/{slug}" method="POST" data-formbox-form>
  <input type="hidden" name="_formbox_secret" value="fbsec_...">
  <input type="text" name="_formbox_hp" tabindex="-1" autocomplete="off" style="position:absolute;left:-9999px" aria-hidden="true">
  <input type="hidden" name="_formbox_started_at" value="">
  <input type="hidden" name="_formbox_field_order" value='["property_type","project_type","project_details","address","city","state","zip","company_name","name","phone","email"]'>
  <fieldset>
    <legend>Property Type</legend>
    <label><input type="radio" name="property_type" value="Residential" checked> Residential</label>
    <label><input type="radio" name="property_type" value="Commercial"> Commercial</label>
  </fieldset>
  <label>
    Project Type
    <input name="project_type" autocomplete="off" required>
  </label>
  <label>
    Project Details
    <textarea name="project_details"></textarea>
  </label>
  <label>
    Street Address
    <input name="address" autocomplete="address-line1" required>
  </label>
  <label>
    City
    <input name="city" autocomplete="address-level2" required>
  </label>
  <label>
    State
    <input name="state" autocomplete="address-level1" maxlength="2" pattern="[A-Za-z]{2}" required>
  </label>
  <label>
    ZIP Code
    <input name="zip" autocomplete="postal-code" inputmode="numeric" pattern="[0-9]{5}" maxlength="5" required>
  </label>
  <label data-company-field hidden>
    Company Name
    <input name="company_name" autocomplete="organization" disabled>
  </label>
  <label>
    Name
    <input name="name" autocomplete="name" required>
  </label>
  <label>
    Phone
    <input name="phone" type="tel" inputmode="tel" autocomplete="tel" pattern="[0-9]{3}-[0-9]{3}-[0-9]{4}" required>
  </label>
  <label>
    Email
    <input name="email" type="email" autocomplete="email" required>
  </label>
  <button type="submit">Send</button>
</form>
<script>
(() => {
  const form = document.currentScript && document.currentScript.previousElementSibling;
  if (!(form instanceof HTMLFormElement)) return;

  const companyField = form.querySelector('[data-company-field]');
  const companyInput = form.querySelector('[name="company_name"]');
  const propertyTypeInputs = form.querySelectorAll('[name="property_type"]');
  const startedAtInput = form.querySelector('[name="_formbox_started_at"]');
  if (startedAtInput instanceof HTMLInputElement) startedAtInput.value = String(Date.now());

  const titleCaseWhenLowercase = (value) => {
    const trimmed = value.replace(/\s+/g, ' ').trimStart();
    if (!/[a-z]/.test(trimmed) || trimmed !== trimmed.toLowerCase()) return trimmed;
    return trimmed.replace(/\b[a-z]/g, (letter) => letter.toUpperCase());
  };

  const collapseRepeatedAddress = (value) => {
    const normalized = String(value || '').trim().replace(/\s*,\s*/g, ', ').replace(/\s+/g, ' ');
    const parts = normalized.split(',').map((part) => part.trim()).filter(Boolean);
    if (parts.length === 2 && parts[0].toLowerCase() === parts[1].toLowerCase()) return parts[0];
    return normalized;
  };

  const formatPhone = (value) => {
    const digits = value.replace(/\D/g, '').slice(0, 10);
    if (digits.length <= 3) return digits;
    if (digits.length <= 6) return digits.slice(0, 3) + '-' + digits.slice(3);
    return digits.slice(0, 3) + '-' + digits.slice(3, 6) + '-' + digits.slice(6);
  };

  const syncCompanyField = () => {
    const checked = form.querySelector('[name="property_type"]:checked');
    const isCommercial = checked && checked.value === 'Commercial';
    if (companyField instanceof HTMLElement) companyField.hidden = !isCommercial;
    if (companyInput instanceof HTMLInputElement) {
      companyInput.disabled = !isCommercial;
      companyInput.required = Boolean(isCommercial);
      if (!isCommercial) companyInput.value = '';
    }
  };

  const normalizeField = (input) => {
    if (['name', 'company_name', 'address', 'city'].includes(input.name)) input.value = titleCaseWhenLowercase(input.name === 'address' ? collapseRepeatedAddress(input.value) : input.value);
    if (input.name === 'email') input.value = input.value.trim().toLowerCase();
    if (input.name === 'phone') input.value = formatPhone(input.value);
    if (input.name === 'state') input.value = input.value.replace(/[^a-z]/gi, '').slice(0, 2).toUpperCase();
    if (input.name === 'zip') input.value = input.value.replace(/\D/g, '').slice(0, 5);
    if (input instanceof HTMLTextAreaElement) input.value = input.value.replace(/\s+/g, ' ').trim();
  };

  propertyTypeInputs.forEach((input) => input.addEventListener('change', syncCompanyField));
  form.addEventListener('input', (event) => {
    const input = event.target;
    if (!(input instanceof HTMLInputElement || input instanceof HTMLTextAreaElement)) return;
    normalizeField(input);
  });
  form.addEventListener('submit', () => {
    form.querySelectorAll('input:not([type="hidden"]):not([type="radio"]), textarea').forEach((field) => {
      if (field instanceof HTMLInputElement || field instanceof HTMLTextAreaElement) normalizeField(field);
    });
  });
  syncCompanyField();
})();
</script>

Agent integration brief

HTML form posts, JSON fetches, and multipart uploads all work. Send the Hook secret as X-FormBox-Secret or hidden field _formbox_secret. Keep _formbox_hp empty and include _formbox_started_at for bot checks. Normalize and validate common contact fields in the website form or same-origin server/function before posting.

Example

JavaScript / Fetch

Best fit when the site already intercepts submits with JavaScript. Send business fields inside payload and source/tracking context inside metadata.

JavaScript
await fetch("https://hooks.formbox.ai/f/{hookId}/{slug}", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-FormBox-Secret": "fbsec_...",
  },
  body: JSON.stringify({
    payload: {
      property_type: "Residential",
      project_type: "Driveway gate",
      project_details: "Hello from FormBox",
      name: "Test Lead",
      phone: "801-555-0100",
      email: "test@example.com",
    },
    metadata: {
      fieldOrder: ["property_type", "project_type", "project_details", "name", "phone", "email"],
      pageUrl: "https://demo-site.com/quote?utm_source=google&utm_medium=cpc",
      landingPageUrl: "https://demo-site.com/?utm_source=google&utm_medium=cpc&gclid=test123",
      referrer: "https://www.google.com/",
      userAgent: navigator.userAgent,
      utm: {
        source: "google",
        medium: "cpc",
        campaign: "spring-gates",
      },
      clickIds: {
        gclid: "test123",
      },
    },
  }),
});

Example

Server-side proxy pattern

This is the safest production pattern for client sites: browser form to same-origin endpoint, validate captcha/Turnstile and required fields there, then forward to FormBox with the server-side Hook secret.

Server-side JavaScript
// Browser -> /api/quote on the same website -> FormBox
export async function POST(request) {
  const body = await request.json();

  // Verify Turnstile/captcha here when the website uses one.
  // Normalize fields before forwarding: phone, email, state, ZIP, etc.

  const response = await fetch(process.env.FORMBOX_INTAKE_URL, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-FormBox-Secret": process.env.FORMBOX_HOOK_SECRET,
    },
    body: JSON.stringify({
      payload: {
        property_type: body.property_type,
        project_details: body.project_details,
        name: body.name,
        phone: body.phone,
        email: body.email,
      },
      metadata: {
        fieldOrder: ["property_type", "project_details", "name", "phone", "email"],
        pageUrl: body._formbox_page_url,
        landingPageUrl: body._formbox_landing_page_url,
        referrer: body._formbox_referrer,
        userAgent: request.headers.get("user-agent") || body._formbox_user_agent,
        utm: {
          source: body._formbox_utm_source,
          medium: body._formbox_utm_medium,
          campaign: body._formbox_utm_campaign,
          term: body._formbox_utm_term,
          content: body._formbox_utm_content,
        },
        clickIds: {
          gclid: body._formbox_gclid,
          gbraid: body._formbox_gbraid,
          wbraid: body._formbox_wbraid,
          fbclid: body._formbox_fbclid,
          msclkid: body._formbox_msclkid,
        },
      },
    }),
  });

  if (!response.ok) return Response.json({ ok: false }, { status: 502 });
  return Response.json({ ok: true });
}

Fail closed on website protection

If the site uses Turnstile or similar protection, a missing token should fail before forwarding to FormBox. Do not silently downgrade to a direct public post just to make a preview submit.

Example

File uploads

Use multipart/form-data for files. FormBox stores private file objects, shows files on submission detail, and includes token-protected file links in notification emails.

Max files5 files per submission
Max size10MB per file
Allowed typesJPG, JPEG, PNG, WEBP, HEIC, HEIF, PDF, DOC, DOCX, and TXT
DownloadsDashboard downloads require session/org access. Email links use signed FormBox routes and private storage redirects.
HTML multipart
<form action="https://hooks.formbox.ai/f/{hookId}/{slug}" method="POST" enctype="multipart/form-data">
  <input type="hidden" name="_formbox_secret" value="fbsec_...">
  <input type="hidden" name="_formbox_field_order" value='["name","phone","project_details","photos"]'>
  <input name="name" required>
  <input name="phone" type="tel" required>
  <textarea name="project_details"></textarea>
  <input name="photos" type="file" accept="image/*,.pdf,.doc,.docx,.txt" multiple>
  <button type="submit">Send</button>
</form>

Data model

Payload vs metadata

Payload is what the visitor submitted. Metadata is context about how the submission arrived. Keeping this split clean is why client notification emails stay readable instead of turning into tracking soup.

Payload examples

  • name
  • phone
  • email
  • property_type
  • project_type
  • project_details
  • address
  • city
  • state
  • zip
  • photos

Metadata examples

  • fieldOrder
  • pageUrl
  • referrer
  • utm source/medium/campaign/term/content
  • gclid / gbraid / wbraid / fbclid / msclkid
  • IP address
  • user agent
  • parsed lead source

Do not send tracking as client-facing fields

Put page URL, referrer, user agent, UTM values, and click IDs in metadata or supported _formbox_* fields. FormBox strips those from visible payload fields and uses them for internal metadata, reporting, and Global BCC email context.

Attribution

Preserve lead source through navigation

Paid traffic usually lands on the homepage or a service page, then the visitor clicks to a quote form later. Capture first-touch source data on every page, store it with first-party browser storage, and inject reserved _formbox_* fields at submit time.

Capture and forward

  • For website forms, preserve first-touch attribution before the visitor reaches the form; do not rely on the form page being the landing page.
  • Use first-party sessionStorage plus localStorage with a reasonable TTL for UTMs, click IDs, landing page URL, and referrer.
  • Forward attribution as metadata or _formbox_* reserved fields only: page URL, landing page URL, referrer, user agent, UTM source/medium/campaign/term/content, and click IDs gclid, gbraid, wbraid, fbclid, and msclkid.
  • Do not send UTM values, click IDs, page URL, referrer, or user agent as visible payload/business fields because they will appear in client-facing submitted-field tables.
  • Verify attribution by landing on a URL with UTMs and a click ID, navigating to the form, and confirming the FormBox metadata keeps the original source values.

Reserved metadata fields

  • _formbox_page_url for the current form page at submit time.
  • _formbox_landing_page_url for the original first-touch landing page.
  • _formbox_referrer and _formbox_user_agent.
  • _formbox_utm_source, _formbox_utm_medium, _formbox_utm_campaign, _formbox_utm_term, and _formbox_utm_content.
  • _formbox_gclid, _formbox_gbraid, _formbox_wbraid, _formbox_fbclid, and _formbox_msclkid.
First-touch attribution helper
// Run on every page, before the visitor reaches the form.
const ATTRIBUTION_KEY = "formbox_attribution_v1";
const TTL_MS = 90 * 24 * 60 * 60 * 1000;
const params = new URLSearchParams(window.location.search);
const stored = JSON.parse(localStorage.getItem(ATTRIBUTION_KEY) || "null");
const fresh = !stored || Date.now() - stored.createdAt > TTL_MS;
const hasSource = ["utm_source", "utm_medium", "utm_campaign", "gclid", "gbraid", "wbraid", "fbclid", "msclkid"].some((key) => params.get(key));

if (fresh || (!stored?.hasClickId && hasSource)) {
  const attribution = {
    createdAt: Date.now(),
    landingPageUrl: window.location.href,
    referrer: document.referrer,
    hasClickId: Boolean(params.get("gclid") || params.get("gbraid") || params.get("wbraid") || params.get("fbclid") || params.get("msclkid")),
    utm: {
      source: params.get("utm_source"),
      medium: params.get("utm_medium"),
      campaign: params.get("utm_campaign"),
      term: params.get("utm_term"),
      content: params.get("utm_content"),
    },
    clickIds: {
      gclid: params.get("gclid"),
      gbraid: params.get("gbraid"),
      wbraid: params.get("wbraid"),
      fbclid: params.get("fbclid"),
      msclkid: params.get("msclkid"),
    },
  };
  localStorage.setItem(ATTRIBUTION_KEY, JSON.stringify(attribution));
  sessionStorage.setItem(ATTRIBUTION_KEY, JSON.stringify(attribution));
}

document.addEventListener("submit", (event) => {
  const form = event.target;
  if (!(form instanceof HTMLFormElement)) return;
  const attribution = JSON.parse(sessionStorage.getItem(ATTRIBUTION_KEY) || localStorage.getItem(ATTRIBUTION_KEY) || "{}");
  const setHidden = (name, value) => {
    if (!value) return;
    let input = form.querySelector('input[name="' + name + '"]');
    if (!input) {
      input = document.createElement("input");
      input.type = "hidden";
      input.name = name;
      form.appendChild(input);
    }
    input.value = String(value);
  };

  setHidden("_formbox_page_url", window.location.href);
  setHidden("_formbox_landing_page_url", attribution.landingPageUrl);
  setHidden("_formbox_referrer", attribution.referrer || document.referrer);
  setHidden("_formbox_user_agent", navigator.userAgent);
  setHidden("_formbox_utm_source", attribution.utm?.source);
  setHidden("_formbox_utm_medium", attribution.utm?.medium);
  setHidden("_formbox_utm_campaign", attribution.utm?.campaign);
  setHidden("_formbox_utm_term", attribution.utm?.term);
  setHidden("_formbox_utm_content", attribution.utm?.content);
  setHidden("_formbox_gclid", attribution.clickIds?.gclid);
  setHidden("_formbox_gbraid", attribution.clickIds?.gbraid);
  setHidden("_formbox_wbraid", attribution.clickIds?.wbraid);
  setHidden("_formbox_fbclid", attribution.clickIds?.fbclid);
  setHidden("_formbox_msclkid", attribution.clickIds?.msclkid);
}, true);

Global BCC emails use this metadata

Client-facing notification field tables stay clean. Separate Global BCC copy emails can show notification To/CC routing, Global BCC recipients, lead source, campaign, click IDs, page URL, referrer, and user agent when includeMetadataForGlobalBcc is enabled.

Display order

Keep form, email, dashboard, API, and CSV order synced

Send the visible business-field order every time an agent builds or reorders a form. FormBox stores it as metadata, removes it from visible payloads, and uses it for notification emails, dashboard Submitted fields, API payloadFields, and CSV exports.

HTML field order
<input type="hidden" name="_formbox_field_order" value='["property_type","project_details","name","phone","email"]'>
JSON field order
{
  "payload": {
    "property_type": "Residential",
    "project_details": "Gate repair",
    "name": "Test Lead",
    "phone": "801-555-0100",
    "email": "test@example.com"
  },
  "metadata": {
    "fieldOrder": ["property_type", "project_details", "name", "phone", "email"]
  }
}

Agents

Agent form-building standards

Do not blindly add every standard field. Choose fields from the form purpose, site context, and lead-quality needs.

Choose fields by workflow

  • Ask one concise question when the context does not make full address vs city/ZIP obvious.
  • Use a full address when the workflow needs an on-site estimate, property lookup, service-area confirmation, or dispatch.
  • Use city or ZIP only when rough service-area qualification is enough.
  • Use a service or project_type dropdown when the business has distinct lead categories.
  • Use checkboxes for multi-select needs and radio buttons or a dropdown for exactly-one choices.
  • Use file/photo uploads only when visuals materially improve quoting or triage.
  • For residential/commercial forms, use a property_type radio group with Residential and Commercial options.
  • Show company_name above name only when Commercial is selected; keep it disabled and not required for Residential.

Normalize before POST

  • Use a single-line address field only when the workflow needs a service address.
  • Keep city, state, and ZIP as separate fields when they are collected.
  • City should be trimmed and conservative Title Case for all-lowercase input.
  • Capitalize the first letter of street words in address fields without rewriting unit numbers or directional abbreviations.
  • State must be exactly two uppercase letters.
  • ZIP must be digits only and exactly five digits.
  • Normalize and validate common fields in the website form or same-origin server/function before posting to FormBox; FormBox preserves submitted business fields exactly and does not clean them later.
  • Trim all submitted string fields before forwarding to FormBox.
  • Email should be lowercase and valid email format before submit.
  • Phone should use type=tel, accept 10 US digits with no country code, and normalize display with dashes as 801-555-1234 before submit.
  • Name should trim spaces and capitalize the first letter when the whole value is lowercase, but do not damage mixed-case names.
  • Freeform message/project_details fields should be trimmed but not rewritten.
  • Use Title Case labels and lowercase/snake_case payload keys.

Preserve attribution

  • For website forms, preserve first-touch attribution before the visitor reaches the form; do not rely on the form page being the landing page.
  • Use first-party sessionStorage plus localStorage with a reasonable TTL for UTMs, click IDs, landing page URL, and referrer.
  • Forward attribution as metadata or _formbox_* reserved fields only: page URL, landing page URL, referrer, user agent, UTM source/medium/campaign/term/content, and click IDs gclid, gbraid, wbraid, fbclid, and msclkid.
  • Do not send UTM values, click IDs, page URL, referrer, or user agent as visible payload/business fields because they will appear in client-facing submitted-field tables.
  • Verify attribution by landing on a URL with UTMs and a click ID, navigating to the form, and confirming the FormBox metadata keeps the original source values.

Before calling it done

  • Visible fields match the client workflow.
  • Payload keys are lowercase/snake_case.
  • _formbox_field_order matches visible order.
  • Contact fields are validated and normalized before POST.
  • Tracking/source data is metadata, not payload.
  • A UTM/click-ID navigation test proves first-touch attribution survives before form submit.
  • Missing secret/captcha paths fail closed.
  • A safe test appears in Inbox with expected Activity.

Dashboard

Dashboard reference

This is the current app surface area. If it exists in the dashboard, it belongs here — no mystery buttons, no tribal knowledge tax.

InboxSearch/filter submissions by date, site, hook, delivery status, files, and payload text. Detail pages show submitted fields, raw JSON, metadata, files, activity, AI review, and owner-only permanent delete.
SitesCreate sites by domain, filter website list, set site timezone, create connected hooks, and review recent submissions for that site.
HooksCopy the live Hook URL and intake secret, send a test inbox item, pause/reactivate, set notification and CC emails, set allowed origins, and review latest submissions.
ReportsRun filtered reports across all time, date presets, custom ranges, sites, and hooks. Export CSV with ordered payload fields.
Settings / GeneralSet the account timezone used when a site does not have its own timezone override.
Settings / EmailConfigure Global BCC copy emails, metadata on Global BCC copies, table/plain text format, subject and sender templates, and reference numbers.
Settings / AI ReviewEnable spam review, choose label-only or block high-confidence spam emails, and set the spam threshold.
Settings / UsersOwner-only user creation, role changes, and non-owner user removal. Roles are owner, manager, and viewer.
Settings / APIOwner-only API key creation, reveal/copy, revoke/reinstate, and delete. Presets include Agent — Read/Write, Agent — Read Only, and Admin.
Settings / OpsFocused failed/stuck delivery review for operational cleanup.
AccountUpdate user profile name, agency workspace name, and see the current role/preferences.

Roles

Owner manages everything, including users, API keys, and permanent deletes. Manager can manage normal records like Sites and Hooks. Viewer is read-only.

Routing

Email notifications, delivery retries, and AI review

Email routing

  • Hook notification emails are the primary client/internal recipients for that form.
  • Hook CC emails are separate per-form CC recipients.
  • Global BCC copies are sent as separate internal copy emails, not literal SMTP BCC.
  • Client-facing notifications stay clean; metadata appears only on Global BCC copy emails when enabled.
  • Subjects can include reference numbers like (Ref: 1001).
  • Sender display can use the submitted name while the real sending address stays verified.

AI review

  • Label-only mode stores and sends normally, then labels Inbox items legit/suspicious/spam.
  • Block-spam mode stores every submission and suppresses email only for high-confidence spam at or above the threshold.
  • Failures and uncertain reviews fail open so legitimate leads are not silently lost.
  • Submission detail includes current status, verdict, confidence, reasons, manual re-run, and raw AI JSON when present.

Delivery model

FormBox stores the Inbox item first, attempts notification delivery, records Activity rows, and uses protected cron processing to retry pending, failed, or stuck deliveries. Failed deliveries can be retried from submission detail or API/MCP.

Automation

Management API, OpenAPI, and MCP

Use API keys for agents and automation. Send the key as a bearer token.

Authentication
Authorization: Bearer fb_...
Provision Site + Hook
curl -X POST https://app.formbox.ai/api/v1/provision-site-intake   -H "Authorization: Bearer fb_..."   -H "Content-Type: application/json"   -d '{
    "domain": "demo-site.com",
    "forms": [
      {
        "name": "Quote Request",
        "notificationEmail": "leads@demo-site.com",
        "notificationCcEmails": ["owner@demo-site.com"],
        "allowedOrigins": ["https://demo-site.com", "https://www.demo-site.com"]
      }
    ]
  }'
SitesGET/POST /api/v1/sitesGET/PATCH/DELETE /api/v1/sites/{siteId}
HooksGET/POST /api/v1/hooksGET/PATCH/DELETE /api/v1/hooks/{hookId}; POST /api/v1/hooks/{hookId}/rotate-secret
InboxGET /api/v1/inboxGET /api/v1/inbox/{inboxItemId}; files, downloads, deliveries, and retry routes
SettingsGET/PUT /api/v1/settings/emailAPI keys, users, and ops delivery routes under /api/v1/settings and /api/v1/ops
ProvisioningPOST /api/v1/provision-site-intakeCreates a site plus one or more hooks in one agent workflow.
SchemaGET /api/v1/openapi.jsonFull machine-readable API reference.

API key presets

Agent — Read/Writesites:*, hooks:*, inbox:*, settings:*, api_keys:*, users:*, ops:*, provision:write
Agent — Read Onlysites:read, hooks:read, inbox:read, settings:read, api_keys:read, users:read, ops:read
Admin*

MCP tools

The local stdio MCP wrapper exposes these workflow tools and maps them to REST:

list_sitescreate_siteget_siteupdate_sitearchive_sitelist_hookscreate_hookget_hookupdate_hookarchive_hookrotate_hook_secretlist_inboxget_inbox_itemlist_inbox_filesget_inbox_file_download_urllist_inbox_deliveriesretry_inbox_deliveryget_email_settingsupdate_email_settingslist_api_keyscreate_api_keyrevoke_api_keydelete_api_keylist_userscreate_userupdate_user_roleremove_userlist_ops_deliveriesprovision_site_intake
View /api/v1/openapi.json

Verification

Smoke testing checklist

Safe checks before live leads

  • POST without a secret should return 403 when the Hook has a secret.
  • Bad Origin should return 403 when allowed origins are configured.
  • Too-fast or honeypot submissions should fail before storage.
  • Valid mocked payload should produce ok:true and an inboxItemId.
  • Inbox detail should show ordered Submitted fields and Raw JSON.
  • Activity should show sent, pending, failed, blocked, or retry-visible delivery state.

Production form checks

  • Server-side proxy keeps secrets out of public JavaScript when practical.
  • Captcha/Turnstile verifies before forwarding to FormBox.
  • Fields normalize before POST because FormBox preserves business fields exactly.
  • _formbox_field_order matches visible DOM order.
  • A UTM/click-ID navigation test proves attribution survives from landing page to form page.
  • No source/tracking keys appear as client-facing payload fields.
  • Do not submit a real client lead unless explicitly approved.
cURL smoke test
curl -X POST https://hooks.formbox.ai/f/{hookId}/{slug}   -H "Content-Type: application/json"   -H "X-FormBox-Secret: fbsec_..."   -d '{"payload":{"name":"Test Lead","email":"test@example.com","message":"Hello from FormBox"},"metadata":{"fieldOrder":["name","email","message"]}}'

Help

Troubleshooting

403 invalid_secretThe Hook secret is missing or wrong.Send X-FormBox-Secret from a server, or _formbox_secret in direct HTML form mode.
403 origin_not_allowedThe browser Origin is not in the Hook allowed origins list.Add the production domain and preview domains to allowed origins, or leave the list empty intentionally.
400 too_fast or honeypot errorsThe bot trap fields look suspicious.Keep _formbox_hp empty and set _formbox_started_at when the form loads.
409 hook_pausedThe Hook is paused.Reactivate the Hook from Hook settings or PATCH the Hook status through the API.
429 rate_limit_exceededToo many submissions hit the same Hook/IP window.Wait for the Retry-After value or reduce automated tests.
Fields appear in the wrong orderNo field-order hint was sent, or it does not match the visible form.Send _formbox_field_order or metadata.fieldOrder with business field keys in display order.
Metadata appears as client-facing fieldsTracking/source values were sent as payload fields.Move page URL, referrer, user agent, UTMs, and click IDs into metadata or _formbox_* fields.
Attribution is missing after navigationThe site only captured UTMs on the form page, but the visitor landed elsewhere first.Capture first-touch attribution on every page with first-party storage, then inject _formbox_* fields before submit.
Submission stores but no email arrivesNo notification recipient, delivery failure, or email provider delay.Check the Hook notification emails, the submission Activity card, and Settings / Ops.
File upload rejectedFile count, size, type, or storage config failed validation.Use up to 5 files, 10MB each, allowed image/PDF/doc/text types, and verify storage is configured.
AI review is pendingClassifier is disabled, still running, or timed out.Use label-only as the safe default and manually run AI review from the submission detail if needed.

When in doubt, inspect the Inbox item

The submission detail page is the source of truth for what FormBox received: readable submitted fields, ordered raw payload JSON, metadata, files, delivery Activity, and AI review state.