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_bonus application is restricted to bonus pushes. Any other Hoory Platform API call with this token returns 401 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 setUser again — 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_url in a new tab, and your tracking_url receives 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), ![img](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_url and cta_url must 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 with content in details. 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_text or tracking_url without cta_url is rejected with cta_url in details, rather than quietly rendering a card with no button.

Markdown, not HTML. description is 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_at a 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; expect validation_failed with content in details.
  • Missing identifier. Omit identifier; expect validation_failed with identifier in details.
  • Button with no destination. Send cta_text without cta_url; expect validation_failed with cta_url in details.
  • Bad URL. Send cta_url: "javascript:alert(1)"; expect validation_failed.
  • Past expiry. Send expires_at in the past; expect validation_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_id if you received one, the identifier, and roughly when you sent it. That is enough to trace the delivery end to end.