Endpoints
GET https://pilotstatus.com.br/v1/referrals
GET https://pilotstatus.com.br/v1/referrals/summary
Both require a number-scoped API key (
ps_*) in the x-api-key header. Tenant-scoped
keys return 403.Campaign names, and when they are null
The response carries the ad’s readable names —adName, adsetName, campaignName and their
ids — beside the raw sourceId.
namesResolvedAt is what makes the three states distinguishable, and they call for different
answers:
sourceId stays the identity even when the names are present. A campaign can be renamed in
Meta at any time, so anything that keys on campaignName loses its own history the first
time the advertiser edits it.The first click on a new ad arrives before its names
The referral row is stored before your webhook fires, sosourceId, sourceUrl, headline
and body are already readable the moment the event reaches you. The names are not: they are
resolved by a separate worker that calls Meta’s Marketing API once per ad, and that call
finishes after the webhook has already left.
Measured in production across 53 ads: the names land 0.7s after the message row at the median,
3.0s at p95, 5.5s at the worst. The spread is Meta’s own latency. So an integration that calls
GET /v1/referrals the instant its webhook arrives is racing that worker, and on the first
click of a brand-new creative it often wins the race — getting the referral back with all five
name fields null and namesResolvedAt: null.
It happens once per ad, ever. Every later click on the same ad reads the cached names and
comes back filled in immediately. That is exactly why the same integration sees names “sometimes”
and not others: the misses are new creatives, the hits are ads that have been clicked before.
Recipe. Treat
namesResolvedAt: null as ask again, never as there are no names: repeat
the same query ~5s later, and once more after ~60s. As soon as namesResolvedAt holds a
timestamp the answer is final — names filled means those are the names, names null means the
partner sharing is missing and retrying will not change it.Only the first message carries the origin
Meta sends the referral on the first message of a conversation and never repeats it. So a row inGET /v1/referrals amounts to one conversation that started from that ad, and
conversations in the summary is the number your ad actually produced.
GET /v1/referrals
Query parameters
string
Restrict to a single ad/post id.
string
ad or post. Anything else returns 400 INVALID_SOURCE_TYPE.string
ISO 8601 datetime. Messages on or after this instant.
string
ISO 8601 datetime. Messages on or before this instant.
integer
default:"1"
Page number (≥ 1).
integer
default:"30"
Results per page (1–100).
The referral object
Example
GET /v1/referrals/summary
Groups the same data by ad, ordered by conversation count descending.
Query parameters
string
ISO 8601 datetime.
string
ISO 8601 datetime.
Counting rule
conversations counts distinct conversations; messages counts messages. Two messages in
the same conversation are 1 conversation and 2 messages — so conversations is the lead count
and messages is the volume.
lastSourceUrl and lastHeadline come from the newest message for that ad: ad copy can be
edited, and the last version Meta sent is the useful one.
The cap is reported, not hidden
The list stops at 200 ads. When there are more,truncated is true and a notice says
so — narrow the window with startDate/endDate instead of paging.
Example
Privacy (PII) mode effect
The ad identity and the ad copy are treated differently on purpose: an ad id identifies a creative, never a person, whilectwaClid is a per-click identifier tied to the individual
and the headline/body are content.
That split is why ad attribution still works on a RELAY_ONLY number instead of returning
nothing.