late-sdk 0.0.716 → 0.0.718

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 (475) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +81 -0
  3. data/docs/AccountsApi.md +142 -1
  4. data/docs/AdCampaignsApi.md +22 -10
  5. data/docs/AdCreative.md +2 -0
  6. data/docs/AdDailyMetrics.md +1 -1
  7. data/docs/AdEngagementCounts.md +4 -4
  8. data/docs/AdInsightsApi.md +164 -0
  9. data/docs/AdMetrics.md +1 -1
  10. data/docs/AdTargetingApi.md +3 -3
  11. data/docs/AdTreeCampaign.md +1 -1
  12. data/docs/AddBroadcastRecipientsRequest.md +1 -1
  13. data/docs/AnalyticsListResponsePostsInner.md +1 -1
  14. data/docs/AnalyticsListResponsePostsInnerMediaItemsInner.md +24 -0
  15. data/docs/AnalyticsSinglePostResponseMediaItemsInner.md +7 -3
  16. data/docs/BillingSnapshot.md +2 -0
  17. data/docs/BillingSnapshotPlan.md +3 -1
  18. data/docs/Blog.md +24 -0
  19. data/docs/BlogArticle.md +44 -0
  20. data/docs/BlogArticleImage.md +20 -0
  21. data/docs/BlogsApi.md +747 -0
  22. data/docs/BlueskyPlatformData.md +2 -0
  23. data/docs/BoostPostRequest.md +2 -2
  24. data/docs/CampaignAnalyticsResponseAnalyticsDailyInner.md +1 -1
  25. data/docs/CommentsApi.md +8 -8
  26. data/docs/ConnectApi.md +149 -1
  27. data/docs/ConnectShopifyWithToken200Response.md +18 -0
  28. data/docs/ConnectShopifyWithToken200ResponseAccount.md +26 -0
  29. data/docs/ConnectShopifyWithTokenRequest.md +22 -0
  30. data/docs/CreateBlog201Response.md +20 -0
  31. data/docs/CreateBlogArticle201Response.md +20 -0
  32. data/docs/CreateBlogArticleRequest.md +36 -0
  33. data/docs/CreateBlogArticleRequestImage.md +20 -0
  34. data/docs/CreateBlogArticleRequestSeo.md +20 -0
  35. data/docs/CreateBlogRequest.md +20 -0
  36. data/docs/CreateCallAdRequest.md +1 -1
  37. data/docs/CreateInboxConversationRequest.md +2 -0
  38. data/docs/CreateMessagingAdRequest.md +1 -1
  39. data/docs/CreatePostRequest.md +1 -1
  40. data/docs/CreateStandaloneAdRequest.md +10 -6
  41. data/docs/CreateStandaloneAdRequestPromotedObject.md +2 -2
  42. data/docs/CreateStandaloneAdRequestVideo.md +1 -1
  43. data/docs/CtwaAdRequestBody.md +1 -1
  44. data/docs/ExternalPostMediaItem.md +7 -3
  45. data/docs/ExternalPostSummary.md +0 -2
  46. data/docs/ExternalPostSummaryAnalytics.md +1 -1
  47. data/docs/GetAccountHealth200Response.md +3 -1
  48. data/docs/GetAccountHealth200ResponsePlatformConnection.md +24 -0
  49. data/docs/GetAccountHealth200ResponsePlatformConnectionMetaError.md +22 -0
  50. data/docs/GetAdAnalytics202Response.md +1 -1
  51. data/docs/GetAdTree202Response.md +1 -1
  52. data/docs/GetAdsTimeline202Response.md +1 -1
  53. data/docs/GetBlueskySettings200Response.md +18 -0
  54. data/docs/GetCampaignAnalytics202Response.md +1 -1
  55. data/docs/GetInboxPostComments200ResponseCommentsInner.md +4 -2
  56. data/docs/GetLinkedInPostAnalytics200ResponseAnalytics.md +1 -1
  57. data/docs/GetPhoneNumberKycForm200Response.md +3 -1
  58. data/docs/GetWhatsAppNumberKycForm200Response.md +3 -1
  59. data/docs/LinkedInAggregateAnalyticsTotalResponseAnalytics.md +1 -1
  60. data/docs/LinkedInPlatformData.md +4 -2
  61. data/docs/LinkedInPlatformDataPoll.md +22 -0
  62. data/docs/ListAds202Response.md +1 -1
  63. data/docs/ListBlogArticles200Response.md +22 -0
  64. data/docs/ListBlogs200Response.md +22 -0
  65. data/docs/ListInboxConversations200ResponseDataInnerMetadata.md +3 -3
  66. data/docs/ListLeads200ResponseLeadsInner.md +3 -3
  67. data/docs/ListLocalServicesLeadConversations200Response.md +22 -0
  68. data/docs/ListLocalServicesLeadConversations200ResponseDataInner.md +28 -0
  69. data/docs/ListLocalServicesLeadConversations200ResponseDataInnerMessage.md +20 -0
  70. data/docs/ListLocalServicesLeadConversations200ResponseDataInnerPhoneCall.md +20 -0
  71. data/docs/ListLocalServicesLeads200Response.md +22 -0
  72. data/docs/ListLocalServicesLeads200ResponseDataInner.md +38 -0
  73. data/docs/ListLocalServicesLeads200ResponseDataInnerContact.md +22 -0
  74. data/docs/ListPhoneNumbers200ResponseConnectedInner.md +3 -1
  75. data/docs/ListPhoneNumbers200ResponseNumbersInner.md +2 -0
  76. data/docs/ListWhatsAppAccountEvents200Response.md +18 -0
  77. data/docs/ListWhatsAppAccountEvents200ResponseEventsInner.md +28 -0
  78. data/docs/MessagesApi.md +4 -4
  79. data/docs/OnVerificationApprovedRequest.md +1 -1
  80. data/docs/OnVerificationFailedRequest.md +1 -1
  81. data/docs/OnWhatsAppAutomaticEventRequest.md +1 -1
  82. data/docs/OnWhatsAppNumberActionRequiredRequest.md +1 -1
  83. data/docs/OnWhatsAppNumberActivatedRequest.md +1 -1
  84. data/docs/OnWhatsAppNumberDeclinedRequest.md +1 -1
  85. data/docs/OnWhatsAppNumberKycSubmittedRequest.md +1 -1
  86. data/docs/OnWhatsAppNumberReactivatedRequest.md +1 -1
  87. data/docs/OnWhatsAppNumberReleasedRequest.md +1 -1
  88. data/docs/OnWhatsAppNumberSuspendedRequest.md +1 -1
  89. data/docs/OnWhatsAppNumberVerificationRequiredRequest.md +1 -1
  90. data/docs/PlatformTarget.md +2 -2
  91. data/docs/PostAnalytics.md +3 -3
  92. data/docs/PostsApi.md +1 -1
  93. data/docs/SMSApi.md +1 -1
  94. data/docs/SearchAdTargeting200ResponseResultsInner.md +1 -1
  95. data/docs/SendInboxMessageRequest.md +4 -2
  96. data/docs/SubmitPhoneNumberKycRequest.md +1 -1
  97. data/docs/SubmitPhoneNumberKycRequestAddress.md +2 -0
  98. data/docs/SubmitWhatsAppNumberKycRequest.md +1 -1
  99. data/docs/TwitterPlatformData.md +2 -0
  100. data/docs/UpdateAdCampaignStatus200Response.md +6 -6
  101. data/docs/UpdateAdSet200Response.md +7 -3
  102. data/docs/UpdateAdSetRequest.md +1 -1
  103. data/docs/UpdateAdSetStatus200Response.md +9 -3
  104. data/docs/UpdateBlogArticleRequest.md +36 -0
  105. data/docs/UpdateBlogRequest.md +20 -0
  106. data/docs/UpdateBlueskySettingsRequest.md +18 -0
  107. data/docs/ValidatePhoneNumberKycAddressRequest.md +2 -0
  108. data/docs/WebhookEventsApi.md +138 -0
  109. data/docs/WebhookPayloadAccountAdsInitialSyncCompleted.md +1 -1
  110. data/docs/WebhookPayloadAccountConnected.md +1 -1
  111. data/docs/WebhookPayloadAccountDisconnected.md +1 -1
  112. data/docs/WebhookPayloadAdStatusChanged.md +1 -1
  113. data/docs/WebhookPayloadCallEnded.md +1 -1
  114. data/docs/WebhookPayloadCallEndedCall.md +4 -0
  115. data/docs/WebhookPayloadCallFailed.md +1 -1
  116. data/docs/WebhookPayloadCallPermissionRequest.md +1 -1
  117. data/docs/WebhookPayloadCallReceived.md +1 -1
  118. data/docs/WebhookPayloadComment.md +1 -1
  119. data/docs/WebhookPayloadCommentCommentAuthor.md +2 -0
  120. data/docs/WebhookPayloadConversationStarted.md +1 -1
  121. data/docs/WebhookPayloadExternalPost.md +1 -1
  122. data/docs/WebhookPayloadLead.md +1 -1
  123. data/docs/WebhookPayloadMessage.md +1 -1
  124. data/docs/WebhookPayloadMessageDeleted.md +1 -1
  125. data/docs/WebhookPayloadMessageDeliveryStatus.md +1 -1
  126. data/docs/WebhookPayloadMessageEdited.md +1 -1
  127. data/docs/WebhookPayloadMessageMetadata.md +5 -1
  128. data/docs/WebhookPayloadMessageMetadataReferral.md +5 -3
  129. data/docs/WebhookPayloadMessageMetadataUnsupported.md +22 -0
  130. data/docs/WebhookPayloadMessageSent.md +1 -1
  131. data/docs/WebhookPayloadPost.md +1 -1
  132. data/docs/WebhookPayloadPostPlatform.md +1 -1
  133. data/docs/WebhookPayloadReaction.md +1 -1
  134. data/docs/WebhookPayloadReferral.md +30 -0
  135. data/docs/WebhookPayloadReferralReferral.md +28 -0
  136. data/docs/WebhookPayloadReferralReferralAdsContextData.md +28 -0
  137. data/docs/WebhookPayloadReferralSender.md +20 -0
  138. data/docs/WebhookPayloadReviewNew.md +1 -1
  139. data/docs/WebhookPayloadReviewUpdated.md +1 -1
  140. data/docs/WebhookPayloadTest.md +1 -1
  141. data/docs/WebhookPayloadWhatsAppTemplateCategoryUpdated.md +26 -0
  142. data/docs/WebhookPayloadWhatsAppTemplateCategoryUpdatedTemplate.md +32 -0
  143. data/docs/WebhookPayloadWhatsAppTemplateStatusUpdated.md +1 -1
  144. data/docs/WhatsAppApi.md +74 -0
  145. data/docs/WorkflowNode.md +3 -1
  146. data/docs/XArticle.md +24 -0
  147. data/docs/XArticleBlock.md +28 -0
  148. data/docs/XArticleBlockData.md +24 -0
  149. data/docs/XArticleContentState.md +20 -0
  150. data/docs/XArticleCover.md +20 -0
  151. data/docs/XArticleEntity.md +57 -0
  152. data/docs/XArticleEntityOneOf.md +20 -0
  153. data/docs/XArticleEntityOneOf1.md +20 -0
  154. data/docs/XArticleEntityOneOf1Value.md +22 -0
  155. data/docs/XArticleEntityOneOf1ValueData.md +18 -0
  156. data/docs/XArticleEntityOneOf2.md +20 -0
  157. data/docs/XArticleEntityOneOf2Value.md +22 -0
  158. data/docs/XArticleEntityOneOf2ValueData.md +22 -0
  159. data/docs/XArticleEntityOneOf3.md +20 -0
  160. data/docs/XArticleEntityOneOf3Value.md +22 -0
  161. data/docs/XArticleEntityOneOf3ValueData.md +18 -0
  162. data/docs/XArticleEntityOneOf4.md +20 -0
  163. data/docs/XArticleEntityOneOf4Value.md +22 -0
  164. data/docs/XArticleEntityOneOf4ValueData.md +18 -0
  165. data/docs/XArticleEntityOneOf5.md +20 -0
  166. data/docs/XArticleEntityOneOf5Value.md +22 -0
  167. data/docs/XArticleEntityOneOfValue.md +22 -0
  168. data/docs/XArticleEntityOneOfValueData.md +18 -0
  169. data/docs/XArticleEntityRange.md +22 -0
  170. data/docs/XArticleInlineStyleRange.md +22 -0
  171. data/docs/XArticleTextRange.md +22 -0
  172. data/docs/YouTubeVideoRetentionResponse.md +1 -1
  173. data/docs/YouTubeVideoRetentionResponseRetentionCurveInner.md +2 -2
  174. data/lib/zernio-sdk/api/accounts_api.rb +139 -2
  175. data/lib/zernio-sdk/api/ad_campaigns_api.rb +35 -8
  176. data/lib/zernio-sdk/api/ad_insights_api.rb +165 -0
  177. data/lib/zernio-sdk/api/ad_targeting_api.rb +5 -5
  178. data/lib/zernio-sdk/api/blogs_api.rb +845 -0
  179. data/lib/zernio-sdk/api/comments_api.rb +10 -10
  180. data/lib/zernio-sdk/api/connect_api.rb +149 -0
  181. data/lib/zernio-sdk/api/messages_api.rb +4 -4
  182. data/lib/zernio-sdk/api/posts_api.rb +2 -2
  183. data/lib/zernio-sdk/api/sms_api.rb +2 -2
  184. data/lib/zernio-sdk/api/webhook_events_api.rb +132 -0
  185. data/lib/zernio-sdk/api/whats_app_api.rb +75 -0
  186. data/lib/zernio-sdk/models/account_with_follower_stats.rb +2 -2
  187. data/lib/zernio-sdk/models/ad_creative.rb +12 -1
  188. data/lib/zernio-sdk/models/ad_daily_metrics.rb +1 -1
  189. data/lib/zernio-sdk/models/ad_engagement_counts.rb +5 -5
  190. data/lib/zernio-sdk/models/ad_metrics.rb +1 -1
  191. data/lib/zernio-sdk/models/ad_tree_campaign.rb +1 -1
  192. data/lib/zernio-sdk/models/add_broadcast_recipients_request.rb +1 -1
  193. data/lib/zernio-sdk/models/analytics_list_response_posts_inner.rb +1 -1
  194. data/lib/zernio-sdk/models/analytics_list_response_posts_inner_media_items_inner.rb +211 -0
  195. data/lib/zernio-sdk/models/analytics_single_post_response_media_items_inner.rb +52 -6
  196. data/lib/zernio-sdk/models/billing_snapshot.rb +14 -3
  197. data/lib/zernio-sdk/models/billing_snapshot_plan.rb +14 -4
  198. data/lib/zernio-sdk/models/blog.rb +211 -0
  199. data/lib/zernio-sdk/models/blog_article.rb +315 -0
  200. data/lib/zernio-sdk/models/blog_article_image.rb +158 -0
  201. data/lib/zernio-sdk/models/bluesky_platform_data.rb +42 -2
  202. data/lib/zernio-sdk/models/boost_post_request.rb +30 -1
  203. data/lib/zernio-sdk/models/campaign_analytics_response_analytics_daily_inner.rb +1 -1
  204. data/lib/zernio-sdk/models/connect_shopify_with_token200_response.rb +147 -0
  205. data/lib/zernio-sdk/models/connect_shopify_with_token200_response_account.rb +218 -0
  206. data/lib/zernio-sdk/models/connect_shopify_with_token_request.rb +219 -0
  207. data/lib/zernio-sdk/models/create_blog201_response.rb +190 -0
  208. data/lib/zernio-sdk/models/create_blog_article201_response.rb +190 -0
  209. data/lib/zernio-sdk/models/create_blog_article_request.rb +262 -0
  210. data/lib/zernio-sdk/models/create_blog_article_request_image.rb +174 -0
  211. data/lib/zernio-sdk/models/create_blog_article_request_seo.rb +157 -0
  212. data/lib/zernio-sdk/models/create_blog_request.rb +202 -0
  213. data/lib/zernio-sdk/models/create_call_ad_request.rb +1 -1
  214. data/lib/zernio-sdk/models/create_inbox_conversation_request.rb +13 -1
  215. data/lib/zernio-sdk/models/create_messaging_ad_request.rb +1 -1
  216. data/lib/zernio-sdk/models/create_post_request.rb +1 -1
  217. data/lib/zernio-sdk/models/create_standalone_ad_request.rb +67 -6
  218. data/lib/zernio-sdk/models/create_standalone_ad_request_creatives_inner_video.rb +1 -1
  219. data/lib/zernio-sdk/models/create_standalone_ad_request_promoted_object.rb +3 -3
  220. data/lib/zernio-sdk/models/create_standalone_ad_request_video.rb +1 -1
  221. data/lib/zernio-sdk/models/ctwa_ad_request_body.rb +1 -1
  222. data/lib/zernio-sdk/models/external_post_media_item.rb +48 -16
  223. data/lib/zernio-sdk/models/external_post_summary.rb +1 -11
  224. data/lib/zernio-sdk/models/external_post_summary_analytics.rb +1 -0
  225. data/lib/zernio-sdk/models/facebook_settings.rb +5 -5
  226. data/lib/zernio-sdk/models/get_account_health200_response.rb +13 -4
  227. data/lib/zernio-sdk/models/get_account_health200_response_platform_connection.rb +213 -0
  228. data/lib/zernio-sdk/models/get_account_health200_response_platform_connection_meta_error.rb +168 -0
  229. data/lib/zernio-sdk/models/get_ad_analytics202_response.rb +6 -29
  230. data/lib/zernio-sdk/models/get_ad_tree202_response.rb +6 -29
  231. data/lib/zernio-sdk/models/get_ads_timeline202_response.rb +6 -29
  232. data/lib/zernio-sdk/models/get_bluesky_settings200_response.rb +150 -0
  233. data/lib/zernio-sdk/models/get_campaign_analytics202_response.rb +6 -29
  234. data/lib/zernio-sdk/models/get_inbox_post_comments200_response_comments_inner.rb +13 -2
  235. data/lib/zernio-sdk/models/get_linked_in_post_analytics200_response_analytics.rb +1 -1
  236. data/lib/zernio-sdk/models/get_phone_number_kyc_form200_response.rb +14 -4
  237. data/lib/zernio-sdk/models/get_whats_app_number_kyc_form200_response.rb +14 -4
  238. data/lib/zernio-sdk/models/inbox_webhook_message.rb +2 -2
  239. data/lib/zernio-sdk/models/linked_in_aggregate_analytics_total_response_analytics.rb +1 -1
  240. data/lib/zernio-sdk/models/linked_in_platform_data.rb +15 -6
  241. data/lib/zernio-sdk/models/linked_in_platform_data_poll.rb +277 -0
  242. data/lib/zernio-sdk/models/list_ads202_response.rb +6 -29
  243. data/lib/zernio-sdk/models/list_blog_articles200_response.rb +203 -0
  244. data/lib/zernio-sdk/models/list_blogs200_response.rb +203 -0
  245. data/lib/zernio-sdk/models/list_broadcast_recipients200_response_recipients_inner.rb +1 -0
  246. data/lib/zernio-sdk/models/list_inbox_conversations200_response_data_inner_metadata.rb +4 -4
  247. data/lib/zernio-sdk/models/list_leads200_response_leads_inner.rb +3 -2
  248. data/lib/zernio-sdk/models/list_local_services_lead_conversations200_response.rb +167 -0
  249. data/lib/zernio-sdk/models/list_local_services_lead_conversations200_response_data_inner.rb +198 -0
  250. data/lib/zernio-sdk/models/list_local_services_lead_conversations200_response_data_inner_message.rb +160 -0
  251. data/lib/zernio-sdk/models/list_local_services_lead_conversations200_response_data_inner_phone_call.rb +159 -0
  252. data/lib/zernio-sdk/models/list_local_services_leads200_response.rb +167 -0
  253. data/lib/zernio-sdk/models/list_local_services_leads200_response_data_inner.rb +249 -0
  254. data/lib/zernio-sdk/models/list_local_services_leads200_response_data_inner_contact.rb +169 -0
  255. data/lib/zernio-sdk/models/list_phone_numbers200_response_connected_inner.rb +15 -5
  256. data/lib/zernio-sdk/models/list_phone_numbers200_response_numbers_inner.rb +11 -1
  257. data/lib/zernio-sdk/models/list_sequence_enrollments200_response_enrollments_inner.rb +3 -0
  258. data/lib/zernio-sdk/models/list_whats_app_account_events200_response.rb +149 -0
  259. data/lib/zernio-sdk/models/list_whats_app_account_events200_response_events_inner.rb +228 -0
  260. data/lib/zernio-sdk/models/on_verification_approved_request.rb +1 -0
  261. data/lib/zernio-sdk/models/on_verification_failed_request.rb +1 -0
  262. data/lib/zernio-sdk/models/on_whats_app_automatic_event_request.rb +1 -0
  263. data/lib/zernio-sdk/models/on_whats_app_number_action_required_request.rb +1 -0
  264. data/lib/zernio-sdk/models/on_whats_app_number_activated_request.rb +1 -0
  265. data/lib/zernio-sdk/models/on_whats_app_number_declined_request.rb +1 -0
  266. data/lib/zernio-sdk/models/on_whats_app_number_kyc_submitted_request.rb +1 -0
  267. data/lib/zernio-sdk/models/on_whats_app_number_reactivated_request.rb +1 -0
  268. data/lib/zernio-sdk/models/on_whats_app_number_released_request.rb +1 -0
  269. data/lib/zernio-sdk/models/on_whats_app_number_suspended_request.rb +1 -0
  270. data/lib/zernio-sdk/models/on_whats_app_number_verification_required_request.rb +1 -0
  271. data/lib/zernio-sdk/models/platform_target.rb +5 -4
  272. data/lib/zernio-sdk/models/post_analytics.rb +2 -1
  273. data/lib/zernio-sdk/models/search_ad_targeting200_response_results_inner.rb +1 -1
  274. data/lib/zernio-sdk/models/send_inbox_message_request.rb +15 -3
  275. data/lib/zernio-sdk/models/social_account.rb +2 -2
  276. data/lib/zernio-sdk/models/submit_phone_number_kyc_request.rb +1 -1
  277. data/lib/zernio-sdk/models/submit_phone_number_kyc_request_address.rb +11 -1
  278. data/lib/zernio-sdk/models/submit_whats_app_number_kyc_request.rb +1 -1
  279. data/lib/zernio-sdk/models/twitter_platform_data.rb +11 -2
  280. data/lib/zernio-sdk/models/update_ad_campaign_status200_response.rb +50 -15
  281. data/lib/zernio-sdk/models/update_ad_set200_response.rb +26 -1
  282. data/lib/zernio-sdk/models/update_ad_set_request.rb +1 -1
  283. data/lib/zernio-sdk/models/update_ad_set_status200_response.rb +72 -4
  284. data/lib/zernio-sdk/models/update_blog_article_request.rb +257 -0
  285. data/lib/zernio-sdk/models/update_blog_request.rb +196 -0
  286. data/lib/zernio-sdk/models/update_bluesky_settings_request.rb +176 -0
  287. data/lib/zernio-sdk/models/validate_phone_number_kyc_address_request.rb +11 -1
  288. data/lib/zernio-sdk/models/webhook_payload_account_ads_initial_sync_completed.rb +1 -0
  289. data/lib/zernio-sdk/models/webhook_payload_account_connected.rb +1 -0
  290. data/lib/zernio-sdk/models/webhook_payload_account_disconnected.rb +1 -0
  291. data/lib/zernio-sdk/models/webhook_payload_ad_status_changed.rb +1 -1
  292. data/lib/zernio-sdk/models/webhook_payload_call_ended.rb +1 -0
  293. data/lib/zernio-sdk/models/webhook_payload_call_ended_call.rb +23 -1
  294. data/lib/zernio-sdk/models/webhook_payload_call_failed.rb +1 -0
  295. data/lib/zernio-sdk/models/webhook_payload_call_permission_request.rb +1 -0
  296. data/lib/zernio-sdk/models/webhook_payload_call_received.rb +1 -0
  297. data/lib/zernio-sdk/models/webhook_payload_comment.rb +1 -0
  298. data/lib/zernio-sdk/models/webhook_payload_comment_comment_author.rb +11 -1
  299. data/lib/zernio-sdk/models/webhook_payload_conversation_started.rb +1 -0
  300. data/lib/zernio-sdk/models/webhook_payload_external_post.rb +1 -0
  301. data/lib/zernio-sdk/models/webhook_payload_lead.rb +1 -0
  302. data/lib/zernio-sdk/models/webhook_payload_message.rb +1 -0
  303. data/lib/zernio-sdk/models/webhook_payload_message_deleted.rb +1 -0
  304. data/lib/zernio-sdk/models/webhook_payload_message_delivery_status.rb +2 -1
  305. data/lib/zernio-sdk/models/webhook_payload_message_edited.rb +1 -0
  306. data/lib/zernio-sdk/models/webhook_payload_message_message.rb +2 -2
  307. data/lib/zernio-sdk/models/webhook_payload_message_metadata.rb +23 -4
  308. data/lib/zernio-sdk/models/webhook_payload_message_metadata_referral.rb +15 -5
  309. data/lib/zernio-sdk/models/webhook_payload_message_metadata_unsupported.rb +169 -0
  310. data/lib/zernio-sdk/models/webhook_payload_message_sent.rb +1 -0
  311. data/lib/zernio-sdk/models/webhook_payload_post.rb +1 -0
  312. data/lib/zernio-sdk/models/webhook_payload_post_platform.rb +1 -0
  313. data/lib/zernio-sdk/models/webhook_payload_reaction.rb +1 -0
  314. data/lib/zernio-sdk/models/webhook_payload_reaction_reaction_sender.rb +1 -1
  315. data/lib/zernio-sdk/models/webhook_payload_referral.rb +347 -0
  316. data/lib/zernio-sdk/models/webhook_payload_referral_referral.rb +198 -0
  317. data/lib/zernio-sdk/models/webhook_payload_referral_referral_ads_context_data.rb +193 -0
  318. data/lib/zernio-sdk/models/webhook_payload_referral_sender.rb +176 -0
  319. data/lib/zernio-sdk/models/webhook_payload_review_new.rb +1 -0
  320. data/lib/zernio-sdk/models/webhook_payload_review_updated.rb +1 -0
  321. data/lib/zernio-sdk/models/webhook_payload_test.rb +1 -0
  322. data/lib/zernio-sdk/models/webhook_payload_whats_app_template_category_updated.rb +295 -0
  323. data/lib/zernio-sdk/models/webhook_payload_whats_app_template_category_updated_template.rb +353 -0
  324. data/lib/zernio-sdk/models/webhook_payload_whats_app_template_status_updated.rb +1 -1
  325. data/lib/zernio-sdk/models/workflow_node.rb +42 -4
  326. data/lib/zernio-sdk/models/x_article.rb +255 -0
  327. data/lib/zernio-sdk/models/x_article_block.rb +273 -0
  328. data/lib/zernio-sdk/models/x_article_block_data.rb +182 -0
  329. data/lib/zernio-sdk/models/x_article_content_state.rb +204 -0
  330. data/lib/zernio-sdk/models/x_article_cover.rb +204 -0
  331. data/lib/zernio-sdk/models/x_article_entity.rb +109 -0
  332. data/lib/zernio-sdk/models/x_article_entity_one_of.rb +199 -0
  333. data/lib/zernio-sdk/models/x_article_entity_one_of1.rb +199 -0
  334. data/lib/zernio-sdk/models/x_article_entity_one_of1_value.rb +242 -0
  335. data/lib/zernio-sdk/models/x_article_entity_one_of1_value_data.rb +164 -0
  336. data/lib/zernio-sdk/models/x_article_entity_one_of2.rb +199 -0
  337. data/lib/zernio-sdk/models/x_article_entity_one_of2_value.rb +242 -0
  338. data/lib/zernio-sdk/models/x_article_entity_one_of2_value_data.rb +214 -0
  339. data/lib/zernio-sdk/models/x_article_entity_one_of3.rb +199 -0
  340. data/lib/zernio-sdk/models/x_article_entity_one_of3_value.rb +242 -0
  341. data/lib/zernio-sdk/models/x_article_entity_one_of3_value_data.rb +173 -0
  342. data/lib/zernio-sdk/models/x_article_entity_one_of4.rb +199 -0
  343. data/lib/zernio-sdk/models/x_article_entity_one_of4_value.rb +242 -0
  344. data/lib/zernio-sdk/models/x_article_entity_one_of4_value_data.rb +164 -0
  345. data/lib/zernio-sdk/models/x_article_entity_one_of5.rb +199 -0
  346. data/lib/zernio-sdk/models/x_article_entity_one_of5_value.rb +242 -0
  347. data/lib/zernio-sdk/models/x_article_entity_one_of_value.rb +242 -0
  348. data/lib/zernio-sdk/models/x_article_entity_one_of_value_data.rb +173 -0
  349. data/lib/zernio-sdk/models/x_article_entity_range.rb +245 -0
  350. data/lib/zernio-sdk/models/x_article_inline_style_range.rb +259 -0
  351. data/lib/zernio-sdk/models/x_article_text_range.rb +237 -0
  352. data/lib/zernio-sdk/models/you_tube_video_retention_response.rb +1 -1
  353. data/lib/zernio-sdk/models/you_tube_video_retention_response_retention_curve_inner.rb +2 -2
  354. data/lib/zernio-sdk/version.rb +1 -1
  355. data/lib/zernio-sdk.rb +65 -0
  356. data/openapi.yaml +1956 -165
  357. data/spec/api/accounts_api_spec.rb +26 -1
  358. data/spec/api/ad_campaigns_api_spec.rb +9 -4
  359. data/spec/api/ad_insights_api_spec.rb +34 -0
  360. data/spec/api/ad_targeting_api_spec.rb +2 -2
  361. data/spec/api/blogs_api_spec.rb +174 -0
  362. data/spec/api/comments_api_spec.rb +5 -5
  363. data/spec/api/connect_api_spec.rb +27 -0
  364. data/spec/api/messages_api_spec.rb +2 -2
  365. data/spec/api/posts_api_spec.rb +1 -1
  366. data/spec/api/sms_api_spec.rb +1 -1
  367. data/spec/api/whats_app_api_spec.rb +13 -0
  368. data/spec/models/account_with_follower_stats_spec.rb +1 -1
  369. data/spec/models/ad_creative_spec.rb +6 -0
  370. data/spec/models/analytics_list_response_posts_inner_media_items_inner_spec.rb +58 -0
  371. data/spec/models/analytics_single_post_response_media_items_inner_spec.rb +20 -0
  372. data/spec/models/billing_snapshot_plan_spec.rb +6 -0
  373. data/spec/models/billing_snapshot_spec.rb +7 -1
  374. data/spec/models/blog_article_image_spec.rb +42 -0
  375. data/spec/models/blog_article_spec.rb +118 -0
  376. data/spec/models/blog_spec.rb +58 -0
  377. data/spec/models/bluesky_platform_data_spec.rb +6 -0
  378. data/spec/models/connect_shopify_with_token200_response_account_spec.rb +64 -0
  379. data/spec/models/connect_shopify_with_token200_response_spec.rb +36 -0
  380. data/spec/models/connect_shopify_with_token_request_spec.rb +48 -0
  381. data/spec/models/create_blog201_response_spec.rb +46 -0
  382. data/spec/models/create_blog_article201_response_spec.rb +46 -0
  383. data/spec/models/create_blog_article_request_image_spec.rb +42 -0
  384. data/spec/models/create_blog_article_request_seo_spec.rb +42 -0
  385. data/spec/models/create_blog_article_request_spec.rb +90 -0
  386. data/spec/models/create_blog_request_spec.rb +42 -0
  387. data/spec/models/create_inbox_conversation_request_spec.rb +6 -0
  388. data/spec/models/create_standalone_ad_request_spec.rb +16 -0
  389. data/spec/models/create_webhook_settings_request_spec.rb +1 -1
  390. data/spec/models/external_post_media_item_spec.rb +20 -0
  391. data/spec/models/external_post_summary_spec.rb +0 -6
  392. data/spec/models/get_account_health200_response_platform_connection_meta_error_spec.rb +48 -0
  393. data/spec/models/get_account_health200_response_platform_connection_spec.rb +58 -0
  394. data/spec/models/get_account_health200_response_spec.rb +6 -0
  395. data/spec/models/get_ad_analytics202_response_spec.rb +0 -4
  396. data/spec/models/get_ad_tree202_response_spec.rb +0 -4
  397. data/spec/models/get_ads_timeline202_response_spec.rb +0 -4
  398. data/spec/models/get_bluesky_settings200_response_spec.rb +36 -0
  399. data/spec/models/get_campaign_analytics202_response_spec.rb +0 -4
  400. data/spec/models/get_inbox_post_comments200_response_comments_inner_spec.rb +6 -0
  401. data/spec/models/get_phone_number_kyc_form200_response_spec.rb +6 -0
  402. data/spec/models/get_whats_app_number_kyc_form200_response_spec.rb +6 -0
  403. data/spec/models/inbox_webhook_message_spec.rb +1 -1
  404. data/spec/models/linked_in_platform_data_poll_spec.rb +52 -0
  405. data/spec/models/linked_in_platform_data_spec.rb +6 -0
  406. data/spec/models/list_ads202_response_spec.rb +0 -4
  407. data/spec/models/list_blog_articles200_response_spec.rb +52 -0
  408. data/spec/models/list_blogs200_response_spec.rb +52 -0
  409. data/spec/models/list_local_services_lead_conversations200_response_data_inner_message_spec.rb +42 -0
  410. data/spec/models/list_local_services_lead_conversations200_response_data_inner_phone_call_spec.rb +42 -0
  411. data/spec/models/list_local_services_lead_conversations200_response_data_inner_spec.rb +66 -0
  412. data/spec/models/list_local_services_lead_conversations200_response_spec.rb +48 -0
  413. data/spec/models/list_local_services_leads200_response_data_inner_contact_spec.rb +48 -0
  414. data/spec/models/list_local_services_leads200_response_data_inner_spec.rb +96 -0
  415. data/spec/models/list_local_services_leads200_response_spec.rb +48 -0
  416. data/spec/models/list_phone_numbers200_response_connected_inner_spec.rb +6 -0
  417. data/spec/models/list_phone_numbers200_response_numbers_inner_spec.rb +6 -0
  418. data/spec/models/list_whats_app_account_events200_response_events_inner_spec.rb +70 -0
  419. data/spec/models/list_whats_app_account_events200_response_spec.rb +36 -0
  420. data/spec/models/platform_target_spec.rb +1 -1
  421. data/spec/models/send_inbox_message_request_spec.rb +6 -0
  422. data/spec/models/social_account_spec.rb +1 -1
  423. data/spec/models/submit_phone_number_kyc_request_address_spec.rb +6 -0
  424. data/spec/models/twitter_platform_data_spec.rb +6 -0
  425. data/spec/models/update_ad_campaign_status200_response_spec.rb +8 -4
  426. data/spec/models/update_ad_set200_response_spec.rb +12 -0
  427. data/spec/models/update_ad_set_status200_response_spec.rb +22 -0
  428. data/spec/models/update_blog_article_request_spec.rb +90 -0
  429. data/spec/models/update_blog_request_spec.rb +42 -0
  430. data/spec/models/update_bluesky_settings_request_spec.rb +36 -0
  431. data/spec/models/update_webhook_settings_request_spec.rb +1 -1
  432. data/spec/models/validate_phone_number_kyc_address_request_spec.rb +6 -0
  433. data/spec/models/webhook_payload_call_ended_call_spec.rb +12 -0
  434. data/spec/models/webhook_payload_comment_comment_author_spec.rb +6 -0
  435. data/spec/models/webhook_payload_message_message_spec.rb +1 -1
  436. data/spec/models/webhook_payload_message_metadata_referral_spec.rb +6 -0
  437. data/spec/models/webhook_payload_message_metadata_spec.rb +12 -0
  438. data/spec/models/webhook_payload_message_metadata_unsupported_spec.rb +48 -0
  439. data/spec/models/webhook_payload_referral_referral_ads_context_data_spec.rb +66 -0
  440. data/spec/models/webhook_payload_referral_referral_spec.rb +66 -0
  441. data/spec/models/webhook_payload_referral_sender_spec.rb +42 -0
  442. data/spec/models/webhook_payload_referral_spec.rb +76 -0
  443. data/spec/models/webhook_payload_whats_app_template_category_updated_spec.rb +64 -0
  444. data/spec/models/webhook_payload_whats_app_template_category_updated_template_spec.rb +94 -0
  445. data/spec/models/webhook_spec.rb +1 -1
  446. data/spec/models/workflow_node_spec.rb +6 -0
  447. data/spec/models/x_article_block_data_spec.rb +54 -0
  448. data/spec/models/x_article_block_spec.rb +70 -0
  449. data/spec/models/x_article_content_state_spec.rb +42 -0
  450. data/spec/models/x_article_cover_spec.rb +42 -0
  451. data/spec/models/x_article_entity_one_of1_spec.rb +42 -0
  452. data/spec/models/x_article_entity_one_of1_value_data_spec.rb +36 -0
  453. data/spec/models/x_article_entity_one_of1_value_spec.rb +56 -0
  454. data/spec/models/x_article_entity_one_of2_spec.rb +42 -0
  455. data/spec/models/x_article_entity_one_of2_value_data_spec.rb +48 -0
  456. data/spec/models/x_article_entity_one_of2_value_spec.rb +56 -0
  457. data/spec/models/x_article_entity_one_of3_spec.rb +42 -0
  458. data/spec/models/x_article_entity_one_of3_value_data_spec.rb +36 -0
  459. data/spec/models/x_article_entity_one_of3_value_spec.rb +56 -0
  460. data/spec/models/x_article_entity_one_of4_spec.rb +42 -0
  461. data/spec/models/x_article_entity_one_of4_value_data_spec.rb +36 -0
  462. data/spec/models/x_article_entity_one_of4_value_spec.rb +56 -0
  463. data/spec/models/x_article_entity_one_of5_spec.rb +42 -0
  464. data/spec/models/x_article_entity_one_of5_value_spec.rb +56 -0
  465. data/spec/models/x_article_entity_one_of_spec.rb +42 -0
  466. data/spec/models/x_article_entity_one_of_value_data_spec.rb +36 -0
  467. data/spec/models/x_article_entity_one_of_value_spec.rb +56 -0
  468. data/spec/models/x_article_entity_range_spec.rb +48 -0
  469. data/spec/models/x_article_entity_spec.rb +32 -0
  470. data/spec/models/x_article_inline_style_range_spec.rb +52 -0
  471. data/spec/models/x_article_spec.rb +58 -0
  472. data/spec/models/x_article_text_range_spec.rb +48 -0
  473. data/zernio-sdk-0.0.718.gem +0 -0
  474. metadata +1867 -1607
  475. data/zernio-sdk-0.0.716.gem +0 -0
data/openapi.yaml CHANGED
@@ -150,21 +150,23 @@ x-documentation:
150
150
  - `account.connected` - Social account connected
151
151
  - `account.disconnected` - Social account disconnected (token expired)
152
152
  - `account.ads.initial_sync_completed` - Initial ads sync (discovery + 90-day backfill) completed for an ads-enabled account
153
- - `message.received` - New DM received
154
- - `conversation.started` - A new conversation opened between one of your accounts and a contact, in either direction (any DM platform); fires only the first time the thread appears
153
+ - `message.received` - New inbound DM or SMS received (SMS arrives with `platform: "sms"`)
154
+ - `conversation.started` - A new conversation opened between one of your accounts and a contact, in either direction (any DM platform, and SMS); fires only the first time the thread appears
155
155
  - `message.sent` - DM sent via the API
156
156
  - `message.edited` - A sender edited a message (Instagram, Messenger, Telegram)
157
157
  - `message.deleted` - A sender deleted ("unsent") a message (Instagram; WhatsApp when the business deletes a sent message)
158
- - `message.delivered` - An outgoing message was delivered (WhatsApp, Messenger)
158
+ - `message.delivered` - An outgoing message was delivered (WhatsApp, Messenger, SMS)
159
159
  - `message.read` - An outgoing message was read by the recipient (WhatsApp, Messenger, Instagram)
160
- - `message.failed` - An outgoing message failed delivery (WhatsApp)
160
+ - `message.failed` - An outgoing message failed delivery (WhatsApp, SMS; on SMS the carrier's error code rides in `error`)
161
161
  - `reaction.received` - A participant added or removed an emoji reaction on a message (WhatsApp, Telegram, Slack, Instagram, Facebook Messenger)
162
+ - `referral.received` - Someone opened an existing Instagram/Messenger thread through an ig.me/m.me `ref` link or a returning Messenger ad click; carries Meta's referral object (a referral that rides an inbound message arrives on `message.received` under `metadata.referral` instead)
162
163
  - `comment.received` - New comment received on a post
163
164
  - `review.new` - New review posted on a connected account (Google Business Profile)
164
165
  - `review.updated` - Review updated or reply added (Google Business Profile, or via reply API)
165
166
  - `lead.received` - New lead submitted against a Meta Lead Gen (Instant) Form. `lead.fields` is the question-key → answer map; `lead.formId`/`lead.adId` give provenance.
166
167
  - `ad.status_changed` - Ad, ad set, or campaign changed status on the ad platform (Meta: `in_process_ad_objects` entry/exit and `with_issues_ad_objects` violations)
167
168
  - `whatsapp.template.status_updated` - WhatsApp Business template completed (re)review by Meta. `template.status` carries the new state (APPROVED, REJECTED, PENDING, PAUSED, DISABLED, IN_APPEAL, PENDING_DELETION); `template.reason` is Meta's free-form reason or "NONE".
169
+ - `whatsapp.template.category_updated` - Meta reclassified a WhatsApp Business template's category (drives tariff and marketing opt-out applicability). `template.changeType` is `scheduled` (24h advance notice) or `applied`; `template.category` is always the category right now.
168
170
  - `call.received` - An inbound call (phone/PSTN or WhatsApp) reached one of your numbers and was routed to its destination
169
171
  - `call.ended` - A call ended; carries duration, end reason, cost breakdown, and the recording when enabled
170
172
  - `call.failed` - A call failed with a hard error before or during bridging
@@ -201,6 +203,12 @@ tags:
201
203
  - name: Posts
202
204
  description: |
203
205
  Create, schedule, list, update, and delete posts across all connected social accounts.
206
+ - name: Blogs
207
+ description: |
208
+ Manage blogs and blog articles on connected accounts. Currently supported
209
+ for Shopify accounts (scopes read_content and write_content). Blogs are
210
+ the containers; articles are the posts inside them. All content lives on
211
+ the platform; Zernio proxies it and stores nothing.
204
212
  - name: Users
205
213
  description: |
206
214
  Read the authenticated user and team members.
@@ -412,7 +420,7 @@ tags:
412
420
  - name: Webhooks
413
421
  description: |
414
422
  Configure webhooks for real-time notifications. Webhooks can be created from the dashboard (Settings → Webhooks) or via this API.
415
- Events: post.scheduled, post.published, post.failed, post.partial, post.cancelled, post.recycled, post.platform.published, post.platform.failed, post.platform.deleted, post.tiktok.url_resolved, post.external.created, post.external.updated, post.external.deleted, account.connected, account.disconnected, account.ads.initial_sync_completed, message.received, conversation.started, call.received, call.ended, call.failed, call.permission_request, message.sent, message.edited, message.deleted, message.delivered, message.read, message.failed, reaction.received, comment.received, review.new, review.updated, lead.received, ad.status_changed, whatsapp.template.status_updated, whatsapp.automatic_event, whatsapp.number.activated, whatsapp.number.declined, whatsapp.number.action_required, whatsapp.number.verification_required, whatsapp.number.suspended, whatsapp.number.reactivated, whatsapp.number.released, whatsapp.number.kyc_submitted, verification.approved, verification.failed, webhook.test.
423
+ Events: post.scheduled, post.published, post.failed, post.partial, post.cancelled, post.recycled, post.platform.published, post.platform.failed, post.platform.deleted, post.tiktok.url_resolved, post.external.created, post.external.updated, post.external.deleted, account.connected, account.disconnected, account.ads.initial_sync_completed, message.received, conversation.started, call.received, call.ended, call.failed, call.permission_request, message.sent, message.edited, message.deleted, message.delivered, message.read, message.failed, reaction.received, referral.received, comment.received, review.new, review.updated, lead.received, ad.status_changed, whatsapp.template.status_updated, whatsapp.template.category_updated, whatsapp.automatic_event, whatsapp.number.activated, whatsapp.number.declined, whatsapp.number.action_required, whatsapp.number.verification_required, whatsapp.number.suspended, whatsapp.number.reactivated, whatsapp.number.released, whatsapp.number.kyc_submitted, verification.approved, verification.failed, webhook.test.
416
424
  Security: optional HMAC-SHA256 signature in X-Zernio-Signature header. Configure a secret key to enable verification. Custom headers supported.
417
425
 
418
426
  Resource groups. Every event belongs to one group: post.* to publishing, message.*, reaction.*, conversation.*, call.* and whatsapp.automatic_event to messages, comment.* and review.* to engagement, lead.* to contacts, ad.* to ads, account.* and whatsapp.template.* to accounts, whatsapp.number.* and verification.* to telephony, webhook.test to webhooks. Two independent controls use that mapping. (1) A restricted (zrk_) API key can only subscribe to, test-fire, redeliver, or read delivery logs for events in the groups it holds, so it can never create a subscription broader than itself. (2) Each subscription carries its own `disabledResourceGroups` denylist: events in a disabled group are dropped before delivery to that endpoint, on live delivery and on every replay path, no matter which key or session created the subscription. Changing that denylist applies to every event emitted after the change; events already queued when it landed can still be delivered for up to five minutes after they were enqueued, because the delivery worker trusts a five-minute enqueue-time snapshot before re-checking. Absent or empty on a subscription means it receives everything it subscribes to, which is the behavior of every subscription created before the field existed.
@@ -892,8 +900,11 @@ components:
892
900
  maxLength: 3
893
901
  description: |
894
902
  ISO 4217 currency code matching the ad account's currency
895
- (e.g. `USD`). Optional; Meta infers from the ad account
896
- when omitted.
903
+ (e.g. `USD`). Optional: Zernio resolves it from the ad account
904
+ when omitted. The value selects the minor-unit exponent Zernio
905
+ converts budget/bid amounts by before calling Meta (most
906
+ currencies are cents; zero-decimal currencies like JPY/KRW are
907
+ sent as-is).
897
908
  endDate:
898
909
  type: string
899
910
  format: date-time
@@ -1197,6 +1208,11 @@ components:
1197
1208
  properties:
1198
1209
  x: { type: number }
1199
1210
  y: { type: number }
1211
+ label:
1212
+ type: string
1213
+ minLength: 1
1214
+ maxLength: 80
1215
+ description: 'Optional display name shown on the builder canvas and inspector, falling back to the node type when absent. The nodes array is replaced wholesale on update, so it must be resent to be kept.'
1200
1216
  WorkflowEdge:
1201
1217
  type: object
1202
1218
  required: [id, source, target]
@@ -1352,9 +1368,6 @@ components:
1352
1368
  mediaType:
1353
1369
  type: string
1354
1370
  description: Media type (e.g. image, video, carousel)
1355
- mediaUrl:
1356
- type: string
1357
- description: Primary media URL
1358
1371
  thumbnailUrl:
1359
1372
  type: string
1360
1373
  description: Thumbnail URL
@@ -1381,7 +1394,7 @@ components:
1381
1394
  views: { type: integer }
1382
1395
  reach: { type: integer }
1383
1396
  impressions: { type: integer }
1384
- engagementRate: { type: number }
1397
+ engagementRate: { type: number, description: 'Percentage, rounded to 2 decimals. Same definition as PostAnalytics.engagementRate: (likes + comments + shares + saves) / (impressions or reach or views) * 100, where the denominator is the first of the three that is non-zero. Clicks and follows are never counted.' }
1385
1398
  lastUpdated: { type: string, format: date-time, description: When these metrics were last refreshed }
1386
1399
  ErrorResponse:
1387
1400
  type: object
@@ -1943,7 +1956,7 @@ components:
1943
1956
  description: 'Present only when the range reaches into YouTube''s ~3-day processing window: the first date whose numbers are provisional and may still be revised by YouTube.'
1944
1957
  retentionCurve:
1945
1958
  type: array
1946
- description: Up to 100 points covering the video timeline, aggregated over the date range. Empty for videos with very few views.
1959
+ description: 'Up to 100 points covering the video timeline, aggregated over the date range. Can be empty when YouTube has no retention data for the video in the given range.'
1947
1960
  items:
1948
1961
  type: object
1949
1962
  properties:
@@ -1958,10 +1971,10 @@ components:
1958
1971
  description: Retention vs videos of similar length (0 = worst, 0.5 = median, 1 = best)
1959
1972
  startedWatching:
1960
1973
  type: integer
1961
- description: Viewers who started watching in this segment
1974
+ description: 'Viewers who started watching in this segment. 0 when YouTube has no segment-level data for the video.'
1962
1975
  stoppedWatching:
1963
1976
  type: integer
1964
- description: Viewers who stopped watching in this segment
1977
+ description: 'Viewers who stopped watching in this segment. 0 when YouTube has no segment-level data for the video.'
1965
1978
  totalSegmentImpressions:
1966
1979
  type: integer
1967
1980
  description: Total views of this segment, including rewatches
@@ -2325,7 +2338,7 @@ components:
2325
2338
  type: array
2326
2339
  items:
2327
2340
  type: string
2328
- enum: [post.scheduled, post.published, post.failed, post.partial, post.cancelled, post.recycled, post.platform.published, post.platform.failed, post.platform.deleted, post.tiktok.url_resolved, post.external.created, post.external.updated, post.external.deleted, account.connected, account.disconnected, account.ads.initial_sync_completed, message.received, conversation.started, call.received, call.ended, call.failed, call.permission_request, message.sent, message.edited, message.deleted, message.delivered, message.read, message.failed, reaction.received, comment.received, review.new, review.updated, lead.received, ad.status_changed, whatsapp.template.status_updated, whatsapp.automatic_event, whatsapp.number.activated, whatsapp.number.declined, whatsapp.number.action_required, whatsapp.number.verification_required, whatsapp.number.suspended, whatsapp.number.reactivated, whatsapp.number.released, whatsapp.number.kyc_submitted, verification.approved, verification.failed]
2341
+ enum: [post.scheduled, post.published, post.failed, post.partial, post.cancelled, post.recycled, post.platform.published, post.platform.failed, post.platform.deleted, post.tiktok.url_resolved, post.external.created, post.external.updated, post.external.deleted, account.connected, account.disconnected, account.ads.initial_sync_completed, message.received, conversation.started, call.received, call.ended, call.failed, call.permission_request, message.sent, message.edited, message.deleted, message.delivered, message.read, message.failed, reaction.received, referral.received, comment.received, review.new, review.updated, lead.received, ad.status_changed, whatsapp.template.status_updated, whatsapp.template.category_updated, whatsapp.automatic_event, whatsapp.number.activated, whatsapp.number.declined, whatsapp.number.action_required, whatsapp.number.verification_required, whatsapp.number.suspended, whatsapp.number.reactivated, whatsapp.number.released, whatsapp.number.kyc_submitted, verification.approved, verification.failed]
2329
2342
  description: Events subscribed to
2330
2343
  isActive:
2331
2344
  type: boolean
@@ -2462,6 +2475,7 @@ components:
2462
2475
  timestamp:
2463
2476
  type: string
2464
2477
  format: date-time
2478
+ description: 'UTC time at which Zernio generated this event (set once when the event payload is built, before delivery is queued). Retries and redeliveries keep the original value, so it reflects the event, not the delivery attempt.'
2465
2479
  WebhookPayloadPostPlatform:
2466
2480
  type: object
2467
2481
  description: |
@@ -2554,6 +2568,7 @@ components:
2554
2568
  timestamp:
2555
2569
  type: string
2556
2570
  format: date-time
2571
+ description: 'UTC time at which Zernio generated this event (set once when the event payload is built, before delivery is queued). Retries and redeliveries keep the original value, so it reflects the event, not the delivery attempt.'
2557
2572
  WebhookPayloadAccountConnected:
2558
2573
  type: object
2559
2574
  description: Webhook payload for account connected events
@@ -2584,6 +2599,7 @@ components:
2584
2599
  timestamp:
2585
2600
  type: string
2586
2601
  format: date-time
2602
+ description: 'UTC time at which Zernio generated this event (set once when the event payload is built, before delivery is queued). Retries and redeliveries keep the original value, so it reflects the event, not the delivery attempt.'
2587
2603
  WebhookPayloadAccountDisconnected:
2588
2604
  type: object
2589
2605
  description: Webhook payload for account disconnected events
@@ -2621,6 +2637,7 @@ components:
2621
2637
  timestamp:
2622
2638
  type: string
2623
2639
  format: date-time
2640
+ description: 'UTC time at which Zernio generated this event (set once when the event payload is built, before delivery is queued). Retries and redeliveries keep the original value, so it reflects the event, not the delivery attempt.'
2624
2641
  WebhookPayloadAccountAdsInitialSyncCompleted:
2625
2642
  type: object
2626
2643
  description: |
@@ -2723,6 +2740,7 @@ components:
2723
2740
  timestamp:
2724
2741
  type: string
2725
2742
  format: date-time
2743
+ description: 'UTC time at which Zernio generated this event (set once when the event payload is built, before delivery is queued). Retries and redeliveries keep the original value, so it reflects the event, not the delivery attempt.'
2726
2744
  WebhookPayloadComment:
2727
2745
  type: object
2728
2746
  description: Webhook payload for comment received events (Instagram, Facebook, Twitter/X, YouTube, LinkedIn, Bluesky, Reddit)
@@ -2766,6 +2784,9 @@ components:
2766
2784
  type: string
2767
2785
  picture:
2768
2786
  type: [string, "null"]
2787
+ isOwnAccount:
2788
+ type: boolean
2789
+ description: 'True when this comment was authored by the connected account itself (Meta re-delivers the account''s own replies as comments events). Populated on the Instagram and Facebook realtime webhooks only; absent means not evaluated, never "not the account".'
2769
2790
  instagramProfile:
2770
2791
  type: object
2771
2792
  description: |
@@ -2867,6 +2888,7 @@ components:
2867
2888
  timestamp:
2868
2889
  type: string
2869
2890
  format: date-time
2891
+ description: 'UTC time at which Zernio generated this event (set once when the event payload is built, before delivery is queued). Retries and redeliveries keep the original value, so it reflects the event, not the delivery attempt.'
2870
2892
  WebhookPayloadLead:
2871
2893
  type: object
2872
2894
  description: Webhook payload for lead.received events (Meta Lead Gen / Instant Forms).
@@ -2931,6 +2953,7 @@ components:
2931
2953
  timestamp:
2932
2954
  type: string
2933
2955
  format: date-time
2956
+ description: 'UTC time at which Zernio generated this event (set once when the event payload is built, before delivery is queued). Retries and redeliveries keep the original value, so it reflects the event, not the delivery attempt.'
2934
2957
  ReviewWebhookReview:
2935
2958
  type: object
2936
2959
  description: Review data shared by review.new and review.updated payloads.
@@ -3007,6 +3030,7 @@ components:
3007
3030
  timestamp:
3008
3031
  type: string
3009
3032
  format: date-time
3033
+ description: 'UTC time at which Zernio generated this event (set once when the event payload is built, before delivery is queued). Retries and redeliveries keep the original value, so it reflects the event, not the delivery attempt.'
3010
3034
  WebhookPayloadReviewUpdated:
3011
3035
  type: object
3012
3036
  description: |
@@ -3040,6 +3064,7 @@ components:
3040
3064
  timestamp:
3041
3065
  type: string
3042
3066
  format: date-time
3067
+ description: 'UTC time at which Zernio generated this event (set once when the event payload is built, before delivery is queued). Retries and redeliveries keep the original value, so it reflects the event, not the delivery attempt.'
3043
3068
  ExternalPostWebhookPost:
3044
3069
  type: object
3045
3070
  description: Native (external) post data shared by all post.external.* payloads.
@@ -3114,6 +3139,7 @@ components:
3114
3139
  timestamp:
3115
3140
  type: string
3116
3141
  format: date-time
3142
+ description: 'UTC time at which Zernio generated this event (set once when the event payload is built, before delivery is queued). Retries and redeliveries keep the original value, so it reflects the event, not the delivery attempt.'
3117
3143
  # ─── Shared sub-schemas for inbox lifecycle webhook payloads ────────────
3118
3144
  # WebhookPayloadMessageEdited / Deleted / DeliveryStatus reference these
3119
3145
  # as $refs. They were added as top-level named schemas (rather than nested
@@ -3133,7 +3159,7 @@ components:
3133
3159
  description: Internal conversation ID
3134
3160
  platform:
3135
3161
  type: string
3136
- enum: [instagram, facebook, telegram, whatsapp]
3162
+ enum: [instagram, facebook, telegram, whatsapp, sms]
3137
3163
  platformMessageId:
3138
3164
  type: string
3139
3165
  description: Platform's message ID
@@ -3303,7 +3329,7 @@ components:
3303
3329
  sender:
3304
3330
  type: object
3305
3331
  required: [id]
3306
- description: 'Whoever added or removed the reaction. Usually the participant, but on Slack, Instagram and Facebook Messenger it is the business own platform id when the reaction was made from the native app: compare it with conversation.participantId.'
3332
+ description: 'Whoever added or removed the reaction. Usually the participant, but on WhatsApp, Slack, Instagram and Facebook Messenger it is the business own platform id when the business reacted from the native app or via the reactions API: compare it with conversation.participantId.'
3307
3333
  properties:
3308
3334
  id:
3309
3335
  type: string
@@ -3329,6 +3355,77 @@ components:
3329
3355
  timestamp:
3330
3356
  type: string
3331
3357
  format: date-time
3358
+ description: 'UTC time at which Zernio generated this event (set once when the event payload is built, before delivery is queued). Retries and redeliveries keep the original value, so it reflects the event, not the delivery attempt.'
3359
+ # ────────────────────────────────────────────────────────────────────────
3360
+ WebhookPayloadReferral:
3361
+ type: object
3362
+ description: 'Webhook payload for referral received events (Instagram, Facebook Messenger)'
3363
+ required: [id, event, referral, sender, conversation, account, timestamp]
3364
+ properties:
3365
+ id:
3366
+ type: string
3367
+ description: Stable webhook event ID
3368
+ event:
3369
+ type: string
3370
+ enum: [referral.received]
3371
+ referral:
3372
+ type: object
3373
+ description: |
3374
+ Meta's referral object, forwarded verbatim. Same shape as
3375
+ `metadata.referral` on `message.received`: `ref` + `source` for
3376
+ ig.me / m.me links, `ad_id` + `ads_context_data` for returning
3377
+ Messenger ad clicks.
3378
+ properties:
3379
+ ref:
3380
+ type: string
3381
+ description: The `ref` parameter of the clicked ig.me / m.me link or ad.
3382
+ source:
3383
+ type: string
3384
+ description: 'Meta-supplied source (`SHORTLINK`, `SHORTLINKS`, `IGME-SOURCE-LINK`, `ADS` - treat as opaque).'
3385
+ type:
3386
+ type: string
3387
+ description: Meta-supplied referral type (e.g. `OPEN_THREAD`).
3388
+ referer_uri:
3389
+ type: string
3390
+ description: URI of the originating site, when Meta supplies one. Facebook Messenger only.
3391
+ ad_id:
3392
+ type: string
3393
+ description: The Meta ad ID, on returning ad clicks. Facebook Messenger only.
3394
+ ads_context_data:
3395
+ type: object
3396
+ description: Snapshot of the ad's public context at click time.
3397
+ properties:
3398
+ ad_title:
3399
+ type: string
3400
+ photo_url:
3401
+ type: string
3402
+ video_url:
3403
+ type: string
3404
+ post_id:
3405
+ type: string
3406
+ product_id:
3407
+ type: string
3408
+ flow_id:
3409
+ type: string
3410
+ sender:
3411
+ type: object
3412
+ required: [id]
3413
+ description: Who clicked - the conversation participant.
3414
+ properties:
3415
+ id:
3416
+ type: string
3417
+ description: Platform-scoped user ID (IGSID / PSID).
3418
+ contactId:
3419
+ type: string
3420
+ description: Zernio CRM Contact id for this sender, when one exists.
3421
+ conversation:
3422
+ $ref: '#/components/schemas/InboxWebhookConversation'
3423
+ account:
3424
+ $ref: '#/components/schemas/InboxWebhookAccount'
3425
+ timestamp:
3426
+ type: string
3427
+ format: date-time
3428
+ description: 'UTC time at which Zernio generated this event (set once when the event payload is built, before delivery is queued). Retries and redeliveries keep the original value, so it reflects the event, not the delivery attempt.'
3332
3429
  # ────────────────────────────────────────────────────────────────────────
3333
3430
  WebhookPayloadMessage:
3334
3431
  type: object
@@ -3353,7 +3450,7 @@ components:
3353
3450
  description: Internal conversation ID
3354
3451
  platform:
3355
3452
  type: string
3356
- enum: [instagram, facebook, telegram, whatsapp]
3453
+ enum: [instagram, facebook, telegram, whatsapp, sms]
3357
3454
  platformMessageId:
3358
3455
  type: string
3359
3456
  description: Platform's message ID
@@ -3606,17 +3703,27 @@ components:
3606
3703
  referral:
3607
3704
  type: [object, "null"]
3608
3705
  description: |
3609
- Ad-click attribution forwarded verbatim from Meta. Populated only on
3706
+ Click attribution forwarded verbatim from Meta. Populated only on
3610
3707
  the FIRST inbound message after the click; absent on subsequent
3611
- messages of the same conversation.
3708
+ messages of the same conversation. On Instagram and Messenger a
3709
+ RETURNING click also attaches it to the first message that
3710
+ follows, so read it on every `message.received` for per-click
3711
+ attribution; a click that opens an existing thread WITHOUT a
3712
+ message arrives as the separate `referral.received` event.
3612
3713
 
3613
- The populated subset identifies the source platform:
3714
+ The populated subset identifies the source:
3614
3715
  - `ctwa_clid` and `source_*` fields: WhatsApp CTWA
3615
3716
  (Click-to-WhatsApp). Attribution window is 7 days from click.
3616
3717
  Forward to Meta Conversions API for Business Messaging replay.
3617
3718
  - `ad_id` and `ads_context_data`: Facebook Messenger CTM
3618
3719
  (Click-to-Message) or Instagram CTD (Click-to-Direct). Use
3619
3720
  `ad_id` to attribute the conversation to a specific ad.
3721
+ - `ref` without `ad_id`: an ig.me / m.me link carrying a
3722
+ `?ref=` parameter (`source` is `SHORTLINK`, `SHORTLINKS` or
3723
+ `IGME-SOURCE-LINK` depending on surface - treat it as
3724
+ opaque). Instagram delivers ig.me refs on new threads only
3725
+ when the account has at least one Ice Breaker configured
3726
+ (`PUT /v1/accounts/{accountId}/instagram-ice-breakers`).
3620
3727
  properties:
3621
3728
  ctwa_clid:
3622
3729
  type: string
@@ -3647,18 +3754,25 @@ components:
3647
3754
  ref:
3648
3755
  type: string
3649
3756
  description: |
3650
- Optional `ref` parameter passed through from the Meta ad
3651
- creative. Facebook Messenger CTM / Instagram CTD only.
3757
+ The `ref` parameter passed through from the Meta ad creative
3758
+ or from an ig.me / m.me link. Instagram / Facebook Messenger
3759
+ only.
3652
3760
  source:
3653
3761
  type: string
3654
3762
  description: |
3655
- Meta-supplied source identifier (e.g. `ADS`). Facebook Messenger
3656
- CTM / Instagram CTD only.
3763
+ Meta-supplied source identifier (`ADS` for ad clicks;
3764
+ `SHORTLINK`, `SHORTLINKS` or `IGME-SOURCE-LINK` for ref
3765
+ links). Instagram / Facebook Messenger only.
3657
3766
  type:
3658
3767
  type: string
3659
3768
  description: |
3660
- Meta-supplied referral type (e.g. `OPEN_THREAD`). Facebook
3661
- Messenger CTM / Instagram CTD only.
3769
+ Meta-supplied referral type (e.g. `OPEN_THREAD`). Instagram /
3770
+ Facebook Messenger only.
3771
+ referer_uri:
3772
+ type: string
3773
+ description: |
3774
+ URI of the originating site, when Meta supplies one (m.me
3775
+ links opened from the web). Facebook Messenger only.
3662
3776
  ads_context_data:
3663
3777
  type: object
3664
3778
  description: |
@@ -3677,9 +3791,39 @@ components:
3677
3791
  type: string
3678
3792
  flow_id:
3679
3793
  type: string
3794
+ unsupported:
3795
+ type: object
3796
+ description: |
3797
+ WhatsApp only. Meta's own reason this message has no
3798
+ renderable body. Present when Meta attached an error to the
3799
+ inbound payload; in practice the `unsupported`, `errors` and
3800
+ `unknown` types (code 131051: message type currently not
3801
+ supported). `text` on those messages is the fixed
3802
+ `[Unsupported message]` placeholder.
3803
+ properties:
3804
+ code:
3805
+ type: integer
3806
+ description: Meta's numeric error code (e.g. 131051).
3807
+ title:
3808
+ type: string
3809
+ description: Meta's short error title.
3810
+ details:
3811
+ type: string
3812
+ description: Meta's human-readable error detail string.
3813
+ noRenderableContent:
3814
+ type: boolean
3815
+ description: |
3816
+ Instagram / Facebook Messenger only. Set when the message
3817
+ carries nothing an integrator can render (a `template`
3818
+ attachment with no text and no parseable content, or Meta's
3819
+ own `is_unsupported` flag). Sibling of `unsupported` above
3820
+ (WhatsApp only, carries Meta's error code/title/details): this
3821
+ field has no error envelope, just the boolean. Absence means
3822
+ "not flagged", never "checked and renderable".
3680
3823
  timestamp:
3681
3824
  type: string
3682
3825
  format: date-time
3826
+ description: 'UTC time at which Zernio generated this event (set once when the event payload is built, before delivery is queued). Retries and redeliveries keep the original value, so it reflects the event, not the delivery attempt.'
3683
3827
  WebhookPayloadMessageSent:
3684
3828
  type: object
3685
3829
  description: Webhook payload for message sent events (fired when a message is sent via the API, or from the WhatsApp Business app on Coexistence numbers)
@@ -3765,6 +3909,7 @@ components:
3765
3909
  timestamp:
3766
3910
  type: string
3767
3911
  format: date-time
3912
+ description: 'UTC time at which Zernio generated this event (set once when the event payload is built, before delivery is queued). Retries and redeliveries keep the original value, so it reflects the event, not the delivery attempt.'
3768
3913
  InboxMessageEditAttachment:
3769
3914
  type: object
3770
3915
  description: Attachment snapshot inside an edit-history entry.
@@ -3817,7 +3962,10 @@ components:
3817
3962
  $ref: '#/components/schemas/InboxWebhookConversation'
3818
3963
  account:
3819
3964
  $ref: '#/components/schemas/InboxWebhookAccount'
3820
- timestamp: { type: string, format: date-time }
3965
+ timestamp:
3966
+ type: string
3967
+ format: date-time
3968
+ description: 'UTC time at which Zernio generated this event (set once when the event payload is built, before delivery is queued). Retries and redeliveries keep the original value, so it reflects the event, not the delivery attempt.'
3821
3969
  WebhookPayloadMessageDeleted:
3822
3970
  type: object
3823
3971
  description: |
@@ -3843,7 +3991,10 @@ components:
3843
3991
  $ref: '#/components/schemas/InboxWebhookConversation'
3844
3992
  account:
3845
3993
  $ref: '#/components/schemas/InboxWebhookAccount'
3846
- timestamp: { type: string, format: date-time }
3994
+ timestamp:
3995
+ type: string
3996
+ format: date-time
3997
+ description: 'UTC time at which Zernio generated this event (set once when the event payload is built, before delivery is queued). Retries and redeliveries keep the original value, so it reflects the event, not the delivery attempt.'
3847
3998
  WebhookPayloadMessageDeliveryStatus:
3848
3999
  type: object
3849
4000
  description: |
@@ -3852,10 +4003,12 @@ components:
3852
4003
  delivery state for an outgoing message.
3853
4004
 
3854
4005
  Platform support:
3855
- * message.delivered — WhatsApp, Facebook Messenger.
3856
- * message.read — WhatsApp, Facebook Messenger, Instagram.
3857
- * message.failed — WhatsApp only (other platforms don't expose
3858
- per-message failure via webhook).
4006
+ * message.delivered: WhatsApp, Facebook Messenger, SMS.
4007
+ * message.read: WhatsApp, Facebook Messenger, Instagram. Not SMS
4008
+ (carriers report delivery, never read).
4009
+ * message.failed: WhatsApp and SMS (other platforms don't expose
4010
+ per-message failure via webhook). On SMS, `error.code` is the
4011
+ carrier's numeric code and `error.message` its reason.
3859
4012
  required: [id, event, message, statusAt, conversation, account, timestamp]
3860
4013
  properties:
3861
4014
  id: { type: string }
@@ -3886,7 +4039,10 @@ components:
3886
4039
  $ref: '#/components/schemas/InboxWebhookConversation'
3887
4040
  account:
3888
4041
  $ref: '#/components/schemas/InboxWebhookAccount'
3889
- timestamp: { type: string, format: date-time }
4042
+ timestamp:
4043
+ type: string
4044
+ format: date-time
4045
+ description: 'UTC time at which Zernio generated this event (set once when the event payload is built, before delivery is queued). Retries and redeliveries keep the original value, so it reflects the event, not the delivery attempt.'
3890
4046
  WebhookPayloadConversationStarted:
3891
4047
  type: object
3892
4048
  description: |
@@ -3931,7 +4087,10 @@ components:
3931
4087
  type: string
3932
4088
  format: date-time
3933
4089
  description: When the conversation document was created.
3934
- timestamp: { type: string, format: date-time }
4090
+ timestamp:
4091
+ type: string
4092
+ format: date-time
4093
+ description: 'UTC time at which Zernio generated this event (set once when the event payload is built, before delivery is queued). Retries and redeliveries keep the original value, so it reflects the event, not the delivery attempt.'
3935
4094
  CallRecord:
3936
4095
  type: object
3937
4096
  description: |
@@ -4027,7 +4186,10 @@ components:
4027
4186
  conversationId: { type: string }
4028
4187
  startedAt: { type: string, format: date-time }
4029
4188
  account: { $ref: '#/components/schemas/InboxWebhookAccount' }
4030
- timestamp: { type: string, format: date-time }
4189
+ timestamp:
4190
+ type: string
4191
+ format: date-time
4192
+ description: 'UTC time at which Zernio generated this event (set once when the event payload is built, before delivery is queued). Retries and redeliveries keep the original value, so it reflects the event, not the delivery attempt.'
4031
4193
  WebhookPayloadCallEnded:
4032
4194
  type: object
4033
4195
  description: |
@@ -4053,6 +4215,8 @@ components:
4053
4215
  endedAt: { type: string, format: date-time }
4054
4216
  durationSeconds: { type: integer }
4055
4217
  endReason: { type: string, enum: [hangup, no_answer, rejected, error] }
4218
+ hangupCause: { type: [string, "null"], description: "Raw carrier hangup cause behind endReason (e.g. normal_clearing, call_rejected, not_found). Null when the carrier reported none." }
4219
+ sipHangupCause: { type: [string, "null"], description: "SIP response code that ended the call when SIP-signalled (e.g. '403', '486', '603'). endReason collapses all three to 'rejected', so this is what separates a refused destination from a busy line. Null on non-SIP legs." }
4056
4220
  recordingUrl: { type: string }
4057
4221
  recordingExpiresAt: { type: string, format: date-time }
4058
4222
  billing:
@@ -4064,7 +4228,10 @@ components:
4064
4228
  billableCostUSD: { type: number, description: 'The amount Zernio bills you = Telnyx leg + recording. Excludes Meta (billed by Meta directly).' }
4065
4229
  totalCostUSD: { type: number, description: 'Full economic cost incl. the Meta portion you pay directly (Meta + Telnyx + recording). Display only, not the Zernio-billed amount.' }
4066
4230
  account: { $ref: '#/components/schemas/InboxWebhookAccount' }
4067
- timestamp: { type: string, format: date-time }
4231
+ timestamp:
4232
+ type: string
4233
+ format: date-time
4234
+ description: 'UTC time at which Zernio generated this event (set once when the event payload is built, before delivery is queued). Retries and redeliveries keep the original value, so it reflects the event, not the delivery attempt.'
4068
4235
  WebhookPayloadCallFailed:
4069
4236
  type: object
4070
4237
  description: |
@@ -4093,7 +4260,10 @@ components:
4093
4260
  code: { type: integer }
4094
4261
  message: { type: string }
4095
4262
  account: { $ref: '#/components/schemas/InboxWebhookAccount' }
4096
- timestamp: { type: string, format: date-time }
4263
+ timestamp:
4264
+ type: string
4265
+ format: date-time
4266
+ description: 'UTC time at which Zernio generated this event (set once when the event payload is built, before delivery is queued). Retries and redeliveries keep the original value, so it reflects the event, not the delivery attempt.'
4097
4267
  WebhookPayloadCallPermissionRequest:
4098
4268
  type: object
4099
4269
  description: |
@@ -4115,7 +4285,10 @@ components:
4115
4285
  expirationTimestamp: { type: string, format: date-time, description: "Present only when temporary" }
4116
4286
  responseSource: { type: string, description: "Meta's response source, typically `user_action`" }
4117
4287
  account: { $ref: '#/components/schemas/InboxWebhookAccount' }
4118
- timestamp: { type: string, format: date-time }
4288
+ timestamp:
4289
+ type: string
4290
+ format: date-time
4291
+ description: 'UTC time at which Zernio generated this event (set once when the event payload is built, before delivery is queued). Retries and redeliveries keep the original value, so it reflects the event, not the delivery attempt.'
4119
4292
  WebhookPayloadAdStatusChanged:
4120
4293
  type: object
4121
4294
  description: |
@@ -4224,7 +4397,7 @@ components:
4224
4397
  timestamp:
4225
4398
  type: string
4226
4399
  format: date-time
4227
- description: ISO-8601 timestamp the webhook was produced.
4400
+ description: 'UTC time at which Zernio generated this event (set once when the event payload is built, before delivery is queued). Retries and redeliveries keep the original value, so it reflects the event, not the delivery attempt.'
4228
4401
  WebhookPayloadWhatsAppTemplateStatusUpdated:
4229
4402
  type: object
4230
4403
  description: |
@@ -4277,7 +4450,70 @@ components:
4277
4450
  timestamp:
4278
4451
  type: string
4279
4452
  format: date-time
4280
- description: ISO-8601 timestamp the webhook was produced.
4453
+ description: 'UTC time at which Zernio generated this event (set once when the event payload is built, before delivery is queued). Retries and redeliveries keep the original value, so it reflects the event, not the delivery attempt.'
4454
+ WebhookPayloadWhatsAppTemplateCategoryUpdated:
4455
+ type: object
4456
+ description: |
4457
+ Webhook payload for the `whatsapp.template.category_updated` event.
4458
+ Fired when Meta reclassifies a template's category attached to a
4459
+ connected WABA. Maps Meta's `template_category_update` field onto
4460
+ our event envelope.
4461
+ required: [id, event, account, template, timestamp]
4462
+ properties:
4463
+ id:
4464
+ type: string
4465
+ description: Stable webhook event ID
4466
+ event:
4467
+ type: string
4468
+ enum: [whatsapp.template.category_updated]
4469
+ account:
4470
+ type: object
4471
+ required: [accountId, profileId, platform, username]
4472
+ properties:
4473
+ accountId: { type: string }
4474
+ profileId: { type: string }
4475
+ platform: { type: string, enum: [whatsapp] }
4476
+ username: { type: string }
4477
+ displayName: { type: string }
4478
+ template:
4479
+ type: object
4480
+ required: [templateId, name, language, changeType, category]
4481
+ properties:
4482
+ templateId:
4483
+ type: string
4484
+ description: Meta's `message_template_id`, returned as a string.
4485
+ name:
4486
+ type: string
4487
+ description: Meta's `message_template_name`.
4488
+ language:
4489
+ type: string
4490
+ description: Meta's `message_template_language` (e.g. `en_US`).
4491
+ changeType:
4492
+ type: string
4493
+ enum: [scheduled, applied]
4494
+ description: |
4495
+ `scheduled` is Meta's 24h advance notice of an upcoming
4496
+ reclassification; `applied` is the change taking effect.
4497
+ category:
4498
+ type: string
4499
+ enum: [UTILITY, MARKETING, AUTHENTICATION]
4500
+ description: The category right now, regardless of changeType.
4501
+ previousCategory:
4502
+ type: string
4503
+ enum: [UTILITY, MARKETING, AUTHENTICATION]
4504
+ description: Present only when changeType is `applied`. The category before this change.
4505
+ scheduledCategory:
4506
+ type: string
4507
+ enum: [UTILITY, MARKETING, AUTHENTICATION]
4508
+ description: Present only when changeType is `scheduled`. The category that will take effect at `effectiveAt`.
4509
+ effectiveAt:
4510
+ type: string
4511
+ format: date-time
4512
+ description: Present only when changeType is `scheduled`. ISO-8601 timestamp when the scheduled category takes effect.
4513
+ timestamp:
4514
+ type: string
4515
+ format: date-time
4516
+ description: 'UTC time at which Zernio generated this event (set once when the event payload is built, before delivery is queued). Retries and redeliveries keep the original value, so it reflects the event, not the delivery attempt.'
4281
4517
  WebhookPayloadTest:
4282
4518
  type: object
4283
4519
  description: Webhook payload for test deliveries
@@ -4295,6 +4531,7 @@ components:
4295
4531
  timestamp:
4296
4532
  type: string
4297
4533
  format: date-time
4534
+ description: 'UTC time at which Zernio generated this test event (set once when the payload is built). Test fires are sent synchronously as a single attempt; a later redelivery of this event keeps the original value.'
4298
4535
  DiscordGuildMember:
4299
4536
  type: object
4300
4537
  description: A Discord guild member, returned verbatim from Discord's API.
@@ -4387,18 +4624,34 @@ components:
4387
4624
  description: >
4388
4625
  A media item on a native (external/synced) post, as carried by post.external.*
4389
4626
  webhook payloads. Distinct from the richer MediaItem used for Zernio-authored
4390
- posts: external items are always already-published (url required) and limited to
4391
- image or video. Kept as a separate schema so the generated SDK model does not
4392
- collide with MediaItem.
4627
+ posts: external items are always already-published and limited to image or
4628
+ video. Kept as a separate schema so the generated SDK model does not collide
4629
+ with MediaItem.
4393
4630
  required: [type, url]
4394
4631
  properties:
4395
4632
  type:
4396
4633
  type: string
4397
4634
  enum: [image, video]
4398
4635
  url:
4399
- type: string
4636
+ type: [string, "null"]
4637
+ description: >-
4638
+ 'Direct URL to the media file. Null when the platform withholds it: check
4639
+ mediaStatus before downloading. Instagram omits the video file for Reels it
4640
+ flags as containing copyrighted material (its docs name audio as the usual
4641
+ cause), so type stays "video" while the file is permanently unreachable.
4642
+ For LinkedIn videos where the platform returns no file, url falls back to
4643
+ the cover image and the item carries mediaStatus: unavailable.'
4400
4644
  thumbnail:
4401
4645
  type: string
4646
+ description: 'Cover image. Still present when url is null.'
4647
+ mediaStatus:
4648
+ type: string
4649
+ enum: [available, unavailable]
4650
+ description: 'unavailable means the media file could not be retrieved (url is null or, for LinkedIn videos, a cover image standing in for the file). available or absent means the file is available at url (older synced items omit the field).'
4651
+ unavailableReason:
4652
+ type: string
4653
+ enum: [platform_withheld]
4654
+ description: 'Why the file is missing. platform_withheld means the platform declined to return it and retrying will not help.'
4402
4655
  PlatformTarget:
4403
4656
  type: object
4404
4657
  properties:
@@ -4449,9 +4702,8 @@ components:
4449
4702
  description: The native post ID on the platform (populated after successful publish)
4450
4703
  example: "1234567890123456789"
4451
4704
  platformPostUrl:
4452
- type: string
4453
- format: uri
4454
- description: Public URL of the published post. Included in the response for immediate posts; for scheduled posts, fetch via GET /v1/posts/{postId} after publish time.
4705
+ type: [string, "null"]
4706
+ description: Public URL of the published post. Included in the response for immediate posts; for scheduled posts, fetch via GET /v1/posts/{postId} after publish time. Empty when the platform confirmed the publish without returning an id a permalink can be built from (TikTok returns a publish id for some uploads); the TikTok reconcile cron backfills it later.
4455
4707
  example: "https://twitter.com/acmecorp/status/1234567890123456789"
4456
4708
  publishedAt:
4457
4709
  type: string
@@ -4488,8 +4740,8 @@ components:
4488
4740
  description: Human-readable error message when status is failed. Contains platform-specific error details explaining why the publish failed.
4489
4741
  errorCategory:
4490
4742
  type: string
4491
- enum: [auth_expired, user_content, user_abuse, account_issue, platform_rejected, platform_error, system_error, unknown]
4492
- description: "Error category for programmatic handling: auth_expired (token expired/revoked), user_content (wrong format/too long), user_abuse (rate limits/spam), account_issue (config problems), platform_rejected (policy violation), platform_error (5xx/maintenance), system_error (Zernio infra), unknown"
4743
+ enum: [auth_expired, user_content, user_abuse, account_issue, platform_rejected, platform_error, platform_rate_limit, quota_exhausted, system_error, unknown]
4744
+ description: "Error category for programmatic handling: auth_expired (token expired/revoked), user_content (wrong format/too long), user_abuse (rate limits/spam), account_issue (config problems), platform_rejected (policy violation), platform_error (5xx/maintenance), platform_rate_limit (platform throttling, retried automatically), quota_exhausted (shared daily API quota empty, resumes at the platform's reset), system_error (Zernio infra), unknown"
4493
4745
  errorSource:
4494
4746
  type: string
4495
4747
  enum: [user, platform, system]
@@ -4631,9 +4883,239 @@ components:
4631
4883
  format: date-time
4632
4884
  description: When the last recycled copy was created (read-only)
4633
4885
 
4886
+ XArticleTextRange:
4887
+ type: object
4888
+ additionalProperties: false
4889
+ required: [from_index, to_index, text]
4890
+ properties:
4891
+ from_index:
4892
+ type: integer
4893
+ minimum: 0
4894
+ description: Inclusive start index in the block text.
4895
+ to_index:
4896
+ type: integer
4897
+ minimum: 0
4898
+ description: Exclusive end index in the block text. Must be greater than or equal to from_index and no greater than the block text length.
4899
+ text: { type: string, description: Text represented by this metadata range. }
4900
+
4901
+ XArticleInlineStyleRange:
4902
+ type: object
4903
+ additionalProperties: false
4904
+ required: [offset, length, style]
4905
+ properties:
4906
+ offset: { type: integer, minimum: 0 }
4907
+ length: { type: integer, minimum: 0 }
4908
+ style: { type: string, enum: [bold, italic, strikethrough] }
4909
+ description: The offset plus length must not exceed the containing block's text length.
4910
+
4911
+ XArticleEntityRange:
4912
+ type: object
4913
+ additionalProperties: false
4914
+ required: [key, offset, length]
4915
+ properties:
4916
+ key:
4917
+ type: integer
4918
+ minimum: 0
4919
+ description: Zero-based index into content_state.entities.
4920
+ offset: { type: integer, minimum: 0 }
4921
+ length: { type: integer, minimum: 0 }
4922
+ description: The referenced entity must exist, and offset plus length must not exceed the containing block's text length.
4923
+
4924
+ XArticleBlock:
4925
+ type: object
4926
+ additionalProperties: false
4927
+ required: [type, text]
4928
+ properties:
4929
+ type:
4930
+ type: string
4931
+ enum: [unstyled, header-one, header-two, header-three, unordered-list-item, ordered-list-item, blockquote, atomic]
4932
+ text: { type: string }
4933
+ key: { type: string, minLength: 1 }
4934
+ data:
4935
+ type: object
4936
+ additionalProperties: false
4937
+ properties:
4938
+ cashtags: { type: array, items: { $ref: '#/components/schemas/XArticleTextRange' } }
4939
+ hashtags: { type: array, items: { $ref: '#/components/schemas/XArticleTextRange' } }
4940
+ mentions: { type: array, items: { $ref: '#/components/schemas/XArticleTextRange' } }
4941
+ urls: { type: array, items: { $ref: '#/components/schemas/XArticleTextRange' } }
4942
+ inline_style_ranges:
4943
+ type: array
4944
+ items: { $ref: '#/components/schemas/XArticleInlineStyleRange' }
4945
+ entity_ranges:
4946
+ type: array
4947
+ items: { $ref: '#/components/schemas/XArticleEntityRange' }
4948
+
4949
+ XArticleEntity:
4950
+ oneOf:
4951
+ - type: object
4952
+ additionalProperties: false
4953
+ required: [key, value]
4954
+ properties:
4955
+ key: { type: string, minLength: 1 }
4956
+ value:
4957
+ type: object
4958
+ additionalProperties: false
4959
+ required: [type, mutability, data]
4960
+ properties:
4961
+ type: { type: string, const: post }
4962
+ mutability: { type: string, enum: [immutable, mutable, segmented] }
4963
+ data:
4964
+ type: object
4965
+ additionalProperties: false
4966
+ required: [post_id]
4967
+ properties:
4968
+ post_id: { type: string, minLength: 1 }
4969
+ - type: object
4970
+ additionalProperties: false
4971
+ required: [key, value]
4972
+ properties:
4973
+ key: { type: string, minLength: 1 }
4974
+ value:
4975
+ type: object
4976
+ additionalProperties: false
4977
+ required: [type, mutability, data]
4978
+ properties:
4979
+ type: { type: string, const: link }
4980
+ mutability: { type: string, enum: [immutable, mutable, segmented] }
4981
+ data:
4982
+ type: object
4983
+ additionalProperties: false
4984
+ required: [url]
4985
+ properties:
4986
+ url: { type: string, format: uri }
4987
+ - type: object
4988
+ additionalProperties: false
4989
+ required: [key, value]
4990
+ properties:
4991
+ key: { type: string, minLength: 1 }
4992
+ value:
4993
+ type: object
4994
+ additionalProperties: false
4995
+ required: [type, mutability, data]
4996
+ properties:
4997
+ type: { type: string, const: image }
4998
+ mutability: { type: string, enum: [immutable, mutable, segmented] }
4999
+ data:
5000
+ type: object
5001
+ additionalProperties: false
5002
+ required: [url]
5003
+ properties:
5004
+ url:
5005
+ type: string
5006
+ format: uri
5007
+ pattern: '^https?://.+\.(?:[jJ][pP][eE]?[gG]|[pP][nN][gG]|[wW][eE][bB][pP]|[gG][iI][fF]|[mM][pP]4)(?:[?#].*)?$'
5008
+ description: Public image, GIF, or MP4 URL. Zernio uploads it to X and replaces it with native media metadata. Caller-supplied media IDs are rejected.
5009
+ caption: { type: string }
5010
+ altText:
5011
+ type: string
5012
+ maxLength: 1000
5013
+ description: Supported for image and GIF URLs; rejected for MP4 media.
5014
+ - type: object
5015
+ additionalProperties: false
5016
+ required: [key, value]
5017
+ properties:
5018
+ key: { type: string, minLength: 1 }
5019
+ value:
5020
+ type: object
5021
+ additionalProperties: false
5022
+ required: [type, mutability, data]
5023
+ properties:
5024
+ type: { type: string, const: emoji }
5025
+ mutability: { type: string, enum: [immutable, mutable, segmented] }
5026
+ data:
5027
+ type: object
5028
+ additionalProperties: false
5029
+ required: [entity_key]
5030
+ properties:
5031
+ entity_key: { type: string, minLength: 1 }
5032
+ - type: object
5033
+ additionalProperties: false
5034
+ required: [key, value]
5035
+ properties:
5036
+ key: { type: string, minLength: 1 }
5037
+ value:
5038
+ type: object
5039
+ additionalProperties: false
5040
+ required: [type, mutability, data]
5041
+ properties:
5042
+ type: { type: string, const: markdown }
5043
+ mutability: { type: string, enum: [immutable, mutable, segmented] }
5044
+ data:
5045
+ type: object
5046
+ additionalProperties: false
5047
+ required: [markdown]
5048
+ properties:
5049
+ markdown: { type: string }
5050
+ - type: object
5051
+ additionalProperties: false
5052
+ required: [key, value]
5053
+ properties:
5054
+ key: { type: string, minLength: 1 }
5055
+ value:
5056
+ type: object
5057
+ additionalProperties: false
5058
+ required: [type, mutability, data]
5059
+ properties:
5060
+ type: { type: string, enum: [divider, latex] }
5061
+ mutability: { type: string, enum: [immutable, mutable, segmented] }
5062
+ data: { type: object, additionalProperties: false }
5063
+ description: Entity keys must be unique decimal strings matching their zero-based array index (`"0"`, `"1"`, ...).
5064
+
5065
+ XArticleContentState:
5066
+ type: object
5067
+ additionalProperties: false
5068
+ externalDocs:
5069
+ description: Official X Article content format reference
5070
+ url: https://docs.x.com/x-api/articles/create-draft-article
5071
+ required: [blocks, entities]
5072
+ properties:
5073
+ blocks:
5074
+ type: array
5075
+ minItems: 1
5076
+ items: { $ref: '#/components/schemas/XArticleBlock' }
5077
+ entities:
5078
+ type: array
5079
+ items: { $ref: '#/components/schemas/XArticleEntity' }
5080
+ description: X's snake_case content-state shape. Standard DraftJS camelCase fields such as entityMap, inlineStyleRanges, and entityRanges are rejected.
5081
+
5082
+ XArticle:
5083
+ type: object
5084
+ additionalProperties: false
5085
+ required: [title, content_state]
5086
+ properties:
5087
+ title: { type: string, minLength: 1 }
5088
+ content_state: { $ref: '#/components/schemas/XArticleContentState' }
5089
+ mode:
5090
+ type: string
5091
+ enum: [publish, draft]
5092
+ default: publish
5093
+ description: Publish creates an X Article draft and then publishes it. Draft stops after draft creation and returns the X draft ID without a public URL.
5094
+ cover:
5095
+ type: object
5096
+ additionalProperties: false
5097
+ required: [url]
5098
+ properties:
5099
+ url:
5100
+ type: string
5101
+ format: uri
5102
+ pattern: '^https?://.+\.(?:[jJ][pP][eE]?[gG]|[pP][nN][gG]|[wW][eE][bB][pP])(?:[?#].*)?$'
5103
+ description: Public JPG, JPEG, PNG, or WebP URL. GIF, MP4, AVIF, extensionless URLs, and caller-supplied native media IDs are rejected.
5104
+ altText: { type: string, maxLength: 1000 }
5105
+ description: |
5106
+ Long-form X Article payload. X Articles require an eligible X Premium+ account.
5107
+ Articles are mutually exclusive with top-level/custom tweet media and with threadItems,
5108
+ poll, quoteTweetId, replyToTweetId, inReplyToTweetId, replySettings, sensitiveMedia,
5109
+ paidPartnership, and madeWithAi. Publishing normally performs two billable X API
5110
+ requests at $0.010 each (draft + publish, $0.020 total); mode `draft` performs only
5111
+ the $0.010 draft request. `articleDraftId` is an internal recovery checkpoint and
5112
+ must not be supplied by API clients.
5113
+
4634
5114
  TwitterPlatformData:
4635
5115
  type: object
4636
5116
  properties:
5117
+ article:
5118
+ $ref: '#/components/schemas/XArticle'
4637
5119
  replyToTweetId:
4638
5120
  type: string
4639
5121
  description: ID of an existing tweet to reply to. The published tweet will appear as a reply in that tweet's thread. For threads, only the first tweet replies to the target; subsequent tweets chain normally. X only permits replying to your own posts or posts you are mentioned in; replying to an arbitrary other account's post is rejected by X.
@@ -4707,9 +5189,9 @@ components:
4707
5189
  type: boolean
4708
5190
  description: Content has other sensitive characteristics.
4709
5191
  description: >
4710
- X (Twitter) geo-restriction applies at the media level. Media in geo-restricted tweets will be
4711
- hidden for users outside the specified countries; the tweet text itself remains visible globally.
4712
- Requires media to be attached (ignored for text-only tweets).
5192
+ X-specific post options. The article field creates a long-form X Article and is mutually
5193
+ exclusive with tweet media and tweet-only options. Geo-restriction applies at the media
5194
+ level: media is hidden outside the specified countries while tweet text remains visible.
4713
5195
 
4714
5196
  ThreadsPlatformData:
4715
5197
  type: object
@@ -4775,7 +5257,7 @@ components:
4775
5257
  carouselCards:
4776
5258
  type: array
4777
5259
  minItems: 2
4778
- maxItems: 5
5260
+ maxItems: 10
4779
5261
  description: >
4780
5262
  Renders the post as a multi-link carousel (organic Page post). When set, mediaItems
4781
5263
  must be provided with the same length and all items must be images (no videos).
@@ -4955,13 +5437,41 @@ components:
4955
5437
  description: Set to true to disable automatic link previews for URLs in the post content (default is false)
4956
5438
  reshareUrl:
4957
5439
  type: string
4958
- description: "LinkedIn post link to repost (use the post's \"Copy link to post\" action), or a urn:li:share / urn:li:ugcPost / urn:li:groupPost URN. The published post becomes a quote-reshare: your content is shown as the commentary and the original post is embedded underneath (LinkedIn's \"repost with your thoughts\"). Mutually exclusive with media. Works on personal profiles and organization pages."
5440
+ description: "LinkedIn post link to repost (use the post's \"Copy link to post\" action), or a urn:li:share / urn:li:ugcPost / urn:li:groupPost URN. With content, the published post is a quote-reshare: your text is the commentary and the original is embedded underneath (LinkedIn's \"repost with your thoughts\"). Leave content empty (and omit customContent) to publish a plain repost with no text, LinkedIn's one-click \"Repost\". Mutually exclusive with media. Works on personal profiles and organization pages."
4959
5441
  geoRestriction:
4960
5442
  $ref: '#/components/schemas/GeoRestriction'
5443
+ poll:
5444
+ type: object
5445
+ description: 'Create a LinkedIn poll with this post. Cannot be combined with media or reshareUrl. Polls cannot be edited after publishing on LinkedIn, and API-created polls are non-sponsored only (they cannot be promoted as ads).'
5446
+ properties:
5447
+ question:
5448
+ type: string
5449
+ minLength: 1
5450
+ maxLength: 140
5451
+ description: 'Poll question (max 140 characters)'
5452
+ options:
5453
+ type: array
5454
+ minItems: 2
5455
+ maxItems: 4
5456
+ items:
5457
+ type: string
5458
+ minLength: 1
5459
+ maxLength: 30
5460
+ description: 'Poll options (2-4 choices, max 30 characters each)'
5461
+ duration:
5462
+ type: string
5463
+ enum: [ONE_DAY, THREE_DAYS, SEVEN_DAYS, FOURTEEN_DAYS]
5464
+ default: SEVEN_DAYS
5465
+ description: How long the poll accepts votes. Defaults to SEVEN_DAYS.
5466
+ required:
5467
+ - question
5468
+ - options
4961
5469
  description: >
4962
5470
  Up to 20 images, no multi-video. Single PDF supported (max 100MB). Link previews auto-generated
4963
5471
  when no media attached. Use organizationUrn for multi-org posting. Geo-restriction only works for
4964
5472
  organization pages (not personal profiles) and requires the targeted audience to exceed 300 followers.
5473
+ Polls are supported via the poll object: 2-4 options, cannot be combined with media or reshareUrl,
5474
+ cannot be edited after publishing, and API-created polls are non-sponsored only.
4965
5475
 
4966
5476
  MoneyAmount:
4967
5477
  type: object
@@ -5649,6 +6159,21 @@ components:
5649
6159
  BlueskyPlatformData:
5650
6160
  type: object
5651
6161
  properties:
6162
+ langs:
6163
+ type: array
6164
+ minItems: 1
6165
+ maxItems: 3
6166
+ items:
6167
+ type: string
6168
+ example: ["pt", "en"]
6169
+ description: >
6170
+ Language(s) of the post text as 1-3 BCP-47 codes (e.g. "pt", "en-US"), written to the
6171
+ post record's langs field. Bluesky feed generators filter on this field, so posts
6172
+ without it never appear in language-scoped feeds. Can only be set at creation
6173
+ (Bluesky has no post editing). When threadItems is used, every item in the thread
6174
+ carries the same langs. When omitted, the account's default (set via
6175
+ PATCH /v1/accounts/{accountId}/bluesky-settings) applies; with no default either,
6176
+ the field is absent from the record.
5652
6177
  threadItems:
5653
6178
  type: array
5654
6179
  description: >
@@ -5666,7 +6191,7 @@ components:
5666
6191
  items:
5667
6192
  $ref: '#/components/schemas/MediaItem'
5668
6193
  description: |
5669
- Bluesky post settings. Supports text posts with up to 4 images or a single video. threadItems creates a reply chain (Bluesky thread). Images exceeding 1MB are automatically compressed. Alt text supported via mediaItem properties.
6194
+ Bluesky post settings. Supports text posts with up to 4 images or a single video. threadItems creates a reply chain (Bluesky thread). Images exceeding 1MB are automatically compressed. Alt text supported via mediaItem properties. Use langs to tag post language for feed-generator filtering.
5670
6195
 
5671
6196
  DiscordPlatformData:
5672
6197
  type: object
@@ -5916,7 +6441,7 @@ components:
5916
6441
  _id: { type: string }
5917
6442
  platform:
5918
6443
  type: string
5919
- enum: [tiktok, instagram, facebook, youtube, linkedin, twitter, threads, pinterest, reddit, bluesky, googlebusiness, telegram, snapchat, discord, slack, whatsapp, linkedinads, metaads, pinterestads, tiktokads, xads, googleads, openaiads]
6444
+ enum: [tiktok, instagram, facebook, youtube, linkedin, twitter, threads, pinterest, reddit, bluesky, googlebusiness, telegram, snapchat, discord, slack, whatsapp, linkedinads, metaads, pinterestads, tiktokads, xads, googleads, openaiads, sms, phone, rcs]
5920
6445
  profileId:
5921
6446
  oneOf:
5922
6447
  - type: string
@@ -6083,12 +6608,18 @@ components:
6083
6608
  properties:
6084
6609
  billingSystem:
6085
6610
  type: string
6086
- enum: [metronome, stripe]
6611
+ enum: [metronome, stripe, shopify]
6087
6612
  plan:
6088
6613
  type: object
6089
6614
  properties:
6090
6615
  name: { type: string }
6091
6616
  isUsageBased: { type: boolean }
6617
+ isPaid:
6618
+ type: boolean
6619
+ description: 'True when the key belongs to an account with an active paid billing relationship (Stripe subscription, Metronome enrollment, or Shopify-managed billing).'
6620
+ shopifyShopDomain:
6621
+ type: [string, "null"]
6622
+ description: 'myshopify.com domain owning the subscription; present only when billingSystem is shopify.'
6092
6623
  period:
6093
6624
  type: object
6094
6625
  description: Current billing cycle. `start`/`end` are resolved for usage-based accounts only.
@@ -6418,14 +6949,14 @@ components:
6418
6949
  likes: { type: integer, example: 0 }
6419
6950
  comments: { type: integer, example: 0 }
6420
6951
  shares: { type: integer, example: 0 }
6421
- saves: { type: integer, example: 0, description: 'Number of saves/bookmarks (Instagram, Pinterest)' }
6952
+ saves: { type: integer, example: 0, description: 'Number of saves/bookmarks (Instagram, Pinterest, X/Twitter)' }
6422
6953
  clicks: { type: integer, example: 0 }
6423
6954
  views: { type: integer, example: 0 }
6424
6955
  follows: { type: integer, example: 0, description: 'Instagram feed posts and stories only: organic accounts that started following from this post. 0 for reels and other platforms.' }
6425
6956
  igReelsAvgWatchTime: { type: integer, example: 0, description: 'Instagram Reels only: average watch time per play, in milliseconds. 0 for non-Reels media and other platforms.' }
6426
6957
  igReelsVideoViewTotalTime: { type: integer, example: 0, description: 'Instagram Reels only: total watch time including replays, in milliseconds. 0 for non-Reels media and other platforms.' }
6427
6958
  videoDurationSeconds: { type: [integer, "null"], example: 30, description: 'Video length in seconds. Currently Instagram Reels only; combine with igReelsAvgWatchTime (ms) to estimate retention. Null when unknown (other platforms, non-video media, or when Instagram does not expose the media URL, e.g. reels with copyrighted audio).' }
6428
- engagementRate: { type: number, example: 0 }
6959
+ engagementRate: { type: number, example: 6.59, description: 'Percentage, rounded to 2 decimals: (likes + comments + shares + saves) / (impressions or reach or views) * 100. Clicks and follows are never counted. The denominator is the FIRST of impressions, reach, views that is non-zero, so it is not the same basis on every post: a post with impressions divides by impressions, one without falls back to reach, then to views. If you need a single consistent basis (e.g. interactions / reach), compute it from the raw fields above. The engagementRate on the LinkedIn account endpoints is a different formula.' }
6429
6960
  lastUpdated: { type: string, format: date-time }
6430
6961
  PlatformAnalytics:
6431
6962
  type: object
@@ -6483,11 +7014,26 @@ components:
6483
7014
  type: object
6484
7015
  properties:
6485
7016
  type: { type: string, enum: [image, video] }
6486
- url: { type: string, format: uri, description: Direct URL to the media }
6487
- thumbnail: { type: string, format: uri, description: Thumbnail URL (same as url for images) }
7017
+ url:
7018
+ type: [string, "null"]
7019
+ format: uri
7020
+ description: >-
7021
+ 'Direct URL to the media file. Null when the platform withholds it: check
7022
+ mediaStatus before downloading. Instagram omits the video file for Reels it
7023
+ flags as containing copyrighted material (its docs name audio as the usual
7024
+ cause), so type stays "video" while the file is permanently unreachable.'
7025
+ thumbnail: { type: [string, "null"], format: uri, description: 'Thumbnail URL (same as url for images). Still present when url is null.' }
6488
7026
  altText:
6489
7027
  type: string
6490
7028
  description: Accessibility alt text set on the media, when present.
7029
+ mediaStatus:
7030
+ type: string
7031
+ enum: [available, unavailable]
7032
+ description: 'unavailable means the media file could not be retrieved (url is null or, for LinkedIn videos, a cover image standing in for the file). available or absent means the file is available at url (older synced items omit the field).'
7033
+ unavailableReason:
7034
+ type: string
7035
+ enum: [platform_withheld]
7036
+ description: 'Why the file is missing. platform_withheld means the platform declined to return it and retrying will not help.'
6491
7037
  AnalyticsListResponse:
6492
7038
  type: object
6493
7039
  properties:
@@ -6563,7 +7109,7 @@ components:
6563
7109
  shares: { type: integer, description: Total reshares across all posts }
6564
7110
  saves: { type: integer, description: Total times posts were saved (personal accounts only) }
6565
7111
  sends: { type: integer, description: Total times posts were sent via LinkedIn messaging (personal accounts only) }
6566
- engagementRate: { type: number, description: Overall engagement rate as percentage }
7112
+ engagementRate: { type: number, description: 'Overall engagement rate, as a percentage rounded to 2 decimals: (reactions + comments + shares + saves + sends) / impressions * 100. Clicks are not counted, and there is no fallback denominator, so this is 0 whenever impressions is 0. This is NOT the same formula as PostAnalytics.engagementRate on GET /v1/analytics.' }
6567
7113
  note: { type: string }
6568
7114
  lastUpdated: { type: string, format: date-time }
6569
7115
  LinkedInAggregateAnalyticsDailyResponse:
@@ -7011,7 +7557,7 @@ components:
7011
7557
  type: object
7012
7558
  additionalProperties:
7013
7559
  type: integer
7014
- description: "Per-action-type counts summed over the date range, keyed by the platform's action-type names. Meta: raw Insights action_type keys (link_click, offsite_conversion.fb_pixel_purchase, onsite_conversion.lead_grouped, ...) — both engagement and conversion events. X: conversion types (purchase, sign_up, site_visit, download, custom). LinkedIn: conversion types (post_click, post_view, lead_gen). Google returns {} (its per-action names aren't synced per ad). Empty object when no actions are reported. NOTE: keys differ by platform, so branch on the ad's platform when interpreting them."
7560
+ description: "Per-action-type counts summed over the date range, keyed by the platform's action-type names. Meta: raw Insights action_type keys (link_click, offsite_conversion.fb_pixel_purchase, onsite_conversion.lead_grouped, ...) — both engagement and conversion events. TikTok: pixel conversions (purchase, add_to_cart, initiate_checkout, view_content, complete_payment, lead) plus the paid-engagement family (follow, post_reaction for paid likes, comment, share) — follow is how FOLLOWERS-goal campaigns report their result. X: conversion types (purchase, sign_up, site_visit, download, custom). LinkedIn: conversion types (post_click, post_view, lead_gen). Google returns {} (its per-action names aren't synced per ad). Empty object when no actions are reported. NOTE: keys differ by platform, so branch on the ad's platform when interpreting them."
7015
7561
  example:
7016
7562
  link_click: 160
7017
7563
  post_engagement: 300
@@ -7108,13 +7654,17 @@ components:
7108
7654
  Use these fields when you need a specific interaction, and `engagement`
7109
7655
  only as the coarse total it has always been.
7110
7656
 
7111
- Meta-only; other platforms leave these at 0.
7657
+ Populated for Meta and, since 2026-08, TikTok (`reactions` = paid
7658
+ likes, `comments`, `shares`; TikTok's `follow` count lives in
7659
+ `actions.follow`, not here). Other platforms leave these at 0.
7660
+ TikTok history note: paused TikTok ads are not re-synced, so
7661
+ campaigns that ended before the rollout keep 0s here.
7112
7662
  properties:
7113
- postEngagement: { type: integer, description: "Meta's own post-engagement total (`post_engagement`)." }
7114
- pageEngagement: { type: integer, description: "Meta's own page-engagement total (`page_engagement`)." }
7115
- reactions: { type: integer, description: "Reactions on the ad's post (`post_reaction`)." }
7663
+ postEngagement: { type: integer, description: "Meta's own post-engagement total (`post_engagement`). Meta-only." }
7664
+ pageEngagement: { type: integer, description: "Meta's own page-engagement total (`page_engagement`). Meta-only." }
7665
+ reactions: { type: integer, description: "Reactions on the ad's post (`post_reaction`). For TikTok these are its paid likes." }
7116
7666
  comments: { type: integer, description: "Comments on the ad's post." }
7117
- shares: { type: integer, description: "Shares of the ad's post. Meta reports these under the action type literally named `post`." }
7667
+ shares: { type: integer, description: "Shares of the ad's post. Meta reports these under the action type literally named `post`; TikTok under `share`." }
7118
7668
  saves: { type: integer, description: "Saves of the ad's post (`onsite_conversion.post_save`)." }
7119
7669
  pageLikes: { type: integer, description: "New Page likes attributed to the ad (`like`)." }
7120
7670
  videoViews: { type: integer, description: "3-second video views (`video_view`). For completion-based counts use `videoThruplayWatchedActions`." }
@@ -7605,6 +8155,7 @@ components:
7605
8155
  imageUrl: { type: string, description: Alternative image URL }
7606
8156
  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." }
7607
8157
  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." }
8158
+ creativeId: { type: [string, "null"], description: "Meta ad creative id backing this ad. Reusable via existingCreativeId on POST /v1/ads/create." }
7608
8159
  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." }
7609
8160
  objectStoryId: { type: [string, "null"], description: "Meta creative `object_story_id` (the SHARE reference). Frequently absent — Meta omits it for SHARE creatives. Use effectiveObjectStoryId instead." }
7610
8161
  effectiveObjectStoryId: { type: [string, "null"], description: "Meta `effective_object_story_id` — `{pageId}_{postId}` of the Facebook post the ad's engagement (comments) lives on. Pass to GET /v1/ads?effectiveObjectStoryId= to map a Business-Manager-visible post back to this ad; GET /v1/ads/{adId}/comments resolves comments against it." }
@@ -7762,7 +8313,7 @@ components:
7762
8313
  profileId: { type: string }
7763
8314
  advertisingChannelType:
7764
8315
  type: [string, "null"]
7765
- description: "Google-only. Raw campaign.advertising_channel_type (SEARCH, PERFORMANCE_MAX, VIDEO, DEMAND_GEN, DISPLAY, SHOPPING, ...). Serving surface, distinct from platformObjective (advertiser intent). Null/absent for non-Google platforms."
8316
+ description: "Google-only. Raw campaign.advertising_channel_type (SEARCH, PERFORMANCE_MAX, LOCAL_SERVICES, VIDEO, DEMAND_GEN, DISPLAY, SHOPPING, ...). Serving surface, distinct from platformObjective (advertiser intent). Null/absent for non-Google platforms."
7766
8317
  platformObjective: { type: [string, "null"], description: "Raw Meta campaign objective (e.g. OUTCOME_SALES, OUTCOME_LEADS, OUTCOME_TRAFFIC)" }
7767
8318
  optimizationGoal:
7768
8319
  type: [string, "null"]
@@ -8065,6 +8616,37 @@ components:
8065
8616
  customConversionId: { type: string, description: "Drops straight into promotedObject.customConversionId on POST /v1/ads/create." }
8066
8617
  reused: { type: boolean, description: "True when an existing conversion matched name + pixelId; the response is then a 200." }
8067
8618
  customConversion: { $ref: '#/components/schemas/CustomConversion' }
8619
+ Blog:
8620
+ type: object
8621
+ description: 'A blog container on the connected platform. All content lives on the platform; Zernio proxies it and stores nothing.'
8622
+ properties:
8623
+ id: { type: string, description: 'Platform-native blog id (numeric string for Shopify).' }
8624
+ platform: { type: string, enum: [shopify] }
8625
+ title: { type: string }
8626
+ handle: { type: string, description: 'URL slug of the blog.' }
8627
+ BlogArticle:
8628
+ type: object
8629
+ description: 'An article inside a blog on the connected platform.'
8630
+ properties:
8631
+ id: { type: string, description: 'Platform-native article id (numeric string for Shopify).' }
8632
+ blogId: { type: string, description: 'Platform-native id of the blog the article belongs to.' }
8633
+ platform: { type: string, enum: [shopify] }
8634
+ title: { type: string }
8635
+ bodyHtml: { type: [string, "null"], description: 'Article body as HTML.' }
8636
+ handle: { type: string, description: 'URL slug of the article.' }
8637
+ tags: { type: array, items: { type: string } }
8638
+ author: { type: [string, "null"], description: 'Display name of the article author.' }
8639
+ excerpt: { type: [string, "null"], description: 'Short summary shown in blog listings.' }
8640
+ image:
8641
+ type: [object, "null"]
8642
+ description: Featured image.
8643
+ properties:
8644
+ url: { type: string }
8645
+ altText: { type: [string, "null"] }
8646
+ isPublished: { type: boolean, description: 'False while the article is a draft or its publish date is still in the future.' }
8647
+ publishedAt: { type: [string, "null"], format: date-time, description: 'When the article was (or is scheduled to be) published; null for drafts.' }
8648
+ createdAt: { type: [string, "null"], format: date-time }
8649
+ updatedAt: { type: [string, "null"], format: date-time }
8068
8650
  TrackingTag:
8069
8651
  type: object
8070
8652
  description: |
@@ -8594,6 +9176,31 @@ webhooks:
8594
9176
  responses:
8595
9177
  '200':
8596
9178
  description: Webhook received successfully
9179
+ referral.received:
9180
+ post:
9181
+ operationId: onReferralReceived
9182
+ summary: Referral received event
9183
+ description: |
9184
+ Fired when someone opens an EXISTING Instagram or Messenger thread
9185
+ through an attributable entry point - an ig.me / m.me link with a
9186
+ `ref` parameter, or (Messenger) a returning Click-to-Message ad click -
9187
+ which Meta delivers as a standalone referral with no message attached.
9188
+ A referral that rides an inbound message (first message of a thread,
9189
+ icebreaker taps, returning ad clicks on Instagram) arrives on
9190
+ `message.received` under `metadata.referral` instead; the two never
9191
+ fire for the same click. The first referral captured on a conversation
9192
+ is also persisted on it (see `metadata` on `GET
9193
+ /v1/inbox/conversations`). Requires the Inbox add-on.
9194
+ tags: [Webhook Events]
9195
+ requestBody:
9196
+ required: true
9197
+ content:
9198
+ application/json:
9199
+ schema:
9200
+ $ref: '#/components/schemas/WebhookPayloadReferral'
9201
+ responses:
9202
+ '200':
9203
+ description: Webhook received successfully
8597
9204
  comment.received:
8598
9205
  post:
8599
9206
  operationId: onCommentReceived
@@ -8839,6 +9446,28 @@ webhooks:
8839
9446
  responses:
8840
9447
  '200':
8841
9448
  description: Webhook received successfully
9449
+ whatsapp.template.category_updated:
9450
+ post:
9451
+ operationId: onWhatsAppTemplateCategoryUpdated
9452
+ summary: WhatsApp template category updated event
9453
+ description: |
9454
+ Fired when Meta reclassifies a WhatsApp Business template's category
9455
+ after approval. Forwarded from Meta's `template_category_update`
9456
+ webhook field on the WhatsApp Business Account. Category drives
9457
+ Meta's per-conversation tariff and whether the template is subject
9458
+ to the recipient's marketing opt-out. `template.changeType` is
9459
+ `scheduled` (24h advance notice) or `applied`; `template.category`
9460
+ is always the category right now.
9461
+ tags: [Webhook Events]
9462
+ requestBody:
9463
+ required: true
9464
+ content:
9465
+ application/json:
9466
+ schema:
9467
+ $ref: '#/components/schemas/WebhookPayloadWhatsAppTemplateCategoryUpdated'
9468
+ responses:
9469
+ '200':
9470
+ description: Webhook received successfully
8842
9471
  whatsapp.automatic_event:
8843
9472
  post:
8844
9473
  operationId: onWhatsAppAutomaticEvent
@@ -8862,7 +9491,10 @@ webhooks:
8862
9491
  properties:
8863
9492
  id: { type: string }
8864
9493
  event: { type: string, enum: [whatsapp.automatic_event] }
8865
- timestamp: { type: string, format: date-time }
9494
+ timestamp:
9495
+ type: string
9496
+ format: date-time
9497
+ description: 'UTC time at which Zernio generated this event (set once when the event payload is built, before delivery is queued). Retries and redeliveries keep the original value, so it reflects the event, not the delivery attempt.'
8866
9498
  accountId:
8867
9499
  type: string
8868
9500
  description: SocialAccount id of the WhatsApp number whose conversation was flagged.
@@ -8907,7 +9539,10 @@ webhooks:
8907
9539
  properties:
8908
9540
  id: { type: string }
8909
9541
  event: { type: string, enum: [whatsapp.number.activated] }
8910
- timestamp: { type: string, format: date-time }
9542
+ timestamp:
9543
+ type: string
9544
+ format: date-time
9545
+ description: 'UTC time at which Zernio generated this event (set once when the event payload is built, before delivery is queued). Retries and redeliveries keep the original value, so it reflects the event, not the delivery attempt.'
8911
9546
  number:
8912
9547
  type: object
8913
9548
  properties:
@@ -8936,7 +9571,10 @@ webhooks:
8936
9571
  properties:
8937
9572
  id: { type: string }
8938
9573
  event: { type: string, enum: [whatsapp.number.declined] }
8939
- timestamp: { type: string, format: date-time }
9574
+ timestamp:
9575
+ type: string
9576
+ format: date-time
9577
+ description: 'UTC time at which Zernio generated this event (set once when the event payload is built, before delivery is queued). Retries and redeliveries keep the original value, so it reflects the event, not the delivery attempt.'
8940
9578
  number:
8941
9579
  type: object
8942
9580
  properties:
@@ -8968,7 +9606,10 @@ webhooks:
8968
9606
  properties:
8969
9607
  id: { type: string }
8970
9608
  event: { type: string, enum: [whatsapp.number.action_required] }
8971
- timestamp: { type: string, format: date-time }
9609
+ timestamp:
9610
+ type: string
9611
+ format: date-time
9612
+ description: 'UTC time at which Zernio generated this event (set once when the event payload is built, before delivery is queued). Retries and redeliveries keep the original value, so it reflects the event, not the delivery attempt.'
8972
9613
  reason: { type: string }
8973
9614
  number:
8974
9615
  type: object
@@ -8998,7 +9639,10 @@ webhooks:
8998
9639
  properties:
8999
9640
  id: { type: string }
9000
9641
  event: { type: string, enum: [whatsapp.number.verification_required] }
9001
- timestamp: { type: string, format: date-time }
9642
+ timestamp:
9643
+ type: string
9644
+ format: date-time
9645
+ description: 'UTC time at which Zernio generated this event (set once when the event payload is built, before delivery is queued). Retries and redeliveries keep the original value, so it reflects the event, not the delivery attempt.'
9002
9646
  number:
9003
9647
  type: object
9004
9648
  properties:
@@ -9029,7 +9673,10 @@ webhooks:
9029
9673
  properties:
9030
9674
  id: { type: string }
9031
9675
  event: { type: string, enum: [whatsapp.number.suspended] }
9032
- timestamp: { type: string, format: date-time }
9676
+ timestamp:
9677
+ type: string
9678
+ format: date-time
9679
+ description: 'UTC time at which Zernio generated this event (set once when the event payload is built, before delivery is queued). Retries and redeliveries keep the original value, so it reflects the event, not the delivery attempt.'
9033
9680
  number:
9034
9681
  type: object
9035
9682
  properties:
@@ -9058,7 +9705,10 @@ webhooks:
9058
9705
  properties:
9059
9706
  id: { type: string }
9060
9707
  event: { type: string, enum: [whatsapp.number.reactivated] }
9061
- timestamp: { type: string, format: date-time }
9708
+ timestamp:
9709
+ type: string
9710
+ format: date-time
9711
+ description: 'UTC time at which Zernio generated this event (set once when the event payload is built, before delivery is queued). Retries and redeliveries keep the original value, so it reflects the event, not the delivery attempt.'
9062
9712
  number:
9063
9713
  type: object
9064
9714
  properties:
@@ -9087,7 +9737,10 @@ webhooks:
9087
9737
  properties:
9088
9738
  id: { type: string }
9089
9739
  event: { type: string, enum: [whatsapp.number.released] }
9090
- timestamp: { type: string, format: date-time }
9740
+ timestamp:
9741
+ type: string
9742
+ format: date-time
9743
+ description: 'UTC time at which Zernio generated this event (set once when the event payload is built, before delivery is queued). Retries and redeliveries keep the original value, so it reflects the event, not the delivery attempt.'
9091
9744
  number:
9092
9745
  type: object
9093
9746
  properties:
@@ -9118,7 +9771,10 @@ webhooks:
9118
9771
  properties:
9119
9772
  id: { type: string }
9120
9773
  event: { type: string, enum: [whatsapp.number.kyc_submitted, verification.approved, verification.failed] }
9121
- timestamp: { type: string, format: date-time }
9774
+ timestamp:
9775
+ type: string
9776
+ format: date-time
9777
+ description: 'UTC time at which Zernio generated this event (set once when the event payload is built, before delivery is queued). Retries and redeliveries keep the original value, so it reflects the event, not the delivery attempt.'
9122
9778
  number:
9123
9779
  type: object
9124
9780
  properties:
@@ -9146,7 +9802,10 @@ webhooks:
9146
9802
  properties:
9147
9803
  id: { type: string }
9148
9804
  event: { type: string, enum: [verification.approved] }
9149
- timestamp: { type: string, format: date-time }
9805
+ timestamp:
9806
+ type: string
9807
+ format: date-time
9808
+ description: 'UTC time at which Zernio generated this event (set once when the event payload is built, before delivery is queued). Retries and redeliveries keep the original value, so it reflects the event, not the delivery attempt.'
9150
9809
  verification:
9151
9810
  type: object
9152
9811
  properties:
@@ -9173,7 +9832,10 @@ webhooks:
9173
9832
  properties:
9174
9833
  id: { type: string }
9175
9834
  event: { type: string, enum: [verification.failed] }
9176
- timestamp: { type: string, format: date-time }
9835
+ timestamp:
9836
+ type: string
9837
+ format: date-time
9838
+ description: 'UTC time at which Zernio generated this event (set once when the event payload is built, before delivery is queued). Retries and redeliveries keep the original value, so it reflects the event, not the delivery attempt.'
9177
9839
  verification:
9178
9840
  type: object
9179
9841
  properties:
@@ -12821,7 +13483,7 @@ paths:
12821
13483
  lastVerified: "2026-03-26"
12822
13484
  tiers:
12823
13485
  - { tier: x_api_005, pricePerCallUsd: 0.005, operationCount: 13 }
12824
- - { tier: x_api_010, pricePerCallUsd: 0.010, operationCount: 8 }
13486
+ - { tier: x_api_010, pricePerCallUsd: 0.010, operationCount: 10 }
12825
13487
  - { tier: x_api_015, pricePerCallUsd: 0.015, operationCount: 3 }
12826
13488
  - { tier: x_api_200, pricePerCallUsd: 0.200, operationCount: 1 }
12827
13489
  operations:
@@ -12851,6 +13513,22 @@ paths:
12851
13513
  tier: x_api_200
12852
13514
  triggeredBy:
12853
13515
  - { method: publishPost, metering: always }
13516
+ - operation: article_draft
13517
+ eventType: x_article_draft
13518
+ displayName: "X API: Article Draft"
13519
+ pricePerCallUsd: 0.010
13520
+ pricePerCallCents: 1.0
13521
+ tier: x_api_010
13522
+ triggeredBy:
13523
+ - { method: createArticleDraft, metering: always }
13524
+ - operation: article_publish
13525
+ eventType: x_article_publish
13526
+ displayName: "X API: Article Publish"
13527
+ pricePerCallUsd: 0.010
13528
+ pricePerCallCents: 1.0
13529
+ tier: x_api_010
13530
+ triggeredBy:
13531
+ - { method: publishArticle, metering: always }
12854
13532
  '401': { $ref: '#/components/responses/Unauthorized' }
12855
13533
  /v1/usage:
12856
13534
  get:
@@ -13296,7 +13974,7 @@ paths:
13296
13974
  summary: Create post
13297
13975
  description: |
13298
13976
  Create and optionally publish a post. Immediate posts (`publishNow: true`) include `platformPostUrl` in the response.
13299
- Content is optional when media is attached or all platforms have `customContent`. See each platform's schema for media constraints.
13977
+ 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 plain repost (`platformSpecificData.reshareUrl` with no text). See each platform's schema for media constraints.
13300
13978
 
13301
13979
  ## Idempotency
13302
13980
 
@@ -13330,7 +14008,7 @@ paths:
13330
14008
  title: { type: string }
13331
14009
  content:
13332
14010
  type: string
13333
- description: Post caption/text. Optional when media is attached or all platforms have customContent. Required for text-only posts.
14011
+ 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 plain repost (platformSpecificData.reshareUrl with no text). Required for other text-only posts.'
13334
14012
  mediaItems:
13335
14013
  type: array
13336
14014
  items: { $ref: '#/components/schemas/MediaItem' }
@@ -13408,6 +14086,69 @@ paths:
13408
14086
  Only used when queuedFromProfile is also provided.
13409
14087
  If omitted, uses the profile's default queue.
13410
14088
  examples:
14089
+ twitterArticle:
14090
+ summary: Publish a long-form X Article
14091
+ description: |
14092
+ Creates an X Article draft and publishes it. This normally makes two
14093
+ billable X API calls at $0.010 each ($0.020 total). The account must
14094
+ be eligible for X Articles (X Premium+). Article media is supplied as
14095
+ public URLs; Zernio uploads it to X and builds the native media shape.
14096
+ value:
14097
+ platforms:
14098
+ - platform: twitter
14099
+ accountId: "64e1f0a9e2b5af0012ab34cd"
14100
+ platformSpecificData:
14101
+ article:
14102
+ title: "Building a better publishing workflow"
14103
+ mode: publish
14104
+ cover:
14105
+ url: "https://cdn.example.com/article-cover.jpg"
14106
+ content_state:
14107
+ blocks:
14108
+ - type: header-one
14109
+ text: "A reliable publishing workflow"
14110
+ - type: unstyled
14111
+ text: "Long-form publishing should be observable. Read the implementation notes."
14112
+ inline_style_ranges:
14113
+ - { offset: 0, length: 9, style: bold }
14114
+ entity_ranges:
14115
+ - { key: 0, offset: 52, length: 20 }
14116
+ - type: atomic
14117
+ text: " "
14118
+ entity_ranges:
14119
+ - { key: 1, offset: 0, length: 1 }
14120
+ entities:
14121
+ - key: "0"
14122
+ value:
14123
+ type: link
14124
+ mutability: mutable
14125
+ data:
14126
+ url: "https://example.com/implementation-notes"
14127
+ - key: "1"
14128
+ value:
14129
+ type: image
14130
+ mutability: immutable
14131
+ data:
14132
+ url: "https://cdn.example.com/workflow.png"
14133
+ caption: "The publishing stages"
14134
+ publishNow: true
14135
+ twitterArticleDraft:
14136
+ summary: Save an X Article as a platform draft
14137
+ description: Creates only the X Article draft ($0.010) and does not publish it publicly.
14138
+ value:
14139
+ platforms:
14140
+ - platform: twitter
14141
+ accountId: "64e1f0a9e2b5af0012ab34cd"
14142
+ platformSpecificData:
14143
+ article:
14144
+ title: "Draft product update"
14145
+ mode: draft
14146
+ content_state:
14147
+ blocks:
14148
+ - type: unstyled
14149
+ text: "This Article is ready for final review."
14150
+ entities: []
14151
+ publishNow: true
13411
14152
  facebookDraft:
13412
14153
  summary: Facebook draft post (visible in Publishing Tools)
13413
14154
  value:
@@ -13421,7 +14162,7 @@ paths:
13421
14162
  facebookCarousel:
13422
14163
  summary: Facebook multi-link carousel post
13423
14164
  description: |
13424
- Posts a 2-5 card carousel where each image has its own click-through link
14165
+ Posts a 2-10 card carousel where each image has its own click-through link
13425
14166
  and optional headline. mediaItems and carouselCards must have the same
13426
14167
  length, in the same order. Images only (no video cards).
13427
14168
  value:
@@ -15270,7 +16011,15 @@ paths:
15270
16011
  operationId: getAccountHealth
15271
16012
  tags: [Accounts]
15272
16013
  summary: Check account health
15273
- description: Returns detailed health info for a specific account including token status, permissions, and recommendations.
16014
+ description: |
16015
+ Returns detailed health info for a specific account including token status, permissions, and recommendations.
16016
+
16017
+ For WhatsApp accounts the response also includes `platformConnection`, a live probe of the
16018
+ Meta link behind the channel (the same read as `GET /v1/whatsapp/number-info`). The OAuth
16019
+ token can be perfectly valid while Meta refuses to serve the phone-number object (for
16020
+ example after a phone-side coexistence disconnect), so `tokenStatus` alone is not a
16021
+ liveness signal for WhatsApp. When the Meta link is dead, `platformConnection.status` is
16022
+ `disconnected` and the overall `status` is `error`.
15274
16023
  parameters:
15275
16024
  - name: accountId
15276
16025
  in: path
@@ -15338,6 +16087,28 @@ paths:
15338
16087
  type: array
15339
16088
  items: { type: string }
15340
16089
  description: Actionable recommendations to fix issues
16090
+ platformConnection:
16091
+ type: object
16092
+ description: 'WhatsApp accounts only. Live probe of the Meta link behind the channel, performed at request time (the same read as GET /v1/whatsapp/number-info).'
16093
+ properties:
16094
+ status:
16095
+ type: string
16096
+ enum: [connected, disconnected, unknown]
16097
+ description: '`connected` = Meta served the channel object. `disconnected` = Meta refused to serve it (Graph error 100, subcode 33), which is how a phone-side coexistence disconnect surfaces. `unknown` = the live read failed for another reason (timeout, transient Meta error), not evidence either way.'
16098
+ checkedAt:
16099
+ type: string
16100
+ format: date-time
16101
+ description: When this live probe ran (always the current request; never cached)
16102
+ phoneStatus:
16103
+ type: [string, "null"]
16104
+ description: 'Meta''s own `status` field from the phone-number node (for example CONNECTED), when the object was readable'
16105
+ metaError:
16106
+ type: [object, "null"]
16107
+ description: Set only when status is `disconnected`
16108
+ properties:
16109
+ code: { type: integer, description: 'Meta Graph error code (100)' }
16110
+ subcode: { type: integer, description: 'Meta Graph error subcode (33)' }
16111
+ message: { type: string }
15341
16112
  example:
15342
16113
  accountId: "abc123"
15343
16114
  platform: "instagram"
@@ -15367,6 +16138,7 @@ paths:
15367
16138
  missingRequired: []
15368
16139
  issues: []
15369
16140
  recommendations: []
16141
+ '400': { $ref: '#/components/responses/BadRequest' }
15370
16142
  '401': { $ref: '#/components/responses/Unauthorized' }
15371
16143
  '404': { $ref: '#/components/responses/NotFound' }
15372
16144
  /v1/accounts/{accountId}/follow-status/{userId}:
@@ -15925,6 +16697,17 @@ paths:
15925
16697
  `facebook_login`: the Facebook Login dialog, i.e. "Instagram API with Facebook Login". The user authorizes a Facebook Page that has a linked Instagram professional account, and every API call for that account then runs through the Page. Use this when the customer manages Instagram through a Page and expects the Facebook consent screen. Because the user has to pick which Page to connect, the callback continues at the account-selection step, `/v1/connect/instagram/select-account`.
15926
16698
 
15927
16699
  `facebook_login` supports `headless=true` like the other selection platforms: the callback redirects to your `redirect_url` with `profileId`, `tempToken`, `platform=instagram`, `step=select_account` and `connect_token`, which you pass into the select-account endpoints to finish. The default `instagram_login` has no selection step, so it connects the account directly.
16700
+ - name: onboarding
16701
+ in: query
16702
+ schema: { type: string, enum: [api, business_app] }
16703
+ description: |
16704
+ WhatsApp only. Ignored for every other platform. Controls which screen Meta's Embedded Signup popup shows.
16705
+
16706
+ If omitted, the connection defaults to coexistence (same as `business_app` below), preserving existing behavior for numbers already on the WhatsApp Business app.
16707
+
16708
+ `api`: standard Embedded Signup, showing Meta's WABA/number picker. Use this to connect a phone number already on Cloud API elsewhere.
16709
+
16710
+ `business_app`: coexistence, i.e. 'Connect existing WhatsApp Business app' (a number shared between Cloud API and the consumer WhatsApp Business app).
15928
16711
  security:
15929
16712
  - bearerAuth: []
15930
16713
  responses:
@@ -16121,6 +16904,124 @@ paths:
16121
16904
  '404':
16122
16905
  description: "Profile or posting account not found"
16123
16906
 
16907
+ /v1/connect/shopify:
16908
+ get:
16909
+ x-resource-group: "accounts"
16910
+ operationId: getShopifyConnectUrl
16911
+ tags: [Connect]
16912
+ summary: Get Shopify OAuth connect URL
16913
+ description: |
16914
+ Initiate the Shopify OAuth flow for a store. Shopify is a connect-only
16915
+ platform: the connected account does not publish social posts, it powers
16916
+ the Blogs API (`/v1/accounts/{accountId}/blogs`). Returns an `authUrl`
16917
+ to redirect the merchant to; after they approve the install, Shopify
16918
+ redirects their browser to Zernio's callback, the account is created on
16919
+ the profile (platform `shopify`), and the browser is redirected to
16920
+ `redirect_url` (or the Zernio dashboard when omitted). Requested scopes
16921
+ are `read_content` and `write_content` (content only; no customer or
16922
+ order data). Connecting the same profile to a store again refreshes the
16923
+ stored token in place.
16924
+ security:
16925
+ - bearerAuth: []
16926
+ parameters:
16927
+ - name: profileId
16928
+ in: query
16929
+ required: true
16930
+ schema: { type: string }
16931
+ description: 'Your Zernio profile ID (get from /v1/profiles).'
16932
+ - name: shop
16933
+ in: query
16934
+ required: true
16935
+ schema: { type: string }
16936
+ description: 'The myshopify.com store domain to connect, e.g. `your-store.myshopify.com` (the bare `your-store` prefix is accepted too).'
16937
+ - name: redirect_url
16938
+ in: query
16939
+ schema: { type: string, format: uri }
16940
+ description: 'Your custom redirect URL after connection completes. Accepts an http(s) URL, a custom app scheme for mobile deeplinks (e.g. myapp://callback), or a relative path. On failure an `error` query param is appended.'
16941
+ responses:
16942
+ '200':
16943
+ description: OAuth authorization URL to redirect the merchant to
16944
+ content:
16945
+ application/json:
16946
+ schema:
16947
+ type: object
16948
+ properties:
16949
+ authUrl:
16950
+ type: string
16951
+ format: uri
16952
+ description: URL to redirect your user to for OAuth authorization
16953
+ state:
16954
+ type: string
16955
+ description: State parameter for security (handled automatically)
16956
+ '400':
16957
+ description: 'Invalid `profileId` format, `shop` is not a myshopify.com store domain, or `redirect_url` uses a script-executing scheme.'
16958
+ '401': { $ref: '#/components/responses/Unauthorized' }
16959
+ '402': { $ref: '#/components/responses/PaymentRequired' }
16960
+ '403':
16961
+ description: 'API key does not have access to this profile.'
16962
+ '404':
16963
+ description: 'Profile not found or access denied.'
16964
+ '500':
16965
+ description: 'Shopify API not configured (missing credentials).'
16966
+
16967
+ /v1/connect/shopify/token:
16968
+ post:
16969
+ x-resource-group: "accounts"
16970
+ operationId: connectShopifyWithToken
16971
+ tags: [Connect]
16972
+ summary: Connect a Shopify store with a custom-app Admin token
16973
+ description: |
16974
+ Token-paste alternative to the OAuth flow: connect a store using the
16975
+ Admin API access token of a custom app the merchant created in their
16976
+ own Shopify admin (Settings → Apps and sales channels → Develop apps,
16977
+ with the `read_content`/`write_content` scopes). Use this when the
16978
+ one-click OAuth connect is unavailable or when your users prefer not
16979
+ to install a third-party app on their store. The token is validated
16980
+ against the store before anything is saved; custom-app tokens do not
16981
+ expire. Connecting the same profile to a store again replaces the
16982
+ stored token in place.
16983
+ security:
16984
+ - bearerAuth: []
16985
+ requestBody:
16986
+ required: true
16987
+ content:
16988
+ application/json:
16989
+ schema:
16990
+ type: object
16991
+ required: [profileId, shop, accessToken]
16992
+ properties:
16993
+ profileId:
16994
+ type: string
16995
+ description: 'Your Zernio profile ID (get from /v1/profiles).'
16996
+ shop:
16997
+ type: string
16998
+ description: 'The myshopify.com store domain, e.g. `your-store.myshopify.com` (the bare `your-store` prefix is accepted too).'
16999
+ accessToken:
17000
+ type: string
17001
+ description: 'Admin API access token of the merchant-created custom app (starts with `shpat_`).'
17002
+ responses:
17003
+ '200':
17004
+ description: Store connected as a platform account
17005
+ content:
17006
+ application/json:
17007
+ schema:
17008
+ type: object
17009
+ properties:
17010
+ account:
17011
+ type: object
17012
+ properties:
17013
+ _id: { type: string }
17014
+ platform: { type: string, enum: [shopify] }
17015
+ username: { type: string, description: 'The myshopify.com domain of the connected store.' }
17016
+ displayName: { type: string }
17017
+ profileId: { type: string }
17018
+ '400':
17019
+ description: 'Invalid `profileId` format, `shop` is not a myshopify.com store domain, or Shopify rejected the access token for that store.'
17020
+ '401': { $ref: '#/components/responses/Unauthorized' }
17021
+ '402': { $ref: '#/components/responses/PaymentRequired' }
17022
+ '403':
17023
+ description: 'API key does not have access to this profile.'
17024
+
16124
17025
  /v1/connect/tiktok-ads:
16125
17026
  patch:
16126
17027
  x-resource-group: "accounts"
@@ -19547,7 +20448,7 @@ paths:
19547
20448
  '403':
19548
20449
  description: Profile limit exceeded for this plan
19549
20450
  '409':
19550
- description: 'The phone number is a Zernio-provisioned number pinned to a different profile. Connect it from that profile, or move it first with PATCH /v1/whatsapp/phone-numbers/{id}/profile.'
20451
+ description: 'Conflict with an existing connection. Either the phone number is a Zernio-provisioned number pinned to a different profile (code WHATSAPP_NUMBER_PINNED_TO_PROFILE, connect it from that profile or move it first with PATCH /v1/whatsapp/phone-numbers/{id}/profile), or the number is already actively connected on another profile or workspace (code WHATSAPP_NUMBER_ALREADY_CONNECTED, disconnect it there first). A number can only be live on one profile.'
19551
20452
 
19552
20453
  /v1/connect/whatsapp/select-phone-number:
19553
20454
  get:
@@ -19704,7 +20605,7 @@ paths:
19704
20605
  application/json:
19705
20606
  schema: { $ref: '#/components/schemas/ErrorResponse' }
19706
20607
  '409':
19707
- description: 'The phone number is a Zernio-provisioned number pinned to a different profile. Connect it from that profile, or move it first with PATCH /v1/whatsapp/phone-numbers/{id}/profile.'
20608
+ description: 'Conflict with an existing connection. Either the phone number is a Zernio-provisioned number pinned to a different profile (code WHATSAPP_NUMBER_PINNED_TO_PROFILE, connect it from that profile or move it first with PATCH /v1/whatsapp/phone-numbers/{id}/profile), or the number is already actively connected on another profile or workspace (code WHATSAPP_NUMBER_ALREADY_CONNECTED, disconnect it there first). A number can only be live on one profile.'
19708
20609
  '500':
19709
20610
  description: Failed to bind phone number
19710
20611
  content:
@@ -20297,7 +21198,7 @@ paths:
20297
21198
  sends: { type: integer, description: Times the post was sent via LinkedIn messaging (personal accounts only; 0 for organization accounts) }
20298
21199
  clicks: { type: integer, description: Clicks on the post (organization accounts only) }
20299
21200
  views: { type: integer, description: Video views (video posts only) }
20300
- engagementRate: { type: number, description: Engagement rate as percentage }
21201
+ engagementRate: { type: number, description: 'Engagement rate, as a percentage rounded to 2 decimals: (likes + comments + shares + clicks + saves + sends) / impressions * 100. Unlike PostAnalytics.engagementRate on GET /v1/analytics, this one DOES count clicks and has no fallback denominator, so it is 0 whenever impressions is 0. For organization accounts the value is the rate LinkedIn returns, not one computed here.' }
20301
21202
  lastUpdated: { type: string, format: date-time }
20302
21203
  example:
20303
21204
  accountId: "64abc123def456"
@@ -21811,6 +22712,65 @@ paths:
21811
22712
  '401': { $ref: '#/components/responses/Unauthorized' }
21812
22713
  '404': { description: Account not found }
21813
22714
 
22715
+ /v1/accounts/{accountId}/bluesky-settings:
22716
+ get:
22717
+ x-resource-group: "accounts"
22718
+ operationId: getBlueskySettings
22719
+ tags: [Accounts]
22720
+ summary: Get Bluesky account settings
22721
+ description: 'Returns the account''s default post languages (defaultLangs), applied at publish time whenever a post''s platformSpecificData.langs is absent. Null when no default is set.'
22722
+ parameters:
22723
+ - name: accountId
22724
+ in: path
22725
+ required: true
22726
+ schema: { type: string }
22727
+ responses:
22728
+ '200':
22729
+ description: Bluesky account settings
22730
+ content:
22731
+ application/json:
22732
+ schema:
22733
+ type: object
22734
+ properties:
22735
+ defaultLangs:
22736
+ type: [array, "null"]
22737
+ items: { type: string }
22738
+ example: ["pt"]
22739
+ '400': { $ref: '#/components/responses/BadRequest' }
22740
+ '401': { $ref: '#/components/responses/Unauthorized' }
22741
+ '404': { description: Account not found }
22742
+ patch:
22743
+ x-resource-group: "accounts"
22744
+ operationId: updateBlueskySettings
22745
+ tags: [Accounts]
22746
+ summary: Update Bluesky account settings
22747
+ description: 'Set or clear the account''s default post languages. 1-3 BCP-47 codes (e.g. "pt", "en-US"), the same validation as per-post langs; explicit null clears the default. Per-post platformSpecificData.langs always overrides this default. Applies to posts published after the change; already-published posts cannot be retagged (Bluesky has no post edit).'
22748
+ parameters:
22749
+ - name: accountId
22750
+ in: path
22751
+ required: true
22752
+ schema: { type: string }
22753
+ requestBody:
22754
+ required: true
22755
+ content:
22756
+ application/json:
22757
+ schema:
22758
+ type: object
22759
+ required: [defaultLangs]
22760
+ properties:
22761
+ defaultLangs:
22762
+ type: [array, "null"]
22763
+ minItems: 1
22764
+ maxItems: 3
22765
+ items: { type: string }
22766
+ example: ["pt"]
22767
+ responses:
22768
+ '200':
22769
+ description: Updated settings
22770
+ '400': { $ref: '#/components/responses/BadRequest' }
22771
+ '401': { $ref: '#/components/responses/Unauthorized' }
22772
+ '404': { description: Account not found }
22773
+
21814
22774
  /v1/accounts/{accountId}/discord-settings:
21815
22775
  get:
21816
22776
  x-resource-group: "accounts"
@@ -23336,7 +24296,7 @@ paths:
23336
24296
  minItems: 1
23337
24297
  items:
23338
24298
  type: string
23339
- enum: [post.scheduled, post.published, post.failed, post.partial, post.cancelled, post.recycled, post.platform.published, post.platform.failed, post.platform.deleted, post.tiktok.url_resolved, post.external.created, post.external.updated, post.external.deleted, account.connected, account.disconnected, account.ads.initial_sync_completed, message.received, conversation.started, call.received, call.ended, call.failed, call.permission_request, message.sent, message.edited, message.deleted, message.delivered, message.read, message.failed, reaction.received, comment.received, review.new, review.updated, lead.received, ad.status_changed, whatsapp.template.status_updated, whatsapp.automatic_event, whatsapp.number.activated, whatsapp.number.declined, whatsapp.number.action_required, whatsapp.number.verification_required, whatsapp.number.suspended, whatsapp.number.reactivated, whatsapp.number.released, whatsapp.number.kyc_submitted, verification.approved, verification.failed]
24299
+ enum: [post.scheduled, post.published, post.failed, post.partial, post.cancelled, post.recycled, post.platform.published, post.platform.failed, post.platform.deleted, post.tiktok.url_resolved, post.external.created, post.external.updated, post.external.deleted, account.connected, account.disconnected, account.ads.initial_sync_completed, message.received, conversation.started, call.received, call.ended, call.failed, call.permission_request, message.sent, message.edited, message.deleted, message.delivered, message.read, message.failed, reaction.received, referral.received, comment.received, review.new, review.updated, lead.received, ad.status_changed, whatsapp.template.status_updated, whatsapp.template.category_updated, whatsapp.automatic_event, whatsapp.number.activated, whatsapp.number.declined, whatsapp.number.action_required, whatsapp.number.verification_required, whatsapp.number.suspended, whatsapp.number.reactivated, whatsapp.number.released, whatsapp.number.kyc_submitted, verification.approved, verification.failed]
23340
24300
  description: Events to subscribe to (at least one required)
23341
24301
  isActive:
23342
24302
  type: boolean
@@ -23455,7 +24415,7 @@ paths:
23455
24415
  minItems: 1
23456
24416
  items:
23457
24417
  type: string
23458
- enum: [post.scheduled, post.published, post.failed, post.partial, post.cancelled, post.recycled, post.platform.published, post.platform.failed, post.platform.deleted, post.tiktok.url_resolved, post.external.created, post.external.updated, post.external.deleted, account.connected, account.disconnected, account.ads.initial_sync_completed, message.received, conversation.started, call.received, call.ended, call.failed, call.permission_request, message.sent, message.edited, message.deleted, message.delivered, message.read, message.failed, reaction.received, comment.received, review.new, review.updated, lead.received, ad.status_changed, whatsapp.template.status_updated, whatsapp.automatic_event, whatsapp.number.activated, whatsapp.number.declined, whatsapp.number.action_required, whatsapp.number.verification_required, whatsapp.number.suspended, whatsapp.number.reactivated, whatsapp.number.released, whatsapp.number.kyc_submitted, verification.approved, verification.failed]
24418
+ enum: [post.scheduled, post.published, post.failed, post.partial, post.cancelled, post.recycled, post.platform.published, post.platform.failed, post.platform.deleted, post.tiktok.url_resolved, post.external.created, post.external.updated, post.external.deleted, account.connected, account.disconnected, account.ads.initial_sync_completed, message.received, conversation.started, call.received, call.ended, call.failed, call.permission_request, message.sent, message.edited, message.deleted, message.delivered, message.read, message.failed, reaction.received, referral.received, comment.received, review.new, review.updated, lead.received, ad.status_changed, whatsapp.template.status_updated, whatsapp.template.category_updated, whatsapp.automatic_event, whatsapp.number.activated, whatsapp.number.declined, whatsapp.number.action_required, whatsapp.number.verification_required, whatsapp.number.suspended, whatsapp.number.reactivated, whatsapp.number.released, whatsapp.number.kyc_submitted, verification.approved, verification.failed]
23459
24419
  description: Events to subscribe to. Must contain at least one event if provided.
23460
24420
  isActive:
23461
24421
  type: boolean
@@ -23985,13 +24945,15 @@ paths:
23985
24945
  metadata:
23986
24946
  type: [object, "null"]
23987
24947
  description: |
23988
- Ad-click attribution for a conversation that started from a Meta ad.
23989
- Absent when the conversation did not originate from an ad click.
23990
-
23991
- Captured from the referral Meta attaches to the first inbound message
23992
- after the click, which is the only message that carries it. If the same
23993
- person later clicks a different ad, the original values are kept, so the
23994
- first ad wins. One exception on WhatsApp: when Meta omits `ctwa_clid`
24948
+ Click attribution for a conversation that started from a Meta ad or
24949
+ a ref-tagged ig.me / m.me link. Absent when the conversation did not
24950
+ originate from an attributable click.
24951
+
24952
+ Captured from the referral Meta delivers for the click. If the same
24953
+ person later arrives through a different ad or link, the original
24954
+ values are kept, so the first referral wins; read the fresh referral
24955
+ per click on the `message.received` / `referral.received` webhooks
24956
+ instead. One exception on WhatsApp: when Meta omits `ctwa_clid`
23995
24957
  from that referral, a later Meta automatic event can supply it and
23996
24958
  refresh `ctwa_captured_at`, so treat `ctwa_captured_at` as the time
23997
24959
  Zernio stored the value, not the time of the click.
@@ -24000,9 +24962,11 @@ paths:
24000
24962
 
24001
24963
  - `ctwa_*` is WhatsApp Click-to-WhatsApp. The ad ID is
24002
24964
  `ctwa_source_id`. There is no `meta_ad_id` on WhatsApp.
24003
- - `meta_ad_*` is Instagram Click-to-Direct and Facebook Messenger
24004
- Click-to-Message. The ad ID is `meta_ad_id`. `ctwa_clid` never
24005
- appears on these platforms.
24965
+ - `meta_ad_*` is Instagram Click-to-Direct, Facebook Messenger
24966
+ Click-to-Message, and ig.me / m.me ref links. The ad ID is
24967
+ `meta_ad_id` (ad clicks only; a link capture carries
24968
+ `meta_ad_ref` without it). `ctwa_clid` never appears on these
24969
+ platforms.
24006
24970
 
24007
24971
  Every key is optional and only the keys Meta supplied are returned, so
24008
24972
  read defensively. Meta does not send a campaign or ad set ID, so none
@@ -24036,16 +25000,16 @@ paths:
24036
25000
  description: "WhatsApp only. When Zernio stored this referral. Always present when a WhatsApp referral was captured."
24037
25001
  meta_ad_id:
24038
25002
  type: string
24039
- description: "Instagram and Facebook only. The Meta ad ID the user clicked. Always present when an Instagram or Facebook referral was captured."
25003
+ description: "Instagram and Facebook only. The Meta ad ID the user clicked. Present for ad clicks; absent when the capture came from an ig.me / m.me ref link."
24040
25004
  meta_ad_source:
24041
25005
  type: string
24042
- description: "Instagram and Facebook only. Meta-supplied source identifier, for example ADS."
25006
+ description: "Instagram and Facebook only. Meta-supplied source identifier: ADS for ad clicks; SHORTLINK, SHORTLINKS or IGME-SOURCE-LINK for ref links (treat as opaque)."
24043
25007
  meta_ad_type:
24044
25008
  type: string
24045
25009
  description: "Instagram and Facebook only. Meta-supplied referral type, for example OPEN_THREAD."
24046
25010
  meta_ad_ref:
24047
25011
  type: string
24048
- description: "Instagram and Facebook only. The ref parameter passed through from the ad creative."
25012
+ description: "Instagram and Facebook only. The ref parameter passed through from the ad creative or the ig.me / m.me link."
24049
25013
  meta_ad_title:
24050
25014
  type: string
24051
25015
  description: "Instagram and Facebook only. Title of the ad creative at click time."
@@ -24154,6 +25118,15 @@ paths:
24154
25118
  already categorized at creation). Utility messages only; marketing
24155
25119
  content is not allowed under this category. Accepted on the JSON
24156
25120
  body only, not on multipart requests.
25121
+ linkPreview:
25122
+ type: boolean
25123
+ default: true
25124
+ description: >-
25125
+ WhatsApp only. Set false to send the Direct Send (category: 'utility')
25126
+ text message without a link-preview thumbnail for the first URL in the
25127
+ text. Defaults to true, which is how every WhatsApp text has been sent
25128
+ to date. Does not apply to template sends. Accepted on the JSON body
25129
+ only, not on multipart requests.
24157
25130
  templateLanguage:
24158
25131
  type: string
24159
25132
  description: WhatsApp only. Template language code (e.g. en_US).
@@ -24316,7 +25289,7 @@ paths:
24316
25289
  - name: cursor
24317
25290
  in: query
24318
25291
  schema: { type: string }
24319
- description: Pagination cursor for next page
25292
+ description: "Opaque pagination cursor. Pass back pagination.nextCursor verbatim; do not construct one."
24320
25293
  responses:
24321
25294
  '200':
24322
25295
  description: Conversations containing the query, most recent match first
@@ -24384,7 +25357,7 @@ paths:
24384
25357
  accountId: { type: string }
24385
25358
  platform: { type: string }
24386
25359
  '400':
24387
- description: Invalid query or unsupported platform
25360
+ description: 'Invalid query, unsupported platform, or malformed cursor'
24388
25361
  '401': { $ref: '#/components/responses/Unauthorized' }
24389
25362
  '403':
24390
25363
  description: Inbox addon required
@@ -24633,7 +25606,7 @@ paths:
24633
25606
  in: query
24634
25607
  required: false
24635
25608
  schema: { type: string }
24636
- description: Opaque pagination cursor. Pass `pagination.nextCursor` from a prior response.
25609
+ description: 'Opaque pagination cursor. Pass `pagination.nextCursor` from a prior response verbatim: a cursor we cannot parse returns 400 rather than silently restarting from the first page.'
24637
25610
  - name: sortOrder
24638
25611
  in: query
24639
25612
  required: false
@@ -24772,6 +25745,7 @@ paths:
24772
25745
  and for inbound interactive taps `interactiveType` / `interactiveId`.
24773
25746
  additionalProperties: true
24774
25747
  lastUpdated: { type: string, format: date-time }
25748
+ '400': { $ref: '#/components/responses/BadRequest' }
24775
25749
  '401': { $ref: '#/components/responses/Unauthorized' }
24776
25750
  '403':
24777
25751
  description: Inbox addon required
@@ -24822,7 +25796,7 @@ paths:
24822
25796
  properties:
24823
25797
  accountId: { type: string, description: Social account ID }
24824
25798
  message: { type: string, description: Message text }
24825
- attachmentUrl: { type: string, description: "URL of the attachment to send (image, video, audio, or file). The URL must be publicly accessible. For binary file uploads, use multipart/form-data instead." }
25799
+ attachmentUrl: { type: string, description: "URL of the attachment to send (image, video, audio, or file). The URL must be publicly accessible. For binary file uploads, use multipart/form-data instead. On WhatsApp, combining an image, video, or file with `buttons` renders the media as the header of one interactive reply-button message; audio cannot be combined with buttons." }
24826
25800
  category:
24827
25801
  type: string
24828
25802
  enum: [utility]
@@ -24840,6 +25814,14 @@ paths:
24840
25814
  Utility messages only; marketing content is not allowed under
24841
25815
  this category. Accepted on the JSON body only, not on multipart
24842
25816
  requests.
25817
+ linkPreview:
25818
+ type: boolean
25819
+ default: true
25820
+ description: >-
25821
+ WhatsApp only. Set false to send the message without a link-preview
25822
+ thumbnail for the first URL in the text. Defaults to true, which is how
25823
+ every WhatsApp text has been sent to date. Ignored on other platforms.
25824
+ Accepted on the JSON body only, not on multipart requests.
24843
25825
  attachmentType:
24844
25826
  type: string
24845
25827
  enum: [image, video, audio, file]
@@ -24885,6 +25867,12 @@ paths:
24885
25867
  reply-button message, provide `title` + `payload` and set
24886
25868
  `type: postback`, e.g.
24887
25869
  `{ "type": "postback", "title": "Yes", "payload": "yes" }`.
25870
+
25871
+ Combine `buttons` with `attachmentUrl` and `attachmentType`
25872
+ `image`, `video`, or `file` to render one WhatsApp message with
25873
+ a media header, body text, and reply buttons. Audio is not a
25874
+ supported interactive header and returns 400 when combined
25875
+ with buttons.
24888
25876
  items:
24889
25877
  type: object
24890
25878
  required: [type, title]
@@ -25260,6 +26248,27 @@ paths:
25260
26248
  properties:
25261
26249
  email: { type: string }
25262
26250
  type: { type: string }
26251
+ examples:
26252
+ whatsappImageWithReplyButtons:
26253
+ summary: WhatsApp image, body text, and reply buttons
26254
+ description: |
26255
+ The recipient sees one WhatsApp message containing the property
26256
+ image as its header, "Reply yes to continue." as the body, and
26257
+ two tappable reply buttons labelled Yes and No. A tap arrives on
26258
+ the `message.received` webhook as `metadata.interactiveType:
26259
+ button_reply`; `metadata.interactiveId` is `btn_0` or `btn_1`.
26260
+ value:
26261
+ accountId: 6a7adc04d0fe733d1a1bed76
26262
+ message: Reply yes to continue.
26263
+ attachmentUrl: https://cdn.example.com/property.jpg
26264
+ attachmentType: image
26265
+ buttons:
26266
+ - type: postback
26267
+ title: Yes
26268
+ payload: btn_0
26269
+ - type: postback
26270
+ title: No
26271
+ payload: btn_1
25263
26272
  multipart/form-data:
25264
26273
  schema:
25265
26274
  type: object
@@ -25270,13 +26279,13 @@ paths:
25270
26279
  attachment:
25271
26280
  type: string
25272
26281
  format: binary
25273
- description: "File attachment (images, videos, documents). Supported formats: JPEG, PNG, GIF, MP4, AAC, WAV. Max 25MB."
26282
+ description: "File attachment (images, videos, documents). Supported formats: JPEG, PNG, GIF, MP4, AAC, WAV. Max 25MB. On WhatsApp, combining an image, video, or document upload with buttons renders the upload as the interactive message header; audio plus buttons is rejected."
25274
26283
  quickReplies:
25275
26284
  type: string
25276
26285
  description: JSON string of quick replies array (same schema as application/json body)
25277
26286
  buttons:
25278
26287
  type: string
25279
- description: JSON string of buttons array (same schema as application/json body)
26288
+ description: JSON string of buttons array (same schema as application/json body). On WhatsApp, an accompanying image, video, or document attachment becomes the interactive message header.
25280
26289
  template:
25281
26290
  type: string
25282
26291
  description: JSON string of template object (same schema as application/json body)
@@ -26060,8 +27069,18 @@ paths:
26060
27069
  (Instagram side); a row whose count can't be read is omitted.
26061
27070
 
26062
27071
  Pagination walks each account's platform listing. Following `nextCursor` reaches past
26063
- the first page on Facebook and Instagram only, since they are the platforms that
26064
- support a server-side date window; on the others the listing stops at its first page.
27072
+ the first page on Facebook, Instagram, Threads, LinkedIn and YouTube, since they are
27073
+ the platforms that support a server-side date window; on the others the listing stops
27074
+ at its first page. Cursor pagination is only coherent for the default sort
27075
+ (`sortBy=date`, `sortOrder=desc`): with `sortOrder=asc`, or with `sortBy=comments`,
27076
+ the cursor filter does not match the sort order and the second page is unreliable.
27077
+
27078
+ `nextCursor` is opaque: pass it back verbatim, never construct or parse it, its
27079
+ composition may change without notice. Because each page re-queries a live window,
27080
+ results can still shift between requests, so dedupe by `id` on the client.
27081
+
27082
+ `commentCount` semantics differ by platform: YouTube's includes replies, Facebook's counts
27083
+ top-level comments only.
26065
27084
  tags: [Comments]
26066
27085
  security: [{ bearerAuth: [] }]
26067
27086
  parameters:
@@ -26158,14 +27177,19 @@ paths:
26158
27177
  x-resource-group: "engagement"
26159
27178
  operationId: getInboxPostComments
26160
27179
  summary: Get post comments
26161
- description: Fetch comments for a specific post. Requires accountId query parameter.
27180
+ description: |
27181
+ Fetch comments for a specific post. Requires accountId query parameter.
27182
+
27183
+ On Facebook and Instagram, passing a COMMENT id as `postId` is also supported and
27184
+ returns that comment's replies instead of the post's top-level comments. This is not
27185
+ available on YouTube, where `postId` must be a video id.
26162
27186
  tags: [Comments]
26163
27187
  security: [{ bearerAuth: [] }]
26164
27188
  parameters:
26165
27189
  - name: postId
26166
27190
  in: path
26167
27191
  required: true
26168
- description: Zernio post ID or platform-specific post ID. Zernio IDs are auto-resolved. LinkedIn third-party posts accept full activity URN or numeric ID.
27192
+ description: Zernio post ID or platform-specific post ID. Zernio IDs are auto-resolved. LinkedIn third-party posts accept full activity URN or numeric ID. On Facebook and Instagram, a comment ID is also accepted here and returns that comment's replies.
26169
27193
  schema: { type: string }
26170
27194
  - name: accountId
26171
27195
  in: query
@@ -26182,7 +27206,7 @@ paths:
26182
27206
  - name: cursor
26183
27207
  in: query
26184
27208
  schema: { type: string }
26185
- description: Pagination cursor
27209
+ description: 'Pagination cursor, returned by a previous call as `pagination.cursor`. This is the platform''s own opaque paging value passed through verbatim: never construct, decode or validate it client-side.'
26186
27210
  - name: commentId
26187
27211
  in: query
26188
27212
  schema: { type: string }
@@ -26217,7 +27241,7 @@ paths:
26217
27241
  enum: [blue, government, business, none]
26218
27242
  description: X/Twitter verified badge type. Only present for Twitter/X comments.
26219
27243
  likeCount: { type: integer }
26220
- replyCount: { type: integer }
27244
+ replyCount: { type: integer, description: "The platform's own reply count, which includes hidden and deleted replies. Can exceed replies[].length even when repliesHasMore is false or absent." }
26221
27245
  platform: { type: string, description: The platform this comment is from }
26222
27246
  url:
26223
27247
  type: [string, "null"]
@@ -26225,6 +27249,9 @@ paths:
26225
27249
  replies:
26226
27250
  type: array
26227
27251
  items: { type: object }
27252
+ repliesHasMore:
27253
+ type: boolean
27254
+ description: "Facebook only. True when replies[] (capped at 10) does not hold the comment's full reply thread; fetch the rest by passing the comment id as postId to GET /v1/inbox/comments/{postId}. Absent (not false) on every other platform, including Instagram, which has no equivalent signal."
26228
27255
  canReply: { type: boolean }
26229
27256
  canDelete: { type: boolean }
26230
27257
  canHide: { type: boolean, description: "Whether this comment can be hidden (Facebook, Instagram, Threads)" }
@@ -26233,7 +27260,7 @@ paths:
26233
27260
  isLiked: { type: boolean, description: Whether the current user has liked this comment }
26234
27261
  likeUri: { type: [string, "null"], description: Bluesky like URI for unliking }
26235
27262
  cid: { type: [string, "null"], description: Bluesky content identifier }
26236
- parentId: { type: [string, "null"], description: Parent comment ID for nested replies }
27263
+ parentId: { type: [string, "null"], description: 'ID of the parent comment. Present on entries inside replies[] for Facebook, Instagram and X/Twitter. On X/Twitter it is also present on top-level entries, where it holds the ID of the post replied to. Omitted entirely (key absent, not null) on top-level Facebook and Instagram entries and on every other platform, which express the parent relationship only through replies[] nesting.' }
26237
27264
  rootUri: { type: [string, "null"], description: Bluesky root post URI }
26238
27265
  rootCid: { type: [string, "null"], description: Bluesky root post CID }
26239
27266
  post:
@@ -26364,6 +27391,7 @@ paths:
26364
27391
  - name: commentId
26365
27392
  in: query
26366
27393
  required: true
27394
+ description: 'For LinkedIn, accepts either the numeric comment ID or the composite comment URN returned by the comments listing (e.g. urn:li:comment:(threadUrn,id))'
26367
27395
  schema: { type: string }
26368
27396
  responses:
26369
27397
  '200':
@@ -29007,6 +30035,15 @@ paths:
29007
30035
  US numbers must have an approved carrier registration
29008
30036
  (`/v1/sms/registrations`) before messages deliver.
29009
30037
 
30038
+ **Replies and delivery status arrive as webhooks**, not by polling:
30039
+ an inbound reply fires `message.received` with `platform: "sms"`, the
30040
+ first message of a new thread also fires `conversation.started`, and
30041
+ this message's own outcome fires `message.delivered` or
30042
+ `message.failed` (the latter carrying the carrier's error code).
30043
+
30044
+ **Opted-out recipients:** a send to a number that replied STOP is
30045
+ refused with `409`, never silently dropped.
30046
+
29010
30047
  **Idempotency:** send an `Idempotency-Key` header to make retries safe:
29011
30048
  same key + same body replays the original response instead of sending a
29012
30049
  second message; same key + different body returns 422; a key still in
@@ -29046,9 +30083,10 @@ paths:
29046
30083
  id: { type: string, description: Message ID }
29047
30084
  conversationId: { type: string, description: Inbox conversation the message was threaded into. }
29048
30085
  status: { type: string, enum: [sent] }
30086
+ '400': { $ref: '#/components/responses/BadRequest' }
29049
30087
  '401': { $ref: '#/components/responses/Unauthorized' }
29050
30088
  '404': { description: "No SMS-enabled number matches `from`" }
29051
- '409': { $ref: '#/components/responses/IdempotencyKeyInFlight' }
30089
+ '409': { description: "Recipient has opted out (replied STOP), or the same Idempotency-Key is still in flight" }
29052
30090
  '422': { $ref: '#/components/responses/IdempotencyKeyReused' }
29053
30091
  '502': { description: Carrier-side send failed }
29054
30092
 
@@ -30573,6 +31611,47 @@ paths:
30573
31611
  '401': { $ref: '#/components/responses/Unauthorized' }
30574
31612
  '404': { description: WhatsApp account not found }
30575
31613
 
31614
+ /v1/whatsapp/account-events:
31615
+ get:
31616
+ x-resource-group: "accounts"
31617
+ operationId: listWhatsAppAccountEvents
31618
+ tags: [WhatsApp]
31619
+ summary: List account notifications
31620
+ description: |
31621
+ Returns Meta-originated events recorded for a WhatsApp account, newest
31622
+ first: template review outcomes (approved, rejected, paused, category
31623
+ changes) and WABA status changes (restricted, disabled, reinstated,
31624
+ disconnected). Events are captured from Meta webhooks as they happen;
31625
+ the feed starts at the account's first recorded event and is not
31626
+ backfilled. Complements the push events `whatsapp.template.status_updated`
31627
+ and `account.disconnected` with a pollable history.
31628
+ security:
31629
+ - bearerAuth: []
31630
+ parameters:
31631
+ - { name: accountId, in: query, required: true, schema: { type: string }, description: WhatsApp social account ID }
31632
+ - { name: limit, in: query, required: false, schema: { type: integer, minimum: 1, maximum: 200, default: 50 }, description: Maximum events to return }
31633
+ responses:
31634
+ '200':
31635
+ description: Recorded events, newest first
31636
+ content:
31637
+ application/json:
31638
+ schema:
31639
+ type: object
31640
+ properties:
31641
+ events:
31642
+ type: array
31643
+ items:
31644
+ type: object
31645
+ properties:
31646
+ id: { type: string }
31647
+ type: { type: string, description: 'Event kind, e.g. template_approved, template_rejected, account_restricted, account_disconnected' }
31648
+ severity: { type: string, enum: [info, success, warning, critical] }
31649
+ title: { type: string }
31650
+ detail: { type: [string, "null"] }
31651
+ createdAt: { type: string, format: date-time }
31652
+ '400': { $ref: '#/components/responses/BadRequest' }
31653
+ '401': { $ref: '#/components/responses/Unauthorized' }
31654
+ '404': { description: WhatsApp account not found }
30576
31655
  /v1/whatsapp/dataset:
30577
31656
  get:
30578
31657
  x-resource-group: "accounts"
@@ -30728,6 +31807,7 @@ paths:
30728
31807
  endUserFirstName: { type: [string, "null"] }
30729
31808
  endUserLastName: { type: [string, "null"] }
30730
31809
  regulatoryDeclineReason: { type: [string, "null"], description: Reviewer rejection reason when status is regulatory_declined. }
31810
+ callingEnabled: { type: boolean, description: 'Whether WhatsApp Business Calling is enabled on this number (manage via /v1/whatsapp/phone-numbers/{id}/calling).' }
30731
31811
  createdAt: { type: string, format: date-time }
30732
31812
  connected:
30733
31813
  type: array
@@ -30745,6 +31825,7 @@ paths:
30745
31825
  displayName: { type: [string, "null"] }
30746
31826
  profileId: { type: [string, "null"] }
30747
31827
  connectedAt: { type: [string, "null"], format: date-time }
31828
+ callingEnabled: { type: boolean, description: 'Whether WhatsApp Business Calling is enabled on this number.' }
30748
31829
  sandbox:
30749
31830
  type: [object, "null"]
30750
31831
  description: |
@@ -31267,6 +32348,7 @@ paths:
31267
32348
  endUserFirstName: { type: [string, "null"] }
31268
32349
  endUserLastName: { type: [string, "null"] }
31269
32350
  regulatoryDeclineReason: { type: [string, "null"], description: Reviewer rejection reason when status is regulatory_declined. }
32351
+ callingEnabled: { type: boolean, description: 'Whether WhatsApp Business Calling is enabled on this number (manage via /v1/whatsapp/phone-numbers/{id}/calling).' }
31270
32352
  createdAt: { type: string, format: date-time }
31271
32353
  connected:
31272
32354
  type: array
@@ -31284,6 +32366,7 @@ paths:
31284
32366
  displayName: { type: [string, "null"] }
31285
32367
  profileId: { type: [string, "null"] }
31286
32368
  connectedAt: { type: [string, "null"], format: date-time }
32369
+ callingEnabled: { type: boolean, description: 'Whether WhatsApp Business Calling is enabled on this number.' }
31287
32370
  sandbox:
31288
32371
  type: [object, "null"]
31289
32372
  description: |
@@ -31643,6 +32726,7 @@ paths:
31643
32726
  label: { type: string }
31644
32727
  value: { type: string }
31645
32728
  documentId: { type: string, description: 'Present on document rows — the Telnyx document id. GET /v1/whatsapp/phone-numbers/kyc/document/{documentId} streams it (auth-scoped, inline PDF).' }
32729
+ pendingReview: { type: boolean, description: 'true when this account already has a number for this country in regulatory review (status pending_regulatory). Scope is the whole account across all profiles, and the country only (any number type), so it is not a per-end-client signal on a multi-tenant setup. Informational only: it never blocks a submission, and several same-country numbers may sit in review at once. For a per-end-client view, call GET /v1/phone-numbers with `profileId` and `status=pending_regulatory`; that view also lists numbers declined in the last 30 days.' }
31646
32730
  '400': { description: Country not available }
31647
32731
  '401': { $ref: '#/components/responses/Unauthorized' }
31648
32732
  post:
@@ -31680,7 +32764,7 @@ paths:
31680
32764
  properties:
31681
32765
  profileId: { type: string }
31682
32766
  country: { type: string }
31683
- submissionId: { type: string, description: Idempotency token for this submission attempt. A retry/double-submit with the same token returns the same number; omit and each call creates a new number. }
32767
+ submissionId: { type: string, description: 'Idempotency token for this submission attempt. Once the number has been ordered, a retry with the same token returns that same number instead of ordering another. A submission that fails before the number is ordered releases the token, so you can correct your details and re-submit with it. Omit it and every call provisions a new number.' }
31684
32768
  quantity: { type: integer, minimum: 1, maximum: 5, default: 1, description: 'Provision several same-country numbers from one submission (1-5). The single verification covers all of them; each number is billed only when it activates. Numbers that fail to order are skipped (best-effort). With `areaCode`, a quantity above that area''s live stock is rejected with a 400.' }
31685
32769
  reuse: { type: boolean, description: Reuse a prior approved verification for this country (skips document/field collection; places the order immediately). }
31686
32770
  reuseOptionId: { type: string, description: 'Which reusable verification to use (GET reusable.options[].id). The unambiguous selection key. Omitted = the approved default. No match = 409.' }
@@ -31717,6 +32801,7 @@ paths:
31717
32801
  first_name: { type: string }
31718
32802
  last_name: { type: string }
31719
32803
  street_address: { type: string }
32804
+ extended_address: { type: string, description: 'Address complement: apartment, suite, unit, or the quadra/lote used in some countries. Optional. Does not substitute for a building number on street_address.' }
31720
32805
  locality: { type: string }
31721
32806
  administrative_area: { type: string }
31722
32807
  postal_code: { type: string }
@@ -31841,6 +32926,7 @@ paths:
31841
32926
  properties:
31842
32927
  country: { type: string, minLength: 2, maxLength: 2, description: ISO 3166-1 alpha-2 country code. }
31843
32928
  street_address: { type: string }
32929
+ extended_address: { type: string, description: 'Address complement: apartment, suite, unit, or the quadra/lote used in some countries. Optional. Does not substitute for a building number on street_address.' }
31844
32930
  locality: { type: string, description: City / town. }
31845
32931
  administrative_area: { type: string, description: "State / province / region. When omitted, the pre-check is skipped (the final submit still validates)." }
31846
32932
  postal_code: { type: string }
@@ -32629,6 +33715,7 @@ paths:
32629
33715
  label: { type: string }
32630
33716
  value: { type: string }
32631
33717
  documentId: { type: string, description: 'Present on document rows — the Telnyx document id. GET /v1/whatsapp/phone-numbers/kyc/document/{documentId} streams it (auth-scoped, inline PDF).' }
33718
+ pendingReview: { type: boolean, description: 'true when this account already has a number for this country in regulatory review (status pending_regulatory). Scope is the whole account across all profiles, and the country only (any number type), so it is not a per-end-client signal on a multi-tenant setup. Informational only: it never blocks a submission, and several same-country numbers may sit in review at once. For a per-end-client view, call GET /v1/phone-numbers with `profileId` and `status=pending_regulatory`; that view also lists numbers declined in the last 30 days.' }
32632
33719
  '400': { description: Country not available }
32633
33720
  '401': { $ref: '#/components/responses/Unauthorized' }
32634
33721
  post:
@@ -32670,7 +33757,7 @@ paths:
32670
33757
  properties:
32671
33758
  profileId: { type: string }
32672
33759
  country: { type: string }
32673
- submissionId: { type: string, description: Idempotency token for this submission attempt. A retry/double-submit with the same token returns the same number; omit and each call creates a new number. }
33760
+ submissionId: { type: string, description: 'Idempotency token for this submission attempt. Once the number has been ordered, a retry with the same token returns that same number instead of ordering another. A submission that fails before the number is ordered releases the token, so you can correct your details and re-submit with it. Omit it and every call provisions a new number.' }
32674
33761
  quantity: { type: integer, minimum: 1, maximum: 5, default: 1, description: 'Provision several same-country numbers from one submission (1-5). The single verification covers all of them; each number is billed only when it activates. Numbers that fail to order are skipped (best-effort). With `areaCode`, a quantity above that area''s live stock is rejected with a 400.' }
32675
33762
  reuse: { type: boolean, description: Reuse a prior approved verification for this country (skips document/field collection; places the order immediately). }
32676
33763
  reuseOptionId: { type: string, description: 'Which reusable verification to use (GET reusable.options[].id). The unambiguous selection key. Omitted = the approved default. No match = 409.' }
@@ -32707,6 +33794,7 @@ paths:
32707
33794
  first_name: { type: string }
32708
33795
  last_name: { type: string }
32709
33796
  street_address: { type: string }
33797
+ extended_address: { type: string, description: 'Address complement: apartment, suite, unit, or the quadra/lote used in some countries. Optional. Does not substitute for a building number on street_address.' }
32710
33798
  locality: { type: string }
32711
33799
  administrative_area: { type: string }
32712
33800
  postal_code: { type: string }
@@ -32810,6 +33898,7 @@ paths:
32810
33898
  properties:
32811
33899
  country: { type: string, minLength: 2, maxLength: 2, description: ISO 3166-1 alpha-2 country code. }
32812
33900
  street_address: { type: string }
33901
+ extended_address: { type: string, description: 'Address complement: apartment, suite, unit, or the quadra/lote used in some countries. Optional. Does not substitute for a building number on street_address.' }
32813
33902
  locality: { type: string, description: City / town. }
32814
33903
  administrative_area: { type: string, description: "State / province / region. When omitted, the pre-check is skipped (the final submit still validates)." }
32815
33904
  postal_code: { type: string }
@@ -35588,7 +36677,7 @@ paths:
35588
36677
  contactId: { type: string }
35589
36678
  channelId: { type: string }
35590
36679
  platformIdentifier: { type: string }
35591
- contactName: { type: string }
36680
+ contactName: { type: [string, "null"] }
35592
36681
  status: { type: string, enum: [pending, sent, delivered, read, failed] }
35593
36682
  messageId: { type: string }
35594
36683
  error: { type: string }
@@ -35623,7 +36712,7 @@ paths:
35623
36712
  schema:
35624
36713
  type: object
35625
36714
  properties:
35626
- contactIds: { type: array, items: { type: string }, description: Specific contact IDs to add }
36715
+ contactIds: { type: array, items: { type: string }, description: 'Specific contact IDs to add. Zernio contact ids (24-character hex), as returned by the list-contacts endpoint. A platform identifier such as a WhatsApp wa_id is rejected with 400; use phones for raw numbers.' }
35627
36716
  phones: { type: array, items: { type: string }, maxItems: 20000, description: Raw phone numbers (auto-creates contacts). Useful for WhatsApp/Telegram manual entry }
35628
36717
  useSegment: { type: boolean, description: Auto-populate from broadcast segment filters }
35629
36718
  responses:
@@ -36572,10 +37661,10 @@ paths:
36572
37661
  contactName: { type: string }
36573
37662
  currentStepIndex: { type: integer }
36574
37663
  status: { type: string, enum: [active, completed, exited, paused] }
36575
- exitReason: { type: string }
36576
- nextStepAt: { type: string, format: date-time }
37664
+ exitReason: { type: [string, "null"] }
37665
+ nextStepAt: { type: [string, "null"], format: date-time }
36577
37666
  stepsSent: { type: integer }
36578
- lastStepSentAt: { type: string, format: date-time }
37667
+ lastStepSentAt: { type: [string, "null"], format: date-time }
36579
37668
  createdAt: { type: string, format: date-time }
36580
37669
  pagination:
36581
37670
  type: object
@@ -37104,7 +38193,7 @@ paths:
37104
38193
  - type: object
37105
38194
  required: [backfillPending]
37106
38195
  properties:
37107
- backfillPending: { type: boolean, const: true }
38196
+ backfillPending: { type: boolean, description: 'Always true on this response. Part of the requested range is still being backfilled; retry until the request returns 200.' }
37108
38197
  '400': { $ref: '#/components/responses/BadRequest' }
37109
38198
  '401': { $ref: '#/components/responses/Unauthorized' }
37110
38199
  '403':
@@ -37168,6 +38257,130 @@ paths:
37168
38257
  '429': { description: "Google Ads operations budget exhausted; retry later." }
37169
38258
  '501': { description: Only available on Google Ads accounts }
37170
38259
 
38260
+ /v1/ads/local-services/leads:
38261
+ get:
38262
+ x-resource-group: "ads"
38263
+ operationId: listLocalServicesLeads
38264
+ tags: ["Ad Insights"]
38265
+ x-platforms: ["google"]
38266
+ summary: Google Local Services Ads leads
38267
+ description: |-
38268
+ Leads generated by Local Services Ads (phone calls, messages, bookings),
38269
+ read live from Google's `local_services_lead` resource, newest first.
38270
+ No persistence: Google is the source of truth and lead/credit statuses
38271
+ keep changing server-side. Google never returns healthcare-category
38272
+ leads, and `WIPED_OUT` leads arrive with contact erased (`contact` is
38273
+ null). Draws on the shared Google Ads operations budget.
38274
+ security:
38275
+ - bearerAuth: []
38276
+ parameters:
38277
+ - { name: accountId, in: query, required: true, schema: { type: string }, description: "Google ads SocialAccount id." }
38278
+ - { name: customerId, in: query, schema: { type: string }, description: "Numeric Google Ads customer id (no dashes). Defaults to the account's connected customer." }
38279
+ - { name: fromDate, in: query, schema: { type: string, format: date }, description: "Leads created at/after this day." }
38280
+ - { name: toDate, in: query, schema: { type: string, format: date }, description: "Leads created at/before this day." }
38281
+ - { name: leadType, in: query, schema: { type: string, enum: [PHONE_CALL, MESSAGE, BOOKING] } }
38282
+ - { name: leadStatus, in: query, schema: { type: string }, description: "Google LocalServicesLeadStatus enum value (e.g. NEW, BOOKED, WIPED_OUT)." }
38283
+ - { name: chargedOnly, in: query, schema: { type: boolean }, description: "true = only leads Google charged for." }
38284
+ - { name: pageToken, in: query, schema: { type: string }, description: "Cursor from paging.nextPageToken of the previous page." }
38285
+ responses:
38286
+ '200':
38287
+ description: Local Services leads
38288
+ content:
38289
+ application/json:
38290
+ schema:
38291
+ type: object
38292
+ properties:
38293
+ customerId: { type: string }
38294
+ data:
38295
+ type: array
38296
+ items:
38297
+ type: object
38298
+ properties:
38299
+ id: { type: [string, "null"], description: "Lead id; pass to /v1/ads/local-services/leads/{leadId}/conversations." }
38300
+ leadType: { type: [string, "null"], description: "PHONE_CALL / MESSAGE / BOOKING." }
38301
+ categoryId: { type: [string, "null"] }
38302
+ serviceId: { type: [string, "null"] }
38303
+ contact:
38304
+ type: [object, "null"]
38305
+ description: "Null for WIPED_OUT leads (contact erased by Google)."
38306
+ properties:
38307
+ consumerName: { type: [string, "null"] }
38308
+ email: { type: [string, "null"] }
38309
+ phoneNumber: { type: [string, "null"] }
38310
+ status: { type: [string, "null"] }
38311
+ createdTime: { type: [string, "null"], description: "Google datetime in the customer's timezone (YYYY-MM-DD HH:MM:SS)." }
38312
+ locale: { type: [string, "null"] }
38313
+ charged: { type: boolean }
38314
+ creditState: { type: [string, "null"] }
38315
+ creditStateLastUpdate: { type: [string, "null"] }
38316
+ paging:
38317
+ type: object
38318
+ properties:
38319
+ nextPageToken: { type: [string, "null"], description: "Null when the last page was returned." }
38320
+ '400': { $ref: '#/components/responses/BadRequest' }
38321
+ '401': { $ref: '#/components/responses/Unauthorized' }
38322
+ '429': { description: "Google Ads operations budget exhausted; retry later." }
38323
+ '501': { description: Only available on Google Ads accounts }
38324
+
38325
+ /v1/ads/local-services/leads/{leadId}/conversations:
38326
+ get:
38327
+ x-resource-group: "ads"
38328
+ operationId: listLocalServicesLeadConversations
38329
+ tags: ["Ad Insights"]
38330
+ x-platforms: ["google"]
38331
+ summary: Conversations of a Local Services lead
38332
+ description: |-
38333
+ Conversation entries of one Local Services lead: phone calls (duration,
38334
+ recording URL) and messages (text, attachment URLs), oldest first. Read
38335
+ live from `local_services_lead_conversation`, always scoped to a single
38336
+ lead. Call-recording URLs require read access on the Google Ads account.
38337
+ Draws on the shared Google Ads operations budget.
38338
+ security:
38339
+ - bearerAuth: []
38340
+ parameters:
38341
+ - { name: leadId, in: path, required: true, schema: { type: string }, description: "Numeric lead id from /v1/ads/local-services/leads." }
38342
+ - { name: accountId, in: query, required: true, schema: { type: string }, description: "Google ads SocialAccount id." }
38343
+ - { name: customerId, in: query, schema: { type: string }, description: "Numeric Google Ads customer id (no dashes). Defaults to the account's connected customer." }
38344
+ - { name: pageToken, in: query, schema: { type: string }, description: "Cursor from paging.nextPageToken of the previous page." }
38345
+ responses:
38346
+ '200':
38347
+ description: Lead conversations
38348
+ content:
38349
+ application/json:
38350
+ schema:
38351
+ type: object
38352
+ properties:
38353
+ customerId: { type: string }
38354
+ data:
38355
+ type: array
38356
+ items:
38357
+ type: object
38358
+ properties:
38359
+ id: { type: [string, "null"] }
38360
+ channel: { type: [string, "null"], description: "PHONE_CALL / MESSAGE / SMS / EMAIL / WHATSAPP / ADS_API." }
38361
+ participantType: { type: [string, "null"], description: "ADVERTISER or CONSUMER." }
38362
+ eventDateTime: { type: [string, "null"] }
38363
+ phoneCall:
38364
+ type: [object, "null"]
38365
+ description: "Only on PHONE_CALL entries."
38366
+ properties:
38367
+ callDurationMillis: { type: [integer, "null"] }
38368
+ callRecordingUrl: { type: [string, "null"] }
38369
+ message:
38370
+ type: [object, "null"]
38371
+ description: "Only on message-channel entries."
38372
+ properties:
38373
+ text: { type: [string, "null"] }
38374
+ attachmentUrls: { type: array, items: { type: string } }
38375
+ paging:
38376
+ type: object
38377
+ properties:
38378
+ nextPageToken: { type: [string, "null"], description: "Null when the last page was returned." }
38379
+ '400': { $ref: '#/components/responses/BadRequest' }
38380
+ '401': { $ref: '#/components/responses/Unauthorized' }
38381
+ '429': { description: "Google Ads operations budget exhausted; retry later." }
38382
+ '501': { description: Only available on Google Ads accounts }
38383
+
37171
38384
  /v1/ads/keywords:
37172
38385
  get:
37173
38386
  x-resource-group: "ads"
@@ -37272,6 +38485,8 @@ paths:
37272
38485
  - { name: profileId, in: query, schema: { type: string }, description: Profile ID }
37273
38486
  - { name: fromDate, in: query, schema: { type: string, format: date }, description: "Start of metrics date range (YYYY-MM-DD, inclusive). Defaults to 90 days ago when both date params are omitted." }
37274
38487
  - { name: toDate, in: query, schema: { type: string, format: date }, description: "End of metrics date range (YYYY-MM-DD, inclusive). Defaults to today. Max 730-day range." }
38488
+ - { name: hasDelivery, in: query, schema: { type: boolean }, description: "Return only campaigns that delivered between `fromDate` and `toDate` — spend above zero, or impressions served at zero spend. Unlike `status`, which reads a campaign's CURRENT state, this filters on what happened inside the window. Filters the campaign set itself, so `pagination.total` counts only matching campaigns. Mirrors the same filter on /v1/ads/tree." }
38489
+ - { name: minSpend, in: query, schema: { type: number, minimum: 0 }, description: "Return only campaigns whose spend between `fromDate` and `toDate` reaches this amount, in each campaign's OWN currency (the `currency` field on the campaign). Implies `hasDelivery`; `minSpend=0` applies no filter. Mirrors the same filter on /v1/ads/tree." }
37275
38490
  responses:
37276
38491
  '200':
37277
38492
  description: Paginated campaigns
@@ -37367,8 +38582,20 @@ paths:
37367
38582
  x-platforms: ["meta", "google", "tiktok", "linkedin", "pinterest", "x", "openai"]
37368
38583
  summary: Pause or resume a campaign
37369
38584
  description: |
37370
- Updates the status of all ads in a campaign. Makes one platform API call (not per-ad) since status cascades through the campaign hierarchy.
37371
- Ads in terminal statuses (rejected, completed, cancelled) are automatically skipped.
38585
+ Writes the campaign's own on/off switch, then lets the platform cascade delivery to its ad sets and ads.
38586
+ Makes one platform API call, not one per ad.
38587
+
38588
+ The switch is always written, whatever delivery status the ads underneath report: an ad still in review
38589
+ does not block resuming its campaign. The echoed `status` is the confirmation that it landed.
38590
+
38591
+ `updated` / `skipped` describe only the ads whose own stored status CHANGED alongside it, so
38592
+ `updated: 0` is a normal successful response, not a no-op. Ads are skipped when they are in a terminal
38593
+ status (rejected, completed, cancelled), already in the target state, or switched on but not yet
38594
+ delivering — the last group keeps its `pending_review` / `error` status until the platform reports what
38595
+ it became. `skippedReasons` names which case applies.
38596
+
38597
+ On Meta this flips the campaign only. An ad set paused in its own right stays paused, so pair this with
38598
+ PUT /v1/ads/ad-sets/{adSetId}/status when you also need the ad set switched back on.
37372
38599
  security:
37373
38600
  - bearerAuth: []
37374
38601
  parameters:
@@ -37391,10 +38618,10 @@ paths:
37391
38618
  schema:
37392
38619
  type: object
37393
38620
  properties:
37394
- updated: { type: integer, description: Number of ads updated }
37395
- skipped: { type: integer, description: Number of ads skipped }
37396
- skippedReasons: { type: array, items: { type: string } }
37397
- message: { type: string, description: Human-readable summary (present when no ads were actionable) }
38621
+ status: { type: string, enum: [active, paused], description: The status written to the campaign }
38622
+ updated: { type: integer, description: 'Number of ads whose own stored status changed too. 0 is normal on a resume whose ads are all awaiting the platform.' }
38623
+ skipped: { type: integer, description: 'Number of ads whose own status was left as it was' }
38624
+ skippedReasons: { type: array, items: { type: string }, description: 'Why each group of ads was skipped' }
37398
38625
  '400':
37399
38626
  description: Invalid input or campaign spans multiple social accounts
37400
38627
  '401': { $ref: '#/components/responses/Unauthorized' }
@@ -37897,7 +39124,7 @@ paths:
37897
39124
  properties:
37898
39125
  amount: { type: number }
37899
39126
  type: { type: string, enum: [daily, lifetime] }
37900
- status: { type: string, enum: [active, paused], description: Omit if not toggling delivery state }
39127
+ status: { type: string, enum: [active, paused], description: "Writes the ad set's own on/off switch (Meta: `configured_status`) on Meta and LinkedIn, whatever delivery status its ads report. Omit if not toggling delivery state." }
37901
39128
  name: { type: string, maxLength: 255, description: "Rename the ad set (Meta only; other platforms return 501). At least one of budget/status/bidStrategy/name is required." }
37902
39129
  bidStrategy:
37903
39130
  allOf: [{ $ref: '#/components/schemas/BidStrategy' }]
@@ -37976,9 +39203,11 @@ paths:
37976
39203
  properties:
37977
39204
  budget: { $ref: '#/components/schemas/AdBudget' }
37978
39205
  budgetLevel: { type: string, enum: [adset] }
37979
- status: { type: string, enum: [active, paused] }
37980
- statusUpdated: { type: integer }
37981
- statusSkipped: { type: integer }
39206
+ status: { type: string, enum: [active, paused], description: 'The status written to the ad set. Absent when nothing was written (see statusMessage).' }
39207
+ statusUpdated: { type: integer, description: 'Number of ads whose own stored status changed alongside the ad set switch' }
39208
+ statusSkipped: { type: integer, description: 'Number of ads whose own status was left as it was' }
39209
+ statusSkippedReasons: { type: array, items: { type: string }, description: 'Why each group of ads was skipped' }
39210
+ statusMessage: { type: string, description: 'Present only where the platform has no ad-set switch and no child ad was actionable; `status` is then absent because nothing was written' }
37982
39211
  bidStrategy: { $ref: '#/components/schemas/BidStrategy' }
37983
39212
  bidAmount: { type: [number, "null"] }
37984
39213
  roasAverageFloor: { type: [number, "null"] }
@@ -38002,6 +39231,23 @@ paths:
38002
39231
  over PUT /v1/ads/ad-sets/{adSetId} for callers that only want the
38003
39232
  status toggle and prefer a symmetric URL to
38004
39233
  /v1/ads/campaigns/{campaignId}/status.
39234
+
39235
+ On Meta and LinkedIn this writes the ad set's own on/off switch
39236
+ (Meta: `configured_status`), whatever delivery status its ads report —
39237
+ an ad still in review does not block resuming its ad set. The echoed
39238
+ `status` is the confirmation that it landed. Where the platform has no
39239
+ ad-set switch (TikTok and others) the toggle is emulated by flipping the
39240
+ child ads; a call with no actionable ad then writes nothing and returns a
39241
+ `message` with no `status`.
39242
+
39243
+ `updated` / `skipped` describe only the ads whose own stored status
39244
+ CHANGED alongside the switch, so `updated: 0` is a normal successful
39245
+ response. See `skippedReasons` for which of the three cases applies
39246
+ (terminal, already in the target state, or switched on but not yet
39247
+ delivering).
39248
+
39249
+ A campaign created paused needs its campaign resumed as well: pair this
39250
+ with PUT /v1/ads/campaigns/{campaignId}/status.
38005
39251
  security:
38006
39252
  - bearerAuth: []
38007
39253
  parameters:
@@ -38024,8 +39270,11 @@ paths:
38024
39270
  schema:
38025
39271
  type: object
38026
39272
  properties:
38027
- updated: { type: integer }
38028
- skipped: { type: integer }
39273
+ status: { type: string, enum: [active, paused], description: 'The status written to the ad set. Absent when nothing was written (see message).' }
39274
+ updated: { type: integer, description: 'Number of ads whose own stored status changed too. 0 is normal on a resume whose ads are all awaiting the platform.' }
39275
+ skipped: { type: integer, description: 'Number of ads whose own status was left as it was' }
39276
+ skippedReasons: { type: array, items: { type: string }, description: 'Why each group of ads was skipped' }
39277
+ message: { type: string, description: 'Present only where the platform has no ad-set switch and no child ad was actionable' }
38029
39278
  '400': { description: Invalid input }
38030
39279
  '401': { $ref: '#/components/responses/Unauthorized' }
38031
39280
  '404': { description: Ad set not found }
@@ -38068,8 +39317,10 @@ paths:
38068
39317
  - { name: accountId, in: query, schema: { type: string }, description: Social account ID }
38069
39318
  - { name: profileId, in: query, schema: { type: string }, description: Profile ID }
38070
39319
  - { name: campaignId, in: query, schema: { type: string }, description: "Restrict the tree to a single campaign by its platform campaign id (the id the platform assigns, e.g. Meta's numeric campaign id). Filters the campaign set itself, so it works regardless of account size and pagination — pass this when you already hold a campaign id instead of paging the tree to find it. Mirrors the `campaignId` filter on GET /v1/ads." }
38071
- - { name: fromDate, in: query, schema: { type: string, format: date }, description: "Start of the METRICS date range (YYYY-MM-DD). Affects only the spend/impression numbers overlaid on each node, NOT which campaigns are returned. Defaults to 90 days ago." }
39320
+ - { name: fromDate, in: query, schema: { type: string, format: date }, description: "Start of the METRICS date range (YYYY-MM-DD). On its own it affects only the spend/impression numbers overlaid on each node, not which campaigns are returned — pass `hasDelivery` or `minSpend` to also filter the campaign set to this window. Defaults to 90 days ago." }
38072
39321
  - { name: toDate, in: query, schema: { type: string, format: date }, description: "End of metrics date range (YYYY-MM-DD). Defaults to today. Max 730-day range." }
39322
+ - { name: hasDelivery, in: query, schema: { type: boolean }, description: "Return only campaigns that delivered between `fromDate` and `toDate` — spend above zero, or impressions served at zero spend. Unlike `status`, which reads a campaign's CURRENT state, this filters on what happened inside the window, so a campaign that spent then and is paused today is still returned. Filters the campaign set itself, so `pagination.total` counts only matching campaigns." }
39323
+ - { name: minSpend, in: query, schema: { type: number, minimum: 0 }, description: "Return only campaigns whose spend between `fromDate` and `toDate` reaches this amount. Expressed in each campaign's OWN currency (the `currency` field on the campaign node): spend is stored per ad account in its native currency and one response can span several. Implies `hasDelivery`; `minSpend=0` applies no filter." }
38073
39324
  - { name: sort, in: query, schema: { type: string, enum: [newest, oldest, spend_desc, spend_asc], default: newest }, description: "Campaign-level sort order. `newest` (default) / `oldest` order by the campaign's newest-ad createdAt. `spend_desc` / `spend_asc` order by aggregated spend in the requested date range; campaigns with no spend land at the end." }
38074
39325
  - { name: timeIncrement, in: query, schema: { type: integer, enum: [1] }, description: "Set to `1` to also return a daily breakdown. Mirrors Meta Insights' `time_increment=1`: each node gains a `daily[]` array of per-day metrics (same fields as the aggregated `metrics`) alongside the range total, so you get per-entity daily trends in ONE call instead of calling the tree once per day. Only `1` (daily) is supported. The daily series covers the same date range and uses the same source data as `metrics`, except `reach` on Meta and TikTok: the range total is the platform's de-duplicated value, so daily reach does not sum to it. See `dailyLevel` to control which levels carry it." }
38075
39326
  - { name: dailyLevel, in: query, schema: { type: string, enum: [campaign, adset, ad], default: campaign }, description: "Which tree levels get the `daily[]` series when `timeIncrement=1`. `campaign` (default) attaches it on campaign nodes only — the common per-campaign-trend case, and the smallest payload. `adset` adds it on ad sets too; `ad` adds it on every ad in `ads[]` as well (heaviest — a long range × up to 100 ads per ad set). Scope with `campaignId` to keep `ad`-level responses small. Ignored when `timeIncrement` is unset." }
@@ -38092,7 +39343,7 @@ paths:
38092
39343
  - type: object
38093
39344
  required: [backfillPending]
38094
39345
  properties:
38095
- backfillPending: { type: boolean, const: true }
39346
+ backfillPending: { type: boolean, description: 'Always true on this response. Part of the requested range is still being backfilled; retry until the request returns 200.' }
38096
39347
  '401': { $ref: '#/components/responses/Unauthorized' }
38097
39348
  '403':
38098
39349
  description: Ads access required. Legacy plans need the Ads add-on; included by default on usage-based plans.
@@ -38145,7 +39396,7 @@ paths:
38145
39396
  - type: object
38146
39397
  required: [backfillPending]
38147
39398
  properties:
38148
- backfillPending: { type: boolean, const: true }
39399
+ backfillPending: { type: boolean, description: 'Always true on this response. Part of the requested range is still being backfilled; retry until the request returns 200.' }
38149
39400
  '400': { $ref: '#/components/responses/BadRequest' }
38150
39401
  '401': { $ref: '#/components/responses/Unauthorized' }
38151
39402
  '403':
@@ -38294,6 +39545,7 @@ paths:
38294
39545
  '401': { $ref: '#/components/responses/Unauthorized' }
38295
39546
  '404': { $ref: '#/components/responses/NotFound' }
38296
39547
  '501': { description: "targeting or creative not supported on the platform (Meta + TikTok only)" }
39548
+ '502': { description: "Meta accepted the request then failed to produce the media (upload session, chunk transfer, processing timeout, or a response with no image hash). Inspect `platformError.reason`." }
38297
39549
  delete:
38298
39550
  x-resource-group: "ads"
38299
39551
  operationId: deleteAd
@@ -38417,7 +39669,7 @@ paths:
38417
39669
  - type: object
38418
39670
  required: [backfillPending]
38419
39671
  properties:
38420
- backfillPending: { type: boolean, const: true }
39672
+ backfillPending: { type: boolean, description: 'Always true on this response. Part of the requested range is still being backfilled; retry until the request returns 200.' }
38421
39673
  '400':
38422
39674
  description: "Invalid parameter (e.g. an unknown `breakdowns` dimension). The message lists the offending value(s) and the supported set."
38423
39675
  content:
@@ -38849,7 +40101,7 @@ paths:
38849
40101
  - type: object
38850
40102
  required: [backfillPending]
38851
40103
  properties:
38852
- backfillPending: { type: boolean, const: true }
40104
+ backfillPending: { type: boolean, description: 'Always true on this response. Part of the requested range is still being backfilled; retry until the request returns 200.' }
38853
40105
  '400':
38854
40106
  description: "Invalid parameter (e.g. an unknown `breakdowns` dimension). The message lists the offending value(s) and the supported set."
38855
40107
  content:
@@ -38960,6 +40212,7 @@ paths:
38960
40212
  '404': { description: "Ad not found" }
38961
40213
  '405': { description: "Platform has no click-URL tracking surface" }
38962
40214
  '422': { description: "Meta creative cannot be rebuilt (e.g. placement-customized/asset-feed/dark creative)" }
40215
+ '502': { description: "Meta accepted the request then failed to produce the media (upload session, chunk transfer, processing timeout, or a response with no image hash). Inspect `platformError.reason`." }
38963
40216
  /v1/ads/{adId}/comments:
38964
40217
  get:
38965
40218
  x-resource-group: "engagement"
@@ -39584,6 +40837,7 @@ paths:
39584
40837
  '401': { $ref: '#/components/responses/Unauthorized' }
39585
40838
  '422': { description: No Facebook Page found to act as the story actor }
39586
40839
  '501': { description: Only supported on Meta (facebook/instagram) }
40840
+ '502': { description: "Meta accepted the request then failed to produce the media (upload session, chunk transfer, processing timeout, or a response with no image hash). Inspect `platformError.reason`." }
39587
40841
 
39588
40842
  /v1/ads/creatives/{creativeId}:
39589
40843
  get:
@@ -40191,8 +41445,20 @@ paths:
40191
41445
 
40192
41446
  `instagramAccountId`, `destinationType` and `adSetId` are Meta-only and
40193
41447
  return 400 on other platforms.
41448
+
41449
+ **Retries.** Boosts are NOT idempotent and can take minutes when Meta requires re-hosting an
41450
+ Instagram video, so do not retry on client timeout. Send an
41451
+ Idempotency-Key header to make retries safe: same key and body replays
41452
+ the original 201, and distinct keys always create distinct ads.
41453
+ Without the header, an identical request is treated as a retry: while
41454
+ one is in flight it returns 409, and within 10 minutes of a completed
41455
+ boost it returns the already-created ad instead of creating another.
41456
+ To intentionally duplicate an ad, send distinct Idempotency-Keys (or
41457
+ vary the body, e.g. the name).
40194
41458
  security:
40195
41459
  - bearerAuth: []
41460
+ parameters:
41461
+ - $ref: '#/components/parameters/IdempotencyKeyHeader'
40196
41462
  requestBody:
40197
41463
  required: true
40198
41464
  content:
@@ -40217,7 +41483,7 @@ paths:
40217
41483
  type: { type: string, enum: [daily, lifetime] }
40218
41484
  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." }
40219
41485
  destinationType: { type: string, enum: [INSTAGRAM_PROFILE, WEBSITE, ON_AD, MESSENGER, WHATSAPP], description: "Meta only. Ad-set destination_type — where the click LANDS, as opposed to instagramAccountId which is who the ad runs as. Lead ads force ON_AD and ignore this." }
40220
- currency: { type: string, example: USD }
41486
+ 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)." }
40221
41487
  schedule:
40222
41488
  type: object
40223
41489
  properties:
@@ -40406,8 +41672,9 @@ paths:
40406
41672
  CTA button label. Send it together with `linkUrl` — a CTA without a
40407
41673
  destination produces a button that goes nowhere, so sending one alone is a 400.
40408
41674
 
40409
- **Meta**: validated against the Meta CTA enum (same values as
40410
- POST /v1/ads/create), e.g. `LEARN_MORE`, `SHOP_NOW`, `SIGN_UP`.
41675
+ **Meta**: the CTA enum of POST /v1/ads/create plus
41676
+ `VIEW_INSTAGRAM_PROFILE`, which is accepted on boost only. For that
41677
+ value `linkUrl` is typically the Instagram profile URL.
40411
41678
 
40412
41679
  **TikTok**: pass-through to `call_to_action` on the Spark Ad creative; the
40413
41680
  platform validates the value. See TikTok's "Enumeration - Call-to-Action".
@@ -40462,6 +41729,10 @@ paths:
40462
41729
  '401': { $ref: '#/components/responses/Unauthorized' }
40463
41730
  '403':
40464
41731
  description: Ads access required. Legacy plans need the Ads add-on; included by default on usage-based plans.
41732
+ '409':
41733
+ description: |
41734
+ An identical boost request is already in progress (with or without
41735
+ an Idempotency-Key). Wait for it to finish instead of retrying.
40465
41736
  '422':
40466
41737
  description: |
40467
41738
  Platform ads connection required (TikTok Ads, X Ads), missing linked
@@ -40469,13 +41740,15 @@ paths:
40469
41740
  as an Identity on the target advertiser. Returned with code
40470
41741
  `ads_connection_required`; the message includes the actionable
40471
41742
  "TikTok Ads Manager → Assets → Identity" remediation step.
41743
+ Also returned as `idempotency_key_reused` when an Idempotency-Key
41744
+ is reused with a different request body.
40472
41745
 
40473
41746
  /v1/ads/create:
40474
41747
  post:
40475
41748
  x-resource-group: "ads"
40476
41749
  operationId: createStandaloneAd
40477
41750
  tags: ["Ad Campaigns"]
40478
- x-platforms: ["meta", "google", "tiktok", "linkedin", "pinterest", "x"]
41751
+ x-platforms: ["meta", "google", "tiktok", "linkedin", "pinterest", "x", "openai"]
40479
41752
  summary: Create standalone ad
40480
41753
  description: >-
40481
41754
  Creates a paid ad with custom creative across Meta, Google Ads,
@@ -40580,7 +41853,11 @@ paths:
40580
41853
  status:
40581
41854
  type: string
40582
41855
  enum: [ACTIVE, PAUSED]
40583
- description: "Meta and TikTok. Publish state of the created entities. Omitted or ACTIVE publishes live (default, back-compat); PAUSED creates them paused and skips activation, so you can review before they spend. On TikTok the whole campaign > ad group > ad hierarchy stays paused."
41856
+ description: "Meta and TikTok. Publish state of the created entities. Omitted or ACTIVE publishes live (default, back-compat); PAUSED creates them paused so you can review before they spend. On Meta the pause is held on the campaign this call creates, leaving the ad set and ad switched on, so a single PUT /v1/ads/campaigns/{campaignId}/status with `active` brings the whole thing live. It is held at every level instead when the pause cannot rely on the campaign: `existingCampaignId` (that campaign may be running and is never touched) or `campaignStatus: ACTIVE`. On TikTok the whole campaign > ad group > ad hierarchy stays paused."
41857
+ campaignStatus:
41858
+ type: string
41859
+ enum: [ACTIVE, PAUSED]
41860
+ description: "Meta only. Overrides `status` for the campaign level alone, so you can create a live campaign whose ad set and ad stay paused, or the reverse. Omitted, it follows `status`."
40584
41861
  budgetLevel:
40585
41862
  type: string
40586
41863
  enum: [adset, campaign]
@@ -40594,7 +41871,7 @@ paths:
40594
41871
  across ad sets automatically.
40595
41872
  Meta requires the budget at exactly one level, never both. Non-Meta platforms ignore
40596
41873
  this field. Ignored on the attach shape (`adSetId`), which inherits the existing budget.
40597
- currency: { type: string }
41874
+ currency: { type: string, minLength: 3, maxLength: 3, description: "ISO 4217 currency code matching the ad account's currency (e.g. `USD`). 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)." }
40598
41875
  headline: { type: string, description: "Required for Meta, Google, Pinterest, LinkedIn, and OpenAI Ads on legacy + attach shapes (skip for multi-creative — use `creatives[].headline`). Ignored for TikTok and X/Twitter. Max: Meta=255, Google=30, Pinterest=100, LinkedIn=400, OpenAI=50 (min 3). On LinkedIn this is the ad's headline (the bold text on the creative); for traffic ads it's the link card title. On OpenAI Ads this is the chat card's title." }
40599
41876
  longHeadline: { type: string, maxLength: 90, description: "Google Display only — defaults to `headline` if omitted. On LinkedIn, reused as the optional secondary description text on traffic (link) ads; omitted if not provided." }
40600
41877
  body: { type: string, description: "Required on legacy + attach shapes. For X/Twitter this is the tweet text (max 280 chars including a ~24-char URL when `linkUrl` is set). On LinkedIn this is the post commentary (the intro text shown above the ad). On OpenAI Ads this is the chat card's body text. Max: Google=90, Pinterest=500, OpenAI=100." }
@@ -40615,7 +41892,7 @@ paths:
40615
41892
  properties:
40616
41893
  url: { type: string, format: uri, description: "Public URL of the video. Meta: uploaded via chunked transfer on /act_X/advideos, then the request blocks on Meta's transcoding until status.video_status === 'ready'. LinkedIn: uploaded via the Videos API (multipart), then the request blocks until LinkedIn finishes transcoding (status AVAILABLE) — short clips take ~10-30s. Provide either `url` or `id`."}
40617
41894
  id: { type: string, description: "Meta only. Reuse a video ALREADY uploaded to this ad account instead of re-uploading the file: pass the `videoId` returned by a previous create. Wins over `url`, so N ads that differ only in copy share one upload (`existingCreativeId` only covers the identical-copy case). Provide either `url` or `id`."}
40618
- thumbnailUrl: { type: string, format: uri, description: "Public URL of a still-image thumbnail for the video. OPTIONAL: when omitted on Meta, the poster is auto-generated from Meta's own preferred video thumbnail (the same candidates Ads Manager shows), so video ads publish without supplying one. Provide it to control the poster frame exactly (uploaded as an ad image and referenced in object_story_spec.video_data). Ignored by LinkedIn (auto-generated poster frame)." }
41895
+ thumbnailUrl: { type: string, format: uri, description: "Public URL of a still-image thumbnail for the video. OPTIONAL: when omitted on Meta, the poster is auto-generated from Meta's own preferred video thumbnail (the same candidates Ads Manager shows), so video ads usually publish without supplying one. When Meta produces no candidate the request fails with a 502 platform_error (reason: video_thumbnail_unavailable) — retry, or supply this field. Provide it to control the poster frame exactly (uploaded as an ad image and referenced in object_story_spec.video_data). Ignored by LinkedIn (auto-generated poster frame)." }
40619
41896
  creatives:
40620
41897
  type: array
40621
41898
  minItems: 1
@@ -40636,7 +41913,7 @@ paths:
40636
41913
  imageUrl: { type: string, format: uri, description: "Image creative. Mutually exclusive with `video`." }
40637
41914
  video:
40638
41915
  type: object
40639
- description: "Video creative for this entry. Mutually exclusive with `imageUrl`. thumbnailUrl is optional — when omitted, the poster is auto-generated from Meta's preferred video thumbnail."
41916
+ description: "Video creative for this entry. Mutually exclusive with `imageUrl`. thumbnailUrl is optional: when omitted, the poster is auto-generated from Meta's preferred video thumbnail, and the request fails with a 502 platform_error (reason: video_thumbnail_unavailable) if Meta produces no candidate."
40640
41917
  required: [url]
40641
41918
  properties:
40642
41919
  url: { type: string, format: uri }
@@ -40657,8 +41934,9 @@ paths:
40657
41934
  The attached ad takes the full single-creative surface:
40658
41935
  `headline`/`body`/`description`/`callToAction` plus either
40659
41936
  `imageUrl`/`video` OR `placementAssets` (its own per-placement
40660
- Feed/Story assets), and `leadGenFormId` when the target is a
40661
- lead ad set (the parent must be ON_AD — true for ad sets
41937
+ Feed/Story assets) OR `translations`/`defaultLocale` (its own
41938
+ per-locale asset feed, Meta only), and `leadGenFormId` when
41939
+ the target is a lead ad set (the parent must be ON_AD, true for ad sets
40662
41940
  created via goal `lead_generation`; Meta rejects a formless ad
40663
41941
  there, so pass the form on EVERY attached ad). This is the way
40664
41942
  to build N full ads sharing one ad set: create the first ad
@@ -40887,8 +42165,9 @@ paths:
40887
42165
  description: |
40888
42166
  Meta only. Override the Instagram account the ad is delivered as — pass an Instagram
40889
42167
  Business Account ID (e.g. 17841...), mapped to the creative's `instagram_user_id`.
40890
- When omitted we auto-resolve the IG account linked to the connected Facebook Page
40891
- (the existing default). Useful when a Page has more than one eligible IG account.
42168
+ When omitted we use the Instagram actor Meta already runs the Page's other ads as,
42169
+ falling back to the Page's page-backed Instagram account. Useful when a Page has more
42170
+ than one eligible IG account.
40892
42171
  dynamicCreative:
40893
42172
  type: object
40894
42173
  description: |
@@ -40998,8 +42277,8 @@ paths:
40998
42277
  works on a normal ad may be rejected with "The following images have invalid
40999
42278
  dimensions for Dynamic Creative" (subcode 1885558). Video is not affected.
41000
42279
 
41001
- Mutually exclusive with `dynamicCreative`, `placementAssets`, `carouselCards` and
41002
- `existingCreativeId` — Meta allows one `asset_feed_spec` shape per creative.
42280
+ Mutually exclusive with `dynamicCreative`, `placementAssets`, `carouselCards`,
42281
+ `existingCreativeId` and `creatives[]`. Meta allows one `asset_feed_spec` shape per creative.
41003
42282
  items:
41004
42283
  type: object
41005
42284
  required: [locale, headline, body, description]
@@ -41135,6 +42414,8 @@ paths:
41135
42414
  Deprecated: send it inside `platformSpecificData` instead (Meta today; TikTok's nested shape is planned). The flat field keeps working during the deprecation window; sending both shapes returns a 400.
41136
42415
 
41137
42416
  Meta bid strategy applied to the ad set.
42417
+
42418
+ OpenAI Ads: required on every ad group via this flat field, the only channel it supports (`platformSpecificData` is Meta/LinkedIn-only and returns 400 for OpenAI). No auto-bid option exists; send `LOWEST_COST_WITH_BID_CAP` or `COST_CAP` together with `bidAmount`, omitting it returns 400.
41138
42419
  bidAmount:
41139
42420
  type: number
41140
42421
  deprecated: true
@@ -41261,6 +42542,21 @@ paths:
41261
42542
  Ads (`POST /v1/ads/boost`) always use `TT_USER` regardless
41262
42543
  of this field — TikTok requires the original organic
41263
42544
  post's author identity for Spark.
42545
+ smartPlus:
42546
+ type: boolean
42547
+ description: |
42548
+ TikTok only. Creates the ad as a TikTok Upgraded Smart+
42549
+ campaign: TikTok automates targeting, bidding and delivery. Supports goals
42550
+ `conversions` (Smart+ Web Conversions), `lead_generation` (Smart+ Lead
42551
+ Generation with a website form on `linkUrl`; TikTok Instant Forms not supported)
42552
+ and `app_promotion` (Smart+ App installs; the ad's destination is the app store,
42553
+ so `linkUrl` is not used). The web goals require `promotedObject.pixelId` AND
42554
+ `promotedObject.customEventType`; `app_promotion` requires
42555
+ `promotedObject.applicationId` instead.
42556
+ Targeting works like on any TikTok ad (defaults to `countries: ["US"]` when
42557
+ omitted); TikTok automates delivery within it.
42558
+ The budget lives on the Smart+ campaign (Campaign Budget Optimization); a `lifetime`
42559
+ budget additionally requires `endDate`. Cannot be combined with `adSetId`.
41264
42560
  promotedObject:
41265
42561
  type: object
41266
42562
  description: |
@@ -41276,12 +42572,15 @@ paths:
41276
42572
 
41277
42573
  Other Meta goals (engagement, traffic, awareness, video_views) ignore this field.
41278
42574
 
41279
- **TikTok**: only `goal: conversions` uses it.
42575
+ **TikTok**: used by `goal: conversions` and the Smart+ goals (`smartPlus: true`).
41280
42576
  - `pixelId` maps to the ad group's `pixel_id`. Required: a TikTok website-conversion
41281
42577
  ad group without a pixel is rejected with `40002: Please select a pixel`.
41282
42578
  - `customEventType` maps to the ad group's `optimization_event` (the pixel event to
41283
- optimise for). Optional: TikTok accepts a pixel-only auto-bid conversion ad group.
42579
+ optimise for). Optional on the regular conversions flow, required on Smart+.
41284
42580
  See the `customEventType` field below for the valid TikTok codes.
42581
+ - `applicationId` (Smart+ `goal: app_promotion` only) maps to the ad group's `app_id`:
42582
+ the App ID of an app registered on the TikTok Ads account (Assets → Events →
42583
+ App Events). Install optimization needs the app's MMP tracking configured.
41285
42584
 
41286
42585
  The remaining `promotedObject.*` fields are Meta-only. Platforms other than
41287
42586
  Meta and TikTok ignore `promotedObject` entirely.
@@ -41290,6 +42589,9 @@ paths:
41290
42589
  type: string
41291
42590
  description: |
41292
42591
  Pixel ID. **Meta:** Facebook Pixel ID, required for `goal: conversions`.
42592
+ Requires `customEventType` alongside it; Meta rejects any promoted_object
42593
+ carrying `pixel_id` without `custom_event_type` (error_subcode 1885014),
42594
+ even when `customConversionId` is also present.
41293
42595
  **TikTok:** TikTok Pixel ID, required for `goal: conversions`.
41294
42596
  To discover the pixels an ad account can use, call
41295
42597
  `GET /v1/accounts/{accountId}/tracking-tags?adAccountId=act_...` (each entry
@@ -41339,7 +42641,12 @@ paths:
41339
42641
  description: "App Store / Play Store listing URL. Required for `goal: app_promotion`."
41340
42642
  customConversionId:
41341
42643
  type: string
41342
- description: Custom Conversion ID, when optimising against one instead of a standard event.
42644
+ description: |
42645
+ Custom Conversion ID, when optimising against one instead of a standard
42646
+ event. Accepted alone by this API, without `pixelId` or `customEventType`.
42647
+ If `pixelId` is also sent, `customEventType` is still required on the
42648
+ promoted_object (Meta rejects `pixel_id` without `custom_event_type`,
42649
+ error_subcode 1885014).
41343
42650
  productCatalogId:
41344
42651
  type: string
41345
42652
  description: Catalog ID for catalog/Advantage+ Shopping campaigns.
@@ -41398,6 +42705,16 @@ paths:
41398
42705
  description: Ads access required. Legacy plans need the Ads add-on; included by default on usage-based plans.
41399
42706
  '422':
41400
42707
  description: Platform ads connection required (TikTok Ads, X Ads) or missing linked account
42708
+ '502':
42709
+ description: |
42710
+ The platform rejected the request, or failed to produce media the ad
42711
+ needs (e.g. Meta generated no poster for an uploaded video when no
42712
+ `video.thumbnailUrl` was supplied). Inspect `platformError` for the
42713
+ upstream payload. Failures we raise carry a `reason`; a payload
42714
+ forwarded verbatim from Meta may not. On the `creatives[]` shape a
42715
+ missing poster also carries `creativeIndex` and `videoUrl` to
42716
+ identify the entry. An upstream 4xx status is forwarded instead
42717
+ of 502.
41401
42718
 
41402
42719
  /v1/ads/leads:
41403
42720
  get:
@@ -41438,16 +42755,16 @@ paths:
41438
42755
  type: object
41439
42756
  properties:
41440
42757
  id: { type: string, description: Zernio lead id. }
41441
- leadgenId: { type: string, description: Meta lead id. }
42758
+ leadgenId: { type: string, description: 'Meta lead id. On LinkedIn, the leadFormResponse id.' }
41442
42759
  formId: { type: string }
41443
42760
  formName: { type: [string, "null"] }
41444
42761
  accountId: { type: string }
41445
42762
  adId: { type: [string, "null"] }
41446
42763
  adsetId: { type: [string, "null"] }
41447
- campaignId: { type: [string, "null"] }
42764
+ campaignId: { type: [string, "null"], description: 'On LinkedIn, this is the LinkedIn Campaign id, which corresponds to platformAdSetId on GET /v1/ads (LinkedIn''s Campaign Group is Zernio''s campaign).' }
41448
42765
  isOrganic: { type: boolean }
41449
42766
  createdTime: { type: [string, "null"], description: ISO 8601. }
41450
- fields: { type: object, additionalProperties: { type: string }, description: "Question key → answer." }
42767
+ fields: { type: object, additionalProperties: { type: string }, description: 'Question key → answer. On LinkedIn, the key is the lowercased predefinedField, else the question name, else the numeric questionId; multiple-choice values are option labels (unlike Meta, which returns the option key).' }
41451
42768
  fieldData: { type: array, items: { type: object }, description: "Raw Meta field_data." }
41452
42769
  pagination:
41453
42770
  type: object
@@ -41803,6 +43120,7 @@ paths:
41803
43120
  '400': { description: "Invalid input, or Meta rejected the image" }
41804
43121
  '401': { $ref: '#/components/responses/Unauthorized' }
41805
43122
  '501': { description: Only supported on Meta (facebook/instagram) }
43123
+ '502': { description: "Meta accepted the request then failed to produce the media (upload session, chunk transfer, processing timeout, or a response with no image hash). Inspect `platformError.reason`." }
41806
43124
 
41807
43125
  get:
41808
43126
  x-resource-group: "ads"
@@ -41906,6 +43224,28 @@ paths:
41906
43224
  markets, and every id is usable in `regions`/`cities`/`metros` keys on
41907
43225
  `POST /v1/ads/create`.
41908
43226
 
43227
+ LinkedIn geo searches also return every matching level in one list, and
43228
+ neither `geoType` nor `countryCode` is applied: LinkedIn's typeahead only
43229
+ returns a name and a URN per result, with no level or country field to
43230
+ filter on. Every result has `type` set to `location`, and its id is a
43231
+ `urn:li:geo:*` URN usable as a `regions[].key` on `POST /v1/ads/create`,
43232
+ `POST /v1/ads/boost` and `POST /v1/ads/targeting/reach-estimate`.
43233
+
43234
+ Pinterest resolves against three whole-catalog endpoints (interests, locations,
43235
+ regions) with no server-side query or pagination, so matching, ranking and the
43236
+ `limit` cutoff all happen in Zernio; the catalog is independent of any ad account
43237
+ and results never carry `audienceSize`. Names come back localized to the connected
43238
+ Pinterest account's language (there is no way to force a locale), so match against
43239
+ whatever language that account returns. `geoType` routes to a different catalog:
43240
+ `country` and `metro_area` read the locations catalog (`type` is `country` or
43241
+ `metro`); `region` reads the regions catalog (`type` is `region`, its id a
43242
+ `regions[].key` on `POST /v1/ads/create`); `all` and the default `city` merge both
43243
+ catalogs with honest per-entry `type`s, since Pinterest has no city-level catalog
43244
+ and `city` is an alias for `all`, not a literal city search. `zip`, `subcity`,
43245
+ `neighborhood`, `place` and `geo_market` return a 400: Pinterest exposes no
43246
+ postal-code catalog, pass postal codes directly as `targeting.zips: [{ key }]` on
43247
+ `POST /v1/ads/create`.
43248
+
41909
43249
  For geo queries, `q` should contain only the locality name (e.g. `"Amsterdam"`,
41910
43250
  not `"Amsterdam, NL"`). Use `countryCode` to disambiguate.
41911
43251
  security:
@@ -41914,7 +43254,7 @@ paths:
41914
43254
  - { name: accountId, in: query, required: true, schema: { type: string }, description: "Social account ID (a connected account on the target ad platform)." }
41915
43255
  - { name: q, in: query, required: true, schema: { type: string }, description: "Search query. For geo, the locality name only (no region/country suffix)." }
41916
43256
  - { name: dimension, in: query, required: false, schema: { type: string, enum: [geo, interest, behavior, income], default: interest }, description: "What to search. `geo` resolves locations (scope further with `geoType`), `interest`/`behavior` resolve audience entities, `income` resolves income-tier options. Defaults to `interest` for backward compatibility with the deprecated /v1/ads/interests alias." }
41917
- - { name: geoType, in: query, required: false, schema: { type: string, enum: [country, region, city, zip, metro], default: city }, description: "Only used when `dimension=geo`. The kind of location to resolve. Defaults to `city`." }
43257
+ - { name: geoType, in: query, required: false, schema: { type: string, enum: [all, country, region, city, subcity, neighborhood, place, zip, metro_area, geo_market], default: city }, description: "Only used when `dimension=geo`. The kind of location to resolve. `all` searches every type in one relevance-ranked call. Defaults to `city`." }
41918
43258
  - { name: countryCode, in: query, required: false, schema: { type: string, minLength: 2, maxLength: 2 }, description: "ISO 3166-1 alpha-2 country code (e.g. NL) to scope a geo search." }
41919
43259
  - { name: limit, in: query, required: false, schema: { type: integer, minimum: 1, maximum: 100, default: 25 }, description: "Maximum results to return." }
41920
43260
  responses:
@@ -41933,7 +43273,7 @@ paths:
41933
43273
  properties:
41934
43274
  id: { type: string, description: "The platform's opaque id. Use as a geo `key` (regions/cities/zips/metros) or an entity `id` (interests/behaviors) in TargetingSpec." }
41935
43275
  name: { type: string, description: "Human-readable label." }
41936
- type: { type: string, description: "What the result is (e.g. city, region, country, zip, metro, interest, behavior, income)." }
43276
+ type: { type: string, description: "What the result is (e.g. city, region, country, zip, metro, location, interest, behavior, income)." }
41937
43277
  path: { type: array, items: { type: string }, description: "Optional breadcrumb of parent labels (e.g. ['United States', 'California', 'Los Angeles']). Disambiguates same-named results." }
41938
43278
  audienceSize: { type: [integer, "null"], description: "Optional estimated reachable users for this option, when the platform returns it." }
41939
43279
  '400':
@@ -43788,6 +45128,7 @@ paths:
43788
45128
  '401': { $ref: '#/components/responses/Unauthorized' }
43789
45129
  '404': { description: Account not found }
43790
45130
  '422': { description: "No Facebook Page resolved for the account" }
45131
+ '502': { description: "Meta accepted the request then failed to produce the media (upload session, chunk transfer, processing timeout, or a response with no image hash). Inspect `platformError.reason`." }
43791
45132
 
43792
45133
  /v1/ads/call:
43793
45134
  post:
@@ -43827,6 +45168,7 @@ paths:
43827
45168
  '401': { $ref: '#/components/responses/Unauthorized' }
43828
45169
  '404': { description: Account not found }
43829
45170
  '422': { description: "No Facebook Page resolved for the account" }
45171
+ '502': { description: "Meta accepted the request then failed to produce the media (upload session, chunk transfer, processing timeout, or a response with no image hash). Inspect `platformError.reason`." }
43830
45172
 
43831
45173
  /v1/ads/ctwa:
43832
45174
  post:
@@ -44018,6 +45360,7 @@ paths:
44018
45360
  '403': { description: 'Ads access required (Ads add-on on legacy plans, included on usage-based plans), or the Meta token lacks ads permissions (reconnect required).' }
44019
45361
  '404': { description: Account not found or not accessible. }
44020
45362
  '405': { description: Platform does not support listing tracking tags. }
45363
+ '502': { description: 'Meta was unreachable or returned an unclassified error (type: platform_error; the raw Meta payload is in platformError). Retryable.' }
44021
45364
 
44022
45365
  post:
44023
45366
  x-resource-group: "ads"
@@ -44083,6 +45426,7 @@ paths:
44083
45426
  '404': { description: Account not found or not accessible. }
44084
45427
  '405': { description: Platform does not support creating tracking tags. }
44085
45428
  '422': { description: 'OpenAI Ads only: the ad account is not enabled for pixel management. Contact your OpenAI partner representative.' }
45429
+ '502': { description: 'Meta was unreachable or returned an unclassified error (type: platform_error; the raw Meta payload is in platformError). Creating a pixel is NOT idempotent, so before retrying confirm with GET /v1/accounts/{accountId}/tracking-tags that no pixel was created.' }
44086
45430
 
44087
45431
  /v1/accounts/{accountId}/tracking-tags/{tagId}:
44088
45432
  get:
@@ -44116,6 +45460,7 @@ paths:
44116
45460
  '403': { description: 'Ads access required (Ads add-on on legacy plans, included on usage-based plans), or the Meta token lacks ads permissions (reconnect required).' }
44117
45461
  '404': { description: Account or tracking tag not found. }
44118
45462
  '405': { description: Platform does not support fetching a tracking tag. }
45463
+ '502': { description: 'Meta was unreachable or returned an unclassified error (type: platform_error; the raw Meta payload is in platformError). Retryable.' }
44119
45464
 
44120
45465
  patch:
44121
45466
  x-resource-group: "ads"
@@ -44182,6 +45527,7 @@ paths:
44182
45527
  '403': { description: 'Ads access required (Ads add-on on legacy plans, included on usage-based plans), or the Meta token lacks ads permissions (reconnect required).' }
44183
45528
  '404': { description: Account or tracking tag not found. }
44184
45529
  '405': { description: Platform does not support updating tracking tags. }
45530
+ '502': { description: 'Meta was unreachable or returned an unclassified error (type: platform_error; the raw Meta payload is in platformError). Retryable.' }
44185
45531
 
44186
45532
  /v1/accounts/{accountId}/tracking-tags/{tagId}/shared-accounts:
44187
45533
  get:
@@ -44212,6 +45558,7 @@ paths:
44212
45558
  '403': { description: 'Ads access required (Ads add-on on legacy plans, included on usage-based plans), or the Meta token lacks ads permissions (reconnect required).' }
44213
45559
  '404': { description: Account or tracking tag not found. }
44214
45560
  '405': { description: Platform does not support shared accounts. }
45561
+ '502': { description: 'Meta was unreachable or returned an unclassified error (type: platform_error; the raw Meta payload is in platformError). Retryable.' }
44215
45562
 
44216
45563
  post:
44217
45564
  x-resource-group: "ads"
@@ -44254,6 +45601,7 @@ paths:
44254
45601
  '403': { description: 'Ads access required (Ads add-on on legacy plans, included on usage-based plans), or the Meta token lacks ads permissions (reconnect required).' }
44255
45602
  '404': { description: Account or tracking tag not found. }
44256
45603
  '405': { description: Platform does not support shared accounts. }
45604
+ '502': { description: 'Meta was unreachable or returned an unclassified error (type: platform_error; the raw Meta payload is in platformError). Retryable.' }
44257
45605
 
44258
45606
  delete:
44259
45607
  x-resource-group: "ads"
@@ -44278,6 +45626,7 @@ paths:
44278
45626
  '403': { description: 'Ads access required (Ads add-on on legacy plans, included on usage-based plans), or the Meta token lacks ads permissions (reconnect required).' }
44279
45627
  '404': { description: Account or tracking tag not found. }
44280
45628
  '405': { description: Platform does not support shared accounts. }
45629
+ '502': { description: 'Meta was unreachable or returned an unclassified error (type: platform_error; the raw Meta payload is in platformError). Retryable.' }
44281
45630
 
44282
45631
  /v1/accounts/{accountId}/tracking-tags/{tagId}/stats:
44283
45632
  get:
@@ -44345,6 +45694,448 @@ paths:
44345
45694
  '403': { description: 'Ads access required (Ads add-on on legacy plans, included on usage-based plans), or the Meta token lacks ads permissions (reconnect required).' }
44346
45695
  '404': { description: Account or tracking tag not found. }
44347
45696
  '405': { description: Platform does not support tracking-tag stats. }
45697
+ '502': { description: 'Meta was unreachable or returned an unclassified error (type: platform_error; the raw Meta payload is in platformError). Retryable.' }
45698
+
45699
+ /v1/accounts/{accountId}/blogs:
45700
+ get:
45701
+ x-resource-group: "publishing"
45702
+ operationId: listBlogs
45703
+ tags: ["Blogs"]
45704
+ x-platforms: ["shopify"]
45705
+ summary: List blogs
45706
+ description: |
45707
+ Lists the blogs on the connected store, newest-first as the platform
45708
+ returns them. Cursor-paginated: pass `limit` (1-50, default 20) and the
45709
+ `cursor` from a previous response's `nextCursor`; `nextCursor` is null
45710
+ when there are no more pages.
45711
+
45712
+ Supported on Shopify (platform `shopify`). Accounts on platforms
45713
+ without blogs support return 400; a blogs-capable platform that lacks
45714
+ this specific operation returns 405.
45715
+ security:
45716
+ - bearerAuth: []
45717
+ parameters:
45718
+ - { name: accountId, in: path, required: true, schema: { type: string }, description: 'Connected Shopify SocialAccount id.' }
45719
+ - { name: limit, in: query, required: false, schema: { type: integer, minimum: 1, maximum: 50, default: 20 }, description: 'Page size (1-50).' }
45720
+ - { name: cursor, in: query, required: false, schema: { type: string }, description: 'Opaque cursor from a previous response. Omit for the first page.' }
45721
+ responses:
45722
+ '200':
45723
+ description: Blogs listed
45724
+ content:
45725
+ application/json:
45726
+ schema:
45727
+ type: object
45728
+ properties:
45729
+ platform: { type: string, enum: [shopify] }
45730
+ blogs:
45731
+ type: array
45732
+ items: { $ref: '#/components/schemas/Blog' }
45733
+ nextCursor: { type: [string, "null"], description: 'Cursor for the next page; null when there are no more pages.' }
45734
+ '400': { $ref: '#/components/responses/BadRequest' }
45735
+ '401': { $ref: '#/components/responses/Unauthorized' }
45736
+ '403': { description: 'The platform rejected the request (code insufficient_permissions); reconnect the Shopify account to restore access.' }
45737
+ '404': { description: 'Account not found or not accessible (code account_not_found).' }
45738
+ '405': { description: Platform does not support listing blogs. }
45739
+ '429': { description: 'Rate limited, either by Zernio or by Shopify. Retry later.' }
45740
+
45741
+ post:
45742
+ x-resource-group: "publishing"
45743
+ operationId: createBlog
45744
+ tags: ["Blogs"]
45745
+ x-platforms: ["shopify"]
45746
+ summary: Create a blog
45747
+ description: |
45748
+ Creates a blog on the connected store. The platform generates the URL
45749
+ `handle` from the title when omitted.
45750
+
45751
+ Supported on Shopify (platform `shopify`). Accounts on platforms
45752
+ without blogs support return 400; a blogs-capable platform that lacks
45753
+ this specific operation returns 405.
45754
+ security:
45755
+ - bearerAuth: []
45756
+ parameters:
45757
+ - { name: accountId, in: path, required: true, schema: { type: string }, description: 'Connected Shopify SocialAccount id.' }
45758
+ requestBody:
45759
+ required: true
45760
+ content:
45761
+ application/json:
45762
+ schema:
45763
+ type: object
45764
+ required: [title]
45765
+ properties:
45766
+ title: { type: string, minLength: 1 }
45767
+ handle: { type: string, minLength: 1, description: 'URL slug. Generated from the title when omitted.' }
45768
+ responses:
45769
+ '201':
45770
+ description: Blog created
45771
+ content:
45772
+ application/json:
45773
+ schema:
45774
+ type: object
45775
+ properties:
45776
+ platform: { type: string, enum: [shopify] }
45777
+ blog: { $ref: '#/components/schemas/Blog' }
45778
+ '400': { $ref: '#/components/responses/BadRequest' }
45779
+ '401': { $ref: '#/components/responses/Unauthorized' }
45780
+ '403': { description: 'The platform rejected the request (code insufficient_permissions); reconnect the Shopify account to restore access.' }
45781
+ '404': { description: 'Account not found or not accessible (code account_not_found).' }
45782
+ '405': { description: Platform does not support creating blogs. }
45783
+ '429': { description: 'Rate limited, either by Zernio or by Shopify. Retry later.' }
45784
+
45785
+ /v1/accounts/{accountId}/blogs/{blogId}:
45786
+ get:
45787
+ x-resource-group: "publishing"
45788
+ operationId: getBlog
45789
+ tags: ["Blogs"]
45790
+ x-platforms: ["shopify"]
45791
+ summary: Get a blog
45792
+ description: |
45793
+ Fetches a single blog. `blogId` is the platform's numeric blog id from
45794
+ `GET /v1/accounts/{accountId}/blogs`, not a Zernio id.
45795
+
45796
+ Supported on Shopify (platform `shopify`). Accounts on platforms
45797
+ without blogs support return 400; a blogs-capable platform that lacks
45798
+ this specific operation returns 405.
45799
+ security:
45800
+ - bearerAuth: []
45801
+ parameters:
45802
+ - { name: accountId, in: path, required: true, schema: { type: string }, description: 'Connected Shopify SocialAccount id.' }
45803
+ - { name: blogId, in: path, required: true, schema: { type: string, pattern: '^\d+$' }, description: 'Platform-native numeric blog id. Non-numeric values return 400.' }
45804
+ responses:
45805
+ '200':
45806
+ description: Blog fetched
45807
+ content:
45808
+ application/json:
45809
+ schema:
45810
+ type: object
45811
+ properties:
45812
+ platform: { type: string, enum: [shopify] }
45813
+ blog: { $ref: '#/components/schemas/Blog' }
45814
+ '400': { $ref: '#/components/responses/BadRequest' }
45815
+ '401': { $ref: '#/components/responses/Unauthorized' }
45816
+ '403': { description: 'The platform rejected the request (code insufficient_permissions); reconnect the Shopify account to restore access.' }
45817
+ '404': { description: 'Account not found or not accessible (code account_not_found), or blog not found (code blog_not_found).' }
45818
+ '405': { description: Platform does not support fetching a blog. }
45819
+ '429': { description: 'Rate limited, either by Zernio or by Shopify. Retry later.' }
45820
+
45821
+ patch:
45822
+ x-resource-group: "publishing"
45823
+ operationId: updateBlog
45824
+ tags: ["Blogs"]
45825
+ x-platforms: ["shopify"]
45826
+ summary: Update a blog
45827
+ description: |
45828
+ Partial-updates a blog. Send any subset of `title` and `handle`; at
45829
+ least one field is required (an empty body returns 400).
45830
+
45831
+ Supported on Shopify (platform `shopify`). Accounts on platforms
45832
+ without blogs support return 400; a blogs-capable platform that lacks
45833
+ this specific operation returns 405.
45834
+ security:
45835
+ - bearerAuth: []
45836
+ parameters:
45837
+ - { name: accountId, in: path, required: true, schema: { type: string }, description: 'Connected Shopify SocialAccount id.' }
45838
+ - { name: blogId, in: path, required: true, schema: { type: string, pattern: '^\d+$' }, description: 'Platform-native numeric blog id. Non-numeric values return 400.' }
45839
+ requestBody:
45840
+ required: true
45841
+ content:
45842
+ application/json:
45843
+ schema:
45844
+ type: object
45845
+ description: 'At least one field is required.'
45846
+ properties:
45847
+ title: { type: string, minLength: 1 }
45848
+ handle: { type: string, minLength: 1, description: 'URL slug. Changing it changes the blog URL on the store.' }
45849
+ responses:
45850
+ '200':
45851
+ description: Blog updated
45852
+ content:
45853
+ application/json:
45854
+ schema:
45855
+ type: object
45856
+ properties:
45857
+ platform: { type: string, enum: [shopify] }
45858
+ blog: { $ref: '#/components/schemas/Blog' }
45859
+ '400': { $ref: '#/components/responses/BadRequest' }
45860
+ '401': { $ref: '#/components/responses/Unauthorized' }
45861
+ '403': { description: 'The platform rejected the request (code insufficient_permissions); reconnect the Shopify account to restore access.' }
45862
+ '404': { description: 'Account not found or not accessible (code account_not_found), or blog not found (code blog_not_found).' }
45863
+ '405': { description: Platform does not support updating a blog. }
45864
+ '429': { description: 'Rate limited, either by Zernio or by Shopify. Retry later.' }
45865
+
45866
+ delete:
45867
+ x-resource-group: "publishing"
45868
+ operationId: deleteBlog
45869
+ tags: ["Blogs"]
45870
+ x-platforms: ["shopify"]
45871
+ summary: Delete a blog
45872
+ description: |
45873
+ Deletes the blog AND every article in it. The delete happens on the
45874
+ platform and is permanent; Zernio stores nothing to restore it from.
45875
+
45876
+ Supported on Shopify (platform `shopify`). Accounts on platforms
45877
+ without blogs support return 400; a blogs-capable platform that lacks
45878
+ this specific operation returns 405.
45879
+ security:
45880
+ - bearerAuth: []
45881
+ parameters:
45882
+ - { name: accountId, in: path, required: true, schema: { type: string }, description: 'Connected Shopify SocialAccount id.' }
45883
+ - { name: blogId, in: path, required: true, schema: { type: string, pattern: '^\d+$' }, description: 'Platform-native numeric blog id. Non-numeric values return 400.' }
45884
+ responses:
45885
+ '204': { description: Blog deleted (no content). }
45886
+ '400': { $ref: '#/components/responses/BadRequest' }
45887
+ '401': { $ref: '#/components/responses/Unauthorized' }
45888
+ '403': { description: 'The platform rejected the request (code insufficient_permissions); reconnect the Shopify account to restore access.' }
45889
+ '404': { description: 'Account not found or not accessible (code account_not_found), or blog not found (code blog_not_found).' }
45890
+ '405': { description: Platform does not support deleting a blog. }
45891
+ '429': { description: 'Rate limited, either by Zernio or by Shopify. Retry later.' }
45892
+
45893
+ /v1/accounts/{accountId}/blogs/{blogId}/articles:
45894
+ get:
45895
+ x-resource-group: "publishing"
45896
+ operationId: listBlogArticles
45897
+ tags: ["Blogs"]
45898
+ x-platforms: ["shopify"]
45899
+ summary: List blog articles
45900
+ description: |
45901
+ Lists the articles of a blog. Cursor-paginated: pass `limit` (1-50,
45902
+ default 20) and the `cursor` from a previous response's `nextCursor`;
45903
+ `nextCursor` is null when there are no more pages.
45904
+
45905
+ Supported on Shopify (platform `shopify`). Accounts on platforms
45906
+ without blogs support return 400; a blogs-capable platform that lacks
45907
+ this specific operation returns 405.
45908
+ security:
45909
+ - bearerAuth: []
45910
+ parameters:
45911
+ - { name: accountId, in: path, required: true, schema: { type: string }, description: 'Connected Shopify SocialAccount id.' }
45912
+ - { name: blogId, in: path, required: true, schema: { type: string, pattern: '^\d+$' }, description: 'Platform-native numeric blog id. Non-numeric values return 400.' }
45913
+ - { name: limit, in: query, required: false, schema: { type: integer, minimum: 1, maximum: 50, default: 20 }, description: 'Page size (1-50).' }
45914
+ - { name: cursor, in: query, required: false, schema: { type: string }, description: 'Opaque cursor from a previous response. Omit for the first page.' }
45915
+ responses:
45916
+ '200':
45917
+ description: Articles listed
45918
+ content:
45919
+ application/json:
45920
+ schema:
45921
+ type: object
45922
+ properties:
45923
+ platform: { type: string, enum: [shopify] }
45924
+ articles:
45925
+ type: array
45926
+ items: { $ref: '#/components/schemas/BlogArticle' }
45927
+ nextCursor: { type: [string, "null"], description: 'Cursor for the next page; null when there are no more pages.' }
45928
+ '400': { $ref: '#/components/responses/BadRequest' }
45929
+ '401': { $ref: '#/components/responses/Unauthorized' }
45930
+ '403': { description: 'The platform rejected the request (code insufficient_permissions); reconnect the Shopify account to restore access.' }
45931
+ '404': { description: 'Account not found or not accessible (code account_not_found), or blog not found (code blog_not_found).' }
45932
+ '405': { description: Platform does not support listing articles. }
45933
+ '429': { description: 'Rate limited, either by Zernio or by Shopify. Retry later.' }
45934
+
45935
+ post:
45936
+ x-resource-group: "publishing"
45937
+ operationId: createBlogArticle
45938
+ tags: ["Blogs"]
45939
+ x-platforms: ["shopify"]
45940
+ summary: Create a blog article
45941
+ description: |
45942
+ Creates an article on the blog. Publishing behavior:
45943
+
45944
+ - `isPublished: false` keeps the article as a draft.
45945
+ - A future `publishDate` schedules publication natively on the
45946
+ platform; the platform publishes it at that time with no Zernio
45947
+ queue involved.
45948
+ - `seo.title` / `seo.description` map to Shopify's global `title_tag`
45949
+ and `description_tag` metafields (the fields Shopify themes read for
45950
+ the page title and meta description).
45951
+
45952
+ Supported on Shopify (platform `shopify`). Accounts on platforms
45953
+ without blogs support return 400; a blogs-capable platform that lacks
45954
+ this specific operation returns 405.
45955
+ security:
45956
+ - bearerAuth: []
45957
+ parameters:
45958
+ - { name: accountId, in: path, required: true, schema: { type: string }, description: 'Connected Shopify SocialAccount id.' }
45959
+ - { name: blogId, in: path, required: true, schema: { type: string, pattern: '^\d+$' }, description: 'Platform-native numeric blog id. Non-numeric values return 400.' }
45960
+ requestBody:
45961
+ required: true
45962
+ content:
45963
+ application/json:
45964
+ schema:
45965
+ type: object
45966
+ required: [title]
45967
+ properties:
45968
+ title: { type: string, minLength: 1 }
45969
+ bodyHtml: { type: string, description: 'Article body as HTML.' }
45970
+ handle: { type: string, description: 'URL slug. Generated from the title when omitted.' }
45971
+ tags: { type: array, items: { type: string } }
45972
+ author: { type: string, description: 'Display name of the article author.' }
45973
+ excerpt: { type: string, description: 'Short summary shown in blog listings.' }
45974
+ image:
45975
+ type: object
45976
+ required: [url]
45977
+ description: 'Featured image. The platform downloads it, so the URL must be publicly reachable.'
45978
+ properties:
45979
+ url: { type: string, format: uri }
45980
+ altText: { type: string }
45981
+ seo:
45982
+ type: object
45983
+ description: 'Search-engine overrides. Maps to Shopify global metafields (title_tag and description_tag).'
45984
+ properties:
45985
+ title: { type: string }
45986
+ description: { type: string }
45987
+ isPublished: { type: boolean, description: 'Set false to create the article as a draft.' }
45988
+ publishDate: { type: string, format: date-time, description: 'ISO 8601 datetime with offset (or Z). A future date schedules publication natively on the platform.' }
45989
+ responses:
45990
+ '201':
45991
+ description: Article created
45992
+ content:
45993
+ application/json:
45994
+ schema:
45995
+ type: object
45996
+ properties:
45997
+ platform: { type: string, enum: [shopify] }
45998
+ article: { $ref: '#/components/schemas/BlogArticle' }
45999
+ '400': { $ref: '#/components/responses/BadRequest' }
46000
+ '401': { $ref: '#/components/responses/Unauthorized' }
46001
+ '403': { description: 'The platform rejected the request (code insufficient_permissions); reconnect the Shopify account to restore access.' }
46002
+ '404': { description: 'Account not found or not accessible (code account_not_found), or blog not found (code blog_not_found).' }
46003
+ '405': { description: Platform does not support creating articles. }
46004
+ '429': { description: 'Rate limited, either by Zernio or by Shopify. Retry later.' }
46005
+
46006
+ /v1/accounts/{accountId}/blogs/{blogId}/articles/{articleId}:
46007
+ get:
46008
+ x-resource-group: "publishing"
46009
+ operationId: getBlogArticle
46010
+ tags: ["Blogs"]
46011
+ x-platforms: ["shopify"]
46012
+ summary: Get a blog article
46013
+ description: |
46014
+ Fetches a single article. An article addressed through a blog it does
46015
+ not belong to is a 404 (code blog_article_not_found).
46016
+
46017
+ Supported on Shopify (platform `shopify`). Accounts on platforms
46018
+ without blogs support return 400; a blogs-capable platform that lacks
46019
+ this specific operation returns 405.
46020
+ security:
46021
+ - bearerAuth: []
46022
+ parameters:
46023
+ - { name: accountId, in: path, required: true, schema: { type: string }, description: 'Connected Shopify SocialAccount id.' }
46024
+ - { name: blogId, in: path, required: true, schema: { type: string, pattern: '^\d+$' }, description: 'Platform-native numeric blog id. Non-numeric values return 400.' }
46025
+ - { name: articleId, in: path, required: true, schema: { type: string, pattern: '^\d+$' }, description: 'Platform-native numeric article id. Non-numeric values return 400.' }
46026
+ responses:
46027
+ '200':
46028
+ description: Article fetched
46029
+ content:
46030
+ application/json:
46031
+ schema:
46032
+ type: object
46033
+ properties:
46034
+ platform: { type: string, enum: [shopify] }
46035
+ article: { $ref: '#/components/schemas/BlogArticle' }
46036
+ '400': { $ref: '#/components/responses/BadRequest' }
46037
+ '401': { $ref: '#/components/responses/Unauthorized' }
46038
+ '403': { description: 'The platform rejected the request (code insufficient_permissions); reconnect the Shopify account to restore access.' }
46039
+ '404': { description: 'Account not found or not accessible (code account_not_found), blog not found (code blog_not_found), or article not found (code blog_article_not_found).' }
46040
+ '405': { description: Platform does not support fetching an article. }
46041
+ '429': { description: 'Rate limited, either by Zernio or by Shopify. Retry later.' }
46042
+
46043
+ patch:
46044
+ x-resource-group: "publishing"
46045
+ operationId: updateBlogArticle
46046
+ tags: ["Blogs"]
46047
+ x-platforms: ["shopify"]
46048
+ summary: Update a blog article
46049
+ description: |
46050
+ Partial-updates an article. Send any subset of the create fields
46051
+ (`title`, `bodyHtml`, `handle`, `tags`, `author`, `excerpt`, `image`,
46052
+ `seo`, `isPublished`, `publishDate`); at least one field is required
46053
+ (an empty body returns 400). `isPublished` and `publishDate` behave as
46054
+ on create: `isPublished: false` unpublishes back to a draft and a
46055
+ future `publishDate` schedules publication natively on the platform.
46056
+
46057
+ Supported on Shopify (platform `shopify`). Accounts on platforms
46058
+ without blogs support return 400; a blogs-capable platform that lacks
46059
+ this specific operation returns 405.
46060
+ security:
46061
+ - bearerAuth: []
46062
+ parameters:
46063
+ - { name: accountId, in: path, required: true, schema: { type: string }, description: 'Connected Shopify SocialAccount id.' }
46064
+ - { name: blogId, in: path, required: true, schema: { type: string, pattern: '^\d+$' }, description: 'Platform-native numeric blog id. Non-numeric values return 400.' }
46065
+ - { name: articleId, in: path, required: true, schema: { type: string, pattern: '^\d+$' }, description: 'Platform-native numeric article id. Non-numeric values return 400.' }
46066
+ requestBody:
46067
+ required: true
46068
+ content:
46069
+ application/json:
46070
+ schema:
46071
+ type: object
46072
+ description: 'At least one field is required.'
46073
+ properties:
46074
+ title: { type: string, minLength: 1 }
46075
+ bodyHtml: { type: string, description: 'Article body as HTML.' }
46076
+ handle: { type: string, description: 'URL slug of the article.' }
46077
+ tags: { type: array, items: { type: string }, description: 'Replaces the full tag list.' }
46078
+ author: { type: string, description: 'Display name of the article author.' }
46079
+ excerpt: { type: string, description: 'Short summary shown in blog listings.' }
46080
+ image:
46081
+ type: object
46082
+ required: [url]
46083
+ description: 'Featured image. The platform downloads it, so the URL must be publicly reachable.'
46084
+ properties:
46085
+ url: { type: string, format: uri }
46086
+ altText: { type: string }
46087
+ seo:
46088
+ type: object
46089
+ description: 'Search-engine overrides. Maps to Shopify global metafields (title_tag and description_tag).'
46090
+ properties:
46091
+ title: { type: string }
46092
+ description: { type: string }
46093
+ isPublished: { type: boolean, description: 'Set false to unpublish the article back to a draft.' }
46094
+ publishDate: { type: string, format: date-time, description: 'ISO 8601 datetime with offset (or Z). A future date schedules publication natively on the platform.' }
46095
+ responses:
46096
+ '200':
46097
+ description: Article updated
46098
+ content:
46099
+ application/json:
46100
+ schema:
46101
+ type: object
46102
+ properties:
46103
+ platform: { type: string, enum: [shopify] }
46104
+ article: { $ref: '#/components/schemas/BlogArticle' }
46105
+ '400': { $ref: '#/components/responses/BadRequest' }
46106
+ '401': { $ref: '#/components/responses/Unauthorized' }
46107
+ '403': { description: 'The platform rejected the request (code insufficient_permissions); reconnect the Shopify account to restore access.' }
46108
+ '404': { description: 'Account not found or not accessible (code account_not_found), blog not found (code blog_not_found), or article not found (code blog_article_not_found).' }
46109
+ '405': { description: Platform does not support updating an article. }
46110
+ '429': { description: 'Rate limited, either by Zernio or by Shopify. Retry later.' }
46111
+
46112
+ delete:
46113
+ x-resource-group: "publishing"
46114
+ operationId: deleteBlogArticle
46115
+ tags: ["Blogs"]
46116
+ x-platforms: ["shopify"]
46117
+ summary: Delete a blog article
46118
+ description: |
46119
+ Deletes the article. The delete happens on the platform and is
46120
+ permanent; Zernio stores nothing to restore it from.
46121
+
46122
+ Supported on Shopify (platform `shopify`). Accounts on platforms
46123
+ without blogs support return 400; a blogs-capable platform that lacks
46124
+ this specific operation returns 405.
46125
+ security:
46126
+ - bearerAuth: []
46127
+ parameters:
46128
+ - { name: accountId, in: path, required: true, schema: { type: string }, description: 'Connected Shopify SocialAccount id.' }
46129
+ - { name: blogId, in: path, required: true, schema: { type: string, pattern: '^\d+$' }, description: 'Platform-native numeric blog id. Non-numeric values return 400.' }
46130
+ - { name: articleId, in: path, required: true, schema: { type: string, pattern: '^\d+$' }, description: 'Platform-native numeric article id. Non-numeric values return 400.' }
46131
+ responses:
46132
+ '204': { description: Article deleted (no content). }
46133
+ '400': { $ref: '#/components/responses/BadRequest' }
46134
+ '401': { $ref: '#/components/responses/Unauthorized' }
46135
+ '403': { description: 'The platform rejected the request (code insufficient_permissions); reconnect the Shopify account to restore access.' }
46136
+ '404': { description: 'Account not found or not accessible (code account_not_found), blog not found (code blog_not_found), or article not found (code blog_article_not_found).' }
46137
+ '405': { description: Platform does not support deleting an article. }
46138
+ '429': { description: 'Rate limited, either by Zernio or by Shopify. Retry later.' }
44348
46139
 
44349
46140
  /v1/verify/verifications:
44350
46141
  post: