late-sdk 0.0.822 → 0.0.824

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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +8 -0
  3. data/docs/AnalyticsApi.md +78 -0
  4. data/docs/AnalyticsDeltaEntry.md +34 -0
  5. data/docs/AnalyticsDeltaEntryMetrics.md +44 -0
  6. data/docs/AnalyticsDeltaResponse.md +22 -0
  7. data/docs/BoostPostRequest.md +3 -1
  8. data/docs/CreateCallAdRequest.md +3 -1
  9. data/docs/CreateMessagingAdRequest.md +3 -1
  10. data/docs/CreateStandaloneAdRequest.md +3 -1
  11. data/docs/CtwaAdRequestBody.md +4 -2
  12. data/docs/WebhookEventsApi.md +69 -0
  13. data/docs/WebhookPayloadAnalyticsSynced.md +26 -0
  14. data/docs/WebhookPayloadAnalyticsSyncedAccount.md +24 -0
  15. data/docs/WebhookPayloadAnalyticsSyncedSync.md +20 -0
  16. data/docs/WebhookPayloadMessageMetadata.md +3 -1
  17. data/docs/WebhookPayloadMessageMetadataQuotedMessage.md +20 -0
  18. data/lib/zernio-sdk/api/analytics_api.rb +77 -0
  19. data/lib/zernio-sdk/api/webhook_events_api.rb +66 -0
  20. data/lib/zernio-sdk/models/analytics_delta_entry.rb +380 -0
  21. data/lib/zernio-sdk/models/analytics_delta_entry_metrics.rb +507 -0
  22. data/lib/zernio-sdk/models/analytics_delta_response.rb +221 -0
  23. data/lib/zernio-sdk/models/boost_post_request.rb +14 -2
  24. data/lib/zernio-sdk/models/create_call_ad_request.rb +14 -2
  25. data/lib/zernio-sdk/models/create_messaging_ad_request.rb +14 -2
  26. data/lib/zernio-sdk/models/create_standalone_ad_request.rb +14 -2
  27. data/lib/zernio-sdk/models/ctwa_ad_request_body.rb +17 -5
  28. data/lib/zernio-sdk/models/webhook_payload_analytics_synced.rb +295 -0
  29. data/lib/zernio-sdk/models/webhook_payload_analytics_synced_account.rb +244 -0
  30. data/lib/zernio-sdk/models/webhook_payload_analytics_synced_sync.rb +193 -0
  31. data/lib/zernio-sdk/models/webhook_payload_message_metadata.rb +11 -2
  32. data/lib/zernio-sdk/models/webhook_payload_message_metadata_quoted_message.rb +159 -0
  33. data/lib/zernio-sdk/version.rb +1 -1
  34. data/lib/zernio-sdk.rb +7 -0
  35. data/openapi.yaml +414 -14
  36. data/spec/api/analytics_api_spec.rb +15 -0
  37. data/spec/models/analytics_delta_entry_metrics_spec.rb +114 -0
  38. data/spec/models/analytics_delta_entry_spec.rb +84 -0
  39. data/spec/models/analytics_delta_response_spec.rb +48 -0
  40. data/spec/models/boost_post_request_spec.rb +6 -0
  41. data/spec/models/create_call_ad_request_spec.rb +6 -0
  42. data/spec/models/create_messaging_ad_request_spec.rb +6 -0
  43. data/spec/models/create_standalone_ad_request_spec.rb +6 -0
  44. data/spec/models/create_webhook_settings_request_spec.rb +1 -1
  45. data/spec/models/ctwa_ad_request_body_spec.rb +6 -0
  46. data/spec/models/update_webhook_settings_request_spec.rb +1 -1
  47. data/spec/models/webhook_payload_analytics_synced_account_spec.rb +54 -0
  48. data/spec/models/webhook_payload_analytics_synced_spec.rb +64 -0
  49. data/spec/models/webhook_payload_analytics_synced_sync_spec.rb +42 -0
  50. data/spec/models/webhook_payload_message_metadata_quoted_message_spec.rb +42 -0
  51. data/spec/models/webhook_payload_message_metadata_spec.rb +6 -0
  52. data/spec/models/webhook_spec.rb +1 -1
  53. data/zernio-sdk-0.0.824.gem +0 -0
  54. metadata +31 -3
  55. data/zernio-sdk-0.0.822.gem +0 -0
data/openapi.yaml CHANGED
@@ -159,6 +159,7 @@ x-documentation:
159
159
  - `account.connected` - Social account connected
160
160
  - `account.disconnected` - Social account disconnected (token expired)
161
161
  - `account.ads.initial_sync_completed` - Initial ads sync (discovery + 90-day backfill) completed for an ads-enabled account
162
+ - `analytics.synced` - One connected account's analytics sync cycle finished. A trigger, not a transport: it carries no metrics and no cursor, so call `GET /v1/analytics/delta` with your own last `nextCursor` to read what changed across every account. Poll-driven (~hourly per account), not real-time. High volume: subscribe to it on a dedicated endpoint.
162
163
  - `message.received` - New inbound DM or SMS received (SMS arrives with `platform: "sms"`)
163
164
  - `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
164
165
  - `message.sent` - DM sent via the API
@@ -431,10 +432,10 @@ tags:
431
432
  - name: Webhooks
432
433
  description: |
433
434
  Configure webhooks for real-time notifications. Webhooks can be created from the dashboard (Settings → Webhooks) or via this API.
434
- 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.account.name_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, phone_number.stock_available, verification.approved, verification.failed, webhook.test.
435
+ 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, analytics.synced, 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.account.name_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, phone_number.stock_available, verification.approved, verification.failed, webhook.test.
435
436
  Security: optional HMAC-SHA256 signature in X-Zernio-Signature header. Configure a secret key to enable verification. Custom headers supported.
436
437
 
437
- 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.*, whatsapp.template.* and whatsapp.account.* 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.
438
+ 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.*, whatsapp.template.* and whatsapp.account.* to accounts, analytics.* to analytics, 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.
438
439
  - name: Webhook Events
439
440
  description: |
440
441
  Incoming webhook deliveries sent by Zernio to your configured endpoint URL.
@@ -1176,8 +1177,23 @@ components:
1176
1177
  a default payor.
1177
1178
  regionalRegulatedCategories:
1178
1179
  type: array
1179
- description: 'Meta only. Regional regulation categories required when the ad set targets certain countries (e.g. SINGAPORE_UNIVERSAL, TAIWAN_UNIVERSAL, THAILAND_UNIVERSAL, AUSTRALIA_FINSERV, INDIA_FINSERV). Forwarded to the ad set.'
1180
+ description: 'Meta only. Regional regulation categories required when the ad set targets certain countries (e.g. BRAZIL_REGULATION, SINGAPORE_UNIVERSAL, TAIWAN_UNIVERSAL, THAILAND_UNIVERSAL, AUSTRALIA_FINSERV, INDIA_FINSERV, TAIWAN_FINSERV). Forwarded to the ad set.'
1180
1181
  items: { type: string }
1182
+ regionalRegulationIdentities:
1183
+ type: object
1184
+ additionalProperties: { type: integer }
1185
+ description: |
1186
+ Meta only. Beneficiary/payer entity IDs required alongside regionalRegulatedCategories.
1187
+ Values are numeric IDs from the advertiser's Meta verification/authorization setup.
1188
+ Keys depend on the declared category: BRAZIL_REGULATION and THAILAND_UNIVERSAL use
1189
+ universal_beneficiary / universal_payer; SINGAPORE_UNIVERSAL uses
1190
+ singapore_universal_beneficiary / singapore_universal_payer; TAIWAN_UNIVERSAL uses
1191
+ taiwan_universal_beneficiary / taiwan_universal_payer; TAIWAN_FINSERV uses
1192
+ taiwan_finserv_beneficiary / taiwan_finserv_payer; AUSTRALIA_FINSERV uses
1193
+ australia_finserv_beneficiary / australia_finserv_payer; INDIA_FINSERV uses
1194
+ india_finserv_beneficiary / india_finserv_payer.
1195
+ Both beneficiary and payer must be included. If omitted and the advertiser has
1196
+ set defaults in Meta Ads Manager advertising settings, Meta auto-fills them.
1181
1197
  WorkflowNode:
1182
1198
  type: object
1183
1199
  required: [id, type]
@@ -2435,7 +2451,7 @@ components:
2435
2451
  type: array
2436
2452
  items:
2437
2453
  type: string
2438
- 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.account.name_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, phone_number.stock_available, verification.approved, verification.failed]
2454
+ 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, analytics.synced, 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.account.name_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, phone_number.stock_available, verification.approved, verification.failed]
2439
2455
  description: Events subscribed to
2440
2456
  isActive:
2441
2457
  type: boolean
@@ -2877,6 +2893,80 @@ components:
2877
2893
  type: string
2878
2894
  format: date-time
2879
2895
  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.'
2896
+ WebhookPayloadAnalyticsSynced:
2897
+ type: object
2898
+ description: |
2899
+ Webhook payload for `analytics.synced`. Fired once per connected account each
2900
+ time its analytics sync cycle completes successfully. Poll-driven (roughly
2901
+ hourly per account), not real-time, and never fired for a skipped or failed
2902
+ cycle.
2903
+
2904
+ A TRIGGER, not a transport: it deliberately carries no metrics and no cursor.
2905
+ When it arrives, call `GET /v1/analytics/delta` with YOUR OWN last `nextCursor`
2906
+ to read what changed, across every account, in one paginated stream.
2907
+
2908
+ The absent cursor is deliberate. The feed's ordering position is assigned inside
2909
+ the analytics store when the row is materialized, which normally has not
2910
+ happened yet at the moment this event fires, so a cursor minted here could sit
2911
+ ahead of the very rows the event announces and make you skip them. Your own
2912
+ `nextCursor` is always in the feed's own ordering and can never do that.
2913
+
2914
+ Because of that same lag, a delta read issued the instant this event lands can
2915
+ legitimately come back empty. That is not "nothing changed": poll again with the
2916
+ same cursor you just used rather than treating the account as done.
2917
+
2918
+ Subscribe to this event on a DEDICATED webhook endpoint. It is high volume
2919
+ (roughly one delivery per connected account per hour) and a subscription's
2920
+ consecutive-failure count is shared across all of its events, so an outage while
2921
+ this event is flowing can suppress the low-volume publishing events that share
2922
+ the same subscription.
2923
+ required: [id, event, account, sync, timestamp]
2924
+ properties:
2925
+ id:
2926
+ type: string
2927
+ description: Stable webhook event ID
2928
+ event:
2929
+ type: string
2930
+ enum: [analytics.synced]
2931
+ account:
2932
+ type: object
2933
+ required: [accountId, profileId, platform, username]
2934
+ properties:
2935
+ accountId:
2936
+ type: string
2937
+ description: 'The account''s unique identifier (same as used in /v1/accounts/{accountId})'
2938
+ profileId:
2939
+ type: string
2940
+ description: The profile this account belongs to
2941
+ platform:
2942
+ type: string
2943
+ example: youtube
2944
+ username:
2945
+ type: string
2946
+ sync:
2947
+ type: object
2948
+ description: Summary of the analytics sync cycle that just completed.
2949
+ required: [syncedAt, postsUpdated]
2950
+ properties:
2951
+ syncedAt:
2952
+ type: string
2953
+ format: date-time
2954
+ description: |
2955
+ When the cycle COMPLETED. Not a join key for the delta feed: the rows a
2956
+ cycle produces carry a `syncedAt` stamped when the cycle STARTED, which
2957
+ is measured at around one second earlier at the median and up to a
2958
+ couple of minutes earlier in the tail. Correlate on `account.accountId`.
2959
+ postsUpdated:
2960
+ type: integer
2961
+ description: |
2962
+ Post records created or modified by this cycle. Not the number of delta
2963
+ feed rows the cycle produced, which the syncer does not report, so a
2964
+ cycle with a non-zero `postsUpdated` can still yield an empty delta page.
2965
+ example: 42
2966
+ timestamp:
2967
+ type: string
2968
+ format: date-time
2969
+ description: 'UTC time at which Zernio generated this event (set once when the event payload is built, before delivery is queued).'
2880
2970
  WebhookPayloadComment:
2881
2971
  type: object
2882
2972
  description: Webhook payload for comment received events (Instagram, Facebook, Twitter/X, YouTube, LinkedIn, Bluesky, Reddit)
@@ -3751,11 +3841,29 @@ components:
3751
3841
  quotedMessageId:
3752
3842
  type: string
3753
3843
  description: |
3754
- platformMessageId of the message this one is a quote-reply to.
3755
- WhatsApp (`context.id`), Instagram and Facebook Messenger
3756
- (`reply_to.mid`). On outgoing messages the same field appears on
3757
- `message.sent`, but only on some surfaces: see
3844
+ Raw platform envelope id (WhatsApp `context.id`; Instagram and
3845
+ Facebook Messenger `reply_to.mid`) of the message this one is a
3846
+ quote-reply to, forwarded verbatim. It may not equal the stored
3847
+ id of that message (see `quotedMessage.platformMessageId`). On
3848
+ outgoing messages the same field appears on `message.sent`, but
3849
+ only on some surfaces: see
3758
3850
  WebhookPayloadMessageSent.metadata.quotedMessageId.
3851
+ quotedMessage:
3852
+ type: object
3853
+ description: >-
3854
+ Zernio's own ids for the message this one quote-replies to.
3855
+ Present only when that message is stored; WhatsApp only today.
3856
+ properties:
3857
+ messageId:
3858
+ type: string
3859
+ description: Internal id of the stored quoted message.
3860
+ platformMessageId:
3861
+ type: string
3862
+ description: >-
3863
+ The STORED message's platform id (what message.sent and
3864
+ list-messages return). Can differ from quotedMessageId,
3865
+ because Meta renders one message under a different wamid
3866
+ per perspective.
3759
3867
  quickReplyPayload:
3760
3868
  type: string
3761
3869
  description: Payload from a quick reply tap (Facebook/Instagram Messenger).
@@ -7521,6 +7629,89 @@ components:
7521
7629
  hasAnalyticsAccess:
7522
7630
  type: boolean
7523
7631
  description: Whether user has analytics add-on access
7632
+ AnalyticsDeltaEntry:
7633
+ type: object
7634
+ description: |
7635
+ One changed analytics snapshot. Metrics are the absolute values recorded at
7636
+ `syncedAt`, not the amount they moved by since the previous snapshot, so a later
7637
+ entry for the same `postId` always supersedes an earlier one.
7638
+ required: [postId, accountId, profileId, platform, platformPostId, publishedAt, syncedAt, isDeleted, metrics]
7639
+ properties:
7640
+ postId:
7641
+ type: string
7642
+ description: 'External post ID. The same identifier as `posts[]._id` in GET /v1/analytics.'
7643
+ accountId:
7644
+ type: string
7645
+ description: Social account this post was published through
7646
+ profileId:
7647
+ type: string
7648
+ description: Profile the account belongs to
7649
+ platform:
7650
+ type: string
7651
+ example: youtube
7652
+ platformPostId:
7653
+ type: string
7654
+ description: 'Platform-side post ID (for example the YouTube video ID)'
7655
+ publishedAt:
7656
+ type: string
7657
+ format: date-time
7658
+ description: 'When the post was published, ISO-8601 UTC'
7659
+ syncedAt:
7660
+ type: string
7661
+ format: date-time
7662
+ description: |
7663
+ When the sync cycle that produced this snapshot STARTED, ISO-8601 UTC. This
7664
+ is NOT the order entries arrive in and it is not a resume point: a slow cycle
7665
+ writes its rows after a faster cycle that started later, so `syncedAt` can go
7666
+ backwards between consecutive entries. Use `nextCursor` to resume.
7667
+ isDeleted:
7668
+ type: boolean
7669
+ description: 'True when the post was detected as deleted on the platform at this sync'
7670
+ metrics:
7671
+ type: object
7672
+ description: 'Metrics a platform does not report are 0, not absent.'
7673
+ required: [impressions, reach, likes, comments, shares, saves, sends, clicks, views, follows, igReelsAvgWatchTime, igReelsVideoViewTotalTime, reposts, reelsSkipRate]
7674
+ properties:
7675
+ impressions: { type: integer }
7676
+ reach: { type: integer }
7677
+ likes: { type: integer }
7678
+ comments: { type: integer }
7679
+ shares: { type: integer }
7680
+ saves: { type: integer }
7681
+ sends: { type: integer }
7682
+ clicks: { type: integer }
7683
+ views: { type: integer }
7684
+ follows: { type: integer, description: 'Follows attributed to this post (Instagram)' }
7685
+ igReelsAvgWatchTime: { type: integer, description: 'Instagram Reels average watch time, in milliseconds' }
7686
+ igReelsVideoViewTotalTime: { type: integer, description: 'Instagram Reels total watch time, in milliseconds' }
7687
+ reposts: { type: integer }
7688
+ reelsSkipRate: { type: number, description: 'Instagram Reels skip rate, 0 to 1' }
7689
+ AnalyticsDeltaResponse:
7690
+ type: object
7691
+ required: [data, nextCursor, hasMore]
7692
+ properties:
7693
+ data:
7694
+ type: array
7695
+ description: |
7696
+ Changed snapshots, oldest first, in the order the feed received them. Empty on
7697
+ the bootstrap call (no `cursor` supplied) and whenever nothing has changed
7698
+ since your cursor.
7699
+ items:
7700
+ $ref: '#/components/schemas/AnalyticsDeltaEntry'
7701
+ nextCursor:
7702
+ type: string
7703
+ description: |
7704
+ Cursor to send on the next call. ALWAYS present, including on an empty page,
7705
+ so you always have something to advance with, and it never moves backwards.
7706
+ Opaque: pass it back verbatim, and do not parse, construct or compare cursors.
7707
+ example: v1.WyIyMDI2LTA5LTAxIDE3OjEyOjA0IiwiNjVmMWMwYTllMmI1YWYwMDEyYWIzNGNkIl0
7708
+ hasMore:
7709
+ type: boolean
7710
+ description: |
7711
+ True when more changes are already waiting past `nextCursor`, so call again
7712
+ immediately. False means you are caught up: keep `nextCursor` and poll again
7713
+ later. This feed never ends, so `hasMore: false` does NOT mean `nextCursor`
7714
+ is null.
7524
7715
  # LinkedIn Aggregate Analytics Responses
7525
7716
  LinkedInAggregateAnalyticsTotalResponse:
7526
7717
  type: object
@@ -9426,6 +9617,38 @@ webhooks:
9426
9617
  responses:
9427
9618
  '200':
9428
9619
  description: Webhook received successfully
9620
+ analytics.synced:
9621
+ post:
9622
+ operationId: onAnalyticsSynced
9623
+ summary: Analytics synced event
9624
+ description: |
9625
+ Fired once per connected account each time its analytics sync cycle completes
9626
+ successfully. Poll-driven (roughly hourly per account), not real-time, and never
9627
+ fired for a skipped or failed cycle.
9628
+
9629
+ A trigger, not a transport: the payload carries no metrics and no cursor. On
9630
+ receipt, call `GET /v1/analytics/delta` with your own last `nextCursor` to read
9631
+ every post whose analytics changed, across every account, in one paginated
9632
+ stream instead of polling analytics once per account.
9633
+
9634
+ The feed holds back its most recent few seconds of writes, so a read issued the
9635
+ instant this event lands often returns an empty page for that account. Poll again
9636
+ with the same cursor rather than reading an empty page as "nothing changed".
9637
+
9638
+ High volume (roughly one delivery per connected account per hour). Subscribe to
9639
+ it on a dedicated webhook endpoint: a subscription's consecutive-failure count is
9640
+ shared across all of its events, so an outage while this event is flowing can
9641
+ suppress the low-volume publishing events on the same subscription.
9642
+ tags: [Webhook Events]
9643
+ requestBody:
9644
+ required: true
9645
+ content:
9646
+ application/json:
9647
+ schema:
9648
+ $ref: '#/components/schemas/WebhookPayloadAnalyticsSynced'
9649
+ responses:
9650
+ '200':
9651
+ description: Webhook received successfully
9429
9652
  message.received:
9430
9653
  post:
9431
9654
  operationId: onMessageReceived
@@ -10909,6 +11132,171 @@ paths:
10909
11132
  schema:
10910
11133
  $ref: '#/components/schemas/ErrorResponse'
10911
11134
 
11135
+ /v1/analytics/delta:
11136
+ get:
11137
+ x-resource-group: "analytics"
11138
+ operationId: getAnalyticsDelta
11139
+ tags: [Analytics]
11140
+ summary: Analytics changed since a cursor
11141
+ description: |
11142
+ Cursor feed of the analytics snapshots that CHANGED, across every account you can
11143
+ read, in one paginated stream. Built for integrations that would otherwise call
11144
+ `GET /v1/analytics` once per connected account. Each page carries changes from
11145
+ many accounts at once, so your call count scales with how much actually changed
11146
+ rather than with how many accounts you have. Measured against a fleet of roughly
11147
+ 1,600 connected accounts: about 1,599 per-account analytics calls an hour became
11148
+ about 205 delta calls an hour, a 7.8x reduction.
11149
+
11150
+ **Bootstrap once, then stay in sync.** Load your baseline from
11151
+ `GET /v1/analytics`, which is the historical endpoint. This one is a rolling
11152
+ 7-day change log and cannot replay history. Then call this endpoint with NO
11153
+ `cursor`: it answers with an empty `data` array plus the feed's current position
11154
+ in `nextCursor`. Send that `nextCursor` back on the next call and you receive
11155
+ everything written since. `nextCursor` is present on every response, empty pages
11156
+ included, so you always have something to advance with.
11157
+
11158
+ **Ordering.** Entries come back oldest first, in the order the feed received
11159
+ them. That order is NOT `syncedAt`: `syncedAt` is stamped when an account's sync
11160
+ cycle started, and a slow cycle writes its rows after a faster cycle that started
11161
+ later, so `syncedAt` can go backwards between consecutive entries. Do not sort,
11162
+ filter or resume on it. The cursor is the only stable position, and it is opaque:
11163
+ pass it back verbatim, and do not parse, construct or compare cursors.
11164
+
11165
+ **`hasMore: false` does not mean the feed ended.** This stream has no end and
11166
+ `nextCursor` is never null. `hasMore: true` means more changes are already
11167
+ waiting, so call again straight away. `hasMore: false` means you are caught up:
11168
+ keep the cursor and poll again on your normal interval.
11169
+
11170
+ **The newest changes settle before they are served.** The feed deliberately holds
11171
+ back its last few seconds of writes, so that a row can never become visible
11172
+ behind a cursor you have already advanced past. A read issued the instant an
11173
+ `analytics.synced` webhook lands will therefore often return an empty page for
11174
+ that account. Do not read an empty page as "nothing changed": poll again with the
11175
+ SAME cursor you just used rather than advancing.
11176
+
11177
+ **Repeats inside one instant.** A sync cycle occasionally records the same post
11178
+ twice at the same feed position. When that happens the feed delivers one of those
11179
+ rows, not both. Measured over a day of production traffic, about 1.3% of rows
11180
+ fall in such a group and 99.4% of those groups are identical rows, so this is far
11181
+ more often deduplication than loss. Metrics are absolute values rather than
11182
+ increments, so a later entry for the same post supersedes an earlier one.
11183
+
11184
+ **Retention is 7 days.** Changes older than that leave the feed. A cursor older
11185
+ than 6 days is rejected with a `400` (a day of margin, because expiry is lazy).
11186
+ Recover by re-bootstrapping from `GET /v1/analytics` and taking a fresh cursor
11187
+ from a call to this endpoint with no `cursor`. A consumer that polls at least
11188
+ daily never reaches this.
11189
+
11190
+ Pairs with the `analytics.synced` webhook, so changes can be read on notification
11191
+ instead of on a timer. That event carries no cursor of its own: keep using the
11192
+ `nextCursor` this endpoint gave you.
11193
+
11194
+ Requires the same analytics access as `GET /v1/analytics`, and shares the
11195
+ stricter per-second rate-limit window applied to analytics endpoints.
11196
+ parameters:
11197
+ - name: cursor
11198
+ in: query
11199
+ schema: { type: string }
11200
+ description: |
11201
+ Opaque cursor from a previous response's `nextCursor`. Omit it to start from
11202
+ now: the response is then an empty page carrying the feed's current position.
11203
+ Rejected with a `400` when malformed, or when older than the retention window.
11204
+ example: v1.WyIyMDI2LTA5LTAxIDE3OjEyOjA0IiwiNjVmMWMwYTllMmI1YWYwMDEyYWIzNGNkIl0
11205
+ - name: limit
11206
+ in: query
11207
+ schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
11208
+ description: 'Page size. Out-of-range values are a 400, never a silent clamp.'
11209
+ - name: platform
11210
+ in: query
11211
+ schema: { type: string }
11212
+ description: 'Filter to a single platform (for example "youtube"). Omit for every platform.'
11213
+ - name: profileId
11214
+ in: query
11215
+ schema: { type: string, default: all }
11216
+ description: 'Filter by profile ID (default "all"). Must be a valid profile ID or "all".'
11217
+ responses:
11218
+ '200':
11219
+ description: One page of changed analytics
11220
+ content:
11221
+ application/json:
11222
+ schema:
11223
+ $ref: '#/components/schemas/AnalyticsDeltaResponse'
11224
+ examples:
11225
+ bootstrap:
11226
+ summary: 'First call, no cursor: an empty page carrying the feed position'
11227
+ value:
11228
+ data: []
11229
+ nextCursor: "v1.WyIyMDI2LTA5LTAxIDE3OjA4OjUxIiwiIl0"
11230
+ hasMore: false
11231
+ page:
11232
+ summary: A page of changed snapshots
11233
+ value:
11234
+ data:
11235
+ - postId: "65f1c0a9e2b5af0012ab34cd"
11236
+ accountId: "64e1f0a9e2b5af0012ab34cd"
11237
+ profileId: "64e1f0a9e2b5af0012ab34aa"
11238
+ platform: "youtube"
11239
+ platformPostId: "dQw4w9WgXcQ"
11240
+ publishedAt: "2026-08-28T10:00:05Z"
11241
+ syncedAt: "2026-09-01T17:11:38Z"
11242
+ isDeleted: false
11243
+ metrics:
11244
+ impressions: 15420
11245
+ reach: 12350
11246
+ likes: 342
11247
+ comments: 28
11248
+ shares: 45
11249
+ saves: 12
11250
+ sends: 0
11251
+ clicks: 189
11252
+ views: 9310
11253
+ follows: 4
11254
+ igReelsAvgWatchTime: 0
11255
+ igReelsVideoViewTotalTime: 0
11256
+ reposts: 0
11257
+ reelsSkipRate: 0
11258
+ nextCursor: "v1.WyIyMDI2LTA5LTAxIDE3OjEyOjA0IiwiNjVmMWMwYTllMmI1YWYwMDEyYWIzNGNkIl0"
11259
+ hasMore: true
11260
+ '400': { $ref: '#/components/responses/BadRequest' }
11261
+ '401': { $ref: '#/components/responses/Unauthorized' }
11262
+ '402':
11263
+ description: Analytics access required. Legacy plans need the Analytics add-on; included by default on usage-based plans.
11264
+ content:
11265
+ application/json:
11266
+ schema:
11267
+ type: object
11268
+ properties:
11269
+ error: { type: string, example: Analytics add-on required }
11270
+ code: { type: string, example: analytics_addon_required }
11271
+ '403':
11272
+ description: |
11273
+ The caller cannot read the requested profile, the API key is scoped to other
11274
+ profiles, or the profile sits outside the plan's profile limit (`code`
11275
+ `PROFILE_OVER_LIMIT`).
11276
+ content:
11277
+ application/json:
11278
+ schema:
11279
+ $ref: '#/components/schemas/ErrorResponse'
11280
+ '404': { $ref: '#/components/responses/NotFound' }
11281
+ '500':
11282
+ description: Internal server error
11283
+ content:
11284
+ application/json:
11285
+ schema:
11286
+ $ref: '#/components/schemas/ErrorResponse'
11287
+ '503':
11288
+ description: |
11289
+ The delta feed is temporarily unavailable, for example in the minutes between
11290
+ an application release and the analytics store catching up with it. Nothing is
11291
+ wrong with the request: retry it unchanged, honouring `Retry-After`. The feed
11292
+ answers this rather than an empty page, because an empty page is
11293
+ indistinguishable from "nothing changed" and would advance you past a window
11294
+ you never received.
11295
+ content:
11296
+ application/json:
11297
+ schema:
11298
+ $ref: '#/components/schemas/ErrorResponse'
11299
+
10912
11300
  /v1/analytics/youtube/channel-insights:
10913
11301
  get:
10914
11302
  x-resource-group: "analytics"
@@ -25387,7 +25775,7 @@ paths:
25387
25775
  minItems: 1
25388
25776
  items:
25389
25777
  type: string
25390
- 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.account.name_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, phone_number.stock_available, verification.approved, verification.failed]
25778
+ 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, analytics.synced, 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.account.name_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, phone_number.stock_available, verification.approved, verification.failed]
25391
25779
  description: Events to subscribe to (at least one required)
25392
25780
  isActive:
25393
25781
  type: boolean
@@ -25506,7 +25894,7 @@ paths:
25506
25894
  minItems: 1
25507
25895
  items:
25508
25896
  type: string
25509
- 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.account.name_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, phone_number.stock_available, verification.approved, verification.failed]
25897
+ 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, analytics.synced, 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.account.name_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, phone_number.stock_available, verification.approved, verification.failed]
25510
25898
  description: Events to subscribe to. Must contain at least one event if provided.
25511
25899
  isActive:
25512
25900
  type: boolean
@@ -43965,8 +44353,12 @@ paths:
43965
44353
  items: { type: string }
43966
44354
  regionalRegulatedCategories:
43967
44355
  type: array
43968
- description: 'Meta only. Regional regulation categories required when the ad set targets certain countries (e.g. SINGAPORE_UNIVERSAL, TAIWAN_UNIVERSAL, THAILAND_UNIVERSAL, AUSTRALIA_FINSERV, INDIA_FINSERV). Forwarded to the ad set.'
44356
+ description: 'Meta only. Regional regulation categories required when the ad set targets certain countries (e.g. BRAZIL_REGULATION, SINGAPORE_UNIVERSAL, TAIWAN_UNIVERSAL, THAILAND_UNIVERSAL, AUSTRALIA_FINSERV, INDIA_FINSERV, TAIWAN_FINSERV). Forwarded to the ad set.'
43969
44357
  items: { type: string }
44358
+ regionalRegulationIdentities:
44359
+ type: object
44360
+ additionalProperties: { type: integer }
44361
+ description: 'Meta only. Beneficiary/payer entity IDs for regionalRegulatedCategories. Values are numeric IDs from Meta verification. Keys vary by category (e.g. universal_beneficiary / universal_payer for BRAZIL_REGULATION and THAILAND_UNIVERSAL). If omitted, Meta uses Ads Manager defaults when configured.'
43970
44362
  linkUrl:
43971
44363
  type: string
43972
44364
  format: uri
@@ -44508,10 +44900,18 @@ paths:
44508
44900
  type: array
44509
44901
  description: |
44510
44902
  Meta only. Regional regulation categories required when the ad set targets certain countries.
44511
- Known values: SINGAPORE_UNIVERSAL, TAIWAN_UNIVERSAL, THAILAND_UNIVERSAL, AUSTRALIA_FINSERV,
44512
- INDIA_FINSERV, TAIWAN_FINSERV. Meta rejects the ad set without this when the targeting geo
44513
- includes the corresponding country.
44903
+ Known values: BRAZIL_REGULATION, SINGAPORE_UNIVERSAL, TAIWAN_UNIVERSAL, THAILAND_UNIVERSAL,
44904
+ AUSTRALIA_FINSERV, INDIA_FINSERV, TAIWAN_FINSERV. Meta rejects the ad set without this when
44905
+ the targeting geo includes the corresponding country.
44514
44906
  items: { type: string }
44907
+ regionalRegulationIdentities:
44908
+ type: object
44909
+ additionalProperties: { type: integer }
44910
+ description: |
44911
+ Meta only. Beneficiary/payer entity IDs for regionalRegulatedCategories. Values are
44912
+ numeric IDs from Meta verification. Keys vary by category (e.g. universal_beneficiary /
44913
+ universal_payer for BRAZIL_REGULATION and THAILAND_UNIVERSAL). If omitted, Meta uses
44914
+ Ads Manager defaults when configured.
44515
44915
  endDate: { type: string, format: date-time, description: Required for lifetime budgets }
44516
44916
  startDate:
44517
44917
  type: string
@@ -54,6 +54,21 @@ describe 'AnalyticsApi' do
54
54
  end
55
55
  end
56
56
 
57
+ # unit tests for get_analytics_delta
58
+ # Analytics changed since a cursor
59
+ # Cursor feed of the analytics snapshots that CHANGED, across every account you can read, in one paginated stream. Built for integrations that would otherwise call `GET /v1/analytics` once per connected account. Each page carries changes from many accounts at once, so your call count scales with how much actually changed rather than with how many accounts you have. Measured against a fleet of roughly 1,600 connected accounts: about 1,599 per-account analytics calls an hour became about 205 delta calls an hour, a 7.8x reduction. **Bootstrap once, then stay in sync.** Load your baseline from `GET /v1/analytics`, which is the historical endpoint. This one is a rolling 7-day change log and cannot replay history. Then call this endpoint with NO `cursor`: it answers with an empty `data` array plus the feed's current position in `nextCursor`. Send that `nextCursor` back on the next call and you receive everything written since. `nextCursor` is present on every response, empty pages included, so you always have something to advance with. **Ordering.** Entries come back oldest first, in the order the feed received them. That order is NOT `syncedAt`: `syncedAt` is stamped when an account's sync cycle started, and a slow cycle writes its rows after a faster cycle that started later, so `syncedAt` can go backwards between consecutive entries. Do not sort, filter or resume on it. The cursor is the only stable position, and it is opaque: pass it back verbatim, and do not parse, construct or compare cursors. **`hasMore: false` does not mean the feed ended.** This stream has no end and `nextCursor` is never null. `hasMore: true` means more changes are already waiting, so call again straight away. `hasMore: false` means you are caught up: keep the cursor and poll again on your normal interval. **The newest changes settle before they are served.** The feed deliberately holds back its last few seconds of writes, so that a row can never become visible behind a cursor you have already advanced past. A read issued the instant an `analytics.synced` webhook lands will therefore often return an empty page for that account. Do not read an empty page as \"nothing changed\": poll again with the SAME cursor you just used rather than advancing. **Repeats inside one instant.** A sync cycle occasionally records the same post twice at the same feed position. When that happens the feed delivers one of those rows, not both. Measured over a day of production traffic, about 1.3% of rows fall in such a group and 99.4% of those groups are identical rows, so this is far more often deduplication than loss. Metrics are absolute values rather than increments, so a later entry for the same post supersedes an earlier one. **Retention is 7 days.** Changes older than that leave the feed. A cursor older than 6 days is rejected with a `400` (a day of margin, because expiry is lazy). Recover by re-bootstrapping from `GET /v1/analytics` and taking a fresh cursor from a call to this endpoint with no `cursor`. A consumer that polls at least daily never reaches this. Pairs with the `analytics.synced` webhook, so changes can be read on notification instead of on a timer. That event carries no cursor of its own: keep using the `nextCursor` this endpoint gave you. Requires the same analytics access as `GET /v1/analytics`, and shares the stricter per-second rate-limit window applied to analytics endpoints.
60
+ # @param [Hash] opts the optional parameters
61
+ # @option opts [String] :cursor Opaque cursor from a previous response's `nextCursor`. Omit it to start from now: the response is then an empty page carrying the feed's current position. Rejected with a `400` when malformed, or when older than the retention window.
62
+ # @option opts [Integer] :limit Page size. Out-of-range values are a 400, never a silent clamp.
63
+ # @option opts [String] :platform Filter to a single platform (for example \"youtube\"). Omit for every platform.
64
+ # @option opts [String] :profile_id Filter by profile ID (default \"all\"). Must be a valid profile ID or \"all\".
65
+ # @return [AnalyticsDeltaResponse]
66
+ describe 'get_analytics_delta test' do
67
+ it 'should work' do
68
+ # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
69
+ end
70
+ end
71
+
57
72
  # unit tests for get_best_time_to_post
58
73
  # Get best times to post
59
74
  # Returns the best times to post based on historical engagement data. Groups all published posts by day of week and hour (UTC), calculating average engagement per slot. Use this to auto-schedule posts at optimal times. Requires the Analytics add-on.