A pre-flight check for Meta (Facebook / Instagram) ads that catches the
silent "no image" delivery failure before you flip a campaign to ACTIVE.
A single Meta ad is delivered across many placements — Feed, Stories, Reels,
and more. With placement asset customization you attach a different image to
each surface (a 1:1 for Feed, a 9:16 for Reels/Stories) via
asset_customization_rules. The trap: if a placement is enabled on the ad set
but no rule matches it — or the matched image is too small for that surface
— Meta does not error. It delivers the ad blank. Budget is spent on an
image-less placement, and you usually find out days later from a bad report.
Ads Manager previews don't reliably surface this, and it is easy to reintroduce
every time you add a placement or swap a creative. meta-ad-preflight turns the
check into a machine gate: it reads the live account through the Marketing API
and exits non-zero if any ad would deliver blank.
For every deliverable ad (status = ACTIVE), it fails (exit 1) on any of:
- Placement coverage — every enabled
(platform, position)on the ad set must match at least oneasset_customization_rulesentry. An uncovered placement delivers with no image. - Image dimensions — each used label's image exists, is
ACTIVE, and meets the minimum for its surface: square 1:1 ≥ 1080×1080, vertical 9:16 ≥ 1080×1920, portrait 4:5 ≥ 1080×1350. Undersized images are dropped on Reels/Stories. An ad with noasset_feed_specat all (single 1:1) is flagged, because vertical surfaces fall back to a letterbox. - Excluded placements — dead / low-quality surfaces such as
instream_video,facebook_reels_overlay,audience_network, andthreadsmust not be enabled. - Auto-optimization opt-out —
adapt_to_placementandadvantage_plus_creativemust beOPT_OUT, so Meta does not re-crop or alter the creative and break the layout.
Only ads whose own status is ACTIVE are inspected — a PAUSED campaign still
holds ACTIVE ads that deliver the moment it is resumed.
git clone https://github.com/highdef-joetsu/meta-ad-preflight.git
cd meta-ad-preflight
pip install -r requirements.txtSet two environment variables (copy .env.example and source it, or export them):
export META_ACCESS_TOKEN=... # a token with ads_read on the account
export META_AD_ACCOUNT_ID=act_123456 # the ad account to inspect
python3 preflight.py # ACTIVE campaigns only (the default gate)
python3 preflight.py <campaign_id> # inspect a single campaign
python3 preflight.py --all # include PAUSED campaignsThe process exits 0 on PASS and 1 on any violation, so you can wire it into a
deploy step or CI job that publishes ads:
python3 preflight.py && echo "safe to activate"== campaign: Spring Sale (120200000000000)
o PASS spring_sale_feed_reels (120210000000000)
x FAIL spring_sale_stories_only (120210000000001)
- *coverage gap: instagram/reels matches no rule -> delivers with no image
- *vertical image too small: 'story_a' 1080x1350 < required 1080x1920 (portrait(4:5)) -> may not render on Reels/Stories
============================================================
ads inspected: 2 / violations: 2
result: FAIL - do not flip ACTIVE until the above are fixed.
The tool pulls each ad's ad-set targeting and creative
(asset_feed_spec / degrees_of_freedom_spec) from the Graph API, expands the
enabled placements and the placements each customization rule covers into
(platform, position) sets, and reports the set difference as coverage gaps. Image
sizes are read from /adimages and classified by aspect ratio. The decision logic
(classify_ratio, expand_placements, excluded_placements, coverage_gaps) is
pure and unit-tested — see tests/.
pip install pytest && python3 -m pytest tests/ -q- Tested against Marketing API
v21.0; override withMETA_API_VERSION. - The excluded-placement and minimum-size policies are opinionated defaults that
suit static-image campaigns; edit
BAD_POSITIONS,BAD_PLATFORMS, and theMIN_*constants for your own standards. - Read-only: it never mutates campaigns. It only reports.
MIT © HIGHDEF INC. — see LICENSE.