Automated Review Request Script (Email + Link)
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:
- Create:
/tools/review-asker/ - Create writable folder:
/tools/review-asker/_db/ - Save the script below as:
/tools/review-asker/index.php - Edit the config at the top (your email “from”, your review link, admin token).
- Use it:
- Admin UI:
/tools/review-asker/?admin=TOKEN - Cron sender:
/tools/review-asker/?send=TOKEN(run every hour)
- Admin UI:
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.