CAPI · engineers · PMs
OpenAI CAPI is not Meta with a new host
Short answer
The ads conversion POST looks familiar until you count the digits and read the field names. OpenAI Ads uses a different path, a different clock, and a different id than Meta CAPI. A shared worker that only knows event_time will 200 and still miss.
Validate this snippet Open the rulepack
Different path, different envelope
Meta CAPI posts to the Graph API events edge with an access token. The body is data[] of events, each with event_name, event_time, action_source, user_data. OpenAI Ads Conversions API posts to bzr.openai.com/v1/events. The body is events[], each with id, type, and timestamp_ms. The image pixel is yet another path: /v1/sdk/events with pid. Mixing the pixel URL and the CAPI URL is a different miss from mixing Meta and OpenAI JSON.
Pixellint pack vendor/openai-conversions-api is the server hop. vendor/openai is the image tag. They are not aliases. Treat them as two contracts, the same way Meta Pixel and Meta CAPI are two contracts.
timestamp_ms is 13 digits
OpenAI timestamp_ms is Unix milliseconds. Date.now() is already 13 digits. Meta event_time is Unix seconds, 10 digits, so the Meta helper is Math.floor(Date.now() / 1000). Sending that 10-digit value as timestamp_ms is too short. Pixellint flags vendor.openai-conversions-api.body.timestamp_ms.invalid.
The fixture in the pack is the usual miss: type present, timestamp_ms a seconds-looking integer, id omitted. A 200 from a gateway that only checks JSON parse is not an attributed conversion.
{
"events": [
{
"type": "order_created",
"timestamp_ms": 1770000000
}
]
}
id is not event_id, type is not event_name
OpenAI events[].id is required. Meta event_id is how you dedup the pixel against CAPI. Same idea, different key, different rules about when it can be absent. Do not send event_id and expect OpenAI to find it. Do not send id to Meta and expect Graph to treat it as event_id.
type is an OpenAI event type such as order_created. Meta event_name is Purchase, Lead, CompleteRegistration. Mapping tables belong in the adapter. A shared enum named Purchase will be wrong on one of the two APIs. Web events also need source_url. That is not Meta event_source_url copied blindly: the field name and when it is required differ. Pixellint checks the OpenAI shape, not a guessed alias list.
What to do in the worker
Store one UTC instant. Convert at the edge: floor(ms/1000) for Meta event_time, Date.now() for OpenAI timestamp_ms. Name outbound fields after the vendor. Mint events[].id once per OpenAI event and persist it for retries, the same way you persist Meta event_id, as a different column.
Paste a redacted fixture in the playground. Empty Test Events on Meta does not tell you whether OpenAI ingested. Each vendor has its own diagnostics. Schema first, then the vendor UI.
pixellint validate json @openai-event.json --rulepack vendor/openai-conversions-api
Check the artifact
Paste the pixel URL or JSON body into the
playground. Same engine as
pixellint validate. Nothing leaves the tab.