Reach & Frequency
Quote, reserve and buy fixed-price reserved Meta campaigns
Reach & Frequency
Reach & Frequency buys reach at a fixed price locked in advance, instead of competing in the auction. You get a predicted reach and a frequency cap before spending anything, and Meta holds the inventory until the campaign runs. It's how brand campaigns with a guaranteed delivery commitment are bought.
Three steps: quote, reserve, buy. No OAuth changes needed, ads_management covers the whole surface.
1. Quote
POST /v1/ads/rf-predictions asks Meta what it can deliver. Nothing is bought, and predictions expire on their own.
Pass exactly one of budgetAmount (Meta predicts the reach you'd get) or reach (Meta predicts the budget you'd need).
const { data } = await zernio.reachandfrequency.createRfPrediction({
body: {
accountId: 'ACCOUNT_ID',
adAccountId: 'act_1234567890',
budgetAmount: 500,
startDate: '2026-08-01T09:00:00Z',
endDate: '2026-08-08T09:00:00Z',
frequencyCap: 2,
targeting: { countries: ['US'], ageMin: 18, ageMax: 65 },
placements: {
publisherPlatforms: ['facebook'],
facebookPositions: ['feed'],
devicePlatforms: ['mobile', 'desktop'],
},
},
});
// data.prediction.id + the estimate and its allowed boundsdata = client.reach_and_frequency.create_rf_prediction(
account_id="ACCOUNT_ID",
ad_account_id="act_1234567890",
budget_amount=500,
start_date="2026-08-01T09:00:00Z",
end_date="2026-08-08T09:00:00Z",
frequency_cap=2,
targeting={"countries": ["US"], "ageMin": 18, "ageMax": 65},
placements={
"publisherPlatforms": ["facebook"],
"facebookPositions": ["feed"],
"devicePlatforms": ["mobile", "desktop"],
},
)curl -X POST "https://zernio.com/api/v1/ads/rf-predictions" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"accountId": "ACCOUNT_ID",
"adAccountId": "act_1234567890",
"budgetAmount": 500,
"startDate": "2026-08-01T09:00:00Z",
"endDate": "2026-08-08T09:00:00Z",
"frequencyCap": 2,
"targeting": { "countries": ["US"], "ageMin": 18, "ageMax": 65 },
"placements": {
"publisherPlatforms": ["facebook"],
"facebookPositions": ["feed"],
"devicePlatforms": ["mobile", "desktop"]
}
}'The response carries the estimate plus the allowed bounds (minimum and maximum budget and reach). Show those bounds in your UI: a reservation outside them is rejected.
GET /v1/ads/rf-predictions/{predictionId} reads a prediction's status at any time, where 1 means ready and 2 means still computing.
Reservation campaigns reject automatic placements (subcode 1885696), so an R&F prediction needs explicit ones. When you omit placements, Zernio defaults to Facebook feed, plus Instagram stream when a linked Instagram professional account resolves. Instagram placements require that linked account (subcode 1885237), so asking for them without one returns a clean 400.
Meta now requires frequencyCap on non-balanced predictions. If a quote comes back rejected for a missing frequency setting, add frequencyCap.
2. Reserve
POST /v1/ads/rf-predictions/{predictionId}/reserve locks the price and the inventory until expiresAt (roughly 14 days out).
Reserving mints a NEW prediction id. The reservation is a different object from the quote. Pass the id returned by /reserve to the create call, not the original prediction id.
DELETE /v1/ads/rf-predictions/{reservedId} releases an unused reservation.
3. Buy
POST /v1/ads/create with buyingType: "RESERVED" and rfPredictionId set to the reserved id.
const { data: reserved } = await zernio.reachandfrequency.reserveRfPrediction({
path: { predictionId: 'PREDICTION_ID' },
body: { accountId: 'ACCOUNT_ID', adAccountId: 'act_1234567890' },
});
const ad = await zernio.adcampaigns.createStandaloneAd({
body: {
accountId: 'ACCOUNT_ID',
adAccountId: 'act_1234567890',
name: 'August brand push',
goal: 'awareness',
buyingType: 'RESERVED',
rfPredictionId: reserved.prediction.id,
headline: 'The new collection',
body: 'Out now.',
imageUrl: 'https://cdn.example.com/brand.jpg',
callToAction: 'LEARN_MORE',
linkUrl: 'https://example.com',
// Same targeting + placements as the prediction
countries: ['US'],
ageMin: 18,
ageMax: 65,
placements: {
publisherPlatforms: ['facebook'],
facebookPositions: ['feed'],
devicePlatforms: ['mobile', 'desktop'],
},
},
});reserved = client.reach_and_frequency.reserve_rf_prediction(
prediction_id="PREDICTION_ID",
account_id="ACCOUNT_ID",
ad_account_id="act_1234567890",
)
ad = client.ad_campaigns.create_standalone_ad(
account_id="ACCOUNT_ID",
ad_account_id="act_1234567890",
name="August brand push",
goal="awareness",
buying_type="RESERVED",
rf_prediction_id=reserved.prediction.id,
headline="The new collection",
body="Out now.",
image_url="https://cdn.example.com/brand.jpg",
call_to_action="LEARN_MORE",
link_url="https://example.com",
countries=["US"],
age_min=18,
age_max=65,
placements={
"publisherPlatforms": ["facebook"],
"facebookPositions": ["feed"],
"devicePlatforms": ["mobile", "desktop"],
},
)curl -X POST "https://zernio.com/api/v1/ads/rf-predictions/PREDICTION_ID/reserve" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "accountId": "ACCOUNT_ID", "adAccountId": "act_1234567890" }'
curl -X POST "https://zernio.com/api/v1/ads/create" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"accountId": "ACCOUNT_ID",
"adAccountId": "act_1234567890",
"name": "August brand push",
"goal": "awareness",
"buyingType": "RESERVED",
"rfPredictionId": "RESERVED_PREDICTION_ID",
"headline": "The new collection",
"body": "Out now.",
"imageUrl": "https://cdn.example.com/brand.jpg",
"callToAction": "LEARN_MORE",
"linkUrl": "https://example.com",
"countries": ["US"],
"ageMin": 18,
"ageMax": 65,
"placements": {
"publisherPlatforms": ["facebook"],
"facebookPositions": ["feed"],
"devicePlatforms": ["mobile", "desktop"]
}
}'Rules for the reserved create:
- Plain single-ad shape only. No
creatives[], noadSetIdattach, and it can't be combined withvalidateOnly. - No budget or bid fields. Omit
budgetAmount,budgetTypeand the bid fields: budget, pricing and the run window all come from the reservation. Zernio persists them as a campaign-level lifetime budget. - Pass the same targeting and placements as the prediction. The reservation was priced against them.
Meta-isms handled for you. Auction-only fields are stripped (budget sharing, subcode 4834012; ad-set bid fields, subcode 1815830). The ad set is pinned to exactly the reserved window. And the first ad is created ACTIVE because Meta refuses to create a paused R&F ad at all (subcode 1487716); delivery is still gated by the campaign and ad-set status plus the future start date, so nothing spends early.