late-sdk 0.0.855 → 0.0.857

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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +19 -2
  3. data/docs/AdCampaignsApi.md +364 -6
  4. data/docs/{ListAdKeywords200ResponseKeywordsInner.md → AdKeyword.md} +7 -3
  5. data/docs/AdKeywordMetrics.md +30 -0
  6. data/docs/AddAdKeywords201Response.md +18 -0
  7. data/docs/AddAdKeywordsRequest.md +24 -0
  8. data/docs/AddAdKeywordsRequestKeywordsInner.md +20 -0
  9. data/docs/AddAdKeywordsRequestKeywordsInnerAnyOf.md +20 -0
  10. data/docs/ConnectApi.md +2 -2
  11. data/docs/CreatePostRequest.md +6 -6
  12. data/docs/CreateStandaloneAdRequest.md +4 -2
  13. data/docs/CreateTrackingTagRequest.md +3 -1
  14. data/docs/KeywordEntry.md +49 -0
  15. data/docs/ListAdKeywords200Response.md +1 -1
  16. data/docs/ListCampaignNegativeKeywords200Response.md +18 -0
  17. data/docs/ListCampaignNegativeKeywords200ResponseKeywordsInner.md +22 -0
  18. data/docs/PostsApi.md +1 -1
  19. data/docs/RemoveAdKeyword200Response.md +20 -0
  20. data/docs/ReplaceCampaignNegativeKeywords200Response.md +22 -0
  21. data/docs/ReplaceCampaignNegativeKeywordsRequest.md +20 -0
  22. data/docs/TrackingTagsApi.md +1 -1
  23. data/docs/UpdateAdKeyword200Response.md +18 -0
  24. data/docs/{UpdateAdStatusRequest.md → UpdateAdKeywordRequest.md} +2 -2
  25. data/docs/WebhooksApi.md +2 -2
  26. data/lib/zernio-sdk/api/ad_campaigns_api.rb +358 -9
  27. data/lib/zernio-sdk/api/connect_api.rb +2 -2
  28. data/lib/zernio-sdk/api/posts_api.rb +2 -2
  29. data/lib/zernio-sdk/api/tracking_tags_api.rb +2 -2
  30. data/lib/zernio-sdk/api/webhooks_api.rb +4 -4
  31. data/lib/zernio-sdk/models/{list_ad_keywords200_response_keywords_inner.rb → ad_keyword.rb} +28 -8
  32. data/lib/zernio-sdk/models/ad_keyword_metrics.rb +207 -0
  33. data/lib/zernio-sdk/models/add_ad_keywords201_response.rb +149 -0
  34. data/lib/zernio-sdk/models/add_ad_keywords_request.rb +250 -0
  35. data/lib/zernio-sdk/models/add_ad_keywords_request_keywords_inner.rb +103 -0
  36. data/lib/zernio-sdk/models/add_ad_keywords_request_keywords_inner_any_of.rb +225 -0
  37. data/lib/zernio-sdk/models/create_post_request.rb +6 -0
  38. data/lib/zernio-sdk/models/create_standalone_ad_request.rb +55 -5
  39. data/lib/zernio-sdk/models/create_tracking_tag_request.rb +48 -4
  40. data/lib/zernio-sdk/models/keyword_entry.rb +105 -0
  41. data/lib/zernio-sdk/models/list_ad_keywords200_response.rb +1 -1
  42. data/lib/zernio-sdk/models/list_campaign_negative_keywords200_response.rb +149 -0
  43. data/lib/zernio-sdk/models/list_campaign_negative_keywords200_response_keywords_inner.rb +199 -0
  44. data/lib/zernio-sdk/models/remove_ad_keyword200_response.rb +157 -0
  45. data/lib/zernio-sdk/models/replace_campaign_negative_keywords200_response.rb +170 -0
  46. data/lib/zernio-sdk/models/replace_campaign_negative_keywords_request.rb +219 -0
  47. data/lib/zernio-sdk/models/update_ad_keyword200_response.rb +147 -0
  48. data/lib/zernio-sdk/models/{update_ad_status_request.rb → update_ad_keyword_request.rb} +3 -3
  49. data/lib/zernio-sdk/version.rb +1 -1
  50. data/lib/zernio-sdk.rb +14 -2
  51. data/openapi.yaml +299 -32
  52. data/spec/api/ad_campaigns_api_spec.rb +64 -1
  53. data/spec/api/connect_api_spec.rb +1 -1
  54. data/spec/api/posts_api_spec.rb +1 -1
  55. data/spec/api/tracking_tags_api_spec.rb +1 -1
  56. data/spec/api/webhooks_api_spec.rb +2 -2
  57. data/spec/models/ad_keyword_metrics_spec.rb +72 -0
  58. data/spec/models/{list_ad_keywords200_response_keywords_inner_spec.rb → ad_keyword_spec.rb} +18 -6
  59. data/spec/models/add_ad_keywords201_response_spec.rb +36 -0
  60. data/spec/models/add_ad_keywords_request_keywords_inner_any_of_spec.rb +46 -0
  61. data/spec/models/add_ad_keywords_request_keywords_inner_spec.rb +21 -0
  62. data/spec/models/add_ad_keywords_request_spec.rb +54 -0
  63. data/spec/models/create_standalone_ad_request_spec.rb +6 -0
  64. data/spec/models/create_tracking_tag_request_spec.rb +10 -0
  65. data/spec/models/keyword_entry_spec.rb +32 -0
  66. data/spec/models/list_campaign_negative_keywords200_response_keywords_inner_spec.rb +52 -0
  67. data/spec/models/list_campaign_negative_keywords200_response_spec.rb +36 -0
  68. data/spec/models/remove_ad_keyword200_response_spec.rb +42 -0
  69. data/spec/models/replace_campaign_negative_keywords200_response_spec.rb +48 -0
  70. data/spec/models/replace_campaign_negative_keywords_request_spec.rb +46 -0
  71. data/spec/models/update_ad_keyword200_response_spec.rb +36 -0
  72. data/spec/models/{update_ad_status_request_spec.rb → update_ad_keyword_request_spec.rb} +6 -6
  73. data/zernio-sdk-0.0.857.gem +0 -0
  74. metadata +58 -10
  75. data/zernio-sdk-0.0.855.gem +0 -0
data/openapi.yaml CHANGED
@@ -31,7 +31,7 @@ info:
31
31
 
32
32
  Key features: Unified posting to 16 platforms, ads management on 7 ad networks (via /v1/ads), aggregated analytics, unified inbox (DMs, comments, reviews), webhooks, OAuth connect, queue scheduling, and white-label support for agencies managing unlimited accounts.
33
33
 
34
- Supported posting platforms: Twitter/X, Instagram, WhatsApp, Facebook, LinkedIn, TikTok, YouTube, Pinterest, Reddit, Bluesky, Threads, Google Business, Telegram, Snapchat, Discord, Slack. Supported ad platforms: Meta Ads, Google Ads, TikTok Ads, LinkedIn Ads, Pinterest Ads, X Ads, OpenAI Ads.
34
+ Supported posting platforms: Twitter/X, Instagram, WhatsApp, Facebook, LinkedIn, TikTok, YouTube, Pinterest, Reddit, Bluesky, Threads, Google Business, Telegram, Snapchat, Discord, Slack. Supported ad platforms: Meta Ads, Google Ads, TikTok Ads, LinkedIn Ads, Pinterest Ads, X Ads, OpenAI Ads. Snapchat is a closed beta with no public release date: connections are gated behind approval and return 403 `PLATFORM_BETA_RESTRICTED` until then.
35
35
  x-category: Social
36
36
  x-website: https://zernio.com
37
37
  x-thumbnail: https://rapidapi-prod-apis.s3.amazonaws.com/b24d3df5-563c-4a50-9e1e-1ad3eb1fce69.png
@@ -121,6 +121,8 @@ x-documentation:
121
121
  | Telegram | Yes | - | - | - |
122
122
  | Snapchat | Yes | - | - | - |
123
123
 
124
+ > **Snapchat Note:** Snapchat is a closed beta with no public release date. Connecting a Snapchat account is gated behind approval and returns 403 `PLATFORM_BETA_RESTRICTED` until then.
125
+
124
126
  > **LinkedIn Analytics Note:** For personal LinkedIn accounts, analytics are only available for posts published through Zernio. This is a LinkedIn API limitation: the `memberCreatorPostAnalytics` endpoint only returns metrics for posts authored by the authenticated user. Company/organization page analytics are not affected and work for all posts.
125
127
 
126
128
  > **Google Business Analytics Note:** Per-post analytics for Google Business Profile are deprecated by Google with no replacement, so Google Business posts always report `syncStatus: "unavailable"` with an explanatory `errorMessage`. Location-level metrics (impressions, clicks, calls, directions, bookings) are available via the dedicated `/v1/analytics/googlebusiness/performance` endpoint.
@@ -9180,6 +9182,49 @@ components:
9180
9182
  page_id: { type: string }
9181
9183
  earliestAd: { type: string, format: date-time }
9182
9184
  latestAd: { type: string, format: date-time }
9185
+ AdKeyword:
9186
+ type: object
9187
+ properties:
9188
+ id: { type: string }
9189
+ accountId: { type: string, description: Social account ID owning the sync }
9190
+ profileId: { type: string }
9191
+ platform: { type: string, enum: [google] }
9192
+ adAccountId: { type: string, description: Google customer ID }
9193
+ campaignId: { type: string }
9194
+ campaignName: { type: [string, "null"] }
9195
+ campaignStatus: { type: [string, "null"] }
9196
+ adSetId: { type: string, description: Google ad group ID }
9197
+ adSetName: { type: [string, "null"] }
9198
+ adSetStatus: { type: [string, "null"] }
9199
+ keyword: { type: string }
9200
+ matchType: { type: string, enum: [exact, phrase, broad, unknown] }
9201
+ status: { type: string, enum: [active, paused] }
9202
+ negative: { type: boolean }
9203
+ qualityScore: { type: [integer, "null"], description: 'Google Quality Score, 1-10. Null when unrated.' }
9204
+ syncedAt: { type: [string, "null"], format: date-time }
9205
+ metrics:
9206
+ type: [object, "null"]
9207
+ description: 'Trailing 30-day window. Null on rows synced before the metrics columns existed (re-synced on the keyword''s next weekly sweep).'
9208
+ properties:
9209
+ windowDays: { type: integer }
9210
+ clicks: { type: integer }
9211
+ impressions: { type: integer }
9212
+ cost: { type: number, description: 'Account currency, not USD-normalized' }
9213
+ conversions: { type: number }
9214
+ firstPageCpc: { type: [number, "null"], description: Account currency }
9215
+ firstPositionCpc: { type: [number, "null"], description: Account currency }
9216
+ KeywordEntry:
9217
+ description: 'A Google Search keyword: a bare string (BROAD match), or an object naming the match type.'
9218
+ oneOf:
9219
+ - type: string
9220
+ minLength: 1
9221
+ maxLength: 80
9222
+ description: 'Keyword text; defaults to BROAD match'
9223
+ - type: object
9224
+ required: [text]
9225
+ properties:
9226
+ text: { type: string, minLength: 1, maxLength: 80 }
9227
+ matchType: { type: string, enum: [exact, phrase, broad] }
9183
9228
  ConversionEvent:
9184
9229
  type: object
9185
9230
  description: |
@@ -15018,6 +15063,10 @@ paths:
15018
15063
  Create and optionally publish a post. Immediate posts (`publishNow: true`) include `platformPostUrl` in the response.
15019
15064
  Content is optional when media is attached, all platforms have `customContent`, every platform entry is an X Article (`platformSpecificData.article`), or every platform entry is a LinkedIn text-free reshare (`platformSpecificData.reshareUrl` with no text). See each platform's schema for media constraints.
15020
15065
 
15066
+ ## Scheduling
15067
+
15068
+ Pick one of `scheduledFor` (schedule), `publishNow: true` (publish synchronously) or `queuedFromProfile` (next queue slot). With none of them and `isDraft` unset, the post is saved as a draft. `platforms` is required unless the post is a draft. `isDraft: true` wins over `publishNow` and `scheduledFor` (the post is saved, never published); `publishNow: true` wins over `scheduledFor`. A `scheduledFor` already in the past is not rejected: the post is published synchronously in the same request, exactly like `publishNow`.
15069
+
15021
15070
  ## Idempotency
15022
15071
 
15023
15072
  Two layers of duplicate-protection apply, so safe-to-retry callers (network blips, n8n / Zapier retries, etc.) don't accidentally double-post.
@@ -15055,6 +15104,7 @@ paths:
15055
15104
  description: 'Post caption/text. Optional when media is attached, all platforms have customContent, every platform entry is an X Article (platformSpecificData.article), or every platform entry is a LinkedIn text-free reshare (platformSpecificData.reshareUrl with no text). Required for other text-only posts.'
15056
15105
  mediaItems:
15057
15106
  type: array
15107
+ description: 'Media attached to every platform in the request (a platform entry can override it with `customMedia`). Each entry needs a publicly reachable HTTPS `url`; `type` (image, video, gif, document) is inferred from the URL extension when omitted and a `type` that contradicts the extension is rejected with 400. Upload files with `POST /v1/media/presign` first; per-platform size, duration and format limits are listed on each platform schema.'
15058
15108
  items: { $ref: '#/components/schemas/MediaItem' }
15059
15109
  platforms:
15060
15110
  type: array
@@ -15092,13 +15142,22 @@ paths:
15092
15142
  - $ref: '#/components/schemas/BlueskyPlatformData'
15093
15143
  - $ref: '#/components/schemas/DiscordPlatformData'
15094
15144
  - $ref: '#/components/schemas/SlackPlatformData'
15095
- scheduledFor: { type: string, format: date-time }
15096
- publishNow: { type: boolean, default: false }
15145
+ scheduledFor:
15146
+ type: string
15147
+ format: date-time
15148
+ description: 'When to publish. Required unless `publishNow` is true, `queuedFromProfile` is set, or the post is a draft. An ISO 8601 value with a `Z` or offset (`2026-01-15T10:00:00Z`, `2026-01-15T11:00:00+01:00`) is taken as-is; a value without one (`2026-01-15T10:00:00` or `2026-01-15 10:00`) is read as local time in `timezone`. A value already in the past is published synchronously in the same request. Ignored when `publishNow` is true.'
15149
+ publishNow:
15150
+ type: boolean
15151
+ default: false
15152
+ description: 'Publish to every platform synchronously in this request instead of scheduling; the response then carries each platform result and `platformPostUrl`, with HTTP 207 when some platforms failed. Takes precedence over `scheduledFor`; ignored when `isDraft` is true.'
15097
15153
  isDraft:
15098
15154
  type: boolean
15099
15155
  default: false
15100
15156
  description: When true, saves the post as a draft. When none of scheduledFor, publishNow, or queuedFromProfile are provided, the post defaults to draft automatically.
15101
- timezone: { type: string, default: UTC }
15157
+ timezone:
15158
+ type: string
15159
+ default: UTC
15160
+ description: 'IANA timezone (`Europe/Madrid`, `America/New_York`) used to interpret a `scheduledFor` (root or per-platform) that carries no `Z` or offset. Has no effect on values that already carry one. An unknown name returns 400 when `scheduledFor` is set.'
15102
15161
  tags:
15103
15162
  type: array
15104
15163
  description: "Tags/keywords. YouTube constraints: each tag max 100 chars, combined max 500 chars, duplicates auto-removed."
@@ -15111,8 +15170,14 @@ paths:
15111
15170
  type: array
15112
15171
  description: "Stored for reference only. This field does NOT automatically create @mentions when publishing. For LinkedIn @mentions, use the /v1/accounts/{accountId}/linkedin-mentions endpoint to resolve profile URLs to URNs, then embed the returned mentionFormat directly in the post content field."
15113
15172
  items: { type: string }
15114
- crosspostingEnabled: { type: boolean, default: true }
15115
- metadata: { type: object, additionalProperties: true }
15173
+ crosspostingEnabled:
15174
+ type: boolean
15175
+ default: true
15176
+ description: 'Stored on the post and echoed back on reads. Publishing does not branch on it: every entry in `platforms` is published regardless, so treat it as a label for your own tooling.'
15177
+ metadata:
15178
+ type: object
15179
+ additionalProperties: true
15180
+ description: 'Free-form key/value pairs of your own, stored on the post and returned on reads and in webhook payloads. Zernio also writes the bookkeeping keys `usageCounted`, `usageRefunded` and `hidden` into this object; do not set them, and they are stripped from webhook payloads.'
15116
15181
  tiktokSettings:
15117
15182
  $ref: '#/components/schemas/TikTokPlatformData'
15118
15183
  description: 'Root-level TikTok settings applied to the TikTok platforms sent in the same request. Merged into each platform''s platformSpecificData, with platform-specific settings taking precedence.'
@@ -18011,7 +18076,7 @@ paths:
18011
18076
  schema:
18012
18077
  type: string
18013
18078
  enum: [facebook, instagram, linkedin, twitter, tiktok, youtube, threads, reddit, pinterest, bluesky, googlebusiness, telegram, snapchat, discord, slack, whatsapp]
18014
- description: Social media platform to connect
18079
+ description: 'Social media platform to connect. `snapchat` is a closed beta with no public release date: it returns 403 `PLATFORM_BETA_RESTRICTED` until the account is approved.'
18015
18080
  - name: profileId
18016
18081
  in: query
18017
18082
  required: true
@@ -18126,7 +18191,7 @@ paths:
18126
18191
  '401': { $ref: '#/components/responses/Unauthorized' }
18127
18192
  '402': { $ref: '#/components/responses/PaymentRequired' }
18128
18193
  '403':
18129
- description: "No access to profile, or BYOK required for AppSumo Twitter"
18194
+ description: "No access to profile, BYOK required for AppSumo Twitter, or Snapchat closed beta (code PLATFORM_BETA_RESTRICTED)"
18130
18195
  '404':
18131
18196
  description: Profile not found
18132
18197
  post:
@@ -21734,6 +21799,10 @@ paths:
21734
21799
  value:
21735
21800
  error: "Cannot connect to this profile. It exceeds your Pro plan limit of 5 profiles."
21736
21801
  code: "PROFILE_LIMIT_EXCEEDED"
21802
+ betaRestricted:
21803
+ value:
21804
+ error: "Snapchat integration is currently in beta. Please wait until it is publicly released."
21805
+ code: "PLATFORM_BETA_RESTRICTED"
21737
21806
  '500':
21738
21807
  description: Failed to connect Snapchat account
21739
21808
 
@@ -25959,7 +26028,7 @@ paths:
25959
26028
 
25960
26029
  `name`, `url` and `events` are required. `url` must be a valid URL and `events` must contain at least one event. Whitespace is trimmed from `url` before validation.
25961
26030
 
25962
- Webhooks are automatically disabled after 10 consecutive delivery failures.
26031
+ Webhooks are auto-disabled only once the endpoint has had no successful delivery for 3 days AND has either reached 20 consecutive terminal failures (each one an event that exhausted the full retry ladder) or been failing continuously for 3 days. The owner is emailed; re-enable it with `isActive: true`.
25963
26032
 
25964
26033
  A restricted (zrk_) API key can only subscribe to events whose resource group
25965
26034
  the key holds; an event outside the key's groups is rejected with 403, so a
@@ -26071,7 +26140,7 @@ paths:
26071
26140
 
26072
26141
  When provided, `name` must be 1-50 characters, `url` must be a valid URL, and `events` must contain at least one event. Whitespace is trimmed from `url` before validation.
26073
26142
 
26074
- Webhooks are automatically disabled after 10 consecutive delivery failures.
26143
+ Webhooks are auto-disabled only once the endpoint has had no successful delivery for 3 days AND has either reached 20 consecutive terminal failures (each one an event that exhausted the full retry ladder) or been failing continuously for 3 days. The owner is emailed; re-enable it with `isActive: true`.
26075
26144
 
26076
26145
  A restricted (zrk_) API key can only set `events` to events whose resource
26077
26146
  group the key holds; an event outside the key's groups is rejected with 403.
@@ -41282,30 +41351,126 @@ paths:
41282
41351
  properties:
41283
41352
  keywords:
41284
41353
  type: array
41285
- items:
41286
- type: object
41287
- properties:
41288
- id: { type: string }
41289
- accountId: { type: string, description: Social account ID owning the sync }
41290
- profileId: { type: string }
41291
- platform: { type: string, enum: [google] }
41292
- adAccountId: { type: string, description: Google customer ID }
41293
- campaignId: { type: string }
41294
- campaignName: { type: [string, "null"] }
41295
- campaignStatus: { type: [string, "null"] }
41296
- adSetId: { type: string, description: Google ad group ID }
41297
- adSetName: { type: [string, "null"] }
41298
- adSetStatus: { type: [string, "null"] }
41299
- keyword: { type: string }
41300
- matchType: { type: string, enum: [exact, phrase, broad, unknown] }
41301
- status: { type: string, enum: [active, paused] }
41302
- negative: { type: boolean }
41303
- syncedAt: { type: [string, "null"], format: date-time }
41354
+ items: { $ref: '#/components/schemas/AdKeyword' }
41304
41355
  pagination: { $ref: '#/components/schemas/Pagination' }
41305
41356
  '400': { $ref: '#/components/responses/BadRequest' }
41306
41357
  '401': { $ref: '#/components/responses/Unauthorized' }
41307
41358
  '403':
41308
41359
  description: Ads access required. Legacy plans need the Ads add-on; included by default on usage-based plans.
41360
+ post:
41361
+ x-resource-group: "ads"
41362
+ operationId: addAdKeywords
41363
+ tags: ["Ad Campaigns"]
41364
+ x-platforms: ["google"]
41365
+ summary: Add Search keywords to an ad group
41366
+ description: |
41367
+ Adds one or more keyword criteria to an existing Google Search ad group,
41368
+ without touching the keywords already there (unlike the whole-set diff on
41369
+ `PUT /v1/ads/{adId}`, `keywords`/`negativeKeywords` in `platformSpecificData`,
41370
+ which replaces the set). Set `negative: true` to add ad-group-level negatives
41371
+ instead of positive keywords.
41372
+ security:
41373
+ - bearerAuth: []
41374
+ requestBody:
41375
+ required: true
41376
+ content:
41377
+ application/json:
41378
+ schema:
41379
+ type: object
41380
+ required: [accountId, adSetId, keywords]
41381
+ properties:
41382
+ accountId: { type: string, description: Social account ID (Google Ads) }
41383
+ adSetId: { type: string, description: Google ad group ID to add the keywords to }
41384
+ keywords:
41385
+ type: array
41386
+ minItems: 1
41387
+ maxItems: 1000
41388
+ items:
41389
+ anyOf:
41390
+ - type: string
41391
+ description: 'Keyword text; defaults to BROAD match'
41392
+ - type: object
41393
+ required: [text]
41394
+ properties:
41395
+ text: { type: string, minLength: 1, maxLength: 80 }
41396
+ matchType: { type: string, enum: [exact, phrase, broad] }
41397
+ negative: { type: boolean, default: false, description: 'Add as ad-group-level negatives instead of positive keywords' }
41398
+ responses:
41399
+ '201':
41400
+ description: Keywords added
41401
+ content:
41402
+ application/json:
41403
+ schema:
41404
+ type: object
41405
+ properties:
41406
+ keywords:
41407
+ type: array
41408
+ items: { $ref: '#/components/schemas/AdKeyword' }
41409
+ '400': { $ref: '#/components/responses/BadRequest' }
41410
+ '401': { $ref: '#/components/responses/Unauthorized' }
41411
+ '404': { description: 'The ad group ("adSetId") was not found for this account.' }
41412
+ '501': { description: Only available on Google Ads accounts }
41413
+
41414
+ /v1/ads/keywords/{keywordId}:
41415
+ patch:
41416
+ x-resource-group: "ads"
41417
+ operationId: updateAdKeyword
41418
+ tags: ["Ad Campaigns"]
41419
+ x-platforms: ["google"]
41420
+ summary: Pause or enable a Search keyword
41421
+ description: |
41422
+ Changes `ad_group_criterion.status` for one keyword criterion (M.140).
41423
+ Negative keywords have no status on Google and cannot be paused or enabled.
41424
+ security:
41425
+ - bearerAuth: []
41426
+ parameters:
41427
+ - { name: keywordId, in: path, required: true, schema: { type: string }, description: Zernio keyword ID (not the Google criterion ID) }
41428
+ requestBody:
41429
+ required: true
41430
+ content:
41431
+ application/json:
41432
+ schema:
41433
+ type: object
41434
+ required: [status]
41435
+ properties:
41436
+ status: { type: string, enum: [active, paused] }
41437
+ responses:
41438
+ '200':
41439
+ description: Keyword updated
41440
+ content:
41441
+ application/json:
41442
+ schema:
41443
+ type: object
41444
+ properties:
41445
+ keyword: { $ref: '#/components/schemas/AdKeyword' }
41446
+ '400': { $ref: '#/components/responses/BadRequest' }
41447
+ '401': { $ref: '#/components/responses/Unauthorized' }
41448
+ '404': { description: Keyword not found }
41449
+ '422': { description: 'Negative keywords have no status on Google; they cannot be paused or enabled.' }
41450
+ delete:
41451
+ x-resource-group: "ads"
41452
+ operationId: removeAdKeyword
41453
+ tags: ["Ad Campaigns"]
41454
+ x-platforms: ["google"]
41455
+ summary: Remove a Search keyword
41456
+ description: Removes one keyword criterion (positive or negative) from its ad group (M.140).
41457
+ security:
41458
+ - bearerAuth: []
41459
+ parameters:
41460
+ - { name: keywordId, in: path, required: true, schema: { type: string }, description: Zernio keyword ID (not the Google criterion ID) }
41461
+ responses:
41462
+ '200':
41463
+ description: Keyword removed
41464
+ content:
41465
+ application/json:
41466
+ schema:
41467
+ type: object
41468
+ properties:
41469
+ removed: { type: boolean, description: Always true on success }
41470
+ keywordId: { type: string }
41471
+ '400': { $ref: '#/components/responses/BadRequest' }
41472
+ '401': { $ref: '#/components/responses/Unauthorized' }
41473
+ '404': { description: Keyword not found }
41309
41474
 
41310
41475
  /v1/ads/campaigns:
41311
41476
  get:
@@ -41627,6 +41792,102 @@ paths:
41627
41792
  '404': { description: Campaign not found }
41628
41793
  '501': { description: Operation not supported on this platform }
41629
41794
 
41795
+ /v1/ads/campaigns/{campaignId}/negative-keywords:
41796
+ get:
41797
+ x-resource-group: "ads"
41798
+ operationId: listCampaignNegativeKeywords
41799
+ tags: ["Ad Campaigns"]
41800
+ x-platforms: ["google"]
41801
+ summary: List campaign-level negative keywords
41802
+ description: |
41803
+ Returns the campaign-level negative keywords (`campaign_criterion.negative`),
41804
+ distinct from the ad-group-level negatives under `GET /v1/ads/keywords`. Read
41805
+ live from Google on every call (not synced to Postgres), and gated by the
41806
+ shared Google Ads operations budget like every other on-demand Google surface.
41807
+
41808
+ The platform is always discovered from the campaign itself; a non-Google
41809
+ campaign returns 501 rather than 404, whether or not `platform` was passed.
41810
+ security:
41811
+ - bearerAuth: []
41812
+ parameters:
41813
+ - { name: campaignId, in: path, required: true, schema: { type: string }, description: Platform campaign ID }
41814
+ - { name: platform, in: query, schema: { type: string, enum: [facebook, instagram, tiktok, linkedin, pinterest, google, twitter, openai] }, description: 'Optional and NOT authoritative: the resolved campaign''s own platform decides 200 vs 501, never this hint.' }
41815
+ responses:
41816
+ '200':
41817
+ description: Campaign-level negative keywords
41818
+ content:
41819
+ application/json:
41820
+ schema:
41821
+ type: object
41822
+ properties:
41823
+ keywords:
41824
+ type: array
41825
+ items:
41826
+ type: object
41827
+ properties:
41828
+ criterionId: { type: string }
41829
+ text: { type: string }
41830
+ matchType: { type: string, enum: [exact, phrase, broad] }
41831
+ '401': { $ref: '#/components/responses/Unauthorized' }
41832
+ '404': { description: Campaign not found }
41833
+ '429': { description: Google Ads operations budget exhausted; retry later }
41834
+ '501': { description: Only available on Google Ads campaigns }
41835
+ put:
41836
+ x-resource-group: "ads"
41837
+ operationId: replaceCampaignNegativeKeywords
41838
+ tags: ["Ad Campaigns"]
41839
+ x-platforms: ["google"]
41840
+ summary: Replace campaign-level negative keywords
41841
+ description: |
41842
+ Replaces the FULL set of campaign-level negative keywords (C.270): the desired
41843
+ list is diffed against what Google already has, and the difference is applied
41844
+ as one `create`/`remove` mutate. Send an empty array to clear every campaign
41845
+ negative.
41846
+
41847
+ The platform is always discovered from the campaign itself; a non-Google
41848
+ campaign returns 501 rather than 404, whether or not `platform` was sent.
41849
+ security:
41850
+ - bearerAuth: []
41851
+ parameters:
41852
+ - { name: campaignId, in: path, required: true, schema: { type: string }, description: Platform campaign ID }
41853
+ requestBody:
41854
+ required: true
41855
+ content:
41856
+ application/json:
41857
+ schema:
41858
+ type: object
41859
+ required: [keywords]
41860
+ properties:
41861
+ platform: { type: string, enum: [facebook, instagram, tiktok, linkedin, pinterest, google, twitter, openai], description: 'Optional and NOT authoritative: the resolved campaign''s own platform decides 200 vs 501, never this hint.' }
41862
+ keywords:
41863
+ type: array
41864
+ maxItems: 1000
41865
+ items: { $ref: '#/components/schemas/KeywordEntry' }
41866
+ responses:
41867
+ '200':
41868
+ description: Campaign-level negative keywords replaced
41869
+ content:
41870
+ application/json:
41871
+ schema:
41872
+ type: object
41873
+ properties:
41874
+ created: { type: integer, description: Negative criteria newly created on Google }
41875
+ removed: { type: integer, description: Negative criteria removed from Google }
41876
+ keywords:
41877
+ type: array
41878
+ description: The full negative-keyword set after the replace
41879
+ items:
41880
+ type: object
41881
+ properties:
41882
+ criterionId: { type: string }
41883
+ text: { type: string }
41884
+ matchType: { type: string, enum: [exact, phrase, broad] }
41885
+ '400': { $ref: '#/components/responses/BadRequest' }
41886
+ '401': { $ref: '#/components/responses/Unauthorized' }
41887
+ '404': { description: Campaign not found }
41888
+ '429': { description: Google Ads operations budget exhausted; retry later }
41889
+ '501': { description: Only available on Google Ads campaigns }
41890
+
41630
41891
  /v1/ads/campaigns/bulk-status:
41631
41892
  post:
41632
41893
  x-resource-group: "ads"
@@ -45560,8 +45821,9 @@ paths:
45560
45821
  items: { type: string, enum: [mobile, desktop] }
45561
45822
  audienceId: { type: string, description: Custom audience ID for targeting }
45562
45823
  campaignType: { type: string, enum: [display, search], default: display, description: Google only }
45563
- keywords: { type: array, maxItems: 1000, items: { type: string, maxLength: 80 }, description: "Google Search only. BROAD-match keywords on the new ad group. Editable later via PUT /v1/ads/{adId} targeting.keywords, which also sets match types." }
45564
- negativeKeywords: { type: array, items: { type: string, maxLength: 80 }, description: "Google Search only; other platforms return 400. BROAD-match negative keywords on the new ad group. Editable later via PUT /v1/ads/{adId} targeting.negativeKeywords." }
45824
+ keywords: { type: array, maxItems: 1000, items: { $ref: '#/components/schemas/KeywordEntry' }, description: "Google Search only. Keywords on the new ad group; entries are strings (BROAD) or { text, matchType }. Editable later via PUT /v1/ads/{adId} targeting.keywords." }
45825
+ negativeKeywords: { type: array, maxItems: 1000, items: { $ref: '#/components/schemas/KeywordEntry' }, description: "Google Search only; other platforms return 400. Ad-group-level negative keywords on the new ad group. Editable later via PUT /v1/ads/{adId} targeting.negativeKeywords." }
45826
+ campaignNegativeKeywords: { type: array, maxItems: 1000, items: { $ref: '#/components/schemas/KeywordEntry' }, description: "Google Search only; other platforms return 400. Campaign-level negative keywords (campaign_criterion.negative), created alongside the ad group. Editable later via PUT /v1/ads/campaigns/{campaignId}/negative-keywords." }
45565
45827
  additionalHeadlines: { type: array, items: { type: string }, description: "Google Search RSA only. Extra headlines." }
45566
45828
  additionalDescriptions: { type: array, items: { type: string }, description: "Google Search RSA only. Extra descriptions." }
45567
45829
  sitelinks:
@@ -49010,7 +49272,8 @@ paths:
49010
49272
  pixel.
49011
49273
 
49012
49274
  NOT idempotent on either platform: each call creates a new pixel (and,
49013
- for OpenAI, a new Conversions API key). Do not retry blindly on
49275
+ for OpenAI, a new Conversions API key plus, with `defaultEventType`, a
49276
+ new conversion event setting). Do not retry blindly on
49014
49277
  timeout. Meta (platform `metaads`) and OpenAI Ads (platform
49015
49278
  `openaiads`); other platforms return 405.
49016
49279
  security:
@@ -49027,6 +49290,10 @@ paths:
49027
49290
  properties:
49028
49291
  adAccountId: { type: string, description: 'Meta ad account id, e.g. `act_123456789`. Required by this endpoint but ignored for OpenAI Ads.' }
49029
49292
  name: { type: string, minLength: 1, maxLength: 200 }
49293
+ defaultEventType:
49294
+ type: string
49295
+ enum: [order_created, lead_created, items_added, contents_viewed, checkout_started, registration_completed, subscription_created, trial_started, appointment_scheduled, page_viewed, app_installed, app_opened]
49296
+ description: 'OpenAI Ads only (ignored by Meta). When set, also provisions a standard conversion event setting wired to the new pixel, so `goal: conversions` ad creates on `POST /v1/ads/create` have an event to reference immediately.'
49030
49297
  responses:
49031
49298
  '201':
49032
49299
  description: Tracking tag created
@@ -32,6 +32,18 @@ describe 'AdCampaignsApi' do
32
32
  end
33
33
  end
34
34
 
35
+ # unit tests for add_ad_keywords
36
+ # Add Search keywords to an ad group
37
+ # Adds one or more keyword criteria to an existing Google Search ad group, without touching the keywords already there (unlike the whole-set diff on `PUT /v1/ads/{adId}`, `keywords`/`negativeKeywords` in `platformSpecificData`, which replaces the set). Set `negative: true` to add ad-group-level negatives instead of positive keywords.
38
+ # @param add_ad_keywords_request
39
+ # @param [Hash] opts the optional parameters
40
+ # @return [AddAdKeywords201Response]
41
+ describe 'add_ad_keywords test' do
42
+ it 'should work' do
43
+ # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
44
+ end
45
+ end
46
+
35
47
  # unit tests for attach_campaign_assets
36
48
  # Attach extension assets to a Google Search campaign
37
49
  # Attach sitelinks, callouts and/or structured snippets to an already-existing Google Search campaign — the same builders POST /v1/ads/create uses, but without rebuilding the hierarchy. At least one of sitelinks, callouts or structuredSnippets is required. Google-only. Other platforms have no equivalent extension surface and return 501. Approval status is Google-async; poll `asset.policy_summary` after review. Assets stay in the account library even if the campaign is later deleted.
@@ -319,6 +331,44 @@ describe 'AdCampaignsApi' do
319
331
  end
320
332
  end
321
333
 
334
+ # unit tests for list_campaign_negative_keywords
335
+ # List campaign-level negative keywords
336
+ # Returns the campaign-level negative keywords (`campaign_criterion.negative`), distinct from the ad-group-level negatives under `GET /v1/ads/keywords`. Read live from Google on every call (not synced to Postgres), and gated by the shared Google Ads operations budget like every other on-demand Google surface. The platform is always discovered from the campaign itself; a non-Google campaign returns 501 rather than 404, whether or not `platform` was passed.
337
+ # @param campaign_id Platform campaign ID
338
+ # @param [Hash] opts the optional parameters
339
+ # @option opts [String] :platform Optional and NOT authoritative: the resolved campaign's own platform decides 200 vs 501, never this hint.
340
+ # @return [ListCampaignNegativeKeywords200Response]
341
+ describe 'list_campaign_negative_keywords test' do
342
+ it 'should work' do
343
+ # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
344
+ end
345
+ end
346
+
347
+ # unit tests for remove_ad_keyword
348
+ # Remove a Search keyword
349
+ # Removes one keyword criterion (positive or negative) from its ad group (M.140).
350
+ # @param keyword_id Zernio keyword ID (not the Google criterion ID)
351
+ # @param [Hash] opts the optional parameters
352
+ # @return [RemoveAdKeyword200Response]
353
+ describe 'remove_ad_keyword test' do
354
+ it 'should work' do
355
+ # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
356
+ end
357
+ end
358
+
359
+ # unit tests for replace_campaign_negative_keywords
360
+ # Replace campaign-level negative keywords
361
+ # Replaces the FULL set of campaign-level negative keywords (C.270): the desired list is diffed against what Google already has, and the difference is applied as one `create`/`remove` mutate. Send an empty array to clear every campaign negative. The platform is always discovered from the campaign itself; a non-Google campaign returns 501 rather than 404, whether or not `platform` was sent.
362
+ # @param campaign_id Platform campaign ID
363
+ # @param replace_campaign_negative_keywords_request
364
+ # @param [Hash] opts the optional parameters
365
+ # @return [ReplaceCampaignNegativeKeywords200Response]
366
+ describe 'replace_campaign_negative_keywords test' do
367
+ it 'should work' do
368
+ # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
369
+ end
370
+ end
371
+
322
372
  # unit tests for update_ad
323
373
  # Update ad
324
374
  # Patch one or more fields on an ad. Status, budget, targeting, and creative changes are propagated to the platform. Per-platform support: - **Meta** (Facebook + Instagram): all fields supported. - **TikTok**: status, budget, targeting (via `/v2/adgroup/update/`), and creative (via `/v2/ad/update/` patch-style — `headline` is ignored, `body` becomes `ad_text`). - **Google**: status, budget, and KEYWORD edits via `targeting.keywords` / `targeting.negativeKeywords` — each list you send becomes the FULL new set of its kind on the ad group (criteria not in the list are removed); a kind left out is untouched. Any other `targeting` field returns 400: Google cannot mutate broad targeting post-create without recreating the campaign. `creative` returns 501. - **LinkedIn**: status, budget, targeting (geo countries only, applied to the LinkedIn Campaign via PARTIAL_UPDATE), and creative (uploads new media, creates a replacement inline creative on the same campaign, pauses the old one). - **Pinterest / X / OpenAI Ads**: status + budget only. Sending `targeting` or `creative` returns 501 with code `unsupported_platform_operation`. OpenAI Ads budget is lifetime-only (see `budget.type` below).
@@ -358,6 +408,19 @@ describe 'AdCampaignsApi' do
358
408
  end
359
409
  end
360
410
 
411
+ # unit tests for update_ad_keyword
412
+ # Pause or enable a Search keyword
413
+ # Changes `ad_group_criterion.status` for one keyword criterion (M.140). Negative keywords have no status on Google and cannot be paused or enabled.
414
+ # @param keyword_id Zernio keyword ID (not the Google criterion ID)
415
+ # @param update_ad_keyword_request
416
+ # @param [Hash] opts the optional parameters
417
+ # @return [UpdateAdKeyword200Response]
418
+ describe 'update_ad_keyword test' do
419
+ it 'should work' do
420
+ # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
421
+ end
422
+ end
423
+
361
424
  # unit tests for update_ad_set
362
425
  # Update an ad set
363
426
  # Ad-set-level writes. Use this for ABO budget updates, ad-set-scoped pause/resume, bid-strategy edits, Meta value-rule-set attach/detach, and Meta-only post-launch delivery settings via `platformSpecificData`. At least one updatable field is required. Value rule sets (Meta only, see `/v1/ads/value-rule-sets`): - ATTACH or REPLACE: send `valueRuleSetId`. Attachment is driven by the id's presence, so `valueRulesApplied: true` is optional. Sending a different id replaces the previous association; there is no separate replace call. - DETACH: send `valueRulesApplied: false` and OMIT `valueRuleSetId`. - Sending `valueRulesApplied: false` TOGETHER with `valueRuleSetId` returns 400 `mutually_exclusive_fields`. This is deliberate: Meta attaches the rule set whenever `value_rule_set_id` is present, even with `value_rules_applied` false, so echoing stored state while asking to detach would silently keep the bid adjustments live. - Eligibility: only ad sets on `LOWEST_COST_WITHOUT_CAP` or `COST_CAP`. Meta rejects the rest server-side. - Read back with `GET /v1/ads/ad-sets/{adSetId}?fields=value_rule_set_id`. Meta does not document `value_rules_applied` as a readable ad-set field, so the boolean cannot be read back. Bid strategy compatibility (per Meta's spec): - `LOWEST_COST_WITHOUT_CAP`: no `bidAmount`, no `roasAverageFloor`. - `LOWEST_COST_WITH_BID_CAP` / `COST_CAP`: `bidAmount` REQUIRED (whole currency units). - `LOWEST_COST_WITH_MIN_ROAS`: `roasAverageFloor` REQUIRED (decimal multiplier, e.g. 2.0 = 2.0x ROAS). - Meta only: send `bidAmount` WITHOUT `bidStrategy` to change the cap amount on an ad set under a COST_CAP / LOWEST_COST_WITH_BID_CAP parent campaign, leaving the strategy itself (inherited from the campaign) untouched. `roasAverageFloor` without `bidStrategy` is rejected (it has no meaning outside LOWEST_COST_WITH_MIN_ROAS). Delivery settings are validated by Meta against the campaign objective; incompatible combinations (e.g. a billingEvent the optimization goal doesn't allow) surface as 400s from Meta. When updating `budget` on an ABO campaign: if the parent campaign is CBO, the response is 409 with code BUDGET_LEVEL_MISMATCH — route to PUT /v1/ads/campaigns/{campaignId} instead.
@@ -388,7 +451,7 @@ describe 'AdCampaignsApi' do
388
451
  # Pause or resume a single ad
389
452
  # Ad-scoped pause/resume — touches ONLY this ad, never its parent ad set or campaign (so sibling ads keep running). Thin wrapper over the `status` field of PUT /v1/ads/{adId}, for callers that want a URL symmetric to /v1/ads/campaigns/{campaignId}/status and /v1/ads/ad-sets/{adSetId}/status. `{adId}` accepts the same identifier dialects as GET/PUT /v1/ads/{adId} (Zernio hex `_id`, Meta numeric `platformAdId`, or the creative's effective story/media IDs). `platform` is inferred from the ad, so it's not required in the body. Ads in terminal statuses (rejected, completed, cancelled) and no-op flips (already in the target state) are skipped.
390
453
  # @param ad_id Zernio `_id` (hex), Meta `platformAdId` (numeric), or one of the creative's effective story/media IDs.
391
- # @param update_ad_status_request
454
+ # @param update_ad_keyword_request
392
455
  # @param [Hash] opts the optional parameters
393
456
  # @return [UpdateAdStatus200Response]
394
457
  describe 'update_ad_status test' do
@@ -201,7 +201,7 @@ describe 'ConnectApi' do
201
201
  # unit tests for get_connect_url
202
202
  # Get OAuth connect URL
203
203
  # Initiate an OAuth connection flow. Returns an authUrl to redirect the user to. Standard flow: Zernio hosts the selection UI, then redirects to your redirect_url. Headless mode (headless=true): user is redirected to your redirect_url with OAuth data for custom UI. Use the platform-specific selection endpoints to complete.
204
- # @param platform Social media platform to connect
204
+ # @param platform Social media platform to connect. `snapchat` is a closed beta with no public release date: it returns 403 `PLATFORM_BETA_RESTRICTED` until the account is approved.
205
205
  # @param profile_id Your Zernio profile ID (get from /v1/profiles). For WhatsApp, a Zernio-provisioned number can only be connected on the profile it was provisioned to; connecting from any other profile is rejected with a 409.
206
206
  # @param [Hash] opts the optional parameters
207
207
  # @option opts [String] :redirect_url Your custom redirect URL after connection completes. MUST be an absolute http(s) URL or a custom app scheme for mobile deeplinks (e.g. myapp://callback); a relative path is rejected with 400 INVALID_REDIRECT_URL. Result params are appended with the URL API, so an existing query string is preserved. Standard mode appends connected={platform}&profileId=X&accountId=Y&username=Z. Headless mode appends OAuth data params for platforms requiring selection (e.g. LinkedIn orgs, Facebook pages). If no selection is needed, the account is created directly and the redirect includes accountId. On failure, the browser is sent to the same redirect_url with `error` and `platform` appended. `error` and `platform` are always present. `error_message`, `is_user_fixable`, `reason` and `dashboard_url` are conditional and must be treated as optional. This list is NOT exhaustive and new values may be added at any time. Treat an unrecognized value as a generic failure rather than matching it exhaustively. Existing values are not renamed or removed without notice. OAuth and callback: oauth_denied, invalid_callback, invalid_state, unsupported_platform, connection_failed, internal_error, token_exchange_failed, byok_config_error, personal_account_not_supported, missing_google_permissions, platform_requires_destination, reconnect_account_mismatch, invalid_request Access and limits: profile_not_found, invalid_profile_id, access_denied, account_limit_exceeded, profile_limit_exceeded, payment_required Destination selection: no_facebook_pages, facebook_pages_error, no_google_locations, google_locations_error, google_permission_denied, no_snapchat_public_profiles, snapchat_profiles_error, discord_no_guild, slack_no_team WhatsApp: whatsapp_error, one_whatsapp_per_profile, whatsapp_number_already_connected, whatsapp_number_pinned_to_profile, connection_cancelled Google Ads (platform=googleads): google_ads_auth_failed, google_ads_invalid_state, google_ads_config_error, google_ads_token_failed, google_ads_quota_exhausted, google_ads_callback_error TikTok Ads (platform=tiktokads): tiktok_ads_auth_failed, tiktok_ads_invalid_state, tiktok_ads_access_denied, tiktok_ads_config_error, tiktok_ads_token_failed, tiktok_ads_account_not_found, tiktok_ads_callback_error X Ads (platform=xads): x_ads_denied, x_ads_auth_failed, x_ads_config_error, x_ads_account_not_found, x_ads_state_error, x_ads_token_failed, x_ads_token_missing, x_ads_callback_error Shopify (platform=shopify): shopify_auth_failed, shopify_config_error, shopify_invalid_state, shopify_invalid_hmac, shopify_invalid_shop, shopify_missing_scopes, shopify_callback_error 1. On this endpoint every upstream OAuth error is collapsed into `oauth_denied`. The provider's own value (for example Meta's `access_denied`) is not forwarded. The dedicated ads flows below are different: they use their own denial slugs and `google_ads_auth_failed` and `tiktok_ads_auth_failed` may carry the provider's raw error string in `error_message`. 2. On the tiktok and twitter ads flows `platform` carries the ads platform id (`tiktokads`, `xads`), not the value used in the request path. The googleads and shopify flows report `googleads` and `shopify`.
@@ -47,7 +47,7 @@ describe 'PostsApi' do
47
47
 
48
48
  # unit tests for create_post
49
49
  # Create post
50
- # Create and optionally publish a post. Immediate posts (`publishNow: true`) include `platformPostUrl` in the response. Content is optional when media is attached, all platforms have `customContent`, every platform entry is an X Article (`platformSpecificData.article`), or every platform entry is a LinkedIn text-free reshare (`platformSpecificData.reshareUrl` with no text). See each platform's schema for media constraints. ## Idempotency Two layers of duplicate-protection apply, so safe-to-retry callers (network blips, n8n / Zapier retries, etc.) don't accidentally double-post. **1. Same-request idempotency (5-minute window).** Pass an `x-request-id` header to mark a logical request. If a second request arrives with the same `x-request-id` while the first is in-flight (or within ~5 minutes of completion), we return **HTTP 200** with the original post in the `existingPost` field — no new post is created. The official Zernio SDKs auto-generate a unique `x-request-id` per call. If you're using a generic HTTP client (curl, n8n's HTTP node, Zapier, custom code), either: - Set a unique `x-request-id` per logical call (recommended — UUIDv4 is fine) - Or simply omit the header — we'll treat each request as new **Common pitfall**: if your workflow tool uses a single execution-level request ID and reuses it across multiple HTTP nodes (e.g. one ID for the whole run, shared across 6 different platform calls), every call after the first will look like a retry of the first and return its post. Generate a fresh ID per node. **2. Content-hash dedup (24-hour window).** Independently, we hash `(platform, accountId, content + media URLs)` and reject duplicates within 24 hours with **HTTP 409**. This catches genuine \"same content posted twice to the same account\" cases regardless of `x-request-id`. Returns `error`, `accountId`, `platform`, and `existingPostId` so you can find the original. To intentionally re-post identical content within 24h, change something (the caption, the media, the account) — the dedup is keyed on the full content fingerprint. Order: same-`x-request-id` retries (200) are checked first; if no idempotency match, the content-hash dedup (409) runs.
50
+ # Create and optionally publish a post. Immediate posts (`publishNow: true`) include `platformPostUrl` in the response. Content is optional when media is attached, all platforms have `customContent`, every platform entry is an X Article (`platformSpecificData.article`), or every platform entry is a LinkedIn text-free reshare (`platformSpecificData.reshareUrl` with no text). See each platform's schema for media constraints. ## Scheduling Pick one of `scheduledFor` (schedule), `publishNow: true` (publish synchronously) or `queuedFromProfile` (next queue slot). With none of them and `isDraft` unset, the post is saved as a draft. `platforms` is required unless the post is a draft. `isDraft: true` wins over `publishNow` and `scheduledFor` (the post is saved, never published); `publishNow: true` wins over `scheduledFor`. A `scheduledFor` already in the past is not rejected: the post is published synchronously in the same request, exactly like `publishNow`. ## Idempotency Two layers of duplicate-protection apply, so safe-to-retry callers (network blips, n8n / Zapier retries, etc.) don't accidentally double-post. **1. Same-request idempotency (5-minute window).** Pass an `x-request-id` header to mark a logical request. If a second request arrives with the same `x-request-id` while the first is in-flight (or within ~5 minutes of completion), we return **HTTP 200** with the original post in the `existingPost` field — no new post is created. The official Zernio SDKs auto-generate a unique `x-request-id` per call. If you're using a generic HTTP client (curl, n8n's HTTP node, Zapier, custom code), either: - Set a unique `x-request-id` per logical call (recommended — UUIDv4 is fine) - Or simply omit the header — we'll treat each request as new **Common pitfall**: if your workflow tool uses a single execution-level request ID and reuses it across multiple HTTP nodes (e.g. one ID for the whole run, shared across 6 different platform calls), every call after the first will look like a retry of the first and return its post. Generate a fresh ID per node. **2. Content-hash dedup (24-hour window).** Independently, we hash `(platform, accountId, content + media URLs)` and reject duplicates within 24 hours with **HTTP 409**. This catches genuine \"same content posted twice to the same account\" cases regardless of `x-request-id`. Returns `error`, `accountId`, `platform`, and `existingPostId` so you can find the original. To intentionally re-post identical content within 24h, change something (the caption, the media, the account) — the dedup is keyed on the full content fingerprint. Order: same-`x-request-id` retries (200) are checked first; if no idempotency match, the content-hash dedup (409) runs.
51
51
  # @param create_post_request
52
52
  # @param [Hash] opts the optional parameters
53
53
  # @option opts [String] :x_request_id Optional client-generated request identifier for safe retry (idempotency). When two requests carry the same value, the second is treated as a retry of the first and returns the original post (HTTP 200) instead of creating a duplicate. Window is ~5 minutes from the first request. Generate a UUID per logical call. SDKs do this automatically; HTTP clients should set it themselves or omit it. See the operation description for the full idempotency contract.
@@ -48,7 +48,7 @@ describe 'TrackingTagsApi' do
48
48
 
49
49
  # unit tests for create_tracking_tag
50
50
  # Create a tracking tag
51
- # Meta: creates a Meta Pixel on the given ad account (`POST /act_{id}/adspixels` — `name` is the only input). Returns the created tag including its install `code`. The pixel is owned by the Business Manager that owns the ad account; a pixel created on a personal (non-BM) ad account ends up with `ownerBusinessId: null` and can't be shared with other ad accounts. Creating a Meta pixel does NOT install it — install the returned `code` snippet on the site, or send events server-side via `POST /v1/ads/conversions`. The check `installed` is derived from `lastFiredTime`. OpenAI Ads: creates an OpenAI pixel AND provisions a Conversions API key for it in the same call (`adAccountId` is required by this endpoint but ignored — one API key maps to exactly one ad account, so there's nothing to select). Returns 422 (`FEATURE_NOT_AVAILABLE`) if the ad account isn't enabled for pixel management; contact your OpenAI partner representative to enable it. There is no delete API for OpenAI pixels. If the pixel is created but the Conversions API key provisioning then fails, the pixel is left live on OpenAI (it cannot be cleaned up) and the error message names the surviving pixel id and warns against retrying, since a retry would create a second, orphaned pixel. NOT idempotent on either platform: each call creates a new pixel (and, for OpenAI, a new Conversions API key). Do not retry blindly on timeout. Meta (platform `metaads`) and OpenAI Ads (platform `openaiads`); other platforms return 405.
51
+ # Meta: creates a Meta Pixel on the given ad account (`POST /act_{id}/adspixels` — `name` is the only input). Returns the created tag including its install `code`. The pixel is owned by the Business Manager that owns the ad account; a pixel created on a personal (non-BM) ad account ends up with `ownerBusinessId: null` and can't be shared with other ad accounts. Creating a Meta pixel does NOT install it — install the returned `code` snippet on the site, or send events server-side via `POST /v1/ads/conversions`. The check `installed` is derived from `lastFiredTime`. OpenAI Ads: creates an OpenAI pixel AND provisions a Conversions API key for it in the same call (`adAccountId` is required by this endpoint but ignored — one API key maps to exactly one ad account, so there's nothing to select). Returns 422 (`FEATURE_NOT_AVAILABLE`) if the ad account isn't enabled for pixel management; contact your OpenAI partner representative to enable it. There is no delete API for OpenAI pixels. If the pixel is created but the Conversions API key provisioning then fails, the pixel is left live on OpenAI (it cannot be cleaned up) and the error message names the surviving pixel id and warns against retrying, since a retry would create a second, orphaned pixel. NOT idempotent on either platform: each call creates a new pixel (and, for OpenAI, a new Conversions API key plus, with `defaultEventType`, a new conversion event setting). Do not retry blindly on timeout. Meta (platform `metaads`) and OpenAI Ads (platform `openaiads`); other platforms return 405.
52
52
  # @param account_id Ads SocialAccount id (platform `metaads` or `openaiads`).
53
53
  # @param create_tracking_tag_request
54
54
  # @param [Hash] opts the optional parameters