Live · status OK
Documentation · OW Forms v1.1.0

OW Forms
Documentation

The WordPress form builder rebuilt for 2026.

v1.1.0GPL-2.0-or-laterDocumentation

OW Forms — Documentation

A modern, GDPR-native form builder for WordPress. Author: OptionWeb — Julien Daniel Plugin home: https://optionweb.dev/en/addons/ow-forms/ License: GPL-2.0-or-later Version covered by this document: 1.0.2


Table of contents

  1. Overview
  2. Installation
  3. Quick start — your first form
  4. Field types
  5. Anti-spam stack
  6. GDPR features
  7. Email notifications
  8. Webhooks
  9. File uploads
  10. Rendering: shortcode, Gutenberg, REST
  11. Contact Form 7 importer
  12. Settings reference
  13. REST API
  14. Developer hooks (filters & actions)
  15. Integration with OW Consent / OW Shield
  16. Internationalization
  17. Troubleshooting
  18. FAQ

Overview

OW Forms is a schema-driven form builder. Each form is a JSON document that describes its fields, validation, and submit behaviour. The plugin renders that schema as accessible HTML, validates submissions on the server, runs them through a four-layer anti-spam stack, persists what you let it persist, and fires email + webhook notifications.

What it ships with (everything free, GPL-2):

  • JSON schema engine with 17 field types
  • 4-layer anti-spam : honeypot, time-trap, multi-provider CAPTCHA, IP reputation
  • Native GDPR consent + retention + DSAR
  • REST API (owf/v1 namespace) — public submit + admin CRUD
  • Gutenberg block + shortcode + PHP render API
  • HMAC-signed outgoing webhooks
  • Contact Form 7 one-click importer
  • Auto-detects OW Canvas theme tokens for visual consistency

What it deliberately does NOT do:

  • Payment processing (use WooCommerce + a dedicated checkout)
  • Multi-page wizards beyond simple next/prev (use a dedicated wizard plugin)
  • Drag-and-drop visual builder in v1 (JSON editor only; visual builder roadmap)

Installation

From WordPress.org (recommended)

  1. WordPress admin → Plugins → Add New
  2. Search for OW Forms
  3. Click Install Now, then Activate

From .zip upload

  1. Download ow-forms-1.0.2.zip from https://optionweb.dev/en/addons/ow-forms/
  2. Plugins → Add New → Upload Plugin
  3. Pick the file, click Install Now, then Activate

Requirements

  • WordPress 6.0 or later
  • PHP 7.4 or later (8.1+ recommended)
  • MySQL/MariaDB with JSON column support (5.7+ / 10.2+)

What gets installed

On activation OW Forms creates three custom tables :

  • {prefix}_owf_submissions — submission payloads + spam metadata
  • {prefix}_owf_files — uploaded files registry
  • {prefix}_owf_log — submission audit trail

Plus a Custom Post Type owf_form (not publicly queryable) used to store the form schemas.


Quick start

After activation a default "Contact" form is created automatically.

  1. Go to OW Forms → Forms in the admin
  2. Note the ID of the default form (e.g. 1)
  3. In any page or post, drop the shortcode :
[owf_form id="1"]

…or insert the OW Forms Gutenberg block and pick the form from the dropdown.

That's it. The form renders, accepts submissions, sends an admin email to admin_email, and stores the submission in the database.

To customise it, open the form in OW Forms → Forms → [Edit] and tweak the JSON schema directly. A visual builder is on the v1.1 roadmap.


Field types

OW Forms supports 17 field types. Each field in the schema is an object with at minimum type and name ; most also accept label, help, required, placeholder, and type-specific options.

typePurposeNotable options
textSingle-line textplaceholder, pattern, maxlength
emailEmail addressAuto-validates RFC 5322
telPhone numberLoose validation (E.164-friendly)
urlURLAuto-validates http(s)://
numberNumericmin, max, step
textareaMulti-line textrows, maxlength
selectSingle dropdownoptions: [{value, label}]
radioSingle choiceoptions: [{value, label}]
checkboxSingle booleandefault: bool
checkbox-groupMulti-selectoptions: [{value, label}]
dateDate pickermin, max (YYYY-MM-DD)
timeTime pickermin, max (HH:MM)
datetimeDate + timemin, max (YYYY-MM-DDTHH:MM)
fileFile uploadmultiple: bool, accept
hiddenHidden valueUseful for tracking
rating1–5 star rating
consentGDPR consent checkboxAuto-injected when GDPR is on

Schema example

{
  "title": "Contact us",
  "fields": [
    {
      "type": "text",
      "name": "name",
      "label": "Your name",
      "required": true,
      "placeholder": "Jane Doe"
    },
    {
      "type": "email",
      "name": "email",
      "label": "Email",
      "required": true
    },
    {
      "type": "select",
      "name": "subject",
      "label": "How can we help?",
      "required": true,
      "options": [
        {"value": "quote",   "label": "Request a quote"},
        {"value": "support", "label": "Technical support"},
        {"value": "other",   "label": "Other"}
      ]
    },
    {
      "type": "textarea",
      "name": "message",
      "label": "Message",
      "required": true,
      "rows": 6
    }
  ],
  "submit": {
    "label": "Send",
    "success_message": "Thanks — we will reply within 24 hours.",
    "redirect_url": ""
  }
}

Field name values become keys in the stored payload and tokens you can use in email templates ({{name}}, {{email}}, etc.).


Anti-spam stack

OW Forms runs four independent layers in parallel. Each layer assigns a score contribution ; a submission scoring ≥ 80 is silently dropped — no error message is returned, so bots cannot iterate against the protection.

1. Honeypot

A visually hidden text input that bots fill and humans don't. Setting:

  • spam_honeypot_enabled (default: true)

2. Time-trap

Rejects submissions completed faster than a configurable threshold (typical bots POST instantly).

  • spam_timetrap_enabled (default: true)
  • spam_timetrap_min_seconds (default: 2)

3. CAPTCHA (multi-provider)

Server-to-server token validation against the vendor's API. Pick one of :

  • turnstile — Cloudflare Turnstile (recommended, privacy-first, no challenge UX)
  • recaptcha_v3 — Google reCAPTCHA v3 (invisible)
  • hcaptcha — hCaptcha (visible widget)
  • friendly_captcha — Friendly Captcha (self-hosted widget, MIT-licensed, EU-friendly)

Set spam_captcha_provider, spam_captcha_site_key, and spam_captcha_secret_key in the Anti-spam settings tab. Default is none.

Note : Turnstile / reCAPTCHA / hCaptcha widget JS is loaded from the vendor's own origin (their JS performs an origin signature check server-side). Friendly Captcha is self-hosted under assets/js/vendor/.

4. IP reputation (optional)

When OW Shield is installed and active, OW Forms uses its IP reputation feed to score submissions :

  • spam_owshield_iprep (default: true)
  • spam_block_disposable_emails (default: true) — rejects @mailinator, @tempmail, and ~120 other throwaway domains

Additional heuristics

  • spam_min_words_per_text_area — require at least N words in textareas
  • spam_max_links_per_submission — reject if more than N URLs in payload
  • spam_blocklist_emails, spam_blocklist_words, spam_blocklist_ips — your own lists

Logging

Every spam decision is logged with its reason. Check OW Forms → Submissions → filter "spam" to tune thresholds without flying blind.


GDPR features

GDPR compliance is native. Defaults are conservative ; loosen them only if you have legal advice to do so.

Consent checkbox

A consent checkbox is auto-injected at the bottom of every form when gdpr_consent_required is true (default). The text is configurable :

'gdpr_consent_text' => __(
    'I agree that my data will be processed to respond to my inquiry, in accordance with the privacy policy.',
    'ow-forms'
)

If gdpr_auto_link_privacy is on (default), the words "privacy policy" / "politique de confidentialité" are auto-linked to the OW Consent privacy policy URL when that plugin is active, or to your WP-configured Privacy Page otherwise.

IP storage modes

store_ip setting :

ValueWhat gets stored
noneNothing — no IP retained
pseudonymized (default)192.168.1.42192.168.1.0 (IPv4) / first 4 groups (IPv6)
fullFull IP — only use if your legal basis allows

User agent storage

store_user_agent setting :

ValueWhat gets stored
noneNothing
hashed (default)SHA-256 of UA + wp_salt()
fullTruncated UA string (max 64 chars)

Retention & purge

A daily cron purges submissions older than gdpr_retention_days (default : 1095 days — the CNIL standard for prospect data). Set to 0 to disable automatic purging.

If gdpr_auto_delete_after_response is true, marking a submission as "handled" deletes it immediately along with any uploaded files.

DSAR — Data Subject Access / Erasure

Form emails are hashed with wp_salt() at storage time. When OW Consent is active and a DSAR erasure is processed for an email address, OW Forms automatically deletes any matching submissions by email_hash and fires :

do_action( 'owf_gdpr_erased_for_email', $hash, $count );

Without OW Consent, you can call the erasure manually :

OWF_GDPR::erase_by_email( 'user@example.com' );  // returns int count

Email notifications

Templates

Both admin notification and user auto-reply support {{token}} substitution :

TokenResolves to
{{site_name}}get_bloginfo('name')
{{form_title}}The form's title
{{form_id}}Numeric form ID
{{submission_id}}Numeric submission ID (0 if not stored)
{{date}}Submission timestamp
{{ip}}IP per store_ip mode
{{page_url}}URL the form was submitted from
{{all_fields}}All field key/value pairs
{{<field_name>}}Any field by its name (e.g. {{email}}, {{message}})

Settings

SettingDefaultNotes
default_recipient_emailempty → admin_emailOverride per-form via schema.mail.recipient
from_nameemptySender display name
from_emailempty → WP defaultSender address
mail_htmltruefalse switches to plain-text
mail_admin_subject[{{site_name}}] New message via {{form_title}}
mail_admin_bodyempty → auto-built from fields
mail_user_autoreply_enabledtrue
mail_user_subjectWe have received your message
mail_user_bodyempty → generic confirmation
mail_failed_alert_emailemptyNotified when mail send fails

Per-form override

The form schema can override globals :

{
  "mail": {
    "recipient": "sales@example.com",
    "subject": "New lead — {{name}}",
    "body": "Lead: {{name}}\\nEmail: {{email}}\\nMessage: {{message}}",
    "autoreply": true,
    "autoreply_subject": "Thanks, {{name}}!",
    "autoreply_body": "Hi {{name}},\\n\\nWe got your message."
  }
}

Webhooks

When webhook_url is set, every successful submission triggers an HTTP POST.

Payload (JSON)

{
  "form_id": 1,
  "form_title": "Contact",
  "submission_id": 42,
  "received_at": "2026-05-13T14:32:18+00:00",
  "fields": { "name": "Jane", "email": "jane@example.com", "message": "Hi" },
  "site": "https://example.com"
}

HMAC signature

If webhook_secret is set, the request includes :

X-OWF-Signature: sha256=<hex digest of HMAC-SHA256(body, secret)>

Receiver-side verification (Node example) :

const expected = crypto
  .createHmac('sha256', SECRET)
  .update(rawBody)
  .digest('hex');
const provided = req.headers['x-owf-signature'].replace(/^sha256=/, '');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(provided))) {
  return res.status(401).end('bad sig');
}

Retries

If the receiver returns 5xx or times out, OW Forms retries with exponential backoff (60 s → 5 min → 30 min). After 3 failures the webhook is logged as permanently failed but does not block the submission.


File uploads

When allow_file_uploads is on (default true) any field of type file is accepted. The pipeline :

  1. Server-side validation — file size (max_file_size_mb, default 8 MB), extension allowlist (allowed_file_types), and a finfo MIME check cross-referenced against wp_get_mime_types().
  2. Storagewp-content/uploads/owf-uploads/YYYY/MM/<sha256>.<ext> with a deny-all .htaccess (Apache) and empty index.php (Nginx/IIS).
  3. Database registry — file metadata + SHA-256 hash in {prefix}_owf_files, linked to the parent submission ID.
  4. Cleanup — when a submission is deleted (manually, by retention, or by DSAR), all linked files are removed.

Important : OW Forms uses a custom isolated upload path, not the WordPress media library, so uploaded files are not discoverable through the standard /wp-json/wp/v2/media endpoint and have no thumbnails generated. This is deliberate — form attachments are private to the form owner.

Allowed file types (default)

pdf, jpg, jpeg, png, webp, doc, docx, xls, xlsx, csv, txt

To extend :

add_filter( 'owf_settings', function( $s ) {
    $s['allowed_file_types'] = array_merge(
        (array) $s['allowed_file_types'],
        array( 'odt', 'ods', 'zip' )
    );
    return $s;
} );

Rendering

Shortcode

[owf_form id="42"]
[owf_form slug="contact"]

id (numeric post ID) or slug (post slug) — one is required.

Gutenberg block

In the block editor, search for "OW Forms" in the block inserter and pick your form from the dropdown.

PHP

echo OWF_Form::render( 42 );
// or
echo do_shortcode( '[owf_form id="42"]' );

REST

The form can also be submitted programmatically — see REST API.


CF7 importer

If Contact Form 7 is or was installed, OW Forms ships with a one-click importer.

OW Forms → Tools → Import from Contact Form 7

The importer :

  1. Scans every wpcf7_contact_form post in your database
  2. Parses each CF7 shortcode-based form and builds the equivalent OW Forms schema
  3. Preserves recipient, subject, success/error messages, and field types
  4. Rewrites every [contact-form-7 id="..."] shortcode in all your post_content (pages, posts, custom post types) to [owf_form id="..."]
  5. Logs results : forms found, imported, skipped (already imported)

The CF7 plugin can then be deactivated and removed.

The importer is idempotent : running it twice does not create duplicates. Already-imported CF7 forms are marked via a _owf_imported_from_cf7 meta key and skipped on subsequent runs.


Settings reference

All settings are stored in a single option key, owf_settings. You can read or override them programmatically :

$value = OWF_Core::setting( 'spam_timetrap_min_seconds' );
OWF_Core::update_settings( array( 'spam_timetrap_min_seconds' => 3 ) );

Complete defaults

array(
    // General
    'default_recipient_email'        => '',     // empty = admin_email
    'from_name'                      => '',
    'from_email'                     => '',
    'sender_uses_dpo'                => true,

    // Anti-spam
    'spam_honeypot_enabled'          => true,
    'spam_timetrap_enabled'          => true,
    'spam_timetrap_min_seconds'      => 2,
    'spam_captcha_provider'          => 'none',
    'spam_captcha_site_key'          => '',
    'spam_captcha_secret_key'        => '',
    'spam_owshield_iprep'            => true,
    'spam_block_disposable_emails'   => true,
    'spam_min_words_per_text_area'   => 0,
    'spam_max_links_per_submission'  => 3,
    'spam_blocklist_emails'          => array(),
    'spam_blocklist_words'           => array(),
    'spam_blocklist_ips'             => array(),

    // GDPR
    'gdpr_consent_required'          => true,
    'gdpr_consent_text'              => '...',
    'gdpr_auto_link_privacy'         => true,
    'gdpr_retention_days'            => 1095,
    'gdpr_auto_delete_after_response' => false,

    // Storage
    'store_submissions'              => true,
    'store_ip'                       => 'pseudonymized',
    'store_user_agent'               => 'hashed',
    'allow_file_uploads'             => true,
    'max_file_size_mb'               => 8,
    'allowed_file_types'             => array( 'pdf', 'jpg', 'jpeg', 'png',
                                                'webp', 'doc', 'docx', 'xls',
                                                'xlsx', 'csv', 'txt' ),

    // Notifications
    'mail_html'                      => true,
    'mail_admin_subject'             => '[{{site_name}}] New message via {{form_title}}',
    'mail_admin_body'                => '',
    'mail_user_autoreply_enabled'    => true,
    'mail_user_subject'              => 'We have received your message',
    'mail_user_body'                 => '',
    'mail_failed_alert_email'        => '',
    'webhook_url'                    => '',
    'webhook_secret'                 => '',

    // Rendering
    'style_preset'                   => 'auto',
    'button_label_default'           => 'Send',
    'success_message_default'        => 'Your message has been sent. We will reply within 24 hours.',
    'error_message_default'          => 'An error occurred. Please try again or contact us directly.',
    'inherit_theme_tokens'           => true,

    // Analytics (opt-in)
    'track_conversions'              => false,
    'conversion_event_name'          => 'generate_lead',
    'conversion_value'               => 0,
    'conversion_currency'            => 'EUR',
);

REST API

All endpoints live under the namespace owf/v1.

Public endpoint

MethodPathPermission
POST/owf/v1/submitNonce-protected (wp_rest)

Body :

{
  "form_id": 1,
  "fields": { "name": "Jane", "email": "jane@example.com", "message": "Hi" },
  "_owf_nonce": "<from wp_create_nonce('owf_submit_form')>",
  "_owf_time": 1736780000,
  "_owf_hp": ""
}

Response (200) :

{ "ok": true, "id": 42, "message": "Your message has been sent." }

Response (422) when validation fails :

{
  "ok": false,
  "errors": { "email": "Please enter a valid email" },
  "message": "Please review the fields in error."
}

Admin endpoints (require manage_options)

MethodPathDescription
GET/owf/v1/formsList all forms
GET/owf/v1/forms/{id}Get one form
PUT/owf/v1/forms/{id}Update form
POST/owf/v1/forms/createCreate a form
GET/owf/v1/submissionsList submissions (filters: form_id, status)
GET/owf/v1/submissions/{id}Get one submission
POST/owf/v1/submissions/{id}/handleMark as handled (may delete)
POST/owf/v1/importer/cf7Run the CF7 importer
GET/POST/owf/v1/settingsRead or write plugin settings

All admin endpoints require both manage_options capability and a valid WP REST nonce.


Hooks (filters & actions)

Actions

/**
 * Fires after a submission has been processed and stored.
 *
 * @param int   $submission_id  Database ID (0 if not stored)
 * @param int   $form_id        Form ID
 * @param array $cleaned        Sanitized field payload
 * @param array $schema         Full form schema
 */
do_action( 'owf_submission_received', $submission_id, $form_id, $cleaned, $schema );
/**
 * Fires after a DSAR erasure has deleted matching submissions by email hash.
 *
 * @param string $email_hash   SHA-256(email + wp_salt())
 * @param int    $deleted_count
 */
do_action( 'owf_gdpr_erased_for_email', $email_hash, $deleted_count );

Filters

/**
 * Filter the resolved settings array (merged defaults + saved + per-request).
 */
apply_filters( 'owf_settings', array $settings );

/**
 * Whitelist of trusted reverse proxy IPs that may set X-Forwarded-For.
 * Without this OW Forms ignores proxy headers (to prevent IP spoofing).
 */
apply_filters( 'owf_trusted_proxies', array $proxies );

/**
 * List of disposable email domains rejected when `spam_block_disposable_emails`
 * is on.
 */
apply_filters( 'owf_disposable_domains', array $domains );

Recommended pattern — extend disposable domain list

add_filter( 'owf_disposable_domains', function( $list ) {
    return array_merge( $list, array(
        'example-throwaway.com',
        'company-blocked-domain.net',
    ) );
} );

Recommended pattern — push submissions to a CRM

add_action( 'owf_submission_received', function( $sid, $form_id, $cleaned ) {
    if ( $form_id !== 7 ) return;  // only the "Sales" form
    wp_remote_post( 'https://crm.example.com/leads', array(
        'headers' => array( 'Authorization' => 'Bearer ' . CRM_TOKEN ),
        'body'    => wp_json_encode( array(
            'email'   => $cleaned['email'],
            'name'    => $cleaned['name'],
            'message' => $cleaned['message'],
            'source'  => 'website-form',
        ) ),
        'headers' => array( 'Content-Type' => 'application/json' ),
    ) );
}, 10, 3 );

Integrations

OW Canvas (theme)

When the OW Canvas theme is active, OW Forms inherits its CSS custom properties (--owc-ink, --owc-paper, --owc-accent, etc.) for visual consistency. Disable with inherit_theme_tokens = false.

OW Consent

When OW Consent is active :

  • The GDPR consent checkbox link is auto-pointed at your OW Consent privacy policy URL
  • Submissions are linked to OW Consent records (when the visitor has a consent ID cookie)
  • DSAR erasure flows from OW Consent automatically purge OW Forms submissions for the same email hash

OW Shield

When OW Shield is active and spam_owshield_iprep is on, every submission's IP is scored against OW Shield's reputation feed before being accepted.


Internationalization

The source language is English. A French translation is shipped under languages/ow-forms-fr_FR.po.

For translators

The .pot template is at languages/ow-forms.pot. Submit translations via translate.wordpress.org/projects/wp-plugins/ow-forms once the plugin is approved on WP.org. For local distribution, drop ow-forms-<locale>.mo into the languages/ folder.

Compiling .mo locally

msgfmt languages/ow-forms-fr_FR.po -o languages/ow-forms-fr_FR.mo

Troubleshooting

Form submits but no email arrives

  1. Check that your site can send mail at all — try a test from any other plugin's contact form or with Mail Tester
  2. Check OW Forms → Submissions to confirm the submission landed (if yes, the issue is delivery, not OW Forms)
  3. Set mail_failed_alert_email to your own address — OW Forms will email you when wp_mail() returns false
  4. Install an SMTP plugin (Fluent SMTP, WP Mail SMTP, etc.) — the default PHP mail() is rejected by 95 % of receiving servers

CAPTCHA doesn't show up

  • Verify spam_captcha_site_key and spam_captcha_secret_key are both set
  • Open the page in DevTools — the vendor script URL should load with a 200
  • For reCAPTCHA, your site domain must be registered in the Google admin
  • For Turnstile, the widget mode in the Cloudflare admin must be "Managed" or "Non-interactive"

Files upload but admin can't download them

This is by design — uploaded files are served by an authenticated PHP handler, not directly. Check that you're logged in as a user with manage_options.

CF7 importer says "0 forms found"

The importer scans the wpcf7_contact_form post type. If CF7 is already deactivated and the posts were deleted, there is nothing left to import. Re-activate CF7 just for the import, then deactivate again.

Submissions table is huge

  • Lower gdpr_retention_days (default 1095 / 3 years)
  • Disable store_submissions if you only need email delivery, not a database audit trail
  • Set gdpr_auto_delete_after_response = true to delete on "handled"

FAQ

Is OW Forms free ? Yes. GPL-2.0-or-later. There is no Pro version, no license key, no feature locked behind payment.

Does it work on WordPress Multisite ? Yes — each site has its own tables and form library, isolated as expected.

Does it support multi-step forms ? Basic next/prev navigation works via field grouping in the schema. A first- class wizard UX is on the v1.1 roadmap.

How do I export submissions ? OW Forms → Submissions → Export CSV. The export honors the current filters (form, date range, status).

Can I host my forms behind a paywall or login ? Yes — wrap the shortcode in any membership plugin's gating logic, or render via PHP inside a is_user_logged_in() check.

Does it integrate with email marketing / CRM tools ? Out of the box : webhooks (HMAC-signed POST to any endpoint), and per-form email recipients. For Brevo / Mailchimp / HubSpot / Pipedrive, hook owf_submission_received and call the vendor's REST API — see the Hooks section.

Is the plugin GDPR-certified ? The plugin's defaults are aligned with CNIL / GDPR best practices, but certification is a legal process specific to your data controller setup. We provide the technical tooling ; your DPO confirms the policy.

Where is the support ?


Built by OptionWeb — Julien Daniel, Châtelet, Belgium.