Skip to content

Add Banners API (list + dismiss) to Preview#539

Open
mapierce wants to merge 2 commits into
mainfrom
mapierce/banners-api-preview
Open

Add Banners API (list + dismiss) to Preview#539
mapierce wants to merge 2 commits into
mainfrom
mapierce/banners-api-preview

Conversation

@mapierce
Copy link
Copy Markdown

@mapierce mapierce commented Jun 5, 2026

Why?

Documents the new Preview Banners API so developers can retrieve the banners a contact matches and record dismissals on surfaces outside the Messenger (native mobile apps, kiosks, embedded tools).

How?

Adds the two banner endpoints, their schemas, and a Banners tag to the Preview spec.

Generated with Claude Code

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mapierce mapierce self-assigned this Jun 5, 2026
@zilleeizad-inter
Copy link
Copy Markdown

Reviewed as part of the Banners API GA trio (docs intercom/developer-docs#959, this spec, version flip intercom/intercom#519329). I cross-checked the spec against the already-merged serializer (Api::V3::Contacts::Presenters::Banner) on the version-flip branch. The paths, schemas, and Banners tag are clean and byte-identical to the docs PR. Two things worth resolving before GA, both at the seam between this contract and the live serializer:

🔴 client_targeting is documented as "always null", but the serializer populates it

The schema says:

client_targetingReserved for future use. Always null in the current version — banners that depend on client-side targeting rules … are not returned by this endpoint.

But Api::V3::Contacts::Presenters::Banner#serialize_client_targeting returns a populated, flat array of URL / time-on-page predicates whenever the matched banner's ruleset carries client predicates, and banner_spec.rb asserts exactly that — including a mixed server + client predicate case.

For "always null" to hold, a banner with client predicates must never be returned by index. I couldn't find a guarantee of that:

  • Banners::Commands::FilterMatches only rejects dismissed banners and de-dupes — it keeps client_matches, it doesn't strip banners carrying client predicates.
  • The matching engine naturally excludes purely client-targeted banners, but a mixed ruleset (e.g. country = US OR url contains /pricing) matches on its server-side branch and is returned — with client_targeting populated. That's the scenario the presenter's "mixed predicates" spec exercises.

Question: can a mixed server+client targeted banner be returned here? If yes, the contract is wrong — we should document the real predicate shape (attribute / type / comparison / value) and drop "Reserved for future use / always null". If no, the presenter's serialize_client_targeting + specs are dead code and should be simplified (and the docs become honest by construction). Note there's currently no test asserting client_targeting at the API boundary, so the claim is unprotected either way.

🟡 action schema is under-specified for SDK generation

The serializer emits a polymorphic action: url{type, label, target}, reaction{type, reaction_set}, email_collector{type}, product_tour{type, tour_id, tour_url}. The schema documents only action.type as a structured property and describes the rest in prose. A generated client will keep only type and silently drop the other fields. Suggest modeling the variants (oneOf keyed on type, or at least listing all properties as optional).

✅ Verified correct

  • Path shapes match the real routes (/contacts/{id}/banners, /contacts/{id}/banners/{view_id}/dismiss).
  • Version mapping is sound — UNSTABLE_VERSION_ID = "Preview", so the version flip surfaces these under the Intercom-Version: Preview documented here.
  • dismiss idempotency matches the implementation (unless banner_view.dismissed?).

~ Automated via Claude

The action object only exposed `type`, so generated SDKs dropped the
variant-specific fields. Add label/target (url), reaction_set (reaction),
and tour_id/tour_url (product_tour) as documented optional properties
matching the API response.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mapierce
Copy link
Copy Markdown
Author

mapierce commented Jun 5, 2026

Thanks @zilleeizad-inter — both points addressed.

action schema: Expanded the action object to document all variant fields — label/target (url), reaction_set with {index, unicode_emoticon} items (reaction), tour_id/tour_url (product_tour) — so generated SDKs no longer drop them. (c7b85ed)

client_targeting: Verified that this endpoint only returns banners that don't depend on client-side targeting, so the documented "always null" is correct. The implementation has been simplified accordingly and the contract is now covered by a test.

Ready for re-review.

Copy link
Copy Markdown

@zilleeizad-inter zilleeizad-inter left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed after the updates — both findings verify out against the merged serializer:

  • action now documents every variant field (label/target for url, reaction_set with {index, unicode_emoticon} items, tour_id/tour_url for product_tour), matching Banner#serialize_action exactly. SDK generators will surface all fields now.
  • client_targeting is now genuinely always-null — the presenter hardcodes client_targeting: nil (in intercom#519329), so the "always null" contract is literally accurate.

Approving. 🚀

~ Automated via Claude

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants