Technical

TikTok Events API for appointment businesses: which events to send, why HTTP 200 lies, and how to hash phones

How we send salon bookings and revenue to TikTok server-side, the event names TikTok changed, and the response check most setups get wrong.

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

If you send bookings to TikTok's Events API and Ads Manager shows fewer conversions than your calendar, check two things first: the event names you send, and how you read TikTok's response.

Short answer: for appointments, send a Lead when someone books and a Purchase with the real value when the visit is completed and paid. TikTok renamed these events from SubmitForm and CompletePayment. The old names still work, but new setups should use the new ones. Then check the response body: code must be 0. TikTok returns HTTP 200 even when it rejects the event.

We send every booking to TikTok server-side for a US salon using Boulevard, along with Meta, Google Ads, Snapchat and Nextdoor. The full pipeline is in our complete server-side guide.

Last verified: November 2026 against TikTok's Events API and standard events documentation. Our own pipeline was built earlier with the legacy event names, which TikTok still accepts.

Which events to send for appointments

An appointment business has two moments that matter to TikTok:

Moment Event (current name) Legacy name Value
Client books Lead SubmitForm 0, or none
Visit completed and paid Purchase CompletePayment Real revenue

The booking is the optimization signal. It happens often and soon after the click, so TikTok can learn from it. The completed visit is the revenue signal, the one that turns ROAS into a real number.

The rename, and what to do about it

TikTok renamed SubmitForm to Lead and CompletePayment to Purchase. According to TikTok:

  • Existing setups keep working. You don't have to rename anything in code.
  • The legacy names are supported indefinitely.
  • New setups should use Lead and Purchase.
  • In Events Manager, your old SubmitForm and CompletePayment events now appear as Lead and Purchase.

Our pipeline was built with the legacy names and still runs fine. For a new build, start with Lead and Purchase. If you migrate an existing one, write down the date you switched, so nobody gets confused reading the reports later.

TikTok also lists Schedule as a standard event. It describes a booking literally, but check which events your campaign objective can optimize toward in Ads Manager before choosing it over Lead.

The request

The Events API takes one endpoint for web, app and offline events:

// Send a booking or completed visit to TikTok's Events API (server-side)
import { sha256Hex, normalizeEmail, toE164 } from './hash.js';

export async function sendTikTok(c, env) {
  const a = c.attribution || {};
  const eventName = c.event_type === 'APPOINTMENT_COMPLETED' ? 'Purchase' : 'Lead';

  const user = {};
  if (c.client.email) user.email = await sha256Hex(normalizeEmail(c.client.email));
  if (c.client.phone) user.phone = await sha256Hex(toE164(c.client.phone));      // "+1..." then hash
  if (c.client.id) user.external_id = await sha256Hex(String(c.client.id).trim().toLowerCase());
  if (a.ttclid) user.ttclid = a.ttclid;                                          // from the landing URL
  if (a.ttp) user.ttp = a.ttp;                                                   // _ttp cookie, if captured
  if (a.user_agent) user.user_agent = a.user_agent;
  if (a.ip) user.ip = a.ip;

  const apptKey = String(c.appointment_id).split(':').pop();
  const body = {
    event_source: 'web',
    event_source_id: env.TIKTOK_PIXEL_ID,
    data: [{
      event: eventName,
      event_time: c.event_time,                       // unix seconds
      event_id: `tt_${apptKey}_${eventName}`,         // unique per appointment + event, stable on retries
      user,
      properties: {
        currency: 'USD',
        value: c.value || 0,
        content_type: 'product',
        content_id: 'salon_appointment',
        order_id: apptKey,
      },
      page: { url: a.landing_url || 'https://YOUR_SITE/' },
    }],
  };
  if (env.TIKTOK_TEST_EVENT_CODE) body.test_event_code = env.TIKTOK_TEST_EVENT_CODE;   // remove after testing

  const res = await fetch('https://business-api.tiktok.com/open_api/v1.3/event/track/', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'Access-Token': env.TIKTOK_ACCESS_TOKEN },
    body: JSON.stringify(body),
  });

  let json; try { json = await res.json(); } catch { json = null; }
  if (!res.ok || !json || json.code !== 0) {                                     // 200 is not enough
    return { ok: false, status: res.status, error: `tiktok code ${json?.code}: ${json?.message}` };
  }
  return { ok: true };
}

The access token is generated in Events Manager, in your pixel's settings. Keep it as a server secret.

HTTP 200 doesn't mean accepted

This is the most common silent failure. TikTok returns HTTP 200 for requests it rejects. The real result is in the body:

  • code: 0 with message: "OK" means the event was accepted.
  • Any other code means it wasn't, and message says why.

If your code only checks res.ok, rejected events look like successes. We log the TikTok result on every booking row in our dashboard, so a column of failures would be obvious. Check code on every call.

Hashing: email, phone and external ID

TikTok matches people using hashed identifiers. Get the normalization wrong and the hash matches nobody, without any error.

  • Email: trim spaces, lowercase, then SHA-256.
  • Phone: E.164 format with the leading + and country code, for example +15551234567, then SHA-256.
  • External ID: your stable customer ID (we use the booking platform's client ID), trimmed and lowercased, then hashed. It helps TikTok connect repeat bookings from the same person.

One trap if you send to several platforms: phone formats differ. For TikTok we hash the number with the +. For Meta we hash digits only, country code included, no +. Use one normalization function per platform, not one for all.

// hash.js
export async function sha256Hex(str) {
  const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(str));
  return [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, '0')).join('');
}
export const normalizeEmail = (e) => String(e).trim().toLowerCase();

// E.164 for TikTok. Assumes US numbers when no country code is present: adjust for other markets.
export function toE164(phone) {
  let d = String(phone).replace(/\D/g, '');
  if (d.length === 10) d = '1' + d;
  return '+' + d;
}

Also send ttclid (from the landing URL), plus the visitor's user agent and IP when you have them. A click ID is the strongest match signal, and TikTok's click ID is only valid for a limited window (7 days, per TikTok's integration docs), so capture it on landing.

event_id: per booking, not per visitor

TikTok deduplicates events that share an event_id. That's what you want for retries, and dangerous if the ID is too broad.

We first reused a visitor-level event ID. That works for a single booking. But if the same person books twice within TikTok's deduplication window, the second booking carries the same ID and gets dropped as a duplicate.

The fix: build the ID from the appointment and the event name, for example tt_{appointmentId}_Lead. It's unique per booking, stable when the webhook retries, and the Lead and Purchase for the same appointment don't collide.

If you also fire the same event from the browser pixel, pass that same ID to ttq.track(…, { event_id }). In our setup, Lead and Purchase come only from the server (the booking happens in an iframe the pixel can't see), so there's no browser twin to match.

Testing without polluting your data

  1. In Events Manager, open Test events and copy the test event code.
  2. Set it as TIKTOK_TEST_EVENT_CODE on your server and make a test booking with ?ttclid=test_… in the URL.
  3. The event should appear in the test tool with code 0.
  4. Remove the test code afterwards. Events sent with it don't count in Ads Manager. Test codes also change, so fetch a fresh one each time you re-test.

"0 conversions on day 1"

Before you blame the tracking, check whether anyone actually booked from a TikTok click. A new campaign often has no bookings on its first day. Run a controlled test booking, confirm it in the test tool, then give the campaign time.

Two diagnostics that look scarier than they are:

  • "Pixel missing vertical funnel events" in Events Manager diagnostics refers to the browser pixel not firing events like ViewContent or AddToCart. It doesn't mean your server events failed. If you want to clear it, fire ViewContent on landing and InitiateCheckout on the Book Now click, with the browser pixel.
  • Low match quality usually means no ttclid, no email and no phone on the event. Check your normalization first.

Common errors

  • HTTP 200, nothing in Ads Manager: code isn't 0. Log and read message.
  • Events only visible in the test tool: the test event code is still set.
  • Second booking from the same person missing: a visitor-level event_id. Make it per appointment and event.
  • Good click volume, poor match rate: phone hashed without + or country code, email not lowercased, or no ttclid captured.
  • Legacy event names in a new build: use Lead and Purchase instead of SubmitForm and CompletePayment.

Testing checklist

  • Landing page captures ttclid (and _ttp if you can).
  • A test booking appears in the test events tool with code 0.
  • Your code treats any code ≠ 0 as a failure.
  • The event_id is unique per appointment and event.
  • Phone is E.164 with + before hashing, email trimmed and lowercased.
  • A completed visit sends Purchase with the real value.
  • The test event code is removed before going live.

Related reading: why our "match by IP" attribution failed (it once made TikTok look like it under-performed), and Google Ads offline click conversions: gbraid errors and silent partial failures.

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

Yes. TikTok renamed SubmitForm to Lead and CompletePayment to Purchase. Existing setups keep working and the old names are supported indefinitely, but TikTok says new setups should use Lead and Purchase.

TikTok reports the real result in the response body. A code of 0 means OK. Any other code means the event was rejected, even though the HTTP status is 200.

Normalize to E.164 with the country code and the leading plus sign, for example +15551234567, then SHA-256 hash it. Email should be trimmed and lowercased before hashing.

Yes. Make it unique per appointment and event, and stable across retries. If you reuse a visitor-level ID, a second booking by the same person within TikTok's deduplication window can be dropped as a duplicate.

Often because nobody booked from a TikTok click yet. Confirm with a controlled test booking and the test events tool before you assume tracking is broken.

Free ad audit

Need this set up
for your salon?