Automated Review Request Script (Email + Link)

March 6, 2026 NEW

This is a lightweight ask for reviews script you can drop into a small site or service workflow. It sends a friendly follow-up email after an order/service date with a direct review link (Google, Yelp, Facebook, Trustpilot, etc.), and tracks whether the request is pending/sent/done.

Why it’s useful: Most customers are happy — they just forget. A simple, polite follow-up (sent once, at the right time) can dramatically increase reviews without you chasing people manually.

What it does:

  • Add a customer + order/service date.
  • Automatically schedules a review request (e.g. 2 days later).
  • Sends a clean email with a one-click review link.
  • Supports unsubscribe (basic compliance-friendly behavior).
  • Runs without a database (stores JSON files), so it’s easy to host anywhere.

Install:

  1. Create: /tools/review-asker/
  2. Create writable folder: /tools/review-asker/_db/
  3. Save the script below as: /tools/review-asker/index.php
  4. Edit the config at the top (your email “from”, your review link, admin token).
  5. Use it:
    • Admin UI: /tools/review-asker/?admin=TOKEN
    • Cron sender: /tools/review-asker/?send=TOKEN (run every hour)

How to automate sending: Set a cron job to hit the ?send=TOKEN URL every hour (or every 15 minutes). Most hosts let you do this in cPanel “Cron Jobs”.

<?php
declare(strict_types=1);

/**
 * Automated Review Request Script — No DB (JSON storage)
 * File: /tools/review-asker/index.php
 *
 * Admin UI:
 *   /tools/review-asker/?admin=TOKEN
 *
 * Sender endpoint (cron):
 *   /tools/review-asker/?send=TOKEN
 *
 * Notes:
 * - Uses PHP mail(). If your host blocks mail(), swap in PHPMailer SMTP.
 * - Keep the token secret.
 */

header('X-Content-Type-Options: nosniff');
header('Referrer-Policy: strict-origin-when-cross-origin');

// ---------------- CONFIG ----------------
const ADMIN_TOKEN   = 'CHANGE_ME_TOKEN';
const STORE_DIR     = __DIR__ . '/_db';

// Your business info
const BUSINESS_NAME = 'Your Business';
const FROM_EMAIL    = 'noreply@yourdomain.com';
const FROM_NAME     = 'Your Business';

// Where you want reviews (put your direct review page link here)
const REVIEW_URL    = 'https://g.page/r/YOUR-REVIEW-LINK-HERE/review';

// Sending behavior
const SEND_DELAY_HOURS = 48; // send review request 48 hours after service date
const MAX_SEND_PER_RUN = 50;
const REQ_TIMEOUT      = 8;
// ---------------- /CONFIG ---------------

function h(string $s): string { return htmlspecialchars($s, ENT_QUOTES, 'UTF-8'); }
function ensure_dir(string $dir): bool { return is_dir($dir) || @mkdir($dir, 0755, true); }

function json_read(string $file): array {
  if (!is_file($file)) return [];
  $raw = @file_get_contents($file);
  if ($raw === false) return [];
  $j = json_decode($raw, true);
  return is_array($j) ? $j : [];
}

function json_write(string $file, array $data): bool {
  $out = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
  if ($out === false) return false;
  return @file_put_contents($file, $out, LOCK_EX) !== false;
}

function uid(): string {
  return gmdate('Ymd_His') . '_' . bin2hex(random_bytes(6));
}

function base_url(): string {
  $https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
    || (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && stripos((string)$_SERVER['HTTP_X_FORWARDED_PROTO'], 'https') === 0);
  $proto = $https ? 'https://' : 'http://';
  $host  = $_SERVER['HTTP_HOST'] ?? '';
  $path  = $_SERVER['SCRIPT_NAME'] ?? '';
  return $proto . $host . $path;
}

function send_mail_plain(string $to, string $subject, string $body): bool {
  $headers = [];
  $headers[] = 'From: ' . FROM_NAME . ' <' . FROM_EMAIL . '>';
  $headers[] = 'MIME-Version: 1.0';
  $headers[] = 'Content-Type: text/plain; charset=UTF-8';
  return @mail($to, $subject, $body, implode("\r\n", $headers));
}

function list_records(): array {
  $files = glob(STORE_DIR . '/*.json') ?: [];
  rsort($files);
  $out = [];
  foreach ($files as $f) {
    $j = json_read($f);
    if (!$j) continue;
    $j['_file'] = basename($f);
    $out[] = $j;
  }
  return $out;
}

ensure_dir(STORE_DIR);

function require_token(string $key): void {
  $tok = (string)($_GET[$key] ?? '');
  if ($tok === '' || !hash_equals(ADMIN_TOKEN, $tok)) {
    http_response_code(403);
    header('Content-Type: text/plain; charset=utf-8');
    echo "Forbidden.\n";
    exit;
  }
}

function unsub_file(): string { return STORE_DIR . '/_unsub.json'; }

function unsubbed(string $email): bool {
  $u = json_read(unsub_file());
  $email = strtolower(trim($email));
  return !empty($u[$email]);
}

function mark_unsub(string $email): void {
  $u = json_read(unsub_file());
  $email = strtolower(trim($email));
  $u[$email] = ['at' => gmdate('c')];
  json_write(unsub_file(), $u);
}

function record_path(string $id): string { return STORE_DIR . '/' . $id . '.json'; }

// -------- Unsubscribe endpoint (public, signed token) --------
if (isset($_GET['unsub'], $_GET['e'])) {
  $sig = (string)($_GET['unsub'] ?? '');
  $email = (string)($_GET['e'] ?? '');
  $good = hash_hmac('sha256', strtolower(trim($email)), ADMIN_TOKEN);
  if ($email === '' || !hash_equals($good, $sig)) {
    http_response_code(403);
    echo "Bad request.";
    exit;
  }
  mark_unsub($email);
  header('Content-Type: text/html; charset=utf-8');
  echo "<!doctype html><meta charset='utf-8'><title>Unsubscribed</title><p>You have been unsubscribed.</p>";
  exit;
}

// -------- Sender endpoint (cron) --------
if (isset($_GET['send'])) {
  require_token('send');

  $all = list_records();
  $sent = 0;

  foreach ($all as $r) {
    if ($sent >= MAX_SEND_PER_RUN) break;

    $status = (string)($r['status'] ?? 'pending');
    if ($status !== 'pending') continue;

    $email = (string)($r['email'] ?? '');
    if ($email === '' || unsubbed($email)) continue;

    $serviceAt = (string)($r['serviceAt'] ?? '');
    $serviceTs = strtotime($serviceAt);
    if ($serviceTs <= 0) continue;

    $due = $serviceTs + (SEND_DELAY_HOURS * 3600);
    if (time() < $due) continue;

    $name = (string)($r['name'] ?? '');
    $hello = $name ? "Hi {$name}," : "Hi there,";

    $unsubSig = hash_hmac('sha256', strtolower(trim($email)), ADMIN_TOKEN);
    $unsubUrl = base_url() . '?e=' . rawurlencode($email) . '&unsub=' . rawurlencode($unsubSig);

    $subject = BUSINESS_NAME . " — quick favor?";
    $body  = "{$hello}\n\n";
    $body .= "Thanks again for choosing " . BUSINESS_NAME . ". If you have 30 seconds, would you leave us a quick review?\n\n";
    $body .= "Review link:\n" . REVIEW_URL . "\n\n";
    $body .= "It really helps small businesses like ours.\n\n";
    $body .= "Thank you,\n" . BUSINESS_NAME . "\n\n";
    $body .= "Unsubscribe: {$unsubUrl}\n";

    if (send_mail_plain($email, $subject, $body)) {
      $r['status'] = 'sent';
      $r['sentAt'] = gmdate('c');
      json_write(STORE_DIR . '/' . (string)$r['_file'], $r);
      $sent++;
    }
  }

  header('Content-Type: application/json; charset=utf-8');
  echo json_encode(['ok'=>true,'sent'=>$sent], JSON_UNESCAPED_SLASHES);
  exit;
}

// -------- Admin UI --------
require_token('admin');

$all = list_records();

// Add record
$ok = '';
$err = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
  $name = trim((string)($_POST['name'] ?? ''));
  $email = trim((string)($_POST['email'] ?? ''));
  $serviceAt = trim((string)($_POST['serviceAt'] ?? ''));

  if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) $err = 'Valid email required.';
  if ($serviceAt === '' || strtotime($serviceAt) <= 0) $err = 'Valid service date/time required.';

  if ($err === '') {
    $id = uid();
    $rec = [
      'id' => $id,
      'name' => $name,
      'email' => $email,
      'serviceAt' => $serviceAt,
      'status' => 'pending',
      'createdAt' => gmdate('c'),
    ];
    json_write(record_path($id), $rec);
    $ok = 'Added.';
    $all = list_records();
  }
}

?><!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Review Request Automation</title>
<meta name="robots" content="noindex,nofollow" />
<style>
  :root{
    --bg:#0b0f16; --panel:#111a26; --panel2:#0c1420;
    --text:#e8eef8; --muted:rgba(232,238,248,.72);
    --line:rgba(255,255,255,.10); --accent:#2a8cff;
    --shadow:0 18px 55px rgba(0,0,0,.45); --r:16px;
    --ok:#7cff9a; --bad:#ff7c7c;
  }
  *{box-sizing:border-box}
  body{margin:0;background:var(--bg);color:var(--text);font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif}
  .wrap{max-width:1050px;margin:0 auto;padding:18px}
  .card{background:var(--panel);border:1px solid var(--line);border-radius:var(--r);padding:14px;margin:12px 0;box-shadow:var(--shadow)}
  .muted{color:var(--muted)}
  .pill{display:inline-block;padding:4px 10px;border-radius:999px;background:rgba(42,140,255,.14);border:1px solid rgba(42,140,255,.25);font-weight:900;font-size:12px}
  input{
    width:100%;padding:10px 12px;border-radius:12px;border:1px solid var(--line);
    background:var(--panel2);color:var(--text);outline:none;font-weight:800;
  }
  .btn{
    display:inline-flex;align-items:center;justify-content:center;gap:8px;
    border:0;border-radius:12px;padding:10px 12px;background:var(--accent);
    color:#06101a;font-weight:950;cursor:pointer;
  }
  table{width:100%;border-collapse:collapse}
  th,td{padding:10px;border-bottom:1px solid rgba(255,255,255,.08);vertical-align:top}
  th{text-align:left;font-size:12px;opacity:.75}
  .ok{color:var(--ok);font-weight:950}
  .bad{color:var(--bad);font-weight:950}
  .code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px}
</style>
</head>
<body>
<div class="wrap">

  <div class="card">
    <div class="pill">Automation</div>
    <h1 style="margin:10px 0 6px;font-size:22px">Review Request Automation</h1>
    <div class="muted">
      Cron URL: <span class="code"><?php echo h(base_url() . '?send=' . ADMIN_TOKEN); ?></span>
      <div class="muted" style="margin-top:6px;font-size:13px">Run it hourly. It will only send when records are due.</div>
    </div>
    <?php if ($ok): ?><div class="ok" style="margin-top:10px"><?php echo h($ok); ?></div><?php endif; ?>
    <?php if ($err): ?><div class="bad" style="margin-top:10px"><?php echo h($err); ?></div><?php endif; ?>
  </div>

  <div class="card">
    <b>Add customer</b>
    <form method="post" style="display:grid;grid-template-columns:1fr 1fr 1fr auto;gap:10px;align-items:end">
      <div>
        <div class="muted" style="font-size:12px;margin-bottom:6px">Name (optional)</div>
        <input name="name" placeholder="Alex" />
      </div>
      <div>
        <div class="muted" style="font-size:12px;margin-bottom:6px">Email</div>
        <input name="email" placeholder="alex@example.com" required />
      </div>
      <div>
        <div class="muted" style="font-size:12px;margin-bottom:6px">Service date/time</div>
        <input name="serviceAt" placeholder="2026-03-06 14:30" required />
      </div>
      <button class="btn" type="submit">➕ Add</button>
    </form>
  </div>

  <div class="card">
    <b>Queue</b>
    <?php if (!$all): ?>
      <div class="muted" style="margin-top:10px">No records yet.</div>
    <?php else: ?>
      <table>
        <thead>
          <tr>
            <th>Created</th>
            <th>Name</th>
            <th>Email</th>
            <th>Service at</th>
            <th>Status</th>
            <th>Sent at</th>
          </tr>
        </thead>
        <tbody>
          <?php foreach ($all as $r): ?>
            <tr>
              <td class="muted"><?php echo h((string)($r['createdAt'] ?? '')); ?></td>
              <td><?php echo h((string)($r['name'] ?? '')); ?></td>
              <td class="code"><?php echo h((string)($r['email'] ?? '')); ?></td>
              <td class="code"><?php echo h((string)($r['serviceAt'] ?? '')); ?></td>
              <td class="<?php echo (($r['status'] ?? '') === 'sent') ? 'ok' : 'muted'; ?>">
                <?php echo h((string)($r['status'] ?? '')); ?>
              </td>
              <td class="muted"><?php echo h((string)($r['sentAt'] ?? '')); ?></td>
            </tr>
          <?php endforeach; ?>
        </tbody>
      </table>
    <?php endif; ?>
  </div>

</div>
</body>
</html>

Comments (0)

No comments yet — be the first.

← Back to all scripts