Creative Library
Standalone ad creatives and the ad account image library
Creative Library
An ad creative on Meta is a first-class object that outlives the ad using it. The endpoints here manage those creatives directly, decoupled from any ad, plus the ad account's image library that feeds them.
Any creative id from this library is reusable: pass it as existingCreativeId on POST /v1/ads/create or POST /v1/ads/preview.
List creatives
GET /v1/ads/creatives returns the ad account's /adcreatives rows verbatim: id, name, status, object type, thumbnail, and the story or asset-feed spec.
const { data } = await zernio.adcreatives.listAdCreatives({
query: { accountId: 'ACCOUNT_ID', adAccountId: 'act_1234567890', limit: 50 },
});
// data.paging?.after -> pass back as `after`data = client.ad_creatives.list_ad_creatives(
account_id="ACCOUNT_ID",
ad_account_id="act_1234567890",
limit=50,
)curl "https://zernio.com/api/v1/ads/creatives?accountId=ACCOUNT_ID&adAccountId=act_1234567890&limit=50" \
-H "Authorization: Bearer YOUR_API_KEY"fields overrides the default projection, limit and after page.
Get one creative
GET /v1/ads/creatives/{creativeId}?accountId=... returns a single creative, verbatim.
A Meta-ism handled for you. The creative node rejects created_time even though the /adcreatives edge happily returns it. Zernio adjusts the projection per endpoint so the same fields value works on both.
Create a creative
POST /v1/ads/creatives with headline, body, linkUrl and exactly one of:
| Media field | Source |
|---|---|
imageUrl | A public URL, uploaded to the ad account server-side |
imageHash | A hash from POST /v1/ads/images or the image library list |
carouselCards | 2-10 cards, same shape as carousel ads |
The Page (and the linked Instagram account, when there is one) resolves from accountId, so you don't pass a page id.
const { data } = await zernio.adcreatives.createAdCreative({
body: {
accountId: 'ACCOUNT_ID',
adAccountId: 'act_1234567890',
name: 'Spring hero, v3',
headline: 'Spring drop is live',
body: 'Three colourways, limited run.',
linkUrl: 'https://example.com/spring',
imageHash: '2f1c8e0a9b7d6c5e4f3a2b1c0d9e8f7a',
creativeFeatures: { enhance_cta: 'OPT_IN' },
},
});
// data.creativeId -> reusable as existingCreativeIddata = client.ad_creatives.create_ad_creative(
account_id="ACCOUNT_ID",
ad_account_id="act_1234567890",
name="Spring hero, v3",
headline="Spring drop is live",
body="Three colourways, limited run.",
link_url="https://example.com/spring",
image_hash="2f1c8e0a9b7d6c5e4f3a2b1c0d9e8f7a",
creative_features={"enhance_cta": "OPT_IN"},
)curl -X POST "https://zernio.com/api/v1/ads/creatives" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"accountId": "ACCOUNT_ID",
"adAccountId": "act_1234567890",
"name": "Spring hero, v3",
"headline": "Spring drop is live",
"body": "Three colourways, limited run.",
"linkUrl": "https://example.com/spring",
"imageHash": "2f1c8e0a9b7d6c5e4f3a2b1c0d9e8f7a",
"creativeFeatures": { "enhance_cta": "OPT_IN" }
}'creativeFeatures is optional, see Advantage+ creative enhancements.
Creative names are unique per ad account, including against deleted creatives. Reusing the name of a creative you deleted last month still collides. Suffix your names (a timestamp or a version counter) if you generate them programmatically.
Rename a creative
PUT /v1/ads/creatives/{creativeId} with { accountId, name }. The name is the only mutable field.
Creatives are immutable on Meta beyond the name. To change copy, media or CTA you create a new creative and swap it onto the ad with PUT /v1/ads/{adId} and a creative object. The old creative keeps its own reporting history, which is exactly what you want when comparing versions.
Renames also have a small read-after-write lag: a GET issued immediately after the rename can briefly return the old name. Re-read after a moment rather than treating it as a failure.
Delete a creative
DELETE /v1/ads/creatives/{creativeId}?accountId=... succeeds only when no ad references the creative. Delete or repoint the ads first.
Image library
GET /v1/ads/images lists the ad account's /adimages rows: hash, a Meta-hosted url, name, dimensions, and status. Any hash is reusable wherever Meta accepts an image hash, including imageHash above.
POST /v1/ads/images uploads an image from base64, for creatives whose media has no public URL:
const { data } = await zernio.adcreatives.uploadAdImage({
body: {
accountId: 'ACCOUNT_ID',
adAccountId: 'act_1234567890',
imageBase64: 'data:image/jpeg;base64,/9j/4AAQSkZJRg...',
filename: 'promo.jpg',
},
});
// data.image.hash -> use as imageHash
// data.image.url -> a Meta-hosted URL that works directly as imageUrl on createdata = client.ad_creatives.upload_ad_image(
account_id="ACCOUNT_ID",
ad_account_id="act_1234567890",
image_base64="data:image/jpeg;base64,/9j/4AAQSkZJRg...",
filename="promo.jpg",
)curl -X POST "https://zernio.com/api/v1/ads/images" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"accountId": "ACCOUNT_ID",
"adAccountId": "act_1234567890",
"imageBase64": "data:image/jpeg;base64,/9j/4AAQSkZJRg...",
"filename": "promo.jpg"
}'Returns { image: { hash, url } }. Max 30 MB decoded.
Both halves of the response are useful. hash goes wherever Meta wants an image hash. url is Meta-hosted and public, so it works directly as imageUrl on the create endpoints, which is the shortest path from "bytes in memory" to "ad running".
Image uploads are content-deduped by Meta, so uploading the same bytes twice returns the same hash instead of filling the library with duplicates.
To group creatives and other objects the way Ads Manager does, read the account's ad labels: see Account & Ops Reads.