late-sdk 0.0.896 → 0.0.898

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -0
  3. data/docs/AdCampaignsApi.md +12 -8
  4. data/docs/AdCreative.md +6 -0
  5. data/docs/AdCreativesApi.md +2 -2
  6. data/docs/BoostPostRequest.md +5 -3
  7. data/docs/CreateAdCreative201Response.md +5 -1
  8. data/docs/CreateAdCreativeRequest.md +4 -2
  9. data/docs/CreateCallAdRequest.md +3 -1
  10. data/docs/CreateMessagingAdRequest.md +3 -1
  11. data/docs/CreateStandaloneAdRequest.md +5 -3
  12. data/docs/CreateStandaloneAdRequestCreativesInner.md +4 -0
  13. data/docs/CreateStandaloneAdRequestPromotedObject.md +2 -2
  14. data/docs/CtwaAdRequestBody.md +3 -1
  15. data/docs/CtwaAdRequestBodyCreativesInner.md +2 -0
  16. data/docs/MetaPromotion.md +26 -0
  17. data/docs/MetaPromotionStatus.md +15 -0
  18. data/docs/UpdateAdRequestCreative.md +4 -0
  19. data/lib/zernio-sdk/api/ad_campaigns_api.rb +11 -8
  20. data/lib/zernio-sdk/api/ad_creatives_api.rb +4 -4
  21. data/lib/zernio-sdk/models/ad_creative.rb +52 -1
  22. data/lib/zernio-sdk/models/boost_post_request.rb +16 -4
  23. data/lib/zernio-sdk/models/create_ad_creative201_response.rb +44 -4
  24. data/lib/zernio-sdk/models/create_ad_creative_request.rb +11 -2
  25. data/lib/zernio-sdk/models/create_call_ad_request.rb +14 -2
  26. data/lib/zernio-sdk/models/create_messaging_ad_request.rb +14 -2
  27. data/lib/zernio-sdk/models/create_standalone_ad_request.rb +12 -3
  28. data/lib/zernio-sdk/models/create_standalone_ad_request_creatives_inner.rb +23 -1
  29. data/lib/zernio-sdk/models/create_standalone_ad_request_dynamic_creative.rb +1 -1
  30. data/lib/zernio-sdk/models/create_standalone_ad_request_promoted_object.rb +2 -2
  31. data/lib/zernio-sdk/models/ctwa_ad_request_body.rb +14 -2
  32. data/lib/zernio-sdk/models/ctwa_ad_request_body_creatives_inner.rb +35 -1
  33. data/lib/zernio-sdk/models/meta_promotion.rb +275 -0
  34. data/lib/zernio-sdk/models/meta_promotion_status.rb +41 -0
  35. data/lib/zernio-sdk/models/update_ad_request_creative.rb +45 -2
  36. data/lib/zernio-sdk/version.rb +1 -1
  37. data/lib/zernio-sdk.rb +2 -0
  38. data/openapi.yaml +141 -18
  39. data/spec/api/ad_campaigns_api_spec.rb +5 -4
  40. data/spec/api/ad_creatives_api_spec.rb +2 -2
  41. data/spec/models/ad_creative_spec.rb +18 -0
  42. data/spec/models/boost_post_request_spec.rb +10 -0
  43. data/spec/models/create_ad_creative201_response_spec.rb +12 -0
  44. data/spec/models/create_ad_creative_request_spec.rb +6 -0
  45. data/spec/models/create_call_ad_request_spec.rb +10 -0
  46. data/spec/models/create_messaging_ad_request_spec.rb +10 -0
  47. data/spec/models/create_standalone_ad_request_creatives_inner_spec.rb +16 -0
  48. data/spec/models/create_standalone_ad_request_spec.rb +6 -0
  49. data/spec/models/ctwa_ad_request_body_creatives_inner_spec.rb +10 -0
  50. data/spec/models/ctwa_ad_request_body_spec.rb +10 -0
  51. data/spec/models/meta_promotion_spec.rb +64 -0
  52. data/spec/models/meta_promotion_status_spec.rb +30 -0
  53. data/spec/models/update_ad_request_creative_spec.rb +16 -0
  54. data/zernio-sdk-0.0.898.gem +0 -0
  55. metadata +10 -2
  56. data/zernio-sdk-0.0.896.gem +0 -0
@@ -14,8 +14,13 @@ require 'date'
14
14
  require 'time'
15
15
 
16
16
  module Zernio
17
- # Replace or patch the ad's creative. Meta, TikTok, and LinkedIn. - **Meta**: patch-style. Pass any subset: fields you omit are preserved from the live creative, including media (`image_hash`/`video_id` are reused, no re-upload) and `url_tags`. Sending the full set (`headline`, `body`, `callToAction`, `linkUrl`, `imageUrl`) rebuilds the creative from scratch instead. Partial patching reads the live `object_story_spec`, which Meta strips on SHARE / page-post / dark / asset_feed creatives. Those return 422 asking for the full set. A `videoUrl`/`videoId` on an image creative is a type change and also needs the full set. `existingCreativeId` repoints the ad at a creative from GET /v1/ads/creatives and ignores every other field. Meta creatives are immutable, so any change creates a new creative and repoints the ad; the old creative is retained on the ad account for historical reporting. - **TikTok**: patch-style. Pass any subset; `headline` is ignored (TikTok creatives have no headline slot). `body` becomes the in-feed `ad_text`; `linkUrl` becomes `landing_page_url`; `videoUrl` triggers a fresh upload. `description`, `videoId` and `existingCreativeId` are Meta-only and return 400. - **LinkedIn**: requires new media (image via `imageUrl` or video via `videoUrl`); a text-only creative update returns 400. Uploads the media, creates a new inline media creative on the same campaign, and pauses the old creative (best-effort). The old creative is retained for historical reporting. `videoId` and `existingCreativeId` are Meta-only and return 400.
17
+ # Replace or patch the ad's creative. Meta, TikTok, and LinkedIn. - **Meta**: patch-style. Pass any subset: fields you omit are preserved from the live creative, including media (`image_hash`/`video_id` are reused, no re-upload) and `url_tags`. Sending the full set (`headline`, `body`, `callToAction`, `linkUrl`, `imageUrl`) rebuilds the creative from scratch instead. Partial patching reads the live `object_story_spec`, which Meta strips on SHARE / page-post / dark / asset_feed creatives. Those return 422 asking for the full set. A `videoUrl`/`videoId` on an image creative is a type change and also needs the full set. `existingCreativeId` repoints the ad at a creative from GET /v1/ads/creatives and ignores every other field. Meta creatives are immutable, so any change creates a new creative and repoints the ad; the old creative is retained on the ad account for historical reporting. `promotion` and `creativeFeatures` are Meta-only. Omitted settings are preserved from the live creative, including full rebuilds. Send `promotion: null` to remove the explicit offer from the replacement. A supplied creativeFeatures map overrides individual existing keys. - **TikTok**: patch-style. Pass any subset; `headline` is ignored (TikTok creatives have no headline slot). `body` becomes the in-feed `ad_text`; `linkUrl` becomes `landing_page_url`; `videoUrl` triggers a fresh upload. `description`, `videoId` and `existingCreativeId` are Meta-only and return 400. - **LinkedIn**: requires new media (image via `imageUrl` or video via `videoUrl`); a text-only creative update returns 400. Uploads the media, creates a new inline media creative on the same campaign, and pauses the old creative (best-effort). The old creative is retained for historical reporting. `videoId` and `existingCreativeId` are Meta-only and return 400.
18
18
  class UpdateAdRequestCreative < ApiModelBase
19
+ attr_accessor :promotion
20
+
21
+ # Meta Advantage+ creative enhancements. Map snake_case feature names to OPT_IN or OPT_OUT; Meta validates supported keys and unspecified features default to OPT_OUT. auto_promotion_tag is an enhancement; use the separate promotion field for an explicit offer. The deprecated standard_enhancements bundle is rejected by Meta.
22
+ attr_accessor :creative_features
23
+
19
24
  # Meta and LinkedIn (TikTok has no headline slot)
20
25
  attr_accessor :headline
21
26
 
@@ -38,9 +43,33 @@ module Zernio
38
43
  # Meta only. Repoint the ad at an existing library creative (from GET /v1/ads/creatives); all other creative fields are ignored.
39
44
  attr_accessor :existing_creative_id
40
45
 
46
+ class EnumAttributeValidator
47
+ attr_reader :datatype
48
+ attr_reader :allowable_values
49
+
50
+ def initialize(datatype, allowable_values)
51
+ @allowable_values = allowable_values.map do |value|
52
+ case datatype.to_s
53
+ when /Integer/i
54
+ value.to_i
55
+ when /Float/i
56
+ value.to_f
57
+ else
58
+ value
59
+ end
60
+ end
61
+ end
62
+
63
+ def valid?(value)
64
+ !value || allowable_values.include?(value)
65
+ end
66
+ end
67
+
41
68
  # Attribute mapping from ruby-style variable name to JSON key.
42
69
  def self.attribute_map
43
70
  {
71
+ :'promotion' => :'promotion',
72
+ :'creative_features' => :'creativeFeatures',
44
73
  :'headline' => :'headline',
45
74
  :'body' => :'body',
46
75
  :'description' => :'description',
@@ -66,6 +95,8 @@ module Zernio
66
95
  # Attribute type mapping.
67
96
  def self.openapi_types
68
97
  {
98
+ :'promotion' => :'MetaPromotion',
99
+ :'creative_features' => :'Hash<String, String>',
69
100
  :'headline' => :'String',
70
101
  :'body' => :'String',
71
102
  :'description' => :'String',
@@ -100,6 +131,16 @@ module Zernio
100
131
  h[k.to_sym] = v
101
132
  }
102
133
 
134
+ if attributes.key?(:'promotion')
135
+ self.promotion = attributes[:'promotion']
136
+ end
137
+
138
+ if attributes.key?(:'creative_features')
139
+ if (value = attributes[:'creative_features']).is_a?(Hash)
140
+ self.creative_features = value
141
+ end
142
+ end
143
+
103
144
  if attributes.key?(:'headline')
104
145
  self.headline = attributes[:'headline']
105
146
  end
@@ -176,6 +217,8 @@ module Zernio
176
217
  def ==(o)
177
218
  return true if self.equal?(o)
178
219
  self.class == o.class &&
220
+ promotion == o.promotion &&
221
+ creative_features == o.creative_features &&
179
222
  headline == o.headline &&
180
223
  body == o.body &&
181
224
  description == o.description &&
@@ -196,7 +239,7 @@ module Zernio
196
239
  # Calculates hash code according to all attributes.
197
240
  # @return [Integer] Hash code
198
241
  def hash
199
- [headline, body, description, call_to_action, link_url, image_url, video_url, video_id, existing_creative_id].hash
242
+ [promotion, creative_features, headline, body, description, call_to_action, link_url, image_url, video_url, video_id, existing_creative_id].hash
200
243
  end
201
244
 
202
245
  # Builds the object from hash
@@ -11,5 +11,5 @@ Generator version: 7.19.0
11
11
  =end
12
12
 
13
13
  module Zernio
14
- VERSION = '0.0.896'
14
+ VERSION = '0.0.898'
15
15
  end
data/lib/zernio-sdk.rb CHANGED
@@ -1200,6 +1200,8 @@ require 'zernio-sdk/models/media_upload_response'
1200
1200
  require 'zernio-sdk/models/meta_ads_platform_data'
1201
1201
  require 'zernio-sdk/models/meta_lead_form_platform_data'
1202
1202
  require 'zernio-sdk/models/meta_lead_form_platform_data_context_card'
1203
+ require 'zernio-sdk/models/meta_promotion'
1204
+ require 'zernio-sdk/models/meta_promotion_status'
1203
1205
  require 'zernio-sdk/models/money'
1204
1206
  require 'zernio-sdk/models/money_amount'
1205
1207
  require 'zernio-sdk/models/move_account_to_profile200_response'
data/openapi.yaml CHANGED
@@ -886,6 +886,9 @@ components:
886
886
  The route enforces this at the Zod boundary; OpenAPI's
887
887
  `required` cannot express the OR cleanly.
888
888
  properties:
889
+ creativeFeatures:
890
+ $ref: '#/components/schemas/MetaCreativeFeatures'
891
+ description: 'Meta enhancement settings for single or attached ads, and defaults for creatives[]. An item replaces the entire map, including with an empty object.'
889
892
  accountId:
890
893
  type: string
891
894
  minLength: 1
@@ -913,7 +916,7 @@ components:
913
916
  whatsappPhoneNumber:
914
917
  type: string
915
918
  pattern: '^\+[1-9]\d{6,14}$'
916
- description: 'WhatsApp only. Optional E.164 number already paired with the Facebook Page. Omit to let Meta select the paired number. Sent to the creative CTA and, when creating a new ad set, its promoted_object. Attach requests do not change the existing ad set.'
919
+ description: 'WhatsApp only. Optional E.164 number already paired with the Facebook Page. Omit to let Meta select the paired number. Sent to the creative CTA and, when creating a new ad set, its promoted_object. Attach requests do not change the existing ad set. Stored as creative.whatsappPhoneNumber on every created ad.'
917
920
  headline:
918
921
  type: string
919
922
  minLength: 1
@@ -986,6 +989,9 @@ components:
986
989
  type: string
987
990
  pattern: '^\d+_\d+$'
988
991
  description: 'Messaging and CTWA only. Raw Facebook pageId_postId reference, used as object_story_id even with an Instagram account. Mutually exclusive with existingPostId and fresh creative fields.'
992
+ creativeFeatures:
993
+ $ref: '#/components/schemas/MetaCreativeFeatures'
994
+ description: 'Replaces the top-level creativeFeatures map for this item. Omit to inherit; an empty object clears inherited enrollment choices.'
989
995
  headline:
990
996
  type: string
991
997
  minLength: 1
@@ -8999,6 +9005,36 @@ components:
8999
9005
  jobFunctions: { type: array, items: { type: string }, description: "LinkedIn B2B only." }
9000
9006
  audienceInclude: { type: array, items: { type: string }, description: 'Platform audience IDs to include, as returned by GET /v1/ads/audiences (Meta custom audience ids, TikTok audience ids, Pinterest customer list ids, LinkedIn segment ids (the platformAudienceId from GET /v1/ads/audiences; Zernio resolves it to the targetable LinkedIn ad segment, an unknown id returns 400), Google user list ids, X custom audience ids). Not supported on OpenAI (400).' }
9001
9007
  audienceExclude: { type: array, items: { type: string }, description: 'Platform audience IDs to exclude; same ID formats as audienceInclude. Not supported on OpenAI (400).' }
9008
+ MetaCreativeFeatures:
9009
+ type: object
9010
+ additionalProperties: { type: string, enum: [OPT_IN, OPT_OUT] }
9011
+ propertyNames: { pattern: '^[a-z0-9_]+$' }
9012
+ description: 'Meta Advantage+ creative enhancements. Map snake_case feature names to OPT_IN or OPT_OUT; Meta validates supported keys and unspecified features default to OPT_OUT. auto_promotion_tag is an enhancement; use the separate promotion field for an explicit offer. The deprecated standard_enhancements bundle is rejected by Meta.'
9013
+ example: { auto_promotion_tag: OPT_IN }
9014
+ MetaPromotion:
9015
+ type: [object, "null"]
9016
+ description: 'Meta explicit Promotion offer. Maps to creative_sourcing_spec.promotion_metadata_spec with promotion_source ADVERTISER_INPUT. Dates become Unix seconds. Send null to omit an explicit offer on a new creative or remove it when rebuilding. Creation success alone does not confirm application: inspect promotionStatus in the response.'
9017
+ required: [type, value]
9018
+ properties:
9019
+ type:
9020
+ type: string
9021
+ enum: [AMOUNT_OFF, FREE_RETURN, FREE_SHIPPING, PERCENTAGE_OFF, PROMO_CODE]
9022
+ description: 'Promotion type accepted by Meta. PERCENTAGE_OFF values cannot exceed 100.'
9023
+ value: { type: number, minimum: 0, description: 'Nonnegative promotion value passed to Meta unchanged. AMOUNT_OFF units are not confirmed, including major versus minor currency units. For PERCENTAGE_OFF this is the percentage discount, at most 100.' }
9024
+ code: { type: string, minLength: 1, description: 'Optional promotion code.' }
9025
+ startDate: { type: string, format: date-time, description: 'Optional ISO 8601 start timestamp with a timezone offset or Z.' }
9026
+ endDate: { type: string, format: date-time, description: 'Optional ISO 8601 end timestamp with a timezone offset or Z. Must be after startDate when both are set.' }
9027
+ example:
9028
+ type: PERCENTAGE_OFF
9029
+ value: 20
9030
+ code: SAVE20
9031
+ startDate: '2026-10-01T00:00:00Z'
9032
+ endDate: '2026-10-31T23:59:59Z'
9033
+ MetaPromotionStatus:
9034
+ type: string
9035
+ enum: [applied, not_returned, unavailable]
9036
+ description: 'Meta creative readback result. applied means Meta returned promotion metadata; not_returned means the read succeeded without promotion metadata; unavailable means the read failed. Only applied confirms the returned offer. Missing metadata is not proof that Ads Manager displays the requested Promotion.'
9037
+ example: not_returned
9002
9038
  Ad:
9003
9039
  type: object
9004
9040
  properties:
@@ -9159,6 +9195,10 @@ components:
9159
9195
  imageUrl: { type: string, description: Alternative image URL }
9160
9196
  videoId: { type: [string, "null"], description: "Meta video ID for VIDEO-type ads. Null for non-video ads. Callers that need an embeddable MP4 can call GET /{videoId}?fields=source with the page access token." }
9161
9197
  videoUrl: { type: [string, "null"], description: "Public Facebook watch URL for VIDEO-type ads (https://www.facebook.com/watch/?v={videoId}). Null for non-video ads." }
9198
+ promotion:
9199
+ $ref: '#/components/schemas/MetaPromotion'
9200
+ description: 'Meta offer read from the live creative on creation or GET /v1/ads/{adId}. Null when metadata is not returned or cannot be read. Requested values are never echoed as applied.'
9201
+ promotionStatus: { $ref: '#/components/schemas/MetaPromotionStatus' }
9162
9202
  creativeId: { type: [string, "null"], description: "Meta ad creative id backing this ad. Reusable via existingCreativeId on POST /v1/ads/create." }
9163
9203
  objectType: { type: string, description: "Meta creative object_type (e.g. SHARE, VIDEO, PRIVACY_CHECK_FAIL, POST_DELETED). Use this to render state-aware previews: when Meta moderation strips image/video fields, only thumbnailUrl at 64x64 is available." }
9164
9204
  objectStoryId: { type: [string, "null"], description: "Meta creative `object_story_id` (the SHARE reference). Frequently absent, because Meta omits it for SHARE creatives. Use effectiveObjectStoryId instead." }
@@ -9188,6 +9228,7 @@ components:
9188
9228
  googleHeadline: { type: string, description: Google Ads headline }
9189
9229
  googleDescription: { type: string, description: Google Ads description }
9190
9230
  linkUrl: { type: string, description: Destination URL }
9231
+ whatsappPhoneNumber: { type: string, description: 'Explicit E.164 WhatsApp number supplied when creating a Meta boost or messaging ad. Absent when omitted by the caller or on older records.', example: '+12025550123' }
9191
9232
  pinterestImageUrl: { type: string }
9192
9233
  pinterestTitle: { type: string }
9193
9234
  pinterestDescription: { type: string }
@@ -43789,7 +43830,9 @@ paths:
43789
43830
  Duplicates a single ad via Meta's native `POST /{ad-id}/copies`. The copy is created
43790
43831
  paused. `adSetId` retargets the copy into another ad set; omitted = the source's own ad
43791
43832
  set. Accepts the Zernio ad id or the platform ad id. Sync discovery is triggered
43792
- automatically (`syncAfter: false` to skip).
43833
+ automatically (`syncAfter: false` to skip). Creative settings returned by Meta,
43834
+ including explicit promotion metadata and creativeFeatures, are preserved when the
43835
+ native copy requires a creative rebuild. Metadata Meta does not return cannot be recovered.
43793
43836
  security:
43794
43837
  - bearerAuth: []
43795
43838
  parameters:
@@ -44281,9 +44324,19 @@ paths:
44281
44324
  - the creative's `effective_instagram_media_id` (Instagram side)
44282
44325
 
44283
44326
  Any of the four resolve to the same ad. Caller doesn't need a translation step.
44327
+ By default, creative.promotion and creative.creativeFeatures contain stored requested
44328
+ settings, which do not confirm platform application. With `refreshPromotion=true`,
44329
+ Meta promotion metadata is read live and exposed as `ad.creative.promotion`
44330
+ with `promotionStatus`. Only `applied` confirms an offer; `not_returned` means the
44331
+ creative read succeeded without promotion metadata, and `unavailable` means it failed.
44284
44332
  security:
44285
44333
  - bearerAuth: []
44286
44334
  parameters:
44335
+ - name: refreshPromotion
44336
+ in: query
44337
+ required: false
44338
+ schema: { type: boolean, default: false }
44339
+ description: 'Meta only. Read current promotion metadata from Meta and include promotionStatus. Omit for stored creative settings with no promotion-specific Graph call.'
44287
44340
  - name: adId
44288
44341
  in: path
44289
44342
  required: true
@@ -44299,6 +44352,7 @@ paths:
44299
44352
  type: object
44300
44353
  properties:
44301
44354
  ad: { $ref: '#/components/schemas/Ad' }
44355
+ '400': { $ref: '#/components/responses/BadRequest' }
44302
44356
  '401': { $ref: '#/components/responses/Unauthorized' }
44303
44357
  '404': { $ref: '#/components/responses/NotFound' }
44304
44358
  put:
@@ -44447,6 +44501,10 @@ paths:
44447
44501
  GET /v1/ads/creatives and ignores every other field. Meta creatives are
44448
44502
  immutable, so any change creates a new creative and repoints the ad; the old
44449
44503
  creative is retained on the ad account for historical reporting.
44504
+ `promotion` and `creativeFeatures` are Meta-only. Omitted settings are
44505
+ preserved from the live creative, including full rebuilds. Send
44506
+ `promotion: null` to remove the explicit offer from the replacement.
44507
+ A supplied creativeFeatures map overrides individual existing keys.
44450
44508
  - **TikTok**: patch-style. Pass any subset; `headline` is ignored (TikTok creatives
44451
44509
  have no headline slot). `body` becomes the in-feed `ad_text`; `linkUrl` becomes
44452
44510
  `landing_page_url`; `videoUrl` triggers a fresh upload. `description`, `videoId`
@@ -44457,6 +44515,8 @@ paths:
44457
44515
  The old creative is retained for historical reporting. `videoId` and
44458
44516
  `existingCreativeId` are Meta-only and return 400.
44459
44517
  properties:
44518
+ promotion: { $ref: '#/components/schemas/MetaPromotion' }
44519
+ creativeFeatures: { $ref: '#/components/schemas/MetaCreativeFeatures' }
44460
44520
  headline: { type: string, description: "Meta and LinkedIn (TikTok has no headline slot)" }
44461
44521
  body: { type: string }
44462
44522
  description: { type: string, maxLength: 255, description: "Link description slot (Meta `link_data.description` / `video_data.link_description`, LinkedIn creative description)." }
@@ -44467,6 +44527,10 @@ paths:
44467
44527
  videoId: { type: string, description: "Meta only. Reuse an already-uploaded ad video (from POST /v1/ads/videos or GET /v1/ads/videos) instead of re-uploading via videoUrl." }
44468
44528
  existingCreativeId: { type: string, description: "Meta only. Repoint the ad at an existing library creative (from GET /v1/ads/creatives); all other creative fields are ignored." }
44469
44529
  name: { type: string, maxLength: 255, description: "Rename the ad. Now propagated to Meta (POST /{ad-id}); non-Meta platforms return 501." }
44530
+ example:
44531
+ creative:
44532
+ promotion: null
44533
+ creativeFeatures: { auto_promotion_tag: OPT_OUT }
44470
44534
  responses:
44471
44535
  '200':
44472
44536
  description: Ad updated
@@ -45885,7 +45949,11 @@ paths:
45885
45949
  `existingCreativeId`. Provide exactly one of `imageUrl` (uploaded server-side),
45886
45950
  `imageHash` (from POST /v1/ads/images or the library list), or `carouselCards` (2-10
45887
45951
  hand-built cards). The Page (and linked Instagram account, when present) is resolved
45888
- from `accountId` as the story actor.
45952
+ from `accountId` as the story actor. `promotion` configures an explicit offer separately
45953
+ from Advantage+ `creativeFeatures`. Only when `promotion` is supplied does the response
45954
+ read the creative back from Meta;
45955
+ `promotionStatus: not_returned` means Meta accepted creation but omitted promotion
45956
+ metadata, so the requested offer is not confirmed as applied.
45889
45957
  security:
45890
45958
  - bearerAuth: []
45891
45959
  requestBody:
@@ -45919,14 +45987,23 @@ paths:
45919
45987
  description: { type: string, maxLength: 255 }
45920
45988
  callToAction: { type: string }
45921
45989
  urlTags: { type: string, description: "Appended to every outbound URL (e.g. utm_source=fb)." }
45990
+ promotion: { $ref: '#/components/schemas/MetaPromotion' }
45922
45991
  creativeFeatures:
45923
- type: object
45924
- additionalProperties: { type: string, enum: [OPT_IN, OPT_OUT] }
45925
- description: 'Advantage+ creative enhancements: partial map of Meta creative feature keys (snake_case) to enroll status, forwarded as degrees_of_freedom_spec.creative_features_spec. Unspecified features default to OPT_OUT.'
45992
+ $ref: '#/components/schemas/MetaCreativeFeatures'
45993
+ description: 'Meta only. Applied to each new creative, including standalone and attach shapes. With creatives[], these are defaults; an item replaces the whole feature map, including an empty map. auto_promotion_tag is an enhancement; an explicit offer uses promotion.'
45926
45994
  multiAdvertiser:
45927
45995
  type: string
45928
45996
  enum: [OPT_IN, OPT_OUT]
45929
45997
  description: "Meta only. Multi-advertiser ads: whether Meta may show this ad alongside other advertisers' in one unit. Meta auto-enrols since Aug 2024, so send OPT_OUT to leave. It is a top-level creative field, NOT a `creativeFeatures` key, and Meta rejects it there."
45998
+ example:
45999
+ accountId: '69fc524892b3d8e85f893e73'
46000
+ adAccountId: act_123456789
46001
+ headline: Save on your next order
46002
+ body: Use SAVE20 at checkout.
46003
+ linkUrl: https://example.com/shop
46004
+ imageUrl: https://example.com/ad.jpg
46005
+ promotion: { type: PERCENTAGE_OFF, value: 20, code: SAVE20 }
46006
+ creativeFeatures: { auto_promotion_tag: OPT_OUT }
45930
46007
  responses:
45931
46008
  '201':
45932
46009
  description: Creative created
@@ -45937,6 +46014,13 @@ paths:
45937
46014
  properties:
45938
46015
  adAccountId: { type: string }
45939
46016
  creativeId: { type: string, description: "Platform creative id, reusable via existingCreativeId." }
46017
+ promotion: { $ref: '#/components/schemas/MetaPromotion' }
46018
+ promotionStatus: { $ref: '#/components/schemas/MetaPromotionStatus' }
46019
+ example:
46020
+ adAccountId: act_123456789
46021
+ creativeId: '123456789012345'
46022
+ promotion: null
46023
+ promotionStatus: not_returned
45940
46024
  '400': { description: "Invalid input, or Meta rejected the create" }
45941
46025
  '401': { $ref: '#/components/responses/Unauthorized' }
45942
46026
  '422': { description: No Facebook Page found to act as the story actor }
@@ -47453,7 +47537,9 @@ paths:
47453
47537
  **Messaging boosts (Meta).** Use `goal: engagement` with
47454
47538
  `callToAction: WHATSAPP_MESSAGE`, `MESSAGE_PAGE`, or `INSTAGRAM_MESSAGE`.
47455
47539
  The CTA implies WHATSAPP, MESSENGER, or INSTAGRAM_DIRECT respectively;
47456
- `destinationType` alone also selects the matching CTA. Omit `linkUrl`.
47540
+ `destinationType` alone does not select a messaging CTA. Omit `linkUrl`
47541
+ only for messaging CTAs. Plain link CTAs keep their goal and link behavior
47542
+ when combined with an independent `destinationType`.
47457
47543
  The campaign uses OUTCOME_ENGAGEMENT and the ad set uses CONVERSATIONS
47458
47544
  with the promoted Page. Optional `whatsappPhoneNumber` selects a number
47459
47545
  already paired with that Page. Conflicting CTA/destination, instant form,
@@ -47483,6 +47569,7 @@ paths:
47483
47569
  type: object
47484
47570
  required: [accountId, adAccountId, name, goal]
47485
47571
  properties:
47572
+ creativeFeatures: { $ref: '#/components/schemas/MetaCreativeFeatures' }
47486
47573
  postId: { type: string, description: Zernio post ID (provide this or platformPostId) }
47487
47574
  platformPostId: { type: string, description: Platform post ID (alternative to postId) }
47488
47575
  accountId: { type: string, description: Account ID }
@@ -47498,8 +47585,8 @@ paths:
47498
47585
  amount: { type: number, description: "Minimum varies: TikTok=$20, Pinterest=$5, others=$1" }
47499
47586
  type: { type: string, enum: [daily, lifetime] }
47500
47587
  instagramAccountId: { type: string, description: "Meta only. Instagram identity the ad runs AS (creative.instagram_user_id), overriding the account linked to the Page. Live-verified against a Page-post creative." }
47501
- destinationType: { type: string, enum: [INSTAGRAM_PROFILE, WEBSITE, ON_AD, MESSENGER, WHATSAPP, INSTAGRAM_DIRECT], description: "Meta only. Ad-set destination_type: where the click LANDS, as opposed to instagramAccountId which is who the ad runs as. Messaging destinations imply their matching CTA and require goal engagement. Lead ads use ON_AD; combining an instant form with a messaging destination is rejected." }
47502
- whatsappPhoneNumber: { type: string, pattern: '^\+[1-9]\d{6,14}$', description: 'Meta WhatsApp only. E.164 number already paired with the Page. Omit to use the default pairing. Requires WHATSAPP destinationType or WHATSAPP_MESSAGE callToAction.' }
47588
+ destinationType: { type: string, enum: [INSTAGRAM_PROFILE, WEBSITE, ON_AD, MESSENGER, WHATSAPP, INSTAGRAM_DIRECT], description: "Meta only. Ad-set destination_type: where the click LANDS, as opposed to instagramAccountId which is who the ad runs as. Independent of plain link CTAs and their goal. A messaging callToAction selects its destination automatically; an explicit destinationType must then match. Lead ads use ON_AD." }
47589
+ whatsappPhoneNumber: { type: string, pattern: '^\+[1-9]\d{6,14}$', description: 'Meta WhatsApp only. E.164 number already paired with the Page. Omit to use the default pairing. Requires WHATSAPP_MESSAGE callToAction. Stored as creative.whatsappPhoneNumber on the ad.' }
47503
47590
  currency: { type: string, minLength: 3, maxLength: 3, example: USD, description: "ISO 4217 currency code matching the ad account's currency. Meta only. Optional: Zernio resolves it from the ad account when omitted. The value selects the minor-unit exponent Zernio converts budget/bid amounts by before calling Meta (most currencies are cents; zero-decimal currencies like JPY/KRW are sent as-is)." }
47504
47591
  schedule:
47505
47592
  type: object
@@ -47734,7 +47821,7 @@ paths:
47734
47821
  status:
47735
47822
  type: string
47736
47823
  enum: [ACTIVE, PAUSED]
47737
- description: 'Meta, TikTok, and LinkedIn. Publish state of the created entities. Omitted or ACTIVE publishes live (default); PAUSED creates them paused so you can review before they spend. On LinkedIn the whole campaign group, campaign, and creative hierarchy stays PAUSED (intendedStatus PAUSED on each).'
47824
+ description: 'Meta, TikTok, and LinkedIn. Publish state of the created entities. Omitted or ACTIVE publishes live (default); PAUSED creates them paused so you can review before they spend. On Meta a new campaign stays paused until explicitly activated; an attached ad is itself paused. On LinkedIn the whole campaign group, campaign, and creative hierarchy stays PAUSED (intendedStatus PAUSED on each).'
47738
47825
  optimizationGoal:
47739
47826
  type: string
47740
47827
  description: |
@@ -47754,6 +47841,7 @@ paths:
47754
47841
  name: WhatsApp post boost
47755
47842
  goal: engagement
47756
47843
  callToAction: WHATSAPP_MESSAGE
47844
+ whatsappPhoneNumber: '+12025550123'
47757
47845
  budget: { amount: 2.61, type: daily }
47758
47846
  status: PAUSED
47759
47847
  responses:
@@ -47801,6 +47889,16 @@ paths:
47801
47889
  - Meta-only multi-creative shape via the creatives array: one ad set with N ads sharing budget and targeting.
47802
47890
  - Attach shape via adSetId: adds one new ad to an existing ad set, inheriting its budget, targeting, and schedule (Meta, Google Ads, TikTok, and LinkedIn). On LinkedIn adSetId is the existing Campaign id, and the budget, schedule, targeting and bidding fields must be omitted.
47803
47891
 
47892
+ Meta accepts `promotion` and `creativeFeatures` on the single and attach shapes and
47893
+ as defaults for `creatives[]`. An item replaces the whole feature map; its `promotion`
47894
+ replaces the default offer, and `promotion: null` disables that default for the item.
47895
+ Reusing `existingCreativeId` uses the existing creative settings instead of new settings.
47896
+ Requested settings are persisted for lists, exports, and default ad-detail reads.
47897
+ Only ads supplied a `promotion` receive live readback; multi-create batches those reads
47898
+ in groups of up to 50 IDs without per-ad fallback. Inspect `ad.creative.promotionStatus` (or
47899
+ `ads[].creative.promotionStatus`). `not_returned` means Meta omitted the metadata;
47900
+ successful creation does not by itself prove the offer was applied or will display.
47901
+
47804
47902
  Per-platform required fields, budget minimums, and video-ad rules are documented on each property below.
47805
47903
 
47806
47904
  LinkedIn creates a Single Image or Single Video Ad backed by a Direct Sponsored Content "dark post" authored by a Company Page (see `organizationId`). Supported goals are engagement, traffic, awareness, and video_views (video ads use the `video` field; video_views requires a video), and traffic ads require `linkUrl`.
@@ -47867,10 +47965,10 @@ paths:
47867
47965
  billingEvent: { type: string, description: "Meta only. Explicit ad-set `billing_event`. Defaults to `IMPRESSIONS`. Forwarded verbatim to Meta, which validates compatibility with the optimization goal." }
47868
47966
  buyingType: { type: string, enum: [AUCTION, RESERVED], description: "Meta only. RESERVED = Reach & Frequency: requires `rfPredictionId` (a RESERVED prediction from /v1/ads/rf-predictions + /reserve). Budget, schedule and pricing come from the reservation, so budgetAmount/budgetType are not required and bid fields are ignored. Only the plain single-ad shape (no creatives[], adSetId, existingCampaignId or dynamicCreative)." }
47869
47967
  rfPredictionId: { type: string, description: "Meta only. The RESERVED prediction id the R&F ad set runs on (reserving mints a new id, so pass that one). Requires buyingType RESERVED." }
47968
+ promotion: { $ref: '#/components/schemas/MetaPromotion' }
47870
47969
  creativeFeatures:
47871
- type: object
47872
- additionalProperties: { type: string, enum: [OPT_IN, OPT_OUT] }
47873
- description: 'Meta only. Advantage+ creative enhancements: a partial map of Meta creative feature keys (snake_case, e.g. enhance_cta, image_brightness_and_contrast, text_optimizations) to enroll status, forwarded as degrees_of_freedom_spec.creative_features_spec. Meta validates the keys; unspecified features default to OPT_OUT. The legacy standard_enhancements bundle is deprecated by Meta and rejected.'
47970
+ $ref: '#/components/schemas/MetaCreativeFeatures'
47971
+ description: 'Meta only. Applied to each new creative, including standalone and attach shapes. With creatives[], these are defaults; an item replaces the whole feature map, including an empty map. auto_promotion_tag is an enhancement; an explicit offer uses promotion.'
47874
47972
  multiAdvertiser:
47875
47973
  type: string
47876
47974
  enum: [OPT_IN, OPT_OUT]
@@ -47966,6 +48064,12 @@ paths:
47966
48064
  required: [headline, body, linkUrl, callToAction]
47967
48065
  description: "Each creative must supply EXACTLY ONE of `imageUrl` (image creative) or `video` (video creative)."
47968
48066
  properties:
48067
+ promotion:
48068
+ $ref: '#/components/schemas/MetaPromotion'
48069
+ description: 'Overrides the top-level offer for this item. Omit to inherit; null disables the inherited offer.'
48070
+ creativeFeatures:
48071
+ $ref: '#/components/schemas/MetaCreativeFeatures'
48072
+ description: 'Replaces the entire top-level creativeFeatures map for this item. Omit to inherit; an empty map clears these defaults.'
47969
48073
  name: { type: string, maxLength: 255, description: "Exact name for this ad. Falls back to `<name> #N` (N = 1-based position)." }
47970
48074
  headline: { type: string, maxLength: 255 }
47971
48075
  body: { type: string }
@@ -47989,7 +48093,8 @@ paths:
47989
48093
  are inherited from the ad set on Meta, and passing `bidStrategy`
47990
48094
  in attach mode returns 400. To change an existing ad set's
47991
48095
  bid, use `PUT /v1/ads/ad-sets/{adSetId}`. Mutually exclusive
47992
- with `creatives[]`.
48096
+ with `creatives[]`. `dynamicCreative` returns 400 in attach mode: create
48097
+ a new dynamic ad set by omitting `adSetId` instead.
47993
48098
 
47994
48099
  The attached ad takes the full single-creative surface:
47995
48100
  `headline`/`body`/`description`/`callToAction` plus either
@@ -48296,7 +48401,10 @@ paths:
48296
48401
  (`imageUrl`, `headline`, `body`, `linkUrl`, `callToAction`) are ignored. Mutually
48297
48402
  exclusive with the `creatives[]` multi-creative shape. Exactly ONE of `imageUrls` /
48298
48403
  `videoUrls` is required (Meta allows one ad format per asset feed; sending both →
48299
- 400). Meta limits: ≤10 images or ≤10 videos, ≤5 bodies / titles / descriptions.
48404
+ 400). Limits remain 10 images or videos and 5 bodies, titles or descriptions.
48405
+ The ad set is created with `is_dynamic_creative: true`. Combining this field
48406
+ with `adSetId` returns 400: omit `adSetId` to create a new dynamic ad set.
48407
+ Multiple headlines go in `titles`; multiple primary texts go in `bodies`.
48300
48408
  properties:
48301
48409
  imageUrls:
48302
48410
  type: array
@@ -48836,10 +48944,10 @@ paths:
48836
48944
  error_subcode 1885014).
48837
48945
  productCatalogId:
48838
48946
  type: string
48839
- description: Catalog ID for catalog/Advantage+ Shopping campaigns.
48947
+ description: "Optional catalog ID. If supplied with productSetId, the set must belong to this catalog. A catalog ID cannot replace productSetId."
48840
48948
  productSetId:
48841
48949
  type: string
48842
- description: Product Set ID inside the catalog.
48950
+ description: "Meta product SET ID from GET /v1/ads/catalogs/{catalogId}/product-sets. Zernio checks that the token can read the set and its product_catalog before creation. A catalog ID or inaccessible set returns a precise 400 naming promotedObject.productSetId. A mismatch with productCatalogId names promotedObject.productCatalogId."
48843
48951
  offlineConversionDataSetId:
48844
48952
  type: string
48845
48953
  description: 'Meta only. Offline event set (dataset) to optimise toward. Post-merger these are datasets: the id is the dataset id (for pixel-backed datasets, the pixel id).'
@@ -48847,6 +48955,21 @@ paths:
48847
48955
  type: string
48848
48956
  description: 'Meta only. WhatsApp number on messaging-destination ad sets.'
48849
48957
  additionalProperties: false
48958
+ example:
48959
+ accountId: '69fc524892b3d8e85f893e73'
48960
+ adAccountId: act_123456789
48961
+ name: Autumn promotion
48962
+ goal: traffic
48963
+ budgetAmount: 5
48964
+ budgetType: daily
48965
+ status: PAUSED
48966
+ headline: Save on your next order
48967
+ body: Use SAVE20 at checkout.
48968
+ callToAction: SHOP_NOW
48969
+ linkUrl: https://example.com/shop
48970
+ imageUrl: https://example.com/ad.jpg
48971
+ promotion: { type: PERCENTAGE_OFF, value: 20, code: SAVE20 }
48972
+ creativeFeatures: { auto_promotion_tag: OPT_OUT }
48850
48973
  responses:
48851
48974
  '200':
48852
48975
  description: 'validateOnly dry-run passed, nothing was created'
@@ -50013,7 +50136,7 @@ paths:
50013
50136
  tags: ["Ad Creatives"]
50014
50137
  x-platforms: ["meta"]
50015
50138
  summary: List a catalog's product sets
50016
- description: "Lists a Meta product catalog's product sets, the unit a catalog ad promotes. Pass the chosen set as `promotedObject.productSetId` on POST /v1/ads/create with `goal: catalog_sales`."
50139
+ description: "Lists a Meta product catalog's product sets, the unit a catalog ad promotes. Pass the chosen set id, not the parent catalog id, as `promotedObject.productSetId` on POST /v1/ads/create with `goal: catalog_sales`. Creation verifies set visibility and returns 400 for a catalog id or an inaccessible set."
50017
50140
  security:
50018
50141
  - bearerAuth: []
50019
50142
  parameters:
@@ -59,7 +59,7 @@ describe 'AdCampaignsApi' do
59
59
 
60
60
  # unit tests for boost_post
61
61
  # Boost post as ad
62
- # Creates a paid ad from an existing published post, keeping the post&#39;s engagement. By default it provisions the whole hierarchy (campaign, ad set, ad). **Attach shape (Meta).** Send &#x60;adSetId&#x60; to put the ad under an EXISTING ad set instead, so that ad set keeps its learning phase. It then owns &#x60;budget&#x60;, &#x60;schedule&#x60; and &#x60;targeting&#x60;, and sending any of those alongside &#x60;adSetId&#x60; is a 400 rather than a silent drop. &#x60;budget&#x60; is required only without &#x60;adSetId&#x60;. &#x60;instagramAccountId&#x60;, &#x60;destinationType&#x60;, &#x60;whatsappPhoneNumber&#x60; and &#x60;adSetId&#x60; are Meta-only and return 400 on other platforms. **Messaging boosts (Meta).** Use &#x60;goal: engagement&#x60; with &#x60;callToAction: WHATSAPP_MESSAGE&#x60;, &#x60;MESSAGE_PAGE&#x60;, or &#x60;INSTAGRAM_MESSAGE&#x60;. The CTA implies WHATSAPP, MESSENGER, or INSTAGRAM_DIRECT respectively; &#x60;destinationType&#x60; alone also selects the matching CTA. Omit &#x60;linkUrl&#x60;. The campaign uses OUTCOME_ENGAGEMENT and the ad set uses CONVERSATIONS with the promoted Page. Optional &#x60;whatsappPhoneNumber&#x60; selects a number already paired with that Page. Conflicting CTA/destination, instant form, goal, or optimizationGoal inputs return 400. Attach requires the target ad set destination to match. Existing post references preserve social proof; an Instagram reel rejected by Meta is not re-uploaded as a new post for a messaging boost. **Retries.** Boosts are NOT idempotent and can take minutes when Meta requires re-hosting an Instagram video, so do not retry on client timeout. Send an Idempotency-Key header to make retries safe: same key and body replays the original 201, and distinct keys always create distinct ads. Without the header, an identical request is treated as a retry: while one is in flight it returns 409, and within 10 minutes of a completed boost it returns the already-created ad instead of creating another. To intentionally duplicate an ad, send distinct Idempotency-Keys (or vary the body, e.g. the name).
62
+ # Creates a paid ad from an existing published post, keeping the post&#39;s engagement. By default it provisions the whole hierarchy (campaign, ad set, ad). **Attach shape (Meta).** Send &#x60;adSetId&#x60; to put the ad under an EXISTING ad set instead, so that ad set keeps its learning phase. It then owns &#x60;budget&#x60;, &#x60;schedule&#x60; and &#x60;targeting&#x60;, and sending any of those alongside &#x60;adSetId&#x60; is a 400 rather than a silent drop. &#x60;budget&#x60; is required only without &#x60;adSetId&#x60;. &#x60;instagramAccountId&#x60;, &#x60;destinationType&#x60;, &#x60;whatsappPhoneNumber&#x60; and &#x60;adSetId&#x60; are Meta-only and return 400 on other platforms. **Messaging boosts (Meta).** Use &#x60;goal: engagement&#x60; with &#x60;callToAction: WHATSAPP_MESSAGE&#x60;, &#x60;MESSAGE_PAGE&#x60;, or &#x60;INSTAGRAM_MESSAGE&#x60;. The CTA implies WHATSAPP, MESSENGER, or INSTAGRAM_DIRECT respectively; &#x60;destinationType&#x60; alone does not select a messaging CTA. Omit &#x60;linkUrl&#x60; only for messaging CTAs. Plain link CTAs keep their goal and link behavior when combined with an independent &#x60;destinationType&#x60;. The campaign uses OUTCOME_ENGAGEMENT and the ad set uses CONVERSATIONS with the promoted Page. Optional &#x60;whatsappPhoneNumber&#x60; selects a number already paired with that Page. Conflicting CTA/destination, instant form, goal, or optimizationGoal inputs return 400. Attach requires the target ad set destination to match. Existing post references preserve social proof; an Instagram reel rejected by Meta is not re-uploaded as a new post for a messaging boost. **Retries.** Boosts are NOT idempotent and can take minutes when Meta requires re-hosting an Instagram video, so do not retry on client timeout. Send an Idempotency-Key header to make retries safe: same key and body replays the original 201, and distinct keys always create distinct ads. Without the header, an identical request is treated as a retry: while one is in flight it returns 409, and within 10 minutes of a completed boost it returns the already-created ad instead of creating another. To intentionally duplicate an ad, send distinct Idempotency-Keys (or vary the body, e.g. the name).
63
63
  # @param boost_post_request
64
64
  # @param [Hash] opts the optional parameters
65
65
  # @option opts [String] :idempotency_key Optional client-generated unique key (e.g. a UUID) that makes retries safe. Same key + same body replays the original response; same key + different body → 422; key still processing → 409.
@@ -122,7 +122,7 @@ describe 'AdCampaignsApi' do
122
122
 
123
123
  # unit tests for create_standalone_ad
124
124
  # Create standalone ad
125
- # Create a paid ad with custom creative across Meta, Google Ads, Pinterest, TikTok, X, LinkedIn, and OpenAI Ads (ChatGPT Ads). Three mutually-exclusive request shapes are selected by the body: - Legacy single-creative shape (all platforms, the default). - Meta-only multi-creative shape via the creatives array: one ad set with N ads sharing budget and targeting. - Attach shape via adSetId: adds one new ad to an existing ad set, inheriting its budget, targeting, and schedule (Meta, Google Ads, TikTok, and LinkedIn). On LinkedIn adSetId is the existing Campaign id, and the budget, schedule, targeting and bidding fields must be omitted. Per-platform required fields, budget minimums, and video-ad rules are documented on each property below. LinkedIn creates a Single Image or Single Video Ad backed by a Direct Sponsored Content \&quot;dark post\&quot; authored by a Company Page (see &#x60;organizationId&#x60;). Supported goals are engagement, traffic, awareness, and video_views (video ads use the &#x60;video&#x60; field; video_views requires a video), and traffic ads require &#x60;linkUrl&#x60;. **Idempotency:** this endpoint is not idempotent at the platform level (a blind retry creates a second campaign/ad set/ad). Send an &#x60;Idempotency-Key&#x60; header to make retries safe: the first request with a given key creates the ad and we store the response; a retry with the same key replays that exact response (with &#x60;Idempotent-Replayed: true&#x60;) instead of creating duplicates. Reusing a key with a different body returns 422; a key whose first request is still in flight returns 409 (retry after a short backoff). Keys are scoped to your credential and expire after 24h.
125
+ # Create a paid ad with custom creative across Meta, Google Ads, Pinterest, TikTok, X, LinkedIn, and OpenAI Ads (ChatGPT Ads). Three mutually-exclusive request shapes are selected by the body: - Legacy single-creative shape (all platforms, the default). - Meta-only multi-creative shape via the creatives array: one ad set with N ads sharing budget and targeting. - Attach shape via adSetId: adds one new ad to an existing ad set, inheriting its budget, targeting, and schedule (Meta, Google Ads, TikTok, and LinkedIn). On LinkedIn adSetId is the existing Campaign id, and the budget, schedule, targeting and bidding fields must be omitted. Meta accepts &#x60;promotion&#x60; and &#x60;creativeFeatures&#x60; on the single and attach shapes and as defaults for &#x60;creatives[]&#x60;. An item replaces the whole feature map; its &#x60;promotion&#x60; replaces the default offer, and &#x60;promotion: null&#x60; disables that default for the item. Reusing &#x60;existingCreativeId&#x60; uses the existing creative settings instead of new settings. Requested settings are persisted for lists, exports, and default ad-detail reads. Only ads supplied a &#x60;promotion&#x60; receive live readback; multi-create batches those reads in groups of up to 50 IDs without per-ad fallback. Inspect &#x60;ad.creative.promotionStatus&#x60; (or &#x60;ads[].creative.promotionStatus&#x60;). &#x60;not_returned&#x60; means Meta omitted the metadata; successful creation does not by itself prove the offer was applied or will display. Per-platform required fields, budget minimums, and video-ad rules are documented on each property below. LinkedIn creates a Single Image or Single Video Ad backed by a Direct Sponsored Content \&quot;dark post\&quot; authored by a Company Page (see &#x60;organizationId&#x60;). Supported goals are engagement, traffic, awareness, and video_views (video ads use the &#x60;video&#x60; field; video_views requires a video), and traffic ads require &#x60;linkUrl&#x60;. **Idempotency:** this endpoint is not idempotent at the platform level (a blind retry creates a second campaign/ad set/ad). Send an &#x60;Idempotency-Key&#x60; header to make retries safe: the first request with a given key creates the ad and we store the response; a retry with the same key replays that exact response (with &#x60;Idempotent-Replayed: true&#x60;) instead of creating duplicates. Reusing a key with a different body returns 422; a key whose first request is still in flight returns 409 (retry after a short backoff). Keys are scoped to your credential and expire after 24h.
126
126
  # @param create_standalone_ad_request
127
127
  # @param [Hash] opts the optional parameters
128
128
  # @option opts [String] :idempotency_key Optional client-generated unique key (e.g. a UUID) that makes retries safe. Same key + same body replays the original response; same key + different body → 422; key still processing → 409.
@@ -172,7 +172,7 @@ describe 'AdCampaignsApi' do
172
172
 
173
173
  # unit tests for duplicate_ad
174
174
  # Duplicate an ad
175
- # Duplicates a single ad via Meta&#39;s native &#x60;POST /{ad-id}/copies&#x60;. The copy is created paused. &#x60;adSetId&#x60; retargets the copy into another ad set; omitted &#x3D; the source&#39;s own ad set. Accepts the Zernio ad id or the platform ad id. Sync discovery is triggered automatically (&#x60;syncAfter: false&#x60; to skip).
175
+ # Duplicates a single ad via Meta&#39;s native &#x60;POST /{ad-id}/copies&#x60;. The copy is created paused. &#x60;adSetId&#x60; retargets the copy into another ad set; omitted &#x3D; the source&#39;s own ad set. Accepts the Zernio ad id or the platform ad id. Sync discovery is triggered automatically (&#x60;syncAfter: false&#x60; to skip). Creative settings returned by Meta, including explicit promotion metadata and creativeFeatures, are preserved when the native copy requires a creative rebuild. Metadata Meta does not return cannot be recovered.
176
176
  # @param ad_id Zernio ad ID or platform ad ID
177
177
  # @param [Hash] opts the optional parameters
178
178
  # @option opts [String] :idempotency_key Optional client-generated unique key (e.g. a UUID) that makes retries safe. Same key + same body replays the original response; same key + different body → 422; key still processing → 409. Only 2xx responses are stored, so a request that failed with a 4xx can be retried with a corrected body under the SAME key.
@@ -214,9 +214,10 @@ describe 'AdCampaignsApi' do
214
214
 
215
215
  # unit tests for get_ad
216
216
  # Get ad details
217
- # Returns an ad with its creative, targeting, status, and performance metrics. The &#x60;{adId}&#x60; path segment accepts any identifier dialect Zernio indexes for the ad: - the Zernio internal &#x60;_id&#x60; (24-char hex) - Meta&#39;s numeric &#x60;platformAdId&#x60; (the value shipped in &#x60;comment.received&#x60; webhooks as &#x60;comment.ad.id&#x60;) - the creative&#39;s &#x60;effective_object_story_id&#x60; (&#x60;{pageId}_{postId}&#x60; shape, Facebook side) - the creative&#39;s &#x60;effective_instagram_media_id&#x60; (Instagram side) Any of the four resolve to the same ad. Caller doesn&#39;t need a translation step.
217
+ # Returns an ad with its creative, targeting, status, and performance metrics. The &#x60;{adId}&#x60; path segment accepts any identifier dialect Zernio indexes for the ad: - the Zernio internal &#x60;_id&#x60; (24-char hex) - Meta&#39;s numeric &#x60;platformAdId&#x60; (the value shipped in &#x60;comment.received&#x60; webhooks as &#x60;comment.ad.id&#x60;) - the creative&#39;s &#x60;effective_object_story_id&#x60; (&#x60;{pageId}_{postId}&#x60; shape, Facebook side) - the creative&#39;s &#x60;effective_instagram_media_id&#x60; (Instagram side) Any of the four resolve to the same ad. Caller doesn&#39;t need a translation step. By default, creative.promotion and creative.creativeFeatures contain stored requested settings, which do not confirm platform application. With &#x60;refreshPromotion&#x3D;true&#x60;, Meta promotion metadata is read live and exposed as &#x60;ad.creative.promotion&#x60; with &#x60;promotionStatus&#x60;. Only &#x60;applied&#x60; confirms an offer; &#x60;not_returned&#x60; means the creative read succeeded without promotion metadata, and &#x60;unavailable&#x60; means it failed.
218
218
  # @param ad_id Zernio &#x60;_id&#x60; (hex), Meta &#x60;platformAdId&#x60; (numeric), or one of the creative&#39;s effective story/media IDs. See description for details.
219
219
  # @param [Hash] opts the optional parameters
220
+ # @option opts [Boolean] :refresh_promotion Meta only. Read current promotion metadata from Meta and include promotionStatus. Omit for stored creative settings with no promotion-specific Graph call.
220
221
  # @return [GetAd200Response]
221
222
  describe 'get_ad test' do
222
223
  it 'should work' do
@@ -34,7 +34,7 @@ describe 'AdCreativesApi' do
34
34
 
35
35
  # unit tests for create_ad_creative
36
36
  # Create a standalone creative
37
- # Creates a creative in the library WITHOUT an ad, reusable on the create endpoints via &#x60;existingCreativeId&#x60;. Provide exactly one of &#x60;imageUrl&#x60; (uploaded server-side), &#x60;imageHash&#x60; (from POST /v1/ads/images or the library list), or &#x60;carouselCards&#x60; (2-10 hand-built cards). The Page (and linked Instagram account, when present) is resolved from &#x60;accountId&#x60; as the story actor.
37
+ # Creates a creative in the library WITHOUT an ad, reusable on the create endpoints via &#x60;existingCreativeId&#x60;. Provide exactly one of &#x60;imageUrl&#x60; (uploaded server-side), &#x60;imageHash&#x60; (from POST /v1/ads/images or the library list), or &#x60;carouselCards&#x60; (2-10 hand-built cards). The Page (and linked Instagram account, when present) is resolved from &#x60;accountId&#x60; as the story actor. &#x60;promotion&#x60; configures an explicit offer separately from Advantage+ &#x60;creativeFeatures&#x60;. Only when &#x60;promotion&#x60; is supplied does the response read the creative back from Meta; &#x60;promotionStatus: not_returned&#x60; means Meta accepted creation but omitted promotion metadata, so the requested offer is not confirmed as applied.
38
38
  # @param create_ad_creative_request
39
39
  # @param [Hash] opts the optional parameters
40
40
  # @return [CreateAdCreative201Response]
@@ -124,7 +124,7 @@ describe 'AdCreativesApi' do
124
124
 
125
125
  # unit tests for list_ad_catalog_product_sets
126
126
  # List a catalog&#39;s product sets
127
- # Lists a Meta product catalog&#39;s product sets, the unit a catalog ad promotes. Pass the chosen set as &#x60;promotedObject.productSetId&#x60; on POST /v1/ads/create with &#x60;goal: catalog_sales&#x60;.
127
+ # Lists a Meta product catalog&#39;s product sets, the unit a catalog ad promotes. Pass the chosen set id, not the parent catalog id, as &#x60;promotedObject.productSetId&#x60; on POST /v1/ads/create with &#x60;goal: catalog_sales&#x60;. Creation verifies set visibility and returns 400 for a catalog id or an inaccessible set.
128
128
  # @param catalog_id Meta product catalog ID (from GET /v1/ads/catalogs)
129
129
  # @param account_id A facebook, instagram, or metaads account ID
130
130
  # @param [Hash] opts the optional parameters
@@ -51,6 +51,18 @@ describe Zernio::AdCreative do
51
51
  end
52
52
  end
53
53
 
54
+ describe 'test attribute "promotion"' do
55
+ it 'should work' do
56
+ # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
57
+ end
58
+ end
59
+
60
+ describe 'test attribute "promotion_status"' do
61
+ it 'should work' do
62
+ # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
63
+ end
64
+ end
65
+
54
66
  describe 'test attribute "creative_id"' do
55
67
  it 'should work' do
56
68
  # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
@@ -141,6 +153,12 @@ describe Zernio::AdCreative do
141
153
  end
142
154
  end
143
155
 
156
+ describe 'test attribute "whatsapp_phone_number"' do
157
+ it 'should work' do
158
+ # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
159
+ end
160
+ end
161
+
144
162
  describe 'test attribute "pinterest_image_url"' do
145
163
  it 'should work' do
146
164
  # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
@@ -27,6 +27,16 @@ describe Zernio::BoostPostRequest do
27
27
  end
28
28
  end
29
29
 
30
+ describe 'test attribute "creative_features"' do
31
+ it 'should work' do
32
+ # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
33
+ # validator = Petstore::EnumTest::EnumAttributeValidator.new('Hash<String, String>', ["OPT_IN", "OPT_OUT"])
34
+ # validator.allowable_values.each do |value|
35
+ # expect { instance.creative_features = value }.not_to raise_error
36
+ # end
37
+ end
38
+ end
39
+
30
40
  describe 'test attribute "post_id"' do
31
41
  it 'should work' do
32
42
  # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
@@ -39,4 +39,16 @@ describe Zernio::CreateAdCreative201Response do
39
39
  end
40
40
  end
41
41
 
42
+ describe 'test attribute "promotion"' do
43
+ it 'should work' do
44
+ # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
45
+ end
46
+ end
47
+
48
+ describe 'test attribute "promotion_status"' do
49
+ it 'should work' do
50
+ # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
51
+ end
52
+ end
53
+
42
54
  end