Skip to main content
Availablev1.0.0 · WordPress 6.9+ · PHP 8.1+

Fennec Forms docs

Setup, then the developer reference.

Getting started

Fennec Forms isn't on WordPress.org yet. Download the free plugin zip and upload it under Plugins → Add New → Upload Plugin.

  1. Install and activate

    Upload the plugin under Plugins → Add New (WordPress.org listing coming at launch) and activate it. WordPress 6.9+ and PHP 8.1+.

  2. Create a form

    Go to Fennec Forms → Add New. The new form is already a working contact form. Add fields from the grouped picker, set widths and help text, and set the form to Published.

  3. Add it to a page

    Insert the “Fennec Form” block, or paste the shortcode shown in the form list, e.g. [fennec_form id="123"].

  4. Set up notifications

    In the form’s settings, edit the admin email (default: {all_fields} plus a link to the entry) and optionally turn on a confirmation email to the person who submitted.

  5. Optional: webhooks and CAPTCHA

    Paste up to ten webhook URLs per form and copy its signing secret. Pick a CAPTCHA provider under Fennec Forms → Settings if you need one.

Verifying webhooks

Each form can post every entry as JSON to up to ten URLs, in the background after the visitor has seen the confirmation. Private and local network addresses are refused. Every request carries these headers:

  • X-Fennec-Signature: t=<unix time>,v1=<hex HMAC-SHA256>
  • X-Fennec-Event, X-Fennec-Event-Id and X-Fennec-Delivery

Compute HMAC-SHA256 over <t>.<raw request body> with the form's signing secret (shown in the form builder), compare in constant time, and reject timestamps older than five minutes. The free plugin makes one attempt; Fennec Forms Pro adds retries with backoff and a delivery log.

PHP
$secret    = 'your-form-signing-secret';
$body      = file_get_contents( 'php://input' );
$header    = $_SERVER['HTTP_X_FENNEC_SIGNATURE'] ?? '';   // t=1727600000,v1=abc…
parse_str( str_replace( ',', '&', $header ), $parts );

$expected  = hash_hmac( 'sha256', $parts['t'] . '.' . $body, $secret );
$fresh     = abs( time() - (int) $parts['t'] ) <= 300;      // reject after 5 minutes

if ( ! $fresh || ! hash_equals( $expected, $parts['v1'] ?? '' ) ) {
    http_response_code( 401 );
    exit;
}
Node.js
import crypto from 'node:crypto';

export function verifyFennec(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${parts.t}.${rawBody}`)
    .digest('hex');
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) <= 300;
  return fresh && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 ?? ''));
}

Merge tags

Use these in email subjects and bodies, the confirmation message and redirect URLs (where they're URL-encoded).

{field:key}{all_fields}{form_title}{entry_id}{entry_url}{site_name}{site_url}{date}

Hidden fields can default to:

{page_url}{referrer}{user_email}{user_id}{query:param}

Embedding

Use the Fennec Form block, or the shortcode [fennec_form id="123"]. Forms render with theme-inheriting styles; override any --fennec-* custom property to adjust them.

Developer hooks

Extensions, including the Pro add-on, use only these public hooks. Data an inactive extension wrote is kept when an item is re-saved.

Fennec Forms hooks
HookTypePurpose
fennec_forms_capabilityfilterCapability for forms and entries (default manage_options).
fennec_forms_rate_limitfilterSubmissions per IP per 10 minutes (default 10).
fennec_forms_expose_entries_to_mcpfilterMake the list-entries ability agent-visible (default false).
fennec_forms_field_typesfilterRegister field types (Type instances keyed by key()).
fennec_forms_sanitize_fieldfilterAdd extension data to a sanitized field definition.
fennec_forms_sanitize_settingsfilterAdd extension settings from the builder POST.
fennec_forms_field_visiblefilterWhether a field takes part in a submission.
fennec_forms_submission_valuesfilterAdjust validated values before the entry is stored.
fennec_forms_webhook_retry_delayfilterSeconds before retrying a failed webhook (default: no retry).
fennec_forms_after_submissionactionFires with the entry ID, form and values.
fennec_forms_before_entry_deleteactionFires before an entry (and its files) is deleted.
fennec_forms_builder_settingsactionRender extra builder settings sections.
fennec_forms_entry_panelsactionRender extra panels on the single-entry screen.

Suite events

Fired on fennec_event and on fennec_event/<name> with the shared envelope (id, event, plugin, occurred_at, data, site).

  • fennec-forms/entry-submitted
  • fennec-forms/entry-deleted
  • fennec-forms/form-saved
  • fennec-forms/form-deleted

Abilities

  • fennec-forms/list-formsagent-visible

    List forms with their fields.

  • fennec-forms/create-formagent-visible

    Draft a new form (always saved as a draft).

  • fennec-forms/list-entrieshidden by default

    Read entries. Personal data, so agents can’t see it unless you opt in.

Data and uninstall

Deactivating never deletes anything. Uninstalling keeps all data unless the fennec_forms_delete_data_on_uninstall option is set first, for example with wp option update fennec_forms_delete_data_on_uninstall 1. Requires WordPress 6.9+ and PHP 8.1+.