Skip to main content
formsieve Docs Support
All docs pages

Developer hooks and WP-CLI for Contact Form 7

Formsieve for CF7 prefixes every hook with fsv_cf7_, bundles its own copy of the Formsieve core under the namespace Formsieve\CF7\Core\… and adds the WP-CLI command wp formsieve-cf7. The examples below are ready to paste into a small plugin or a must-use plugin (wp-content/mu-plugins/). Filters that run when a form is submitted also work from a theme's functions.php, but fsv_cf7_booted fires on plugins_loaded, before any theme is loaded, so a listener for it must live in a plugin or mu-plugin.

Filters

Filter Arguments Return
fsv_cf7_should_check bool $check, Context $ctx false skips the check (logged as should_check, delivered).
fsv_cf7_payload array $payload (state, questions), Context $ctx The request body parts. Invalid values are ignored.
fsv_cf7_thresholds array $thresholds (review, block), Context $ctx Thresholds between 0 and 1.
fsv_cf7_verdict Verdict $verdict, Context $ctx A Verdict, before it is logged and applied (other types are ignored).
fsv_cf7_api_base string $base_url, string $provider_id The base URL, after the FSV_CF7_API_BASE constant.
fsv_cf7_api_key string $stored_key The key. The FSV_CF7_API_KEY and FORMSIEVE_API_KEY constants win over this filter.
fsv_cf7_pipeline_stages Stage[] $stages The ordered pre-filter stages.
fsv_cf7_cli_sample_context Context $ctx, int $form_id, string $kind (spam or ham) The Context used by wp formsieve-cf7 test <form_id>; other types are ignored.
formsieve_integrations array $integrations Shared by every Formsieve plugin: the list on the Formsieve overview page.

Actions

Action Arguments When
fsv_cf7_decided Verdict $verdict, Context $ctx After every decision has been logged (the verdict carries log_id()).
fsv_cf7_before_request Request $request, Context $ctx Just before a live API call.
fsv_cf7_feedback int $entry_ref, string $label (spam or ham), ?array $log_row After an administrator's correction. $entry_ref is the Flamingo message ID (0 for a correction made in the Log tab on a row without a message). Moving a Flamingo message that has no Formsieve log row fires nothing.
fsv_cf7_tested array $result After Test connection sent its request (not when it was refused for missing consent or key).
fsv_cf7_booted Plugin $plugin Once the plugin has started, on plugins_loaded.
fsv_cf7_daily none The daily retention task (a cron event).
formsieve_global_notice string $integration_id Shared by every Formsieve plugin: after a plugin has printed its one notice outside its own admin page.

Context (Formsieve\CF7\Core\Pipeline\Context) gives you the submission as the integration built it: form_id(), fields(), email_domain(), message(), text(), settings(), setting( $key ), site() and more. It contains visitor data: never log or send it as a whole. Verdict (Formsieve\CF7\Core\Classifier\Verdict) offers verdict(), is_allow(), is_review(), is_block(), p(), spam_percent(), category(), reason(), reason_code(), stage(), model(), model_status(), provider(), request_id(), latency_ms(), input_tokens(), cost_micro_usd(), is_checked(), is_demo(), log_id() and with( array $changes ).

Examples

Skip one form entirely:

add_filter( 'fsv_cf7_should_check', function ( $check, $ctx ) {
	return 12 === $ctx->form_id() ? false : $check;
}, 10, 2 );

Stricter blocking on one form:

add_filter( 'fsv_cf7_thresholds', function ( $thresholds, $ctx ) {
	if ( 5 === $ctx->form_id() ) {
		$thresholds['block'] = 0.95;
	}
	return $thresholds;
}, 10, 2 );

Never block on one form; send its would-be blocks to review:

add_filter( 'fsv_cf7_verdict', function ( $verdict, $ctx ) {
	if ( 3 === $ctx->form_id() && $verdict->is_block() ) {
		return $verdict->with( array( 'verdict' => 'review' ) );
	}
	return $verdict;
}, 10, 2 );

Tell the model more about your site (keep visitor text out of everything except state.submission):

add_filter( 'fsv_cf7_payload', function ( $payload, $ctx ) {
	$payload['state']['site']['description'] .= ' We never buy SEO, marketing or web design services.';
	return $payload;
}, 10, 2 );

Changing the questions changes what the thresholds mean; see Calibration.

Log blocked submissions somewhere else:

add_action( 'fsv_cf7_decided', function ( $verdict, $ctx ) {
	if ( $verdict->is_block() && 'model' === $verdict->stage() ) {
		error_log( sprintf( 'Formsieve blocked form %d at %d%% (request %s)', $ctx->form_id(), $verdict->spam_percent(), $verdict->request_id() ) );
	}
}, 10, 2 );

Read the key from the environment:

add_filter( 'fsv_cf7_api_key', function ( $stored ) {
	$key = getenv( 'FORMSIEVE_KEY' );
	return $key ? $key : $stored;
} );

Add your own pre-filter stage (it runs first here). The class is declared only when Formsieve for CF7 is active, so the code cannot break the site when the plugin is deactivated:

add_action( 'plugins_loaded', function () {
	if ( ! interface_exists( 'Formsieve\CF7\Core\Pipeline\Stage' ) ) {
		return;
	}

	final class My_Partner_Allowlist implements \Formsieve\CF7\Core\Pipeline\Stage {
		public function id(): string {
			return 'partner_allowlist';
		}

		public function run( \Formsieve\CF7\Core\Pipeline\Context $ctx, \Formsieve\CF7\Core\Pipeline\Result $result ): void {
			if ( 'partner.example' === $ctx->email_domain() ) {
				$result->decided( 'allow', 'allowlist:partner' );
			}
		}
	}

	add_filter( 'fsv_cf7_pipeline_stages', function ( $stages ) {
		array_unshift( $stages, new My_Partner_Allowlist() );
		return $stages;
	} );
}, 30 );

WP-CLI

wp formsieve-cf7 test [<form_id>] [--sample=spam|ham] [--yes] [--format=table|json]
wp formsieve-cf7 stats [--days=<n>] [--format=table|json]
  • test without a form ID sends the canned Test connection sample with the saved route and key (about 500 tokens) and prints the result; nothing is logged. Like the button, it sends nothing until consent is given for the saved route. It exits with an error when the connection fails, so you can use it in monitoring.
  • test <form_id> builds a synthetic spam (default) or ham (--sample=ham) submission for that form and classifies it exactly like a real one: pre-filters, one API call (or Test mode), decision and one log row, marked as a WP-CLI sample (source = cli). Its API call counts in the API calls, tokens and cost, but the sample is not counted as a submission. Outside Test mode it spends one API call, so it needs --yes. The category field of its output is the raw category ID.
  • stats prints decision counts per band, API share, tokens, cost, latency and corrections for the last --days days (default 30), plus the month to date and the projection, with the same rules as the dashboard: the decision counts cover visitor submissions only, while API calls, tokens and cost include re-checks and WP-CLI samples. --format=json adds the daily breakdown, categories and breaker state.

Data for developers

  • Decision log: table {$wpdb->prefix}fsv_cf7_log. Numbers and identifiers only (times in UTC); read it, but do not write to it. The source column tells a visitor's submission (submission) from a WP-CLI sample (cli); only submission rows count as submissions on the dashboard. category holds the raw ID (for example vendor_solicitation); \Formsieve\CF7\Core\Admin\Format::category( $id ) returns the translated label the screens show. When a form uses do_not_store, or the visitor leaves a consent_for:storage box unticked, the row has no IP or e-mail hash.
  • Options: fsv_cf7_settings, fsv_cf7_lists (the allow and block lists, not autoloaded), fsv_cf7_route, fsv_cf7_api_key (obfuscated, never read it directly) and more, all prefixed fsv_cf7_.
  • During a submission, Formsieve pushes its result into Contact Form 7's submission as formsieve: WPCF7_Submission::get_instance()->pull( 'formsieve' ) returns the verdict, the outcome, the note and the subject tag (numbers, identifiers and Formsieve's own texts only).
  • Flamingo messages: the Flamingo meta field formsieve holds the human-readable Formsieve note, for blocked, review-band and not-checked messages only (for every message when Notes is on); the post meta _fsv_cf7_verdict holds a summary array {log_id, form_id, verdict, percent, checked, demo, outcome} for every checked message.
  • REST (both require manage_options and the wp_rest nonce):
    • POST /wp-json/formsieve-cf7/v1/test-connection backs the admin's Test connection button. It sends nothing until consent covers the tested route (error_code no_consent, sent: false), and it sends the saved key only to the saved route (no_key otherwise; type the key to test another route).
    • POST /wp-json/formsieve-cf7/v1/log/<id>/label with label = spam or ham records an administrator's correction for one log row, as the Log tab does.