# Webhooks

Subscribe to events with signed POSTs: creating the subscription, verifying the signature, de-duplication and retries; widget, Node.js, Go, PHP and curl samples.

URL: https://getclickfix.com/docs/integration-guide/webhooks?lang=en

Webhooks deliver events (new conversation, incoming message, contact update, SLA breach, satisfaction…) to you as signed `POST`s; no polling. Create the subscription with the `webhooks:write` scope through `POST /v1/public-api/webhooks`; `data.secret` is returned in that response only. The URL must be public `https`. Every delivery carries `ClickFix-Webhook-Id`, `ClickFix-Webhook-Event`, `ClickFix-Webhook-Timestamp` and `ClickFix-Webhook-Signature: v1=…`. Verification: `hex(HMAC-SHA256(secret, timestamp + "." + raw_body))` over the raw body, constant-time comparison, five minutes tolerance, deduplication by `ClickFix-Webhook-Id`; then `2xx`. Failed deliveries are retried with growing intervals for up to 24 hours. Event list and body format: [Webhooks](/docs/webhooks?lang=en).

{/* example: widget/07-webhooks.js */}

```js tab="Widget"
// Adım 7: tarayıcı tarafında webhook yoktur; konuşma olaylarını SDK verir.
// @ts-check
ClickFix("on", "conversation:started", ({ conversation_uuid }) =>
  console.log("yeni", conversation_uuid),
);
ClickFix("on", "conversation:ended", ({ conversation_uuid, status }) =>
  console.log(conversation_uuid, status),
);
ClickFix("on", "csat", ({ score }) => console.log("memnuniyet", score));
```

{/* example: node/07-webhooks.ts */}

```ts tab="Node.js"
// Adım 7: webhook aboneliği aç ve teslimatları ham gövde üzerinde doğrula.
import { createServer } from "node:http";
import { createClient, verifyWebhook } from "@getclickfix/node";

const client = createClient({
  baseURL: process.env.CLICKFIX_BASE_URL!,
  apiKey: process.env.CLICKFIX_API_KEY!, // webhooks:write
});

const created = await client.request(
  "/v1/public-api/webhooks",
  "post",
  {
    name: "CRM senkronizasyonu",
    url: "https://example.com/clickfix/webhook",
    events: ["conversation.created", "message.received", "contact.updated"],
    enabled: true,
  },
  { idempotencyKey: `webhook-crm-${Date.now()}` },
);
// data.secret yalnız bu yanıtta döner: güvenli saklayın (ör. gizli değer deposu),
// loglamayın. Doğrulama sunucunuz onu oradan okur.
const { secret: issued, ...subscription } = created.data as {
  secret: string;
  uuid: string;
};
const secret = process.env.CLICKFIX_WEBHOOK_SECRET ?? issued;
console.log(JSON.stringify({ subscription }));

createServer(async (req, res) => {
  const chunks: Buffer[] = [];
  for await (const c of req) chunks.push(c as Buffer);
  const raw = Buffer.concat(chunks);
  const ok = verifyWebhook(
    secret,
    String(req.headers["clickfix-webhook-timestamp"] ?? ""),
    String(req.headers["clickfix-webhook-signature"] ?? ""),
    raw,
  );
  if (!ok) {
    res.writeHead(401).end();
    return;
  }
  // ClickFix-Webhook-Id ile tekilleştirin, kaydedin, sonra 2xx dönün.
  const event = JSON.parse(raw.toString()) as { event: string; data: unknown };
  console.log(JSON.stringify({ received: event.event }));
  res.writeHead(200).end();
}).listen(Number(process.env.PORT ?? 3000), () => console.log("listening"));
```

{/* example: go/webhooks.go */}

```go tab="Go"
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"time"

	clickfix "github.com/ClickSolveMainTR/clickfix-sdks/go"
)

// Adım 7: webhook aboneliği aç ve teslimatları ham gövde üzerinde doğrula.
func webhooks(ctx context.Context) error {
	client, err := clickfix.New(os.Getenv("CLICKFIX_BASE_URL"), os.Getenv("CLICKFIX_API_KEY"))
	if err != nil {
		return err
	}
	res, err := client.PostV1PublicApiWebhooksWithResponse(ctx,
		&clickfix.PostV1PublicApiWebhooksParams{IdempotencyKey: fmt.Sprintf("webhook-crm-%d", time.Now().UnixNano())},
		clickfix.PostV1PublicApiWebhooksJSONRequestBody{
			Name: "CRM senkronizasyonu", Url: "https://example.com/clickfix/webhook",
			Events: []string{"conversation.created", "message.received", "contact.updated"}, Enabled: true,
		})
	if err != nil {
		return err
	}
	if res.StatusCode() != 201 {
		return fmt.Errorf("clickfix: HTTP %d", res.StatusCode())
	}
	var created struct {
		Data struct {
			Secret string `json:"secret"` // yalnız bu yanıtta döner
		} `json:"data"`
	}
	if err := json.Unmarshal(res.Body, &created); err != nil {
		return err
	}
	// Yalnız bu yanıtta döner: güvenli saklayın, loglamayın; sunucu onu oradan okur.
	secret := os.Getenv("CLICKFIX_WEBHOOK_SECRET")
	if secret == "" {
		secret = created.Data.Secret
	}
	fmt.Println(`{"subscription":"created"}`)

	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		raw, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
		if err != nil {
			w.WriteHeader(http.StatusBadRequest)
			return
		}
		if err := clickfix.VerifyWebhook(secret, r.Header.Get("ClickFix-Webhook-Timestamp"),
			r.Header.Get("ClickFix-Webhook-Signature"), raw, time.Now(), 5*time.Minute); err != nil {
			w.WriteHeader(http.StatusUnauthorized)
			return
		}
		// ClickFix-Webhook-Id ile tekilleştirin, kaydedin, sonra 2xx dönün.
		var event struct {
			Event string `json:"event"`
		}
		_ = json.Unmarshal(raw, &event)
		fmt.Printf(`{"received":%q}`+"\n", event.Event)
		w.WriteHeader(http.StatusOK)
	})
	port := os.Getenv("PORT")
	if port == "" {
		port = "3000"
	}
	fmt.Println("listening")
	return http.ListenAndServe("127.0.0.1:"+port, nil)
}
```

{/* example: php/07-webhooks.php */}

```php tab="PHP"
<?php
// Adım 7: webhook aboneliği aç ve teslimatları ham gövde üzerinde doğrula.
// Komut satırından çalışınca aboneliği açar; web sunucusu altında
// (php -S 127.0.0.1:3000 07-webhooks.php ya da framework rotanız) teslimatları doğrular.
require __DIR__.'/vendor/autoload.php';

use ClickFix\Client;
use ClickFix\Webhooks;

if (PHP_SAPI === 'cli') {
    $client = new Client(
        getenv('CLICKFIX_BASE_URL'),
        getenv('CLICKFIX_API_KEY'), // webhooks:write
    );
    $created = $client->webhooks()->create([
        'name' => 'CRM senkronizasyonu',
        'url' => 'https://example.com/clickfix/webhook',
        'events' => ['conversation.created', 'message.received', 'contact.updated'],
        'enabled' => true,
    ], idempotencyKey: 'webhook-crm-'.(int) (microtime(true) * 1000));
    // secret yalnız bu yanıtta döner: güvenli saklayın (ör. gizli değer deposu),
    // loglamayın. Doğrulama sunucunuz onu oradan okur (örnekte CLICKFIX_WEBHOOK_SECRET).
    unset($created['secret']);
    echo json_encode(['subscription' => $created]), PHP_EOL;
    exit;
}

$secret = getenv('CLICKFIX_WEBHOOK_SECRET');
$raw = file_get_contents('php://input'); // ham gövde; JSON'a çevirmeden doğrulayın
$ok = Webhooks::verify(
    $secret,
    $_SERVER['HTTP_CLICKFIX_WEBHOOK_TIMESTAMP'] ?? '',
    $_SERVER['HTTP_CLICKFIX_WEBHOOK_SIGNATURE'] ?? '',
    $raw,
);
if (!$ok) {
    http_response_code(401);
    exit;
}
// ClickFix-Webhook-Id ile tekilleştirin, kaydedin, sonra 2xx dönün.
$event = json_decode($raw, true);
error_log(json_encode(['received' => $event['event'] ?? null]));
http_response_code(200);
```

{/* example: curl/07-webhooks.sh */}

```bash tab="curl"
# Adım 7: webhook aboneliği (webhooks:write). data.secret yalnız bu yanıtta döner.
curl -sS --fail-with-body -X POST "$CLICKFIX_BASE_URL/v1/public-api/webhooks" \
  -H "Authorization: Bearer $CLICKFIX_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: webhook-crm-$(date +%s)" \
  -d '{ "name": "CRM senkronizasyonu", "url": "https://example.com/clickfix/webhook", "events": ["conversation.created", "message.received", "contact.updated"], "enabled": true }'
```

For Zapier, Make and n8n, `POST /v1/public-api/subscriptions` (REST hook) and `GET /v1/public-api/triggers` (cursor polling) use the same infrastructure: [Zapier, Make and n8n](/docs/mcp-integrations?lang=en).
