pixellint

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.

Identity still is not a shared schema

Hashing rules from Meta (SHA-256 hex on em and ph, plaintext IP and user-agent) do not automatically apply to every new CAPI. Read the OpenAI field list for the identifiers they accept. Over-hashing a field they want in the clear, or sending raw email because another vendor allowed it, is a privacy incident or a match miss depending on the field.

Pixellint is not affiliated with OpenAI or Meta. The packs encode the published envelopes. They do not merge them. Run pixellint validate json --rulepack vendor/openai-conversions-api on the body you POST to bzr.openai.com, and vendor/meta-conversions-api on the body you POST to Graph. One fixture cannot serve both.

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.