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.
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 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
{
"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": "conversation.created",
"event_id": "9b1d73e8-...-a4c1",
"timestamp": 1714502400,
"payload": { /* event-specific, see below */ }
}
// 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)
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)
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)
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.