Blog · Nextdoor CAPI
Nextdoor CAPI wants event_time_epoch in seconds. pixel_id is deprecated. data_source_id is the pixel.
Nextdoor conversions are a server POST to ads.nextdoor.com, not a pixel_id you copy from a Meta worker. The required clock is event_time_epoch in Unix seconds. The pixel is data_source_id. The first body most stacks will ship is the getting-started sample with the deprecated names still on it.
That paste looks close enough to ship. There is an event name, a clock, a hashed email, a click id. Every required name on the live track schema is different from the Python snippet on getting-started. The host is ads.nextdoor.com, path /v2/api/conversions/track, Authorization Bearer from NAM Ads API. The required object wants event_name, action_source, customer, event_id, event_time_epoch, and data_source_id. The getting-started snippet still posts client_id and event_time and never sets data_source_id.
A Meta-shaped Purchase with pixel_id on that host is how the order never attributes. The request can still 200. The deprecation banner on conversions/track says to remove event_time, client_id, and pixel_id. data_source_id is the pixel now. That is the news, not a new validator feature.
The live sample still posts the deprecated fields
Getting-started is the page people will follow first. Create an advertiser profile in NAM. Generate a token under Business Settings, Ads API. POST JSON with Content-Type application/json. The Python block then sends client_id, event_name, event_time, event_id, action_source, and a customer object with hashed email and phone. The recommended-practices list still says client_id is needed in all calls, and to work with account managers to get it.
The conversions/track page opens with a deprecation notice. event_time, client_id, and pixel_id should be removed from the CAPI implementation. pixel_id is now known as data_source_id. The OpenAPI required array on the same page lists event_name, action_source, customer, event_id, event_time_epoch, and data_source_id. client_id is described as NAM Advertiser ID, aka ad account id. It is not the pixel. Putting the ad account on the event and omitting data_source_id is how two pixels in one account cannot dedupe.
The data-types page repeats the deprecation: do not send event_time, client_id, or pixel_id. It also says event_id is recommended for dedup even while the track schema marks event_id required. Ship the track required list. Treat getting-started as a stale envelope. A 200 from a body that still has client_id is not proof the pixel joined.
Two clocks, one required integer
event_time_epoch is an integer. The track schema says number of seconds since 1970-01-01, no millis. Date.now() is milliseconds. Math.floor(Date.now() / 1000) is the ten-digit value Meta already wanted for event_time. Sending thirteen digits as event_time_epoch is not a Nextdoor millisecond clock. The field description says no millis.
event_time still exists on the OpenAPI object as an ISO 8601 UTC string, example 2022-11-23T03:30:52Z. The property text says require one of event_time or event_time_epoch. The banner and the data-types deprecation list say stop sending event_time. I would not ship both and hope the parser picks the epoch. I would not ship ISO only because the getting-started sample used event_time. The required key is event_time_epoch. Optional event_timezone is a separate string for where the conversion happened. It is not a substitute for the integer clock.
The envelope is not Graph
Meta CAPI posts data[] of events, each with event_name Purchase, event_time in seconds, action_source website, and user_data. Nextdoor posts a single event object. There is no data[] wrapper on conversions/track. event_name on the track page is a closed list in lowercase: purchase, lead, sign_up, add_to_cart, initiate_checkout, page_view, search, view_content, add_to_wishlist, subscribe, then custom_conversion_1 through custom_conversion_10. Purchase is not in that list. The data-types page shows Purchase, Lead, PageView as examples. The enum that can only accept certain input values is the lowercase list on conversions/track. Copying the Meta spelling is a silent miss on the page that enumerates purchase.
action_source values are email, website, app, phone_call, chat, physical_store, system_generated, other. website is the web channel. OpenAI uses web. Snap uses WEB. Copying those spellings is a miss here. action_source_url is required for website events. It must begin with http:// or https:// and should match the verified domain. That is not event_source_url. client_user_agent is required for website events on CAPI. Do not hash it. client_ip_address is a valid IPv4 or IPv6 address. Do not hash it.
customer is a required object. Matching wants hashed email, hashed phone, and/or click_id. Getting-started says at least one of those three. Email is SHA-256 after trim and lowercase. Phone on the track schema is SHA-256 of exactly ten digits, no special characters, no international country code. The data-types page says E.164 before hash. Those two pages disagree. The request schema for /v2/api/conversions/track is the ten-digit rule. Hashing an E.164 value with a plus and country code is how the phone never matches if NAM stored ten digits.
custom.order_value is required for purchase. The format is ISO 4217 currency glued to the numeric amount, up to two decimal places: USD123, USD123.12, EUR432. It is not Meta value 123.12 plus currency USD. order_id is required for offline events. App events need an app object with app_id, and app_tracking_enabled for ATT. Putting app_id inside custom because the example input nested it there is not the same as the app object the schema marks required for app events.
The body people will send first
Getting-started plus a Meta event name. This is not a Nextdoor conversions/track event.
{
"client_id": "5316447586",
"event_name": "Purchase",
"event_time": "2022-11-23T03:30:52Z",
"event_id": "order-123",
"action_source": "website",
"customer": {
"email": "64ec88ca...",
"pixel_id": "5b0b9e9a-156a-11ee-be56-0242ac120002"
}
}
That object is what the getting-started Python block teaches, with Purchase copied from Graph. Posted to ads.nextdoor.com it is the deprecated client_id, the deprecated ISO clock, the wrong event_name case, no event_time_epoch, no data_source_id, no action_source_url, no client_user_agent, and no ndclid. A gateway that only checks JSON parse can still 200. NAM will not treat it as a purchase conversion on the pixel.
The Nextdoor shape is one object with event_name purchase, event_id shared with the pixel, event_time_epoch as ten digits, action_source website, data_source_id as the pixel, action_source_url, customer.email hashed, and customer.click_id when you have ndclid. test_event is a string flag for QA. partner_id is free text for tealium, gtm, zapier, or an agency. It is not an attribution key. delivery_optimization true means the event can train. false means attribution only.
Names that do not translate
- Graph data[] is a single Nextdoor event object. There is no data array on conversions/track.
- event_name Purchase is event_name purchase.
- event_time in seconds is event_time_epoch in seconds. ISO event_time is the other clock, and the deprecation banner says to stop sending it.
- pixel_id is data_source_id. It belongs on the event, not as customer.pixel_id.
- client_id is the NAM advertiser id. It is deprecated. It is not the pixel.
- event_source_url is action_source_url.
- user_data.em is customer.email, SHA-256 after lowercase trim.
- fbclid and _fbc are not ndclid. The query parameter is ndclid. The CAPI field is customer.click_id.
- value plus currency is custom.order_value as USD123.12.
ndclid is the click id, and it is not captured for you
Nextdoor appends ndclid to click-through URLs. The track schema says the ndclid parameter is automatically added to those URLs. The CAPI field is customer.click_id, not ndclid. The data-types page shows a landing URL with ndclid as a query parameter. If the thank-you host is not the landing host, the query string is gone unless you copied the value through the redirect. Server events then POST without click_id. Click-through matching has nothing to join except hashed PII.
Getting-started documents the windows: click attribution is 7 days, view attribution is 1 day. A conversion more than seven days after the click is not that click. Sending ndclid on a purchase from last month does not reopen the window. The same page says to pass events promptly and within one hour. Late batches are a different miss from a wrong field name.
Dedup is event_id plus event_name across pixel and CAPI. Getting-started says if you use conversion pixels, the same events on both pipes share the same event_id. The track description says event_id should be any deterministic id that can identify a unique event for CAPI and pixel tracking. If those disagree, you double-count or you drop one pipe. data_source_id ties the CAPI event to the pixel. It is softly required for multiple pixel implementations. One account, two pixels, one omitted data_source_id is how the wrong dataset gets the purchase.
Auth is a NAM token, not a Graph access token
The token comes from NAM, Ads API, Generate token. It goes on Authorization Bearer. A Meta system user token on ads.nextdoor.com is a 401 that looks like a broken worker. It is not an envelope bug. Restricted data usage is a separate integer on the event: true or 1 means reporting only. restricted_data_usage_country currently accepts 1 for the United States. restricted_data_usage_state is a coded list starting at 1000 for California. Those flags are not match keys.
opt_out on the data-types page is a boolean for this event. It is not a substitute for omitting the event. If the user opted out, do not POST a purchase and hope opt_out strips it from optimization. The conversion API overview is still a server hop: events from the backend, not a third-party pixel that Safari can drop. The dialect is still not Graph.
Please note that the following fields are deprecated and should be removed from your CAPI implementation and any related documentation: event_time, client_id, and pixel_id (now known as data_source_id).
Nextdoor conversions/track
What to do
Keep one internal event: name, UTC instant, order id, hashed email, landing URL, click references. At the edge, emit the vendor envelope. Persist Meta event_id and Nextdoor event_id as the same string if both pixels fire for the same order, and persist data_source_id as its own column. Convert the clock there: floor milliseconds to seconds for event_time_epoch. Capture ndclid on the landing hit and copy it through checkout into customer.click_id.
Do not point a shared CAPI JSON at ads.nextdoor.com. Lint the body you actually POST. The Nextdoor pack is vendor/nextdoor-conversions-api. The Meta pack is vendor/meta-conversions-api. One fixture cannot serve both. Strip test_event in production the same way you strip Meta test_event_code, or the event never trains.
The contract page for the field list is conversions/track. This post is the market fact: Nextdoor CAPI is a seconds epoch and a data_source_id, and the getting-started sample is not that contract. Pixellint is not affiliated with Nextdoor or Meta. Passing a linter means the artifact matches the published envelope. It does not mean NAM attributed the order.
Checklist
- POST to https://ads.nextdoor.com/v2/api/conversions/track with a NAM Ads API Bearer token, not a Graph token.
- Send event_name, action_source, customer, event_id, event_time_epoch, and data_source_id. Use purchase, not Purchase.
- event_time_epoch is Unix seconds, no millis. Do not send Date.now() as the epoch.
- Pass customer.click_id when you have ndclid. Capture it on the landing URL. The server will not invent it.
- Reuse the pixel event_id on CAPI. Set data_source_id to the pixel. Drop pixel_id and client_id.
- Website events need action_source_url and client_user_agent. Hash email and phone. Leave IP and UA in the clear.
- custom.order_value is USD123.12, not a separate value and currency pair.
Sources
Contract pages
The dated argument is above. These pages are the field lists.