🔔 Webhooks
Webhooks позволяют получать уведомления о событиях в Fixorix — например, когда создаётся тикет, обновляется статус или добавляется сообщение.
После настройки Webhooks Fixorix будет отправлять события на ваш сервер в режиме реального времени через HTTP POST запросы.
🧭 1. Как это работает
- Вы настраиваете URL в интерфейсе Fixorix.
- Вы выбираете типы событий, которые хотите получать.
- Fixorix отправляет POST-запрос на ваш URL при каждом событии.
- Ваш сервер принимает JSON и отвечает
200 OK.
⚙️ 2. Настройка в интерфейсе
В разделе:
Настройки проекта → Интеграции → Webhooks
вы указываете:
- адрес приёма (ваш URL)
- события, которые необходимо получать
- секрет для подписи (не обязателен, но рекомендуется)
Вы можете создать несколько Webhooks для разных интеграций.
🧾 3. Формат webhook-уведомления
Fixorix отправляет POST-запрос следующего формата:
HTTP запрос
POST <ваш URL>
Content-Type: application/json
X-Fixorix-Signature: sha256=<подпись>
Тело события
{
"id": "evt_8736ba78",
"type": "ticket.created",
"createdAt": "2025-01-02T10:00:00Z",
"projectId": "tenant-001",
"data": {
"ticketId": "tck_475746",
"title": "Cannot login",
"status": "new",
"priority": "high"
}
}
📋 4. Типы событий
Webhook уведомления делятся на несколько категорий:
- 🎫 Tickets — события, связанные с тикетами
- 💬 Messages — сообщения и заметки
- 📨 Appeals — события по обращениям
Полный список событий и структуры их data смотрите на странице
Events — Webhooks.
🔐 5. Проверка подлинности (подпись)
Чтобы убедиться, что событие действительно от Fixorix, в заголовке передается подпись:
X-Fixorix-Signature: sha256=<hex-значение>
Подпись формируется как:
HMAC_SHA256(<секрет>, <сырой JSON>)
Пример проверки подписи
- Python
- Java
- Node.js
import hmac
import hashlib
def verify(body: bytes, signature: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(
key=secret.encode("utf-8"),
msg=body,
digestmod=hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature)
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
public class WebhookVerifier {
public static boolean verify(byte[] body, String signature, String secret) throws Exception {
var mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
var digest = mac.doFinal(body);
var hex = new StringBuilder();
for (var b : digest) {
hex.append(String.format("%02x", b));
}
var expected = "sha256=" + hex;
return MessageDigest.isEqual(
expected.getBytes(StandardCharsets.UTF_8),
signature.getBytes(StandardCharsets.UTF_8)
);
}
}
const crypto = require("crypto");
function verify(body, signature, secret) {
const expected =
"sha256=" +
crypto.createHmac("sha256", secret).update(body).digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
Если подпись некорректна — верните 400 или 401.
🧪 6. Ответ сервера
Ваш сервер должен ответить:
HTTP 200 OK
Можно также вернуть:
202 Accepted204 No Content
🔁 7. Повторная доставка
Если Fixorix не получает ответ 2xx:
- выполняется повторная отправка
- до 10 попыток
- с увеличивающимися интервалами
- в течение 24 часов
Повторные события содержат:
"retry": true
События имеют постоянное поле "id", поэтому их можно дедуплицировать.
📌 8. Требования к вашему серверу
Ваш обработчик должен:
- Принимать POST с JSON
- Возвращать ответ < 3 секунд
- Быть доступным по HTTPS
- Понимать дубликаты событий (идемпотентность)
- (опционально) проверять подпись
🧩 9. Пример простого обработчика
- Python
- Java
- Node.js
import hashlib
import hmac
import json
import os
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.getenv("FIXORIX_WEBHOOK_SECRET")
def verify_signature(body: bytes, signature: str) -> bool:
expected = "sha256=" + hmac.new(
SECRET.encode("utf-8"),
body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
@app.post("/fixorix-webhook")
def webhook():
signature = request.headers.get("x-fixorix-signature")
if not signature or not verify_signature(request.data, signature):
abort(400, "Invalid signature")
event = json.loads(request.data.decode())
if event["type"] == "ticket.created":
print("New ticket:", event["data"]["ticketId"])
return "", 200
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.Request;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
public class WebhookHandler {
private static final String SECRET = System.getenv("FIXORIX_WEBHOOK_SECRET");
private static boolean verify(byte[] body, String signature) throws Exception {
var mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(SECRET.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
var digest = mac.doFinal(body);
var hex = new StringBuilder();
for (var b : digest) {
hex.append(String.format("%02x", b));
}
var expected = "sha256=" + hex;
return MessageDigest.isEqual(
expected.getBytes(StandardCharsets.UTF_8),
signature.getBytes(StandardCharsets.UTF_8)
);
}
public static void main(String[] args) throws Exception {
var server = new Server(8080);
server.setHandler((target, baseRequest, request, response) -> {
if (!"/fixorix-webhook".equals(target) || !"POST".equalsIgnoreCase(request.getMethod())) return;
var body = request.getInputStream().readAllBytes();
var signature = request.getHeader("x-fixorix-signature");
if (!verify(body, signature)) {
response.setStatus(400);
response.getWriter().write("Invalid signature");
baseRequest.setHandled(true);
return;
}
var mapper = new ObjectMapper();
var event = mapper.readTree(body);
if ("ticket.created".equals(event.get("type").asText())) {
System.out.println("New ticket: " + event.get("data").get("ticketId").asText());
}
response.setStatus(200);
baseRequest.setHandled(true);
});
server.start();
server.join();
}
}
app.post("/fixorix-webhook", express.raw({ type: "*/*" }), (req, res) => {
const signature = req.headers["x-fixorix-signature"];
const secret = process.env.FIXORIX_WEBHOOK_SECRET;
if (!verify(req.body, signature, secret)) {
return res.status(400).send("Invalid signature");
}
const event = JSON.parse(req.body.toString());
if (event.type === "ticket.created") {
console.log("New ticket:", event.data.ticketId);
}
res.sendStatus(200);
});
❓ FAQ
Нужно ли использовать REST API вместе с webhook?
Webhooks уведомляют о событиях. REST API позволяет получить больше данных или выполнить действия.
Обычно интеграции используют оба метода.
Можно ли тестировать webhooks локально?
Да, с помощью инструментов:
- ngrok
- webhook.site
- localtunnel
Что делать при ошибках?
- Если ваш сервер недоступен, Fixorix повторит доставку автоматически.
- Если вы возвращаете 500, Fixorix тоже повторит попытку.
- Если вы возвращаете 400 (например, неверная подпись или неверные данные) — повторов не будет.
- Ответ 2xx означает успешную обработку и повторов не будет.