Install Nitpick. It is one line.
One script tag on your site, a list of the addresses it may run on, and a PIN for your team. Five minutes, most of which is finding the right box to paste into.
The tag
Every site you add in the dashboard gets its own tag. Paste it into the <head> of every page you want reviewed, or just before </body> if that is the only place your platform offers.
<script src="https://getnitpick.com/nitpick.js" data-site="np_your_site_key" defer></script>
Swap np_your_site_keyfor your site’s key: open the site in the dashboard and the Install panel at the top shows the whole tag, ready to copy.
- Ordinary visitors barely notice. They get a loader under half a kilobyte, which fetches the overlay once (the browser keeps it after that). The overlay looks at the address, finds no reason to wake up, and goes back to sleep. Nothing is drawn and nothing is sent.
- It wakes up for reviewers. Open any page with
?nitpick=trueon the end, or follow an invite link, and the sign-in card appears. After that it stays on for that browser until the session ends. - Want visitors to get nothing at all? Print the tag only for reviewers. The WordPress must-use plugin below does exactly that, and the same idea works on any server you control.
Platforms
Where the paste goes on the usual suspects. The tag is the same everywhere.
Three ways in. Pick the one your site already lives with.
The theme’s header
Edit header.php in a child theme, so the next theme update does not quietly delete your tag.
<!-- wp-content/themes/your-child-theme/header.php -->
<!-- Paste it just above wp_head(), inside <head>. -->
<script src="https://getnitpick.com/nitpick.js" data-site="np_your_site_key" defer></script>
<?php wp_head(); ?>
A header and footer plugin
Any plugin in the “Insert Headers and Footers” family will do. Paste the tag into its header box and save. Nothing to maintain, and it survives theme changes.
<!-- In the plugin's "Header" box, on every page. -->
<script src="https://getnitpick.com/nitpick.js" data-site="np_your_site_key" defer></script>
A must-use plugin, for reviewers only
The fussiest option, and our favourite. Save this as wp-content/mu-plugins/nitpick.php. It prints the tag only when someone arrives with ?nitpick in the address, or came that way in the last eight hours. Everyone else gets a page with no trace of us. A must-use plugin cannot be switched off from the admin and is untouched by updates.
<?php
/**
* Plugin Name: Nitpick
* Description: Prints the Nitpick tag for reviewers only. Visitors get nothing at all.
*/
// A reviewer arrives with ?nitpick in the address. Remember them for eight hours,
// so the tag is still there once they click through to other pages.
add_action('init', function () {
if (isset($_GET['nitpick']) && !headers_sent()) {
setcookie('nitpick_review', '1', time() + 8 * HOUR_IN_SECONDS, '/', COOKIE_DOMAIN, is_ssl(), true);
$_COOKIE['nitpick_review'] = '1';
}
});
add_action('wp_head', function () {
$reviewer = isset($_GET['nitpick']) || isset($_GET['nppin']) || isset($_COOKIE['nitpick_review']);
if (!$reviewer) {
return;
}
echo '<script src="https://getnitpick.com/nitpick.js" data-site="np_your_site_key" defer></script>' . "\n";
}, 1);
Behind a page cache? Tell the cache to skip requests with nitpick in the query string or a nitpick_review cookie. Otherwise it serves one answer to everybody, and that answer is usually wrong.
Origins
An origin is the front part of an address: the scheme, the host and, if there is one, the port. Each site in the dashboard has a list of them, and the tag answers only on these. Anywhere else, including a copy of your page on someone else’s server, it stays politely silent.
https://larkspur-dental.example- The live site. Add the
www.version too if both are in use. https://staging.larkspur-dental.example- Staging, so the client can review before anything goes live. Staging counts as the same site.
https://*.larkspur-dental.example- Every subdomain at once. Handy when each branch gets its own preview address.
http://localhost:3000- Your own machine. Local review works; give the port your dev server uses.
Paste a full address if that is easier. The dashboard keeps only the origin and drops the rest.
Reviewers
Nobody makes an account to leave a note. There are two ways in.
The PIN, for your team
One per site, 4 to 12 letters or digits, set on the site’s page in the dashboard. Give it to the people who build the site. They open a page with ?nitpick=true, type their name and the PIN, and their name goes on every note.
Invite links, for clients
Make one per person, with their name on it. Following the link signs them straight in, no PIN. Each lasts thirty days, and you can revoke one from the dashboard the moment it is no longer wanted.
- A sign-in lasts eight hours in that browser, across every page of the site. After that the reviewer signs in again; their unsent drafts are kept.
- To stop, press Turn Nitpick off on the bar. That signs the browser out and keeps any drafts. To simply browse without it for a moment, pick the browse tool.
Where notes go
Every note lands in the dashboard regardless. Add a destination on the site’s page to send it on as well, and use its test button before trusting it with a client.
Webhook
Each note is a POST of JSON to your address. Screenshots and recordings arrive as signed links that last seven days, so copy them somewhere if you need them longer. A failed delivery is tried again over the next half day, and every attempt shows in the delivery log.
{
"event": "note.created",
"sentAt": 1759852800000,
"note": {
"id": "nt_8f2k1q",
"number": 3,
"reviewer": "Priya, Larkspur Dental",
"pageUrl": "https://larkspur-dental.example/pricing",
"message": "The middle column says $49, the checkout says $59.",
"viewport": "1440x900",
"url": "https://getnitpick.com/app/notes/nt_8f2k1q"
},
"review": { "id": "rv_3n7x", "noteCount": 5 },
"site": { "id": "st_2w9m", "name": "Larkspur Dental", "key": "np_your_site_key" },
"media": [
{ "kind": "screenshot", "type": "image/jpeg", "url": "https://..." }
]
}
X-Nitpick-Eventnote.createdfor a note,testfor the test button.X-Nitpick-Delivery- A unique id per delivery. Retries keep it, so you can ignore one you have already handled.
X-Nitpick-Signaturesha256=<hmac of body>: the HMAC-SHA256 of the raw body, keyed with the signing secret, in hex.
import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody is the request body exactly as it arrived, before any JSON parsing.
export function fromNitpick(rawBody, header, secret) {
const want = Buffer.from("sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex"));
const got = Buffer.from(header ?? "");
return want.length === got.length && timingSafeEqual(want, got);
}
ClickUp
Paste a ClickUp personal token (in ClickUp: your avatar, Settings, Apps), then pick the list from the ones that token can see. Each note becomes a task: the note as its title and description, with the page, the device, the reviewer and the transcript of any recording, and the screenshot, markup and recordings attached. Comments travel both ways, so a reviewer’s reply appears on the task and your team’s comment reaches the reviewer. Status follows along every few minutes: close the task and the reviewer sees it fixed.
Slack
Create an incoming webhook for the channel (in Slack: Apps, Incoming Webhooks) and paste its address. Each note posts as one message with the words, the page, the reviewer and the screenshot.
Add the addresses that should hear about each note. Every address gets its own email, so nobody’s reply-all ends up in somebody else’s inbox.
Staying out of the way
Things the overlay should leave alone
Put data-nitpick-ignore on anything the overlay should never photograph or intercept: a chat widget, a cookie banner, your own admin bar. It is left out of screenshots, and it keeps working while a reviewer has a tool in hand.
<!-- The overlay never photographs this, and it keeps working while a tool is in hand. -->
<div class="chat-widget" data-nitpick-ignore>...</div>
A review button of your own
A plain link to ?nitpick=true is all it takes. Hide it from visitors however you like: behind a login, in a footer only staff read, or on staging only.
<!-- A way in for reviewers. Opens the sign-in card on the page they are already on. -->
<a href="?nitpick=true" data-nitpick-ignore>Review this page</a>
Content Security Policy
No policy, or a relaxed one? Skip this. A site with a strict default-src needs to let the service in. Merge these into the policy you already have rather than replacing it.
Content-Security-Policy:
script-src 'self' https://getnitpick.com;
connect-src 'self' https://getnitpick.com;
img-src 'self' https://getnitpick.com data: blob:;
media-src 'self' blob:;
style-src 'self' 'unsafe-inline';
script-srcloads the loader and the overlay.connect-srclets reviewers sign in and send notes.img-srccovers screenshots while they are drawn on, and sent notes shown back to the reviewer.media-src blob:plays recordings back before they are sent.style-srclets the overlay style itself. It keeps its styles to itself, in its own shadow root.
Troubleshooting
I added the tag and nothing appears.
Four things, in the order they usually turn out to be the answer.
- The address needs
?nitpick=trueon the end. Without it, the overlay stays asleep on purpose. - The page’s origin is not on the site’s list. The service answers only on listed origins, and to everyone else it looks as if nothing is there. Check the exact scheme and subdomain.
- A cache is serving yesterday’s page, from before the tag. Purge the page cache or the CDN and look again.
- An ad or tracker blocker is stopping the script. Try a private window with extensions off. If that works, allow the service’s address in the blocker.
Signing in says “Not found”.
That is the origin, every time. The service gives one polite answer to a site it does not recognise, so add the address you are on (with its scheme, and its port for a local one) under origins and sign in again.
The PIN is refused.
PINs are 4 to 12 letters or digits and are checked exactly, capitals included. If the PIN was changed in the dashboard, the old one stops working at once. If the site has no PIN at all, only invite links get in; set one on the site’s page.
Recordings will not play.
Your site has a Content Security Policy without media-src blob:. Recordings are played back from the browser’s own memory, which is what a blob address is. Add it, along with the rest of the lines in the policy above.
Screenshots look wrong.
Usually images from another domain, such as a CDN or an image service, that do not send CORS headers. The browser will not let any script copy those pixels, so they come out blank. Have the image host send Access-Control-Allow-Origin for your site, or serve the images from your own domain.
Tag in. Now go and be picky.
Open your own site with ?nitpick=true on the end and leave the first note yourself.