CRM Bonus Push — End-to-End Testing Guide
CRM Bonus Push — End-to-End Testing Guide
Hoory widget integration · QA environment · https://qa2.hoory.ai
What this integration does. Your CRM sends a bonus offer for one identified end user. Hoory delivers it into that user’s live chat widget as a card with a call-to-action button. If the user has the page open, the card appears immediately and the widget opens by itself. When they click the button they go to your page, and Hoory fires your tracking URL.
1. What you need before you start
| Item | Detail |
|---|---|
| API token | A Platform API token for the crm_bonus application. Request it from the Hoory team — it is created in Hoory SuperAdmin and cannot be self-served. |
| Endpoint | POST https://qa2.hoory.ai/platform/api/v1/bonuses |
| Test page | https://qa2.hoory.ai/test-channel — a page with the Hoory widget embedded. |
| A user identifier | Your own stable user id, e.g. bc_user_id. Same value on both sides. |
Your token only works on this one endpoint. The
crm_bonusapplication is restricted to bonus pushes. Any other Hoory Platform API call with this token returns401 Non permissible endpoint. That is intended, not a misconfiguration.
2. How a bonus reaches the user
your CRM ──POST /platform/api/v1/bonuses──▶ Hoory
│ find the contact by "identifier"
│ find their web-chat session
│ write a card into their conversation
▼
user's widget
(live, or on next visit)
There is no account id in the request. Hoory finds the account from the
identifier you send, so that identifier must have been registered against a
widget session first — that is step 1 below.
3. Step-by-step test run
1 · Register the identifier on a widget session
Open https://qa2.hoory.ai/test-channel in your browser. Open the chat widget once so the session is created, then open the browser console (F12) and run:
window.$hoory.setUser("bc_user_id", {
name: "QA Test User",
email: "qa.test@example.com"
});
This links the widget session in that browser to the identifier your CRM will send. Do it once per browser or after clearing site data.
Keep this browser tab. An unidentified widget session can only see its own conversations. If you clear cookies, use a different browser, or open a private window, you get a new session and must run
setUseragain — otherwise the bonus is delivered to the old session and you will not see it.
2 · Send the bonus from your CRM
curl -X POST https://qa2.hoory.ai/platform/api/v1/bonuses \
-H "api_access_token: <YOUR_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"identifier": "bc_user_id",
"title": "100 free spins",
"description": "Claim before Friday and get **100 free spins** on Book of Ra.",
"image_url": "https://cdn.example.com/bonus.png",
"cta_text": "Claim now",
"cta_url": "https://operator.example.com/promotions",
"tracking_url": "https://crm.example.com/track/click/abc-123",
"expires_at": "2026-08-20T12:00:00Z"
}'
A success response looks like this. Keep conversation_id — the Hoory team can
use it to find the thread if something looks wrong.
{ "status": "created", "conversation_id": 92, "message_id": 2968 }
3 · Check the widget
Go back to the browser tab from step 1. You should see:
- The card appears immediately, with no page reload.
- If the widget was closed, it opens by itself.
- Whichever of title, description, image and CTA button you sent are rendered — and only those.
- Clicking the button opens
cta_urlin a new tab, and yourtracking_urlreceives a request.
If the browser tab is not open when you send the bonus, the card is still saved and the user will see it the next time they open the chat — but no unread badge appears on the closed bubble. In this version, bonuses are designed to be seen live.
4. Field reference
| Field | Required | Notes |
|---|---|---|
identifier |
Yes | Your user id. Must already be registered on a widget session (step 1). |
title |
No† | Card heading. Plain text. |
description |
No† | Markdown. **bold**, _italic_, [link](url),  all work. See the note below. |
image_url |
No† | Shown above the text. Must be http or https. Omit it if you prefer to place images inside the markdown description. |
cta_url |
No† | Where the button sends the user. Must be http or https. Omit it for a card with no button. |
cta_text |
No | Button label. Defaults to Claim. Only valid together with cta_url. |
tracking_url |
No | Called when the user clicks the button. Must accept POST. See section 6. Only valid together with cta_url. |
expires_at |
No | ISO-8601 UTC. After this time the button is hidden and the card shows “Expired”. Must be in the future. |
† At least one of
title,description,image_urlandcta_urlmust be sent. Each is optional on its own — a card can be text-only, image-only, or just a button — but a payload carrying none of them would be an empty bubble and is rejected withcontentindetails. Only the parts you send are rendered: no empty heading, no blank space where an image would have been.
A button needs a destination.
cta_textortracking_urlwithoutcta_urlis rejected withcta_urlindetails, rather than quietly rendering a card with no button.
Markdown, not HTML.
descriptionis rendered as markdown. If you send HTML such as<b>text</b>, the user sees the tags as literal characters rather than formatting. This is deliberate — it keeps third-party content from injecting scripts into the widget.
5. Response reference
| Code | Body | What it means / what to do |
|---|---|---|
| 200 | status: "created" |
Delivered. |
| 401 | Invalid access_token |
Token missing, wrong, or not a Platform API token. |
| 401 | Non permissible endpoint |
Valid token, but not the crm_bonus application. Ask Hoory for the right token. |
| 422 | validation_failed |
A field is missing or malformed. The details array names the offending fields. |
| 422 | contact_not_found |
No user with that identifier. Most often step 1 was skipped, or the identifier does not match exactly. |
| 422 | no_widget_session |
The user exists but has never opened the web chat. There is nowhere to show a card. |
| 422 | ambiguous_identifier |
That identifier exists in more than one Hoory account. Hoory refuses to guess. Contact the Hoory team. |
| 422 | inbox_disabled |
The chat channel is switched off. |
6. Click tracking
When the user clicks the CTA, the browser sends a request to your tracking_url
and then navigates to cta_url. Two requirements:
- It must accept POST. The browser uses
navigator.sendBeacon, which can only issue POST. A GET-only tracking pixel will never be called, and nothing will report an error. - It must contain no secrets. The URL is visible in the page source, so treat it as public and safe to replay.
Expect under-counting. The request is sent from the user’s browser, so ad-blockers and strict privacy browsers silently drop some of them. Treat your click counts as a lower bound. Repeat clicks each send a request — de-duplicate on your side if you need unique clicks.
7. Test scenarios to run
- Happy path, widget open. Card appears live, without reload.
- Widget closed, page open. Widget opens by itself and shows the card.
- Markdown. Send
**bold**,_italic_and a[link](https://example.com); confirm all three render as formatting. - CTA. Click the button — new tab opens at
cta_url, and your tracking endpoint records a POST. - Two bonuses. Send two in a row; both cards appear in the same conversation.
- Expiry. Send with
expires_ata few minutes ahead, wait, reload — the button is gone and the card reads “Expired”. - Unknown user. Send a made-up identifier; expect
contact_not_found. - Partial cards. Send each of title-only, description-only, image-only and
cta_url-only; each renders with just that part and no empty gaps. - Empty card. Send only
identifier; expectvalidation_failedwithcontentindetails. - Missing identifier. Omit
identifier; expectvalidation_failedwithidentifierindetails. - Button with no destination. Send
cta_textwithoutcta_url; expectvalidation_failedwithcta_urlindetails. - Bad URL. Send
cta_url: "javascript:alert(1)"; expectvalidation_failed. - Past expiry. Send
expires_atin the past; expectvalidation_failed. - Wrong token. Use any other token; expect a 401.
8. Troubleshooting
| Symptom | Likely cause |
|---|---|
200 created, but nothing in the widget |
You are looking at a different browser session than the one you ran setUser in. Re-run step 1 in the tab you are watching. |
contact_not_found |
Step 1 was skipped, or the identifier differs (whitespace, case, prefix). |
no_widget_session |
The identifier is attached to a user who has never opened the web chat. |
Card shows <b> and other tags as text |
You sent HTML. Use markdown instead. |
| Tracking endpoint never called | It only accepts GET. Change it to accept POST. |
| Two identical cards | Your CRM sent the request twice. See the note below. |
| Widget does not open by itself | The page was not open at the moment you sent the bonus. |
There is no duplicate protection. Every accepted request creates a card. If your CRM retries on timeout, the user sees the bonus twice. Retry only when you are sure the first request did not reach Hoory.
Reporting a problem to Hoory. Include: the exact request body you sent, the full response, the
conversation_idif you received one, theidentifier, and roughly when you sent it. That is enough to trace the delivery end to end.