Skip to content

Google reCAPTCHA v3

formsub asks Google for a token on submit and appends it as recaptcha_response. Only your server can decide if the token is valid. The client never talks to the secret key.

Front end

  1. Load the reCAPTCHA v3 script so grecaptcha exists:
html
<script src="https://www.google.com/recaptcha/api.js?render=YOUR_SITE_KEY"></script>
  1. Enable it in newForm:
js
formsub.newForm({
  formId: 'contact-form',
  submitButton: '#submit-button',
  ajaxUrl: '/api/contact',
  recaptcha: true,
  recaptcha_site_key: 'YOUR_SITE_KEY',
});

If grecaptcha is missing at submit time, formsub treats that as a failure and does not send the form.

The token is single-use and expires after about two minutes. formsub requests it with action: "submit" — your server check must use the same action string.

What you receive

recaptcha_response=03AGdBq24...long-token...
email=user@example.com
name=Jane

Verify with Google

Use the siteverify API. POST:

ParameterValue
secretYour reCAPTCHA secret key (never in front-end code)
responseThe recaptcha_response field from the form

A successful Google reply looks like:

json
{
  "success": true,
  "score": 0.9,
  "action": "submit",
  "challenge_ts": "2026-08-03T08:00:00Z",
  "hostname": "example.com"
}

Accept the form only when:

  • success === true
  • action === "submit"
  • score meets your threshold (start around >= 0.5 and tune)

Otherwise return a formsub error payload.

PHP (vanilla)

Store the secret outside the web root or in an environment variable:

php
<?php
// config.php — do not commit this file
define('RECAPTCHA_SECRET', 'YOUR_SECRET_KEY');

Helper:

php
<?php
function verify_recaptcha(string $token): array
{
    $response = file_get_contents('https://www.google.com/recaptcha/api/siteverify', false, stream_context_create([
        'http' => [
            'method'  => 'POST',
            'header'  => 'Content-Type: application/x-www-form-urlencoded',
            'content' => http_build_query([
                'secret'   => RECAPTCHA_SECRET,
                'response' => $token,
            ]),
        ],
    ]));

    return json_decode($response, true) ?: ['success' => false];
}

At the start of the form handler:

php
<?php
require_once 'config.php';

$token = $_POST['recaptcha_response'] ?? '';
$result = verify_recaptcha($token);

if (
    empty($result['success']) ||
    ($result['action'] ?? '') !== 'submit' ||
    ($result['score'] ?? 0) < 0.5
) {
    http_response_code(400);
    echo json_encode([
        'status'  => 'error',
        'code'    => 400,
        'message' => 'reCAPTCHA verification failed.',
    ]);
    exit;
}

// continue with form processing...

TIP

Prefer curl or Guzzle in production if allow_url_fopen is disabled.

WordPress (PHP)

In wp-config.php (above “That's all, stop editing!”):

php
define('RECAPTCHA_SECRET', 'YOUR_SECRET_KEY');

Helper:

php
<?php
function myplugin_verify_recaptcha(string $token): array
{
    $response = wp_remote_post('https://www.google.com/recaptcha/api/siteverify', [
        'body' => [
            'secret'   => RECAPTCHA_SECRET,
            'response' => $token,
        ],
    ]);

    if (is_wp_error($response)) {
        return ['success' => false];
    }

    return json_decode(wp_remote_retrieve_body($response), true) ?: ['success' => false];
}

In your REST route, admin-ajax.php handler, or custom endpoint:

php
<?php
$token  = $_POST['recaptcha_response'] ?? '';
$result = myplugin_verify_recaptcha($token);

if (
    empty($result['success']) ||
    ($result['action'] ?? '') !== 'submit' ||
    ($result['score'] ?? 0) < 0.5
) {
    wp_send_json_error([
        'status'  => 'error',
        'code'    => 400,
        'message' => 'reCAPTCHA verification failed.',
    ], 400);
}

// continue with form processing...

Return JSON in the shape formsub expects so the client can show success or error copy.

Released under the MIT License.