Janore
Documentation
Webhooks

Webhooks — both directions.

Plug Janore into Zapier, Make, or any tool that can POST JSON. Or have Janore POST conversation events to your own server.

9 min readUpdated 2026-05-03v0.6

Overview

Webhooks are the most flexible integration. They cover use cases the SDK and channel integrations don't: bridging into a custom workflow, syncing into a CRM, triggering an email, exporting to a data warehouse, or just powering an internal Slack notification.

Two flavours, both covered on this page:

  • Inbound (you → Janore). POST a JSON payload to Janore from any HTTP-capable tool (Zapier, Make, a Bash script). Janore replies with a translation/response, the same way the chat widget does.
  • Outbound (Janore → you). Janore POSTs every conversation event to your server in real time. Useful for syncing into your CRM, triggering downstream workflows, or auditing every message your assistant sends.

Inbound — call Janore from anything

When you don't want to write SDK code or store an API key in your tool, the inbound webhook is the simplest path. Optional shared-secret Bearer auth.

Enable the channel

Open the dashboard and connect the channel. Optionally set a verify token — a shared secret you'll send as a Bearer header. Skip it for fully open access. Dashboard → Channels → Generic Webhook → Connect.

POST a message

post.httphttp
POST https://janore.com/api/v1/channels/webhook/{ASSISTANT_ID}
Content-Type: application/json
Authorization: Bearer {VERIFY_TOKEN}        (optional)
 
{
  "session_id": "any-string",
  "message": "Hello",
  "language": "en"
}

session_id is any stable string per user. language is optional — Janore auto-detects if omitted. The Authorization header is required only if you set a verify token.

Response shape

response.jsonjson
{
  "reply": "Hello! …",
  "suggestedActions": [
    { "label": "View pricing", "type": "link", "payload": "/pricing" }
  ],
  "conversationId": "uuid",
  "messageId": "uuid"
}

Outbound — receive Janore events

Add a webhook URL in Settings → Outbound webhooks. Janore POSTs every event in real time, signed with HMAC-SHA256 so you can verify the request is genuine.

Event types

  • conversation.created — a new visitor session started.
  • message.received — a visitor message hit the pipeline (content truncated to 500 chars).
  • conversation.escalated — the assistant handed off to a human.
  • conversation.resolved — the operator marked the conversation done (data-only for now).
  • tag.added — an operator tagged the conversation.
  • webhook.test — fired manually from the dashboard Test button.

Delivery contract

  • HTTP POST, Content-Type: application/json.
  • 5-second timeout, fire-and-forget.
  • No automatic retries (v0). Make your endpoint idempotent — every event carries a unique event_id UUID.
  • Headers: X-Factory-Signature (Stripe-style), X-Factory-Event, X-Factory-Event-Id.

Body schema

event.jsonjson
{
  "event": "conversation.created",
  "event_id": "9b1d73e8-...-a4c1",
  "timestamp": 1714502400,
  "payload": { /* event-specific, see below */ }
}
payloads.jsonjson
// conversation.created
{ "conversation_id": "...", "session_id": "...", "channel": "web", "language": "fr" }
 
// message.received
{ "conversation_id": "...", "role": "user", "content": "Hello…", "channel": "web", "language": "en" }
 
// conversation.escalated
{ "conversation_id": "...", "reason": "user asked for human", "lead_score": 0.8, "language": "fr" }
 
// conversation.resolved
{ "conversation_id": "...", "resolved_by": "operator-uuid" }
 
// tag.added
{ "conversation_id": "...", "tag": "lead-hot", "color": null }
 
// webhook.test
{ "ping": "hello" }

Verifying the signature (Node)

verify.tstypescript
import { createHmac, timingSafeEqual } from 'node:crypto';
 
export function verifyJanoreSignature(rawBody: string, header: string, secret: string): boolean {
  // header: "t=<unix>,v1=<hex>"
  const parts = Object.fromEntries(header.split(',').map(p => p.split('=')));
  if (!parts.t || !parts.v1) return false;
  const expected = createHmac('sha256', secret)
    .update(`${parts.t}.${rawBody}`)
    .digest('hex');
  const a = Buffer.from(expected, 'hex');
  const b = Buffer.from(parts.v1, 'hex');
  if (a.length !== b.length) return false;
  return timingSafeEqual(a, b);
}

Verifying the signature (Python)

verify.pypython
import hmac, hashlib
 
def verify_janore_signature(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split('=', 1) for p in header.split(','))
    t, v1 = parts.get('t'), parts.get('v1')
    if not t or not v1:
        return False
    mac = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(mac, v1)

Example consumer (Express)

server.tstypescript
import express from 'express';
import { verifyJanoreSignature } from './verify';
 
const app = express();
app.post('/janore-events',
  express.text({ type: 'application/json' }), // capture the raw body
  (req, res) => {
    const sig = req.header('X-Factory-Signature') ?? '';
    if (!verifyJanoreSignature(req.body, sig, process.env.JANORE_WEBHOOK_SECRET!)) {
      return res.status(401).send('bad signature');
    }
    const evt = JSON.parse(req.body);
    // De-duplicate on evt.event_id — we don't retry but upstream tools sometimes do.
    console.log('janore event', evt.event, evt.event_id);
    res.status(200).send('ok');
  });

Retry policy

v0 sends each event once with a 5-second timeout. There are no automatic retries — if your endpoint is down, that event is gone. Treat events as best-effort notifications, not as a source of truth: every event includes the conversation_id, so you can always fetch the canonical state via the REST API after the fact.

Limits

  • Same limit as the REST API: 60 requests/minute per assistant. Higher tiers lift the cap.
  • Outbound: at-most-once delivery — we do not retry on 5xx today (planned for v0.7).
  • Your endpoint must respond within 5 seconds; longer requests are dropped and logged.

Security & RGPD

Inbound: optional Bearer shared secret in the Authorization header. Outbound: HMAC-SHA256 signature in the X-Factory-Signature header (verification snippets above for Node and Python). All traffic is TLS 1.3.

Troubleshooting

  • 401 on outbound consumer. Verify the secret in Settings matches what your code computes against. The signature includes the timestamp prefix t=<unix>. — don't forget the dot.
  • Events not arriving. Open Settings → Outbound webhooks → Recent deliveries. Failed responses (5xx, timeout) are listed with the response body.
  • Same event arrived twice. Upstream tools occasionally re-emit. De-duplicate on event_id (uuid) — Janore generates a stable id per event.
On this page