diff --git a/docs/content/scripts/tiktok-pixel.md b/docs/content/scripts/tiktok-pixel.md
index d35513ec3..49b459ad0 100644
--- a/docs/content/scripts/tiktok-pixel.md
+++ b/docs/content/scripts/tiktok-pixel.md
@@ -70,5 +70,61 @@ function rejectAds() {
See the [TikTok cookie consent docs](https://business-api.tiktok.com/portal/docs?id=1739585600931842) for the full behaviour.
+## Data Residency Region
+
+Enterprises with US data-residency requirements can route the Pixel SDK through `analytics.us.tiktok.com` by setting `region: 'us'` (default `'global'`):
+
+```ts
+useScriptTikTokPixel({
+ id: 'YOUR_PIXEL_ID',
+ region: 'us',
+})
+```
+
+## Server-Side Event Deduplication
+
+For the Pixel + Events API (CAPI) pattern, pass the same `event_id` on both the browser and server sides so TikTok deduplicates the pair:
+
+```vue
+
+```
+
+See [TikTok's event-deduplication guide](https://ads.tiktok.com/help/article/event-deduplication?lang=en) for full rules.
+
+## Test Events Sandbox
+
+Set `test_event_code` on the 4th `track` argument to route an event into TikTok's Test Events panel without affecting production reporting:
+
+```ts
+proxy.ttq('track', 'Purchase', { value: 99 }, { test_event_code: 'TEST12345' })
+```
+
+## Advanced Matching
+
+TikTok requires identify fields (`email`, `phone_number`, `external_id`, `first_name`, `last_name`, `city`, `state`, `country`, `zip_code`) to be SHA-256-hashed lowercase. Raw values are silently ignored by TikTok; in development, Nuxt Scripts logs a warning when an unhashed value is detected:
+
+```ts
+import { sha256 } from 'ohash'
+
+const { proxy } = useScriptTikTokPixel({ id: 'YOUR_PIXEL_ID' })
+proxy.ttq('identify', {
+ email: sha256('user@example.com'.trim().toLowerCase()),
+ phone_number: sha256('+15551234567'),
+})
+```
+
::script-types
::
diff --git a/packages/script/src/registry.ts b/packages/script/src/registry.ts
index fc9b22c96..376e1d7a3 100644
--- a/packages/script/src/registry.ts
+++ b/packages/script/src/registry.ts
@@ -496,11 +496,12 @@ export async function registry(resolve?: (path: string) => Promise): Pro
resolve(options?: TikTokPixelInput) {
if (!options?.id)
return false
- return withQuery('https://analytics.tiktok.com/i18n/pixel/events.js', { sdkid: options.id, lib: 'ttq' })
+ const host = options.region === 'us' ? 'analytics.us.tiktok.com' : 'analytics.tiktok.com'
+ return withQuery(`https://${host}/i18n/pixel/events.js`, { sdkid: options.id, lib: 'ttq' })
},
},
proxy: {
- domains: ['analytics.tiktok.com', 'mon.tiktok.com', 'mcs.tiktok.com'],
+ domains: ['analytics.tiktok.com', 'analytics.us.tiktok.com', 'mon.tiktok.com', 'mcs.tiktok.com'],
privacy: PRIVACY_FULL,
},
partytown: { forwards: ['ttq.track', 'ttq.page', 'ttq.identify', 'ttq.grantConsent', 'ttq.revokeConsent', 'ttq.holdConsent'] },
diff --git a/packages/script/src/runtime/registry/schemas.ts b/packages/script/src/runtime/registry/schemas.ts
index 64b7a5c48..d6be8661d 100644
--- a/packages/script/src/runtime/registry/schemas.ts
+++ b/packages/script/src/runtime/registry/schemas.ts
@@ -1049,6 +1049,12 @@ export const TikTokPixelOptions = object({
* @see https://business-api.tiktok.com/portal/docs?id=1739585600931842
*/
defaultConsent: optional(union([literal('granted'), literal('denied'), literal('hold')])),
+ /**
+ * Data residency region for the Pixel SDK.
+ * - `'global'` (default) -> `analytics.tiktok.com`
+ * - `'us'` -> `analytics.us.tiktok.com` (US enterprise data residency)
+ */
+ region: optional(union([literal('global'), literal('us')])),
})
export const UmamiAnalyticsOptions = object({
diff --git a/packages/script/src/runtime/registry/tiktok-pixel.ts b/packages/script/src/runtime/registry/tiktok-pixel.ts
index 4f97c3836..3b413a641 100644
--- a/packages/script/src/runtime/registry/tiktok-pixel.ts
+++ b/packages/script/src/runtime/registry/tiktok-pixel.ts
@@ -20,6 +20,11 @@ type StandardEvents
| 'CompleteRegistration'
| 'Subscribe'
| 'StartTrial'
+ | 'ApplicationApproval'
+ | 'CustomizeProduct'
+ | 'FindLocation'
+ | 'Schedule'
+ | 'SubmitApplication'
interface EventProperties {
content_id?: string
@@ -30,18 +35,37 @@ interface EventProperties {
value?: number
description?: string
query?: string
+ /** Order/transaction identifier; complements `event_id` for transaction-level dedup. */
+ order_id?: string
[key: string]: any
}
+/**
+ * Advanced matching parameters. TikTok requires SHA-256-hashed values for `email`,
+ * `phone_number`, `external_id`, and the name/address fields to enable matching.
+ * Passing raw values disables matching silently; a dev-mode warning is logged.
+ * @see https://business-api.tiktok.com/portal/docs?id=1739585702922241
+ */
interface IdentifyProperties {
email?: string
phone_number?: string
external_id?: string
+ first_name?: string
+ last_name?: string
+ city?: string
+ state?: string
+ country?: string
+ zip_code?: string
}
interface TrackOptions {
/** Used to deduplicate events sent from both the browser Pixel and the server-side Events API. */
event_id?: string
+ /**
+ * Sandbox test-event identifier. When set, events route to TikTok's Test Events panel
+ * without affecting production reporting.
+ */
+ test_event_code?: string
[key: string]: any
}
@@ -75,6 +99,26 @@ export { TikTokPixelOptions }
export type TikTokPixelInput = RegistryScriptInput
+/** Resolve the Pixel SDK URL for a given data-residency region. */
+export function tiktokPixelSrc(region?: 'global' | 'us'): string {
+ return region === 'us'
+ ? 'https://analytics.us.tiktok.com/i18n/pixel/events.js'
+ : 'https://analytics.tiktok.com/i18n/pixel/events.js'
+}
+
+const SHA256_HEX = /^[a-f0-9]{64}$/i
+
+function warnUnhashedIdentify(props: Record): void {
+ const hashFields = ['email', 'phone_number', 'external_id', 'first_name', 'last_name', 'city', 'state', 'country', 'zip_code']
+ const offenders = hashFields.filter((f) => {
+ const v = props[f]
+ return typeof v === 'string' && v.length > 0 && !SHA256_HEX.test(v)
+ })
+ if (offenders.length) {
+ console.warn(`[nuxt-scripts:tiktokPixel] identify() received unhashed value(s) for ${offenders.join(', ')}. TikTok requires SHA-256 hashing for advanced matching; raw values will be ignored. See https://business-api.tiktok.com/portal/docs?id=1739585702922241`)
+ }
+}
+
export interface TikTokPixelConsent {
/** Call `ttq.grantConsent()`. */
grant: () => void
@@ -87,7 +131,7 @@ export interface TikTokPixelConsent {
export function useScriptTikTokPixel(_options?: TikTokPixelInput): UseScriptContext {
const instance = useRegistryScript('tiktokPixel', options => ({
scriptInput: {
- src: withQuery('https://analytics.tiktok.com/i18n/pixel/events.js', {
+ src: withQuery(tiktokPixelSrc(options?.region), {
sdkid: options?.id,
lib: 'ttq',
}),
@@ -104,6 +148,8 @@ export function useScriptTikTokPixel(_options?: TikTok
: () => {
window.TiktokAnalyticsObject = 'ttq'
const ttq: TikTokPixelApi['ttq'] = window.ttq = function (...params: any[]) {
+ if (import.meta.dev && params[0] === 'identify' && params[1])
+ warnUnhashedIdentify(params[1])
// @ts-expect-error untyped
if (ttq.callMethod) {
// @ts-expect-error untyped
diff --git a/test/types/types.test-d.ts b/test/types/types.test-d.ts
index 1789ffdc3..88f022893 100644
--- a/test/types/types.test-d.ts
+++ b/test/types/types.test-d.ts
@@ -1,7 +1,7 @@
import type { ModuleOptions } from '../../packages/script/src/module'
import type { CrispApi } from '../../packages/script/src/runtime/registry/crisp'
import type { DefaultEventName } from '../../packages/script/src/runtime/registry/google-analytics'
-import type { TikTokPixelApi } from '../../packages/script/src/runtime/registry/tiktok-pixel'
+import type { TikTokPixelApi, useScriptTikTokPixel } from '../../packages/script/src/runtime/registry/tiktok-pixel'
import type { NuxtConfigScriptRegistry, NuxtConfigScriptRegistryEntry, NuxtUseScriptOptions, RegistryScriptInput, ScriptRegistry, UseScriptContext } from '../../packages/script/src/runtime/types'
import { describe, expectTypeOf, it } from 'vitest'
@@ -174,4 +174,10 @@ describe('tiktok pixel ttq', () => {
it('track still accepts arbitrary custom event names', () => {
expectTypeOf().toBeCallableWith('track', 'CustomEvent')
})
+
+ it('proxy.ttq preserves the track overload with event_id', () => {
+ type ProxyTtq = ReturnType['proxy']['ttq']
+ expectTypeOf().toBeCallableWith('track', 'Purchase', { value: 10 }, { event_id: 'abc' })
+ expectTypeOf().toBeCallableWith('track', 'StartTrial')
+ })
})