Guide

PHP

Plain PHP with the curl extension, without a framework or SDK.

Browser

<script src="https://nolgard.com/v1/client.js" data-site-key="SITE_KEY" defer></script>
<script>
  document.querySelector('#signup').addEventListener('submit', async (event) => {
    event.preventDefault();
    await Nolgard.ready;
    const token = await Nolgard.execute({ action: 'signup' });
    const form = event.target;
    const response = await fetch('/signup.php', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ token, email: form.elements.email.value })
    });
    // Show your own success or error message here.
  });
</script>

Server

<?php
// signup.php
function nolgard_verify(string $token, string $action, array $account = []): ?array
{
    $payload = ['secret' => getenv('NOLGARD_SECRET'), 'token' => $token, 'action' => $action];
    if (!empty($_SERVER['REMOTE_ADDR'])) {
        $payload['remoteIp'] = $_SERVER['REMOTE_ADDR'];
    }
    if ($account) {
        $payload['account'] = $account;
    }

    $ch = curl_init('https://nolgard.com/v1/verify');
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
        CURLOPT_POSTFIELDS => json_encode($payload),
        CURLOPT_TIMEOUT => 3,
    ]);
    $body = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    curl_close($ch);

    if ($body === false || $status >= 500) {
        return null; // timeout, network or 5xx
    }
    $result = json_decode($body, true);
    return is_array($result) ? $result : null;
}

$input = json_decode(file_get_contents('php://input'), true) ?? [];
$email = (string) ($input['email'] ?? '');
$result = nolgard_verify((string) ($input['token'] ?? ''), 'signup', ['email' => $email]);

header('Content-Type: application/json');

if (($result['error'] ?? '') === 'invalid-secret') {
    http_response_code(503); // config error: fail closed
    exit(json_encode(['error' => 'try-again']));
}
if ($result === null || ($result['error'] ?? '') === 'over-quota') {
    $result = ['success' => true, 'decision' => 'allow']; // timeout, 5xx or over quota: fail open
}
if (empty($result['success'])) {
    http_response_code(403);
    exit(json_encode(['error' => 'rejected']));
}

switch ($result['decision']) {
    case 'allow':
        create_account($email);
        break;
    case 'limit':
        create_account($email, ['trial_days' => 3]);
        break;
    case 'shadow':
        break; // same response, nothing created
    case 'challenge':
        http_response_code(403);
        exit(json_encode(['error' => 'confirm-email']));
    default:
        http_response_code(403);
        exit(json_encode(['error' => 'rejected']));
}
echo json_encode(['ok' => true]);

Supply your own create_account function. Behind a proxy, REMOTE_ADDR contains the proxy's address. Use the address reported by your proxy only if you control it. On WordPress, use the plugin instead.