Technical

Snapchat & Nextdoor conversion tracking for local service businesses

Two smaller platforms, two sets of traps. How we send bookings to Snapchat's and Nextdoor's Conversions APIs, and what each one gets wrong by default.

Hamza El Asri Hamza El AsriFounder & lead strategist, RM Click PublishedNovember 10, 2026 Reading time9 min read AudienceOwners & developers

Snapchat and Nextdoor don't get as much attention as Meta and Google, but for local service businesses they can bring cheap bookings. Both also have server-side conversion APIs, and both have defaults that quietly hurt you.

Short answer: on Snapchat, a booking maps naturally to RESERVE, but web campaigns couldn't optimize for it in our setup. Send SIGN_UP alongside it and optimize toward Pixel Sign Up. On Nextdoor, send lead and purchase events with the ndclid click ID in customer.click_id and your data_source_id. On both, skip bookings that don't carry the platform's identifier.

This is part of the pipeline we built for a US salon using Boulevard that advertises on five platforms. The full build is in our complete server-side guide.

Last verified: November 2026 against Snapchat's Conversions API v3 and Nextdoor's Conversion API reference. Snapchat optimization behavior is from our own campaigns.

The shared rule: skip what isn't yours

A booking pipeline fans every booking out to every platform. Most bookings come from Meta, Google or organic search, and carry no Snapchat or Nextdoor identifier.

Sending those anyway doesn't help either platform credit its own ads. It adds noise, and on platforms with thin data it can blur what the algorithm learns. So both senders start with a skip-guard:

  • Snapchat: send only if you have the click ID (ScCid) or the pixel cookie (_scid).
  • Nextdoor: send only if you have the click ID (ndclid).

Return a clear "skipped" result, and count skips separately from failures in your dashboard.

Snapchat

Capture the identifiers

When someone clicks or swipes up on a Snapchat ad, Snapchat appends &ScCid= to your landing URL. Capture it on landing, along with the _scid cookie set by the Snap Pixel if you run it. Send them as sc_click_id and sc_cookie1.

RESERVE vs SIGN_UP: the optimization trap

For appointments, the obvious mapping is:

  • Booked → RESERVE
  • Completed and paid → PURCHASE with the value

RESERVE is a valid Snapchat event and shows up in reporting. The problem appeared when we set up the ad squads: for web campaigns, we couldn't choose RESERVE as the optimization goal. The options were Pixel Sign Up, Page View, Add to Cart and Purchase.

Our fix: on every booking, send both RESERVE and SIGN_UP.

  • RESERVE keeps reporting honest ("this is a reservation").
  • SIGN_UP gives the ad squad something it can optimize toward. We optimize toward Pixel Sign Up as "booking made."

Completed visits send PURCHASE with the real revenue.

The request

Snapchat's v3 endpoint takes your pixel ID in the path and an access token. We send the token as a query parameter and as a Bearer header:

import { sha256Hex, normalizeEmail, digitsWithCountry } from './hash.js';

export async function sendSnap(c, env) {
  const a = c.attribution || {};
  if (!a.sc_click_id && !a.sc_cookie1) return { ok: true, skipped: 'no snap identifier' };   // skip-guard

  const user_data = {};
  if (c.client.email) user_data.em = [await sha256Hex(normalizeEmail(c.client.email))];
  if (c.client.phone) user_data.ph = [await sha256Hex(digitsWithCountry(c.client.phone))];
  if (a.sc_click_id) user_data.sc_click_id = a.sc_click_id;      // from &ScCid=
  if (a.sc_cookie1) user_data.sc_cookie1 = a.sc_cookie1;         // _scid cookie
  if (a.user_agent) user_data.client_user_agent = a.user_agent;
  if (a.ip) user_data.client_ip_address = a.ip;

  const apptKey = String(c.appointment_id).split(':').pop();
  const make = (name) => ({
    event_name: name,
    action_source: 'website',
    event_time: c.event_time,                      // unix seconds
    event_id: `snap_${apptKey}_${name}`,           // unique per appointment + event
    event_source_url: a.landing_url || 'https://YOUR_SITE/',
    user_data,
    custom_data: { currency: 'USD', value: c.value || 0, order_id: apptKey },
  });

  const completed = c.event_type === 'APPOINTMENT_COMPLETED';
  const events = completed ? [make('PURCHASE')] : [make('RESERVE'), make('SIGN_UP')];

  // A pasted token can carry a space or quotes: Snap rejects it as "Invalid access token"
  const token = String(env.SNAP_ACCESS_TOKEN).trim().replace(/^["']|["']$/g, '');
  const path = env.SNAP_VALIDATE === '1' ? 'events/validate' : 'events';   // validate while testing

  const res = await fetch(
    `https://tr.snapchat.com/v3/${env.SNAP_PIXEL_ID.trim()}/${path}?access_token=${encodeURIComponent(token)}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
    body: JSON.stringify({ data: events }),
  });
  const body = await res.json().catch(() => null);
  return res.ok ? { ok: true, body } : { ok: false, status: res.status, error: JSON.stringify(body) };
}

Two details from our build:

  • Use the validate endpoint while testing. POST /v3/{pixel_id}/events/validate checks your payload without recording a conversion. Switch it with a setting, not by editing code.
  • Trim the token. A secret pasted with a trailing space, newline or quotes came back as "Invalid access token", which sent us looking in the wrong place.

Nextdoor

Nextdoor is built around neighborhoods, which suits salons, clinics and home services. Its Conversion API works much like the others, with its own naming.

Before you start

  • Request Conversion API access. It's separate from running ads, so request it early.
  • Find your data_source_id. Nextdoor deprecated pixel_id, client_id and event_time. The pixel ID is now data_source_id, and the timestamp is event_time_epoch. Older examples online still use the old names.
  • Capture ndclid. Nextdoor appends it to the landing URL on ad clicks. Send it as customer.click_id.

Events

Nextdoor's event names are lowercase. For appointments:

  • Booked → lead
  • Completed and paid → purchase, with the value in custom.order_value

The request

export async function sendNextdoor(c, env) {
  const a = c.attribution || {};
  if (!a.ndclid) return { ok: true, skipped: 'no ndclid' };       // skip-guard

  const completed = c.event_type === 'APPOINTMENT_COMPLETED';
  const apptKey = String(c.appointment_id).split(':').pop();
  const eventName = completed ? 'purchase' : 'lead';

  const customer = { click_id: a.ndclid };
  if (c.client.email) customer.email = await sha256Hex(normalizeEmail(c.client.email));
  if (c.client.phone) customer.phone_number = await sha256Hex(digitsWithCountry(c.client.phone));

  const res = await fetch('https://ads.nextdoor.com/v2/api/conversions/track', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${env.NEXTDOOR_API_TOKEN}` },
    body: JSON.stringify({
      event_name: eventName,
      event_id: `nd_${apptKey}_${eventName}`,       // deterministic, also usable by the pixel
      action_source: 'website',
      data_source_id: env.NEXTDOOR_DATA_SOURCE_ID,  // formerly pixel_id
      event_time_epoch: c.event_time,
      customer,
      ...(completed && c.value > 0 ? { custom: { order_value: String(c.value), order_id: apptKey } } : {}),
    }),
  });
  const body = await res.json().catch(() => null);
  return res.ok ? { ok: true, body } : { ok: false, status: res.status, error: JSON.stringify(body) };
}

Check Nextdoor's current reference for the exact authorization header and how it wants email and phone normalized before hashing. The reference lists the fields but left those details to other pages when we checked.

Two Nextdoor pixel gotchas

  • Leave URL-based events empty when booking happens in an iframe. Nextdoor's pixel setup can map events to URLs, like a thank-you page. A booking widget in an iframe has no thank-you URL on your site, so URL rules either never fire or fire on the wrong page. Let the Conversion API report bookings instead.
  • Remove the pixel's <noscript> image if your site is built with Vite. Many AI-built and React sites use Vite, and Vite builds reject a <noscript> inside <head>. The noscript image only matters for visitors with JavaScript off, so drop it.

Phone numbers: one format per platform

Each platform wants phone numbers normalized slightly differently before hashing. In our pipeline, TikTok gets E.164 with the +, while Meta and Snapchat get digits only, country code included. Keep a small normalizer per platform instead of one shared function, and test each with a known number.

// Digits only, with country code (assumes US when 10 digits: adjust for other markets)
export function digitsWithCountry(phone) {
  const d = String(phone).replace(/\D/g, '');
  return d.length === 10 ? '1' + d : d;
}

Common errors

  • Snapchat "Invalid access token": trailing whitespace or quotes in the stored secret.
  • Can't select RESERVE as the optimization goal: send SIGN_UP too and optimize for Pixel Sign Up.
  • Snapchat or Nextdoor credited with Meta bookings: no skip-guard.
  • Nextdoor rejects the payload: deprecated fields (pixel_id, client_id, event_time) instead of data_source_id and event_time_epoch.
  • Nextdoor events with no attribution: ndclid missing because it wasn't captured on landing.
  • Build fails after adding the Nextdoor pixel: <noscript> in <head> with Vite.

Testing checklist

  • Landing page captures ScCid, the _scid cookie and ndclid.
  • A booking without those identifiers is skipped for that platform.
  • Snapchat test payload passes events/validate before going live.
  • A booking sends both RESERVE and SIGN_UP; a completed visit sends PURCHASE with value.
  • The Snapchat ad squad optimizes toward Pixel Sign Up.
  • Nextdoor events use data_source_id and event_time_epoch, with click_id inside customer.
  • Event IDs are unique per appointment and event.

Related reading: TikTok Events API for bookings, and why your Facebook ads show 0 bookings when you use Boulevard.

Hamza El Asri
Written by Hamza El Asri

Media buyer and tracking specialist, founder of RM Click. Top Rated on Fiverr with 413 reviews. We built the pipeline this article describes. LinkedIn · Fiverr

FAQ

Frequently asked
questions

In our setup, no. RESERVE was reported but couldn't be chosen as an optimization goal for web ad squads. We send SIGN_UP alongside it and optimize toward Pixel Sign Up as the booking signal.

Snapchat's click ID. Snapchat appends &ScCid= to the landing URL when someone clicks or swipes up on an ad. Capture it on landing and send it as sc_click_id in Conversions API events.

Nextdoor's click ID, added to the landing URL when someone clicks a Nextdoor ad. Send it as click_id inside the customer object of the Conversion API event.

API access is requested separately from your Nextdoor Ads account. Plan for that wait before launch.

No. Only send bookings that carry that platform's identifier, such as a Snapchat click ID or cookie, or a Nextdoor click ID. Otherwise bookings from other channels just add noise.

Free ad audit

Need this set up
for your salon?