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
- Overview
- Installation
- Quick start — your first form
- Field types
- Anti-spam stack
- GDPR features
- Email notifications
- Webhooks
- File uploads
- Rendering: shortcode, Gutenberg, REST
- Contact Form 7 importer
- Settings reference
- REST API
- Developer hooks (filters & actions)
- Integration with OW Consent / OW Shield
- Internationalization
- Troubleshooting
- 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/v1namespace) — 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)
- WordPress admin → Plugins → Add New
- Search for
OW Forms - Click Install Now, then Activate
From .zip upload
- Download
ow-forms-1.0.2.zipfrom https://optionweb.dev/en/addons/ow-forms/ - Plugins → Add New → Upload Plugin
- 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
JSONcolumn 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.
- Go to OW Forms → Forms in the admin
- Note the ID of the default form (e.g.
1) - 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.
type | Purpose | Notable options |
|---|---|---|
text | Single-line text | placeholder, pattern, maxlength |
email | Email address | Auto-validates RFC 5322 |
tel | Phone number | Loose validation (E.164-friendly) |
url | URL | Auto-validates http(s):// |
number | Numeric | min, max, step |
textarea | Multi-line text | rows, maxlength |
select | Single dropdown | options: [{value, label}] |
radio | Single choice | options: [{value, label}] |
checkbox | Single boolean | default: bool |
checkbox-group | Multi-select | options: [{value, label}] |
date | Date picker | min, max (YYYY-MM-DD) |
time | Time picker | min, max (HH:MM) |
datetime | Date + time | min, max (YYYY-MM-DDTHH:MM) |
file | File upload | multiple: bool, accept |
hidden | Hidden value | Useful for tracking |
rating | 1–5 star rating | — |
consent | GDPR consent checkbox | Auto-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 textareasspam_max_links_per_submission— reject if more than N URLs in payloadspam_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 :
| Value | What gets stored |
|---|---|
none | Nothing — no IP retained |
pseudonymized (default) | 192.168.1.42 → 192.168.1.0 (IPv4) / first 4 groups (IPv6) |
full | Full IP — only use if your legal basis allows |
User agent storage
store_user_agent setting :
| Value | What gets stored |
|---|---|
none | Nothing |
hashed (default) | SHA-256 of UA + wp_salt() |
full | Truncated 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 :
| Token | Resolves 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
| Setting | Default | Notes |
|---|---|---|
default_recipient_email | empty → admin_email | Override per-form via schema.mail.recipient |
from_name | empty | Sender display name |
from_email | empty → WP default | Sender address |
mail_html | true | false switches to plain-text |
mail_admin_subject | [{{site_name}}] New message via {{form_title}} | |
mail_admin_body | empty → auto-built from fields | |
mail_user_autoreply_enabled | true | |
mail_user_subject | We have received your message | |
mail_user_body | empty → generic confirmation | |
mail_failed_alert_email | empty | Notified 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 :
- Server-side validation — file size (
max_file_size_mb, default 8 MB), extension allowlist (allowed_file_types), and afinfoMIME check cross-referenced againstwp_get_mime_types(). - Storage —
wp-content/uploads/owf-uploads/YYYY/MM/<sha256>.<ext>with a deny-all.htaccess(Apache) and emptyindex.php(Nginx/IIS). - Database registry — file metadata + SHA-256 hash in
{prefix}_owf_files, linked to the parent submission ID. - 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/mediaendpoint 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 :
- Scans every
wpcf7_contact_formpost in your database - Parses each CF7 shortcode-based form and builds the equivalent OW Forms schema
- Preserves recipient, subject, success/error messages, and field types
- Rewrites every
[contact-form-7 id="..."]shortcode in all yourpost_content(pages, posts, custom post types) to[owf_form id="..."] - 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_cf7meta 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
| Method | Path | Permission |
|---|---|---|
POST | /owf/v1/submit | Nonce-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)
| Method | Path | Description |
|---|---|---|
GET | /owf/v1/forms | List all forms |
GET | /owf/v1/forms/{id} | Get one form |
PUT | /owf/v1/forms/{id} | Update form |
POST | /owf/v1/forms/create | Create a form |
GET | /owf/v1/submissions | List submissions (filters: form_id, status) |
GET | /owf/v1/submissions/{id} | Get one submission |
POST | /owf/v1/submissions/{id}/handle | Mark as handled (may delete) |
POST | /owf/v1/importer/cf7 | Run the CF7 importer |
GET/POST | /owf/v1/settings | Read 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
- Check that your site can send mail at all — try a test from any other plugin's contact form or with Mail Tester
- Check OW Forms → Submissions to confirm the submission landed (if yes, the issue is delivery, not OW Forms)
- Set
mail_failed_alert_emailto your own address — OW Forms will email you whenwp_mail()returnsfalse - 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_keyandspam_captcha_secret_keyare 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_submissionsif you only need email delivery, not a database audit trail - Set
gdpr_auto_delete_after_response = trueto 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 ?
- Support portal : https://optionweb.dev/en/addons/support/
- Email : support@optionweb.dev
- Documentation : https://optionweb.dev/en/addons/ow-forms/
Built by OptionWeb — Julien Daniel, Châtelet, Belgium.