zernio-sdk 0.0.821 → 0.0.823

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +7 -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/lib/zernio-sdk/api/analytics_api.rb +77 -0
  17. data/lib/zernio-sdk/api/webhook_events_api.rb +66 -0
  18. data/lib/zernio-sdk/models/analytics_delta_entry.rb +380 -0
  19. data/lib/zernio-sdk/models/analytics_delta_entry_metrics.rb +507 -0
  20. data/lib/zernio-sdk/models/analytics_delta_response.rb +221 -0
  21. data/lib/zernio-sdk/models/boost_post_request.rb +14 -2
  22. data/lib/zernio-sdk/models/create_call_ad_request.rb +14 -2
  23. data/lib/zernio-sdk/models/create_messaging_ad_request.rb +14 -2
  24. data/lib/zernio-sdk/models/create_standalone_ad_request.rb +14 -2
  25. data/lib/zernio-sdk/models/ctwa_ad_request_body.rb +17 -5
  26. data/lib/zernio-sdk/models/webhook_payload_analytics_synced.rb +295 -0
  27. data/lib/zernio-sdk/models/webhook_payload_analytics_synced_account.rb +244 -0
  28. data/lib/zernio-sdk/models/webhook_payload_analytics_synced_sync.rb +193 -0
  29. data/lib/zernio-sdk/version.rb +1 -1
  30. data/lib/zernio-sdk.rb +6 -0
  31. data/openapi.yaml +392 -10
  32. data/spec/api/analytics_api_spec.rb +15 -0
  33. data/spec/models/analytics_delta_entry_metrics_spec.rb +114 -0
  34. data/spec/models/analytics_delta_entry_spec.rb +84 -0
  35. data/spec/models/analytics_delta_response_spec.rb +48 -0
  36. data/spec/models/boost_post_request_spec.rb +6 -0
  37. data/spec/models/create_call_ad_request_spec.rb +6 -0
  38. data/spec/models/create_messaging_ad_request_spec.rb +6 -0
  39. data/spec/models/create_standalone_ad_request_spec.rb +6 -0
  40. data/spec/models/create_webhook_settings_request_spec.rb +1 -1
  41. data/spec/models/ctwa_ad_request_body_spec.rb +6 -0
  42. data/spec/models/update_webhook_settings_request_spec.rb +1 -1
  43. data/spec/models/webhook_payload_analytics_synced_account_spec.rb +54 -0
  44. data/spec/models/webhook_payload_analytics_synced_spec.rb +64 -0
  45. data/spec/models/webhook_payload_analytics_synced_sync_spec.rb +42 -0
  46. data/spec/models/webhook_spec.rb +1 -1
  47. metadata +1773 -1749
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)
@@ -7521,6 +7611,89 @@ components:
7521
7611
  hasAnalyticsAccess:
7522
7612
  type: boolean
7523
7613
  description: Whether user has analytics add-on access
7614
+ AnalyticsDeltaEntry:
7615
+ type: object
7616
+ description: |
7617
+ One changed analytics snapshot. Metrics are the absolute values recorded at
7618
+ `syncedAt`, not the amount they moved by since the previous snapshot, so a later
7619
+ entry for the same `postId` always supersedes an earlier one.
7620
+ required: [postId, accountId, profileId, platform, platformPostId, publishedAt, syncedAt, isDeleted, metrics]
7621
+ properties:
7622
+ postId:
7623
+ type: string
7624
+ description: 'External post ID. The same identifier as `posts[]._id` in GET /v1/analytics.'
7625
+ accountId:
7626
+ type: string
7627
+ description: Social account this post was published through
7628
+ profileId:
7629
+ type: string
7630
+ description: Profile the account belongs to
7631
+ platform:
7632
+ type: string
7633
+ example: youtube
7634
+ platformPostId:
7635
+ type: string
7636
+ description: 'Platform-side post ID (for example the YouTube video ID)'
7637
+ publishedAt:
7638
+ type: string
7639
+ format: date-time
7640
+ description: 'When the post was published, ISO-8601 UTC'
7641
+ syncedAt:
7642
+ type: string
7643
+ format: date-time
7644
+ description: |
7645
+ When the sync cycle that produced this snapshot STARTED, ISO-8601 UTC. This
7646
+ is NOT the order entries arrive in and it is not a resume point: a slow cycle
7647
+ writes its rows after a faster cycle that started later, so `syncedAt` can go
7648
+ backwards between consecutive entries. Use `nextCursor` to resume.
7649
+ isDeleted:
7650
+ type: boolean
7651
+ description: 'True when the post was detected as deleted on the platform at this sync'
7652
+ metrics:
7653
+ type: object
7654
+ description: 'Metrics a platform does not report are 0, not absent.'
7655
+ required: [impressions, reach, likes, comments, shares, saves, sends, clicks, views, follows, igReelsAvgWatchTime, igReelsVideoViewTotalTime, reposts, reelsSkipRate]
7656
+ properties:
7657
+ impressions: { type: integer }
7658
+ reach: { type: integer }
7659
+ likes: { type: integer }
7660
+ comments: { type: integer }
7661
+ shares: { type: integer }
7662
+ saves: { type: integer }
7663
+ sends: { type: integer }
7664
+ clicks: { type: integer }
7665
+ views: { type: integer }
7666
+ follows: { type: integer, description: 'Follows attributed to this post (Instagram)' }
7667
+ igReelsAvgWatchTime: { type: integer, description: 'Instagram Reels average watch time, in milliseconds' }
7668
+ igReelsVideoViewTotalTime: { type: integer, description: 'Instagram Reels total watch time, in milliseconds' }
7669
+ reposts: { type: integer }
7670
+ reelsSkipRate: { type: number, description: 'Instagram Reels skip rate, 0 to 1' }
7671
+ AnalyticsDeltaResponse:
7672
+ type: object
7673
+ required: [data, nextCursor, hasMore]
7674
+ properties:
7675
+ data:
7676
+ type: array
7677
+ description: |
7678
+ Changed snapshots, oldest first, in the order the feed received them. Empty on
7679
+ the bootstrap call (no `cursor` supplied) and whenever nothing has changed
7680
+ since your cursor.
7681
+ items:
7682
+ $ref: '#/components/schemas/AnalyticsDeltaEntry'
7683
+ nextCursor:
7684
+ type: string
7685
+ description: |
7686
+ Cursor to send on the next call. ALWAYS present, including on an empty page,
7687
+ so you always have something to advance with, and it never moves backwards.
7688
+ Opaque: pass it back verbatim, and do not parse, construct or compare cursors.
7689
+ example: v1.WyIyMDI2LTA5LTAxIDE3OjEyOjA0IiwiNjVmMWMwYTllMmI1YWYwMDEyYWIzNGNkIl0
7690
+ hasMore:
7691
+ type: boolean
7692
+ description: |
7693
+ True when more changes are already waiting past `nextCursor`, so call again
7694
+ immediately. False means you are caught up: keep `nextCursor` and poll again
7695
+ later. This feed never ends, so `hasMore: false` does NOT mean `nextCursor`
7696
+ is null.
7524
7697
  # LinkedIn Aggregate Analytics Responses
7525
7698
  LinkedInAggregateAnalyticsTotalResponse:
7526
7699
  type: object
@@ -9426,6 +9599,38 @@ webhooks:
9426
9599
  responses:
9427
9600
  '200':
9428
9601
  description: Webhook received successfully
9602
+ analytics.synced:
9603
+ post:
9604
+ operationId: onAnalyticsSynced
9605
+ summary: Analytics synced event
9606
+ description: |
9607
+ Fired once per connected account each time its analytics sync cycle completes
9608
+ successfully. Poll-driven (roughly hourly per account), not real-time, and never
9609
+ fired for a skipped or failed cycle.
9610
+
9611
+ A trigger, not a transport: the payload carries no metrics and no cursor. On
9612
+ receipt, call `GET /v1/analytics/delta` with your own last `nextCursor` to read
9613
+ every post whose analytics changed, across every account, in one paginated
9614
+ stream instead of polling analytics once per account.
9615
+
9616
+ The feed holds back its most recent few seconds of writes, so a read issued the
9617
+ instant this event lands often returns an empty page for that account. Poll again
9618
+ with the same cursor rather than reading an empty page as "nothing changed".
9619
+
9620
+ High volume (roughly one delivery per connected account per hour). Subscribe to
9621
+ it on a dedicated webhook endpoint: a subscription's consecutive-failure count is
9622
+ shared across all of its events, so an outage while this event is flowing can
9623
+ suppress the low-volume publishing events on the same subscription.
9624
+ tags: [Webhook Events]
9625
+ requestBody:
9626
+ required: true
9627
+ content:
9628
+ application/json:
9629
+ schema:
9630
+ $ref: '#/components/schemas/WebhookPayloadAnalyticsSynced'
9631
+ responses:
9632
+ '200':
9633
+ description: Webhook received successfully
9429
9634
  message.received:
9430
9635
  post:
9431
9636
  operationId: onMessageReceived
@@ -10909,6 +11114,171 @@ paths:
10909
11114
  schema:
10910
11115
  $ref: '#/components/schemas/ErrorResponse'
10911
11116
 
11117
+ /v1/analytics/delta:
11118
+ get:
11119
+ x-resource-group: "analytics"
11120
+ operationId: getAnalyticsDelta
11121
+ tags: [Analytics]
11122
+ summary: Analytics changed since a cursor
11123
+ description: |
11124
+ Cursor feed of the analytics snapshots that CHANGED, across every account you can
11125
+ read, in one paginated stream. Built for integrations that would otherwise call
11126
+ `GET /v1/analytics` once per connected account. Each page carries changes from
11127
+ many accounts at once, so your call count scales with how much actually changed
11128
+ rather than with how many accounts you have. Measured against a fleet of roughly
11129
+ 1,600 connected accounts: about 1,599 per-account analytics calls an hour became
11130
+ about 205 delta calls an hour, a 7.8x reduction.
11131
+
11132
+ **Bootstrap once, then stay in sync.** Load your baseline from
11133
+ `GET /v1/analytics`, which is the historical endpoint. This one is a rolling
11134
+ 7-day change log and cannot replay history. Then call this endpoint with NO
11135
+ `cursor`: it answers with an empty `data` array plus the feed's current position
11136
+ in `nextCursor`. Send that `nextCursor` back on the next call and you receive
11137
+ everything written since. `nextCursor` is present on every response, empty pages
11138
+ included, so you always have something to advance with.
11139
+
11140
+ **Ordering.** Entries come back oldest first, in the order the feed received
11141
+ them. That order is NOT `syncedAt`: `syncedAt` is stamped when an account's sync
11142
+ cycle started, and a slow cycle writes its rows after a faster cycle that started
11143
+ later, so `syncedAt` can go backwards between consecutive entries. Do not sort,
11144
+ filter or resume on it. The cursor is the only stable position, and it is opaque:
11145
+ pass it back verbatim, and do not parse, construct or compare cursors.
11146
+
11147
+ **`hasMore: false` does not mean the feed ended.** This stream has no end and
11148
+ `nextCursor` is never null. `hasMore: true` means more changes are already
11149
+ waiting, so call again straight away. `hasMore: false` means you are caught up:
11150
+ keep the cursor and poll again on your normal interval.
11151
+
11152
+ **The newest changes settle before they are served.** The feed deliberately holds
11153
+ back its last few seconds of writes, so that a row can never become visible
11154
+ behind a cursor you have already advanced past. A read issued the instant an
11155
+ `analytics.synced` webhook lands will therefore often return an empty page for
11156
+ that account. Do not read an empty page as "nothing changed": poll again with the
11157
+ SAME cursor you just used rather than advancing.
11158
+
11159
+ **Repeats inside one instant.** A sync cycle occasionally records the same post
11160
+ twice at the same feed position. When that happens the feed delivers one of those
11161
+ rows, not both. Measured over a day of production traffic, about 1.3% of rows
11162
+ fall in such a group and 99.4% of those groups are identical rows, so this is far
11163
+ more often deduplication than loss. Metrics are absolute values rather than
11164
+ increments, so a later entry for the same post supersedes an earlier one.
11165
+
11166
+ **Retention is 7 days.** Changes older than that leave the feed. A cursor older
11167
+ than 6 days is rejected with a `400` (a day of margin, because expiry is lazy).
11168
+ Recover by re-bootstrapping from `GET /v1/analytics` and taking a fresh cursor
11169
+ from a call to this endpoint with no `cursor`. A consumer that polls at least
11170
+ daily never reaches this.
11171
+
11172
+ Pairs with the `analytics.synced` webhook, so changes can be read on notification
11173
+ instead of on a timer. That event carries no cursor of its own: keep using the
11174
+ `nextCursor` this endpoint gave you.
11175
+
11176
+ Requires the same analytics access as `GET /v1/analytics`, and shares the
11177
+ stricter per-second rate-limit window applied to analytics endpoints.
11178
+ parameters:
11179
+ - name: cursor
11180
+ in: query
11181
+ schema: { type: string }
11182
+ description: |
11183
+ Opaque cursor from a previous response's `nextCursor`. Omit it to start from
11184
+ now: the response is then an empty page carrying the feed's current position.
11185
+ Rejected with a `400` when malformed, or when older than the retention window.
11186
+ example: v1.WyIyMDI2LTA5LTAxIDE3OjEyOjA0IiwiNjVmMWMwYTllMmI1YWYwMDEyYWIzNGNkIl0
11187
+ - name: limit
11188
+ in: query
11189
+ schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
11190
+ description: 'Page size. Out-of-range values are a 400, never a silent clamp.'
11191
+ - name: platform
11192
+ in: query
11193
+ schema: { type: string }
11194
+ description: 'Filter to a single platform (for example "youtube"). Omit for every platform.'
11195
+ - name: profileId
11196
+ in: query
11197
+ schema: { type: string, default: all }
11198
+ description: 'Filter by profile ID (default "all"). Must be a valid profile ID or "all".'
11199
+ responses:
11200
+ '200':
11201
+ description: One page of changed analytics
11202
+ content:
11203
+ application/json:
11204
+ schema:
11205
+ $ref: '#/components/schemas/AnalyticsDeltaResponse'
11206
+ examples:
11207
+ bootstrap:
11208
+ summary: 'First call, no cursor: an empty page carrying the feed position'
11209
+ value:
11210
+ data: []
11211
+ nextCursor: "v1.WyIyMDI2LTA5LTAxIDE3OjA4OjUxIiwiIl0"
11212
+ hasMore: false
11213
+ page:
11214
+ summary: A page of changed snapshots
11215
+ value:
11216
+ data:
11217
+ - postId: "65f1c0a9e2b5af0012ab34cd"
11218
+ accountId: "64e1f0a9e2b5af0012ab34cd"
11219
+ profileId: "64e1f0a9e2b5af0012ab34aa"
11220
+ platform: "youtube"
11221
+ platformPostId: "dQw4w9WgXcQ"
11222
+ publishedAt: "2026-08-28T10:00:05Z"
11223
+ syncedAt: "2026-09-01T17:11:38Z"
11224
+ isDeleted: false
11225
+ metrics:
11226
+ impressions: 15420
11227
+ reach: 12350
11228
+ likes: 342
11229
+ comments: 28
11230
+ shares: 45
11231
+ saves: 12
11232
+ sends: 0
11233
+ clicks: 189
11234
+ views: 9310
11235
+ follows: 4
11236
+ igReelsAvgWatchTime: 0
11237
+ igReelsVideoViewTotalTime: 0
11238
+ reposts: 0
11239
+ reelsSkipRate: 0
11240
+ nextCursor: "v1.WyIyMDI2LTA5LTAxIDE3OjEyOjA0IiwiNjVmMWMwYTllMmI1YWYwMDEyYWIzNGNkIl0"
11241
+ hasMore: true
11242
+ '400': { $ref: '#/components/responses/BadRequest' }
11243
+ '401': { $ref: '#/components/responses/Unauthorized' }
11244
+ '402':
11245
+ description: Analytics access required. Legacy plans need the Analytics add-on; included by default on usage-based plans.
11246
+ content:
11247
+ application/json:
11248
+ schema:
11249
+ type: object
11250
+ properties:
11251
+ error: { type: string, example: Analytics add-on required }
11252
+ code: { type: string, example: analytics_addon_required }
11253
+ '403':
11254
+ description: |
11255
+ The caller cannot read the requested profile, the API key is scoped to other
11256
+ profiles, or the profile sits outside the plan's profile limit (`code`
11257
+ `PROFILE_OVER_LIMIT`).
11258
+ content:
11259
+ application/json:
11260
+ schema:
11261
+ $ref: '#/components/schemas/ErrorResponse'
11262
+ '404': { $ref: '#/components/responses/NotFound' }
11263
+ '500':
11264
+ description: Internal server error
11265
+ content:
11266
+ application/json:
11267
+ schema:
11268
+ $ref: '#/components/schemas/ErrorResponse'
11269
+ '503':
11270
+ description: |
11271
+ The delta feed is temporarily unavailable, for example in the minutes between
11272
+ an application release and the analytics store catching up with it. Nothing is
11273
+ wrong with the request: retry it unchanged, honouring `Retry-After`. The feed
11274
+ answers this rather than an empty page, because an empty page is
11275
+ indistinguishable from "nothing changed" and would advance you past a window
11276
+ you never received.
11277
+ content:
11278
+ application/json:
11279
+ schema:
11280
+ $ref: '#/components/schemas/ErrorResponse'
11281
+
10912
11282
  /v1/analytics/youtube/channel-insights:
10913
11283
  get:
10914
11284
  x-resource-group: "analytics"
@@ -25387,7 +25757,7 @@ paths:
25387
25757
  minItems: 1
25388
25758
  items:
25389
25759
  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]
25760
+ 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
25761
  description: Events to subscribe to (at least one required)
25392
25762
  isActive:
25393
25763
  type: boolean
@@ -25506,7 +25876,7 @@ paths:
25506
25876
  minItems: 1
25507
25877
  items:
25508
25878
  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]
25879
+ 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
25880
  description: Events to subscribe to. Must contain at least one event if provided.
25511
25881
  isActive:
25512
25882
  type: boolean
@@ -43965,8 +44335,12 @@ paths:
43965
44335
  items: { type: string }
43966
44336
  regionalRegulatedCategories:
43967
44337
  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.'
44338
+ 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
44339
  items: { type: string }
44340
+ regionalRegulationIdentities:
44341
+ type: object
44342
+ additionalProperties: { type: integer }
44343
+ 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
44344
  linkUrl:
43971
44345
  type: string
43972
44346
  format: uri
@@ -44508,10 +44882,18 @@ paths:
44508
44882
  type: array
44509
44883
  description: |
44510
44884
  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.
44885
+ Known values: BRAZIL_REGULATION, SINGAPORE_UNIVERSAL, TAIWAN_UNIVERSAL, THAILAND_UNIVERSAL,
44886
+ AUSTRALIA_FINSERV, INDIA_FINSERV, TAIWAN_FINSERV. Meta rejects the ad set without this when
44887
+ the targeting geo includes the corresponding country.
44514
44888
  items: { type: string }
44889
+ regionalRegulationIdentities:
44890
+ type: object
44891
+ additionalProperties: { type: integer }
44892
+ description: |
44893
+ Meta only. Beneficiary/payer entity IDs for regionalRegulatedCategories. Values are
44894
+ numeric IDs from Meta verification. Keys vary by category (e.g. universal_beneficiary /
44895
+ universal_payer for BRAZIL_REGULATION and THAILAND_UNIVERSAL). If omitted, Meta uses
44896
+ Ads Manager defaults when configured.
44515
44897
  endDate: { type: string, format: date-time, description: Required for lifetime budgets }
44516
44898
  startDate:
44517
44899
  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.
@@ -0,0 +1,114 @@
1
+ =begin
2
+ #Zernio API
3
+
4
+ #API reference for Zernio. Authenticate with a Bearer API key. Base URL: https://zernio.com/api Versioning and deprecation: all endpoints are versioned in the URL path (current version: /v1). Breaking changes only ship in a new path version; existing versions keep working. Deprecated operations are marked 'deprecated: true' in this spec and announced in the changelog (https://zernio.com/changelog) before removal. Errors: every 4xx/5xx response is application/json with a machine-readable 'code' and a human-readable 'error' message (see the ErrorResponse schema).
5
+
6
+ The version of the OpenAPI document: 1.0.4
7
+ Contact: support@zernio.com
8
+ Generated by: https://openapi-generator.tech
9
+ Generator version: 7.19.0
10
+
11
+ =end
12
+
13
+ require 'spec_helper'
14
+ require 'json'
15
+ require 'date'
16
+
17
+ # Unit tests for Zernio::AnalyticsDeltaEntryMetrics
18
+ # Automatically generated by openapi-generator (https://openapi-generator.tech)
19
+ # Please update as you see appropriate
20
+ describe Zernio::AnalyticsDeltaEntryMetrics do
21
+ #let(:instance) { Zernio::AnalyticsDeltaEntryMetrics.new }
22
+
23
+ describe 'test an instance of AnalyticsDeltaEntryMetrics' do
24
+ it 'should create an instance of AnalyticsDeltaEntryMetrics' do
25
+ # uncomment below to test the instance creation
26
+ #expect(instance).to be_instance_of(Zernio::AnalyticsDeltaEntryMetrics)
27
+ end
28
+ end
29
+
30
+ describe 'test attribute "impressions"' do
31
+ it 'should work' do
32
+ # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
33
+ end
34
+ end
35
+
36
+ describe 'test attribute "reach"' do
37
+ it 'should work' do
38
+ # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
39
+ end
40
+ end
41
+
42
+ describe 'test attribute "likes"' do
43
+ it 'should work' do
44
+ # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
45
+ end
46
+ end
47
+
48
+ describe 'test attribute "comments"' do
49
+ it 'should work' do
50
+ # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
51
+ end
52
+ end
53
+
54
+ describe 'test attribute "shares"' do
55
+ it 'should work' do
56
+ # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
57
+ end
58
+ end
59
+
60
+ describe 'test attribute "saves"' do
61
+ it 'should work' do
62
+ # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
63
+ end
64
+ end
65
+
66
+ describe 'test attribute "sends"' 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
+
72
+ describe 'test attribute "clicks"' do
73
+ it 'should work' do
74
+ # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
75
+ end
76
+ end
77
+
78
+ describe 'test attribute "views"' do
79
+ it 'should work' do
80
+ # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
81
+ end
82
+ end
83
+
84
+ describe 'test attribute "follows"' do
85
+ it 'should work' do
86
+ # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
87
+ end
88
+ end
89
+
90
+ describe 'test attribute "ig_reels_avg_watch_time"' do
91
+ it 'should work' do
92
+ # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
93
+ end
94
+ end
95
+
96
+ describe 'test attribute "ig_reels_video_view_total_time"' do
97
+ it 'should work' do
98
+ # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
99
+ end
100
+ end
101
+
102
+ describe 'test attribute "reposts"' do
103
+ it 'should work' do
104
+ # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
105
+ end
106
+ end
107
+
108
+ describe 'test attribute "reels_skip_rate"' do
109
+ it 'should work' do
110
+ # assertion here. ref: https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/
111
+ end
112
+ end
113
+
114
+ end