API and webhooks
For connecting a booking tool, a CRM or another channel. Every conversation you create here rings the team like a chat, and lands in the same inbox.
Keys
An owner creates a key under Settings → Integrations. A key belongs to one website and sees only its conversations. Send it on every request, from your server only (browsers can't call the API):
Authorization: Bearer key_…
Up to 120 requests a minute per key. Errors come back as { "error": { "code": "…", "message": "…" } } with a 4xx or 5xx status.
Create a conversation
Give the visitor's email or phone, so the team can reply. They get a short "we have your message" email (or text), the team is rung during your hours, and replies go to them by email (or text).
curl -X POST https://helpnera.com/api/public/v1/conversations \
-H "Authorization: Bearer $KEY" -H "content-type: application/json" \
-d '{"message":"Can you quote for a new boiler?","name":"Maria","email":"maria@example.com"}'Fields: message (required, up to 4,000 characters), name, email, phone (E.164, like +12065550100), page_url. Answers 201 with the conversation.
Read conversations
GET https://helpnera.com/api/public/v1/conversations?state=open&limit=50 GET https://helpnera.com/api/public/v1/conversations?state=closed&before=<next_before from the last page> GET https://helpnera.com/api/public/v1/conversations/<id>
Lists are newest first, with next_before for the next page. One conversation comes with its messages (visitor, team and assistant; internal notes stay inside).
Add a visitor message, or close
POST https://helpnera.com/api/public/v1/conversations/<id>/messages {"body":"Here are the photos."}
POST https://helpnera.com/api/public/v1/conversations/<id>/close {"summary":"Quote for a new boiler"}Webhooks
Add an https address under Settings → Integrations and choose the events. We POST JSON to it, and count any 2xx answer as delivered. Anything else is retried after 1, 5, 30, 120, 360, 720 minutes, then given up. An address that keeps failing is switched off until an owner turns it back on.
conversation.created,conversation.answered,conversation.message_taken,conversation.unreached,conversation.closedping, when someone presses Send a test
POST https://example.com/hooks/chat
webhook-id: 6f1c… (the same on every retry: use it to ignore repeats)
webhook-event: conversation.created
webhook-signature: t=1792330000,v1=5d41…
{"id":"6f1c…","event":"conversation.created","created_at":"…","data":{"conversation":{
"id":"…","source":"widget","state":"ringing","visitor":{"name":null,"email":null,"phone":null,"user_id":null},
"first_message":"Do you service boilers?","url":"…/inbox?c=…", …}}}Checking the signature
v1 is the HMAC-SHA256 of <t>.<raw body> with the webhook's signing secret, in hex. Compare in constant time, and refuse a t more than five minutes old.
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(rawBody, header, secret) {
const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const want = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return v1.length === want.length && timingSafeEqual(Buffer.from(v1), Buffer.from(want));
}Signed-in visitors
If your website has its own sign-in, tell the chat who is signed in. Your team then sees their name, email and your user id on every chat, earlier chats count from any device, and the user_id comes back in webhooks. Your server signs a short token, so a visitor can't pretend to be someone else.
- Create the website's identity secret on Settings → Integrations. Keep it on your server, like an API key.
- On each page view for a signed-in user, make a JWT signed with it (HS256).
sub(your user id, up to 200 characters) andexp(at most 30 days away; an hour is plenty) are required;nameandemailare optional. - Give the token to the chat: on the tag as
data-user-token, or from your script withhelpnera('identify', token). On sign-out callhelpnera('reset').
// On your server (Node; any JWT library works the same way)
import jwt from "jsonwebtoken";
const userToken = jwt.sign(
{ sub: user.id, name: user.name, email: user.email },
process.env.HELPNERA_IDENTITY_SECRET,
{ algorithm: "HS256", expiresIn: "1h" },
);
<!-- In the page -->
<script async src="https://helpnera.com/widget/w.js" data-key="wk_…" data-user-token="{{ userToken }}"></script>
// Or from your script, for example after signing in without a page load
window.helpnera = window.helpnera || function () { (window.helpnera.q = window.helpnera.q || []).push(arguments); };
helpnera("identify", userToken);
// Signing out
helpnera("reset");- A chat started while signed in is shown again only to that same user. After a sign-out, or when someone else signs in on the same computer, the old chat stays hidden and the next message starts a new one.
- A chat started before signing in becomes the signed-in user's chat once a token arrives (same browser, same person).
- A token that doesn't check out (wrong secret, expired, no
sub) is treated as signed out; the chat still works. Visitors who aren't signed in are told apart by browser, as before: the inbox shows their earlier chats from the same browser.
Contact forms
A plain HTML form doesn't need a key: point it at the address on Settings → Install. It only works from your own website.