CRM Bonus Push — Partner FAQ
CRM Bonus Push — Partner FAQ
Answers to integration questions raised by the CRM team (2026-09-17), sourced from the current implementation. Companion to crm-bonus-e2e-testing-guide.md.
Reference points in code:
| Concern | Where |
|---|---|
| Endpoint, accepted fields, responses | app/controllers/platform/api/v1/bonuses_controller.rb |
| Contact/session resolution, delivery | app/services/bonuses/push_service.rb |
| Per-session conversation visibility | app/controllers/api/v1/widget/base_controller.rb |
| Card rendering, image box, expiry | app/javascript/shared/components/ChatCard.vue |
| Collapsed-view preview text | app/javascript/widget/helpers/messagePreview.js |
1. Campaign reports “success” but the message never arrived
Case: CJ ID 121852 · Partner ID 1 · Campaign ID 823287 — every campaign status reads success, no Hoory message received.
Our only success response is:
200 {
"status": "created",
"conversation_id": 92, "message_id": 2968,
"deliveries": [{ "conversation_id": 92, "message_id": 2968 }]
}
conversation_id / message_id are the first delivery. deliveries lists every
copy written — see item 3 for when there is more than one.
Everything else is a 422, not a 5xx — so a CRM that only checks “did the
request complete” will record it as success:
| Response | Meaning |
|---|---|
422 contact_not_found |
Identifier unknown to Hoory. Usually the push fired before login registered it. |
422 no_widget_session |
Contact exists but has never opened the web chat. |
422 validation_failed |
Missing/malformed field — details names it. |
422 inbox_disabled |
The chat channel is switched off. |
422 ambiguous_identifier |
Identifier exists in more than one Hoory account; we refuse to guess. |
401 Non permissible endpoint |
Valid token, but not the crm_bonus application token. |
Even a created response is not proof the user saw it
One contact can hold several widget sessions (contact_inboxes), each with its
own active conversations. As of now the bonus is delivered to only one of them —
the most recent session, into its latest conversation. A created response
guarantees the card was written; it does not guarantee the user is looking at the
thread it was written into. See §3.
Why this campaign differs from the CJ
The CJ to the same user works (text renders, firstname variable resolves), so the
identifier and the widget session are fine. That points at the campaign path
itself — a different token, environment, identifier field, or a send that fired
before login.
What we need to trace it
- The exact request body sent
- The exact HTTP status and response body Hoory returned
- The
identifiervalue - The timestamp (and
conversation_id/message_idif one came back)
Please log Hoory’s response body per send. With the status and body recorded, this whole class of issue becomes self-diagnosing on your side.
2. Image size limits
There is no size limit on Hoory’s side — we never receive the file, only
image_url. The end user’s browser loads it, so your CRM’s 5 MB cap is the only
hard limit.
Requirements
- Must be
httporhttps— anything else is rejected with422 validation_failed - Must be publicly reachable — an internal CRM URL renders as a broken image
Render box (ChatCard.vue)
| Property | Value |
|---|---|
| Card width | max 220 px |
| Image | width: 100%, max-height: 150px, object-fit: contain |
Recommended: ~440 × 300 px (2× for retina), roughly 3:2 landscape, under ~500 KB. Portrait and square images are letterboxed inside the 220 × 150 box. Clicking the image opens the full-size original in a new tab.
3. Mobile — message not visible after login
Applies to the web widget in a mobile browser.
Because only the identifier is sent (no identifier_hash), each browser is a
separate unverified session, and a session can see only the conversations created
in it. The contact accumulates multiple sessions, so Hoory has to choose which one
receives the card. The rule, in order:
- Every session whose widget is open right now (socket connected within the last 90s),
up to 3 in parallel. Desktop and phone both open → both get a copy, and
deliverieslists each one. Claim links are identical, so double-claim protection stays on your side. Verified sessions share one view, so they count as one screen and get one copy between them. - Otherwise a verified session (
identifier_hashsent) — it can see every verified session’s threads. - Otherwise the session the customer most recently opened a conversation in.
- Otherwise the newest session.
Consequence:
- A push while the customer is on the site lands on the device they are using
- A push while they are away lands in the session they last used, and shows when they return there
- A copy is never written into a session that is neither open nor the last used one
The only way to share conversations across devices today is to send
identifier_hash (HMAC of the identifier, computed server-side with the inbox key)
alongside setUser. That marks sessions as verified, and verified sessions of the
same contact share one view. Hoory can provide the key and a snippet.
Without it, per-device isolation is expected behavior, not a bug; the rule above only decides where a card goes, it cannot make one session see another’s history.
4. Message starts with “Bonus offer”, which is not in the template
That text was Hoory’s, not yours. It has been removed.
Collapsed views — the unread-message popup and the conversation-list row — have no
plain text to preview for a card message, because the content lives in the card
structure. The widget used to prefix its own Bonus offer label there. It now shows
only your copy:
| Card | Preview shown |
|---|---|
| With title | <your title> |
| No title, with description | <your description> |
| No title, no description, with CTA | <your cta_text> |
| Image only | a generic media marker (📑) |
Inside the opened card only your content is rendered, as before.
5. What expires_at (validity period) actually does
It is not a display duration. The card stays in chat history permanently.
What it does:
- After that moment the CTA button is hidden and the card shows “Expired”
- It is evaluated by the user’s browser clock
- Format: ISO-8601 UTC, e.g.
2026-08-20T12:00:00Z - It must be in the future at push time — a past value is rejected with
422 validation_failed,details: ["expires_at"]
“Message disappears after X” does not exist today; that would be a new feature.
6. Subject filled in the template, absent from the delivered message
Expected — the Hoory bonus API has no subject field. Accepted fields:
| Field | Required | Notes |
|---|---|---|
identifier |
Yes | Your user id, already registered on a widget session |
title |
No† | Card heading, plain text |
description |
No† | Markdown |
image_url |
No† | http/https |
cta_url |
No† | http/https |
cta_text |
No | Button label, defaults to Claim; only with cta_url |
tracking_url |
No | Must accept POST; only with cta_url |
expires_at |
No | ISO-8601 UTC, must be in the future |
† At least one of title, description, image_url, cta_url is required.
Anything else in the request body is dropped silently — which is why there is
neither an error nor any text. Map the template’s subject to title (it renders
as the card heading). This is a CRM-side mapping fix.
Open items for the CRM team
- Campaign 823287: request body, Hoory status + response body, identifier, timestamp
Bonus offerpreview label — removed- Confirm whether
identifier_hashwill be sent, to make bonuses cross-device