Skip to content
Documentation

SDKs

The Node.js, visitor-core, React, Go, PHP, Laravel and Flutter packages: installation, the smallest example, the example app; creating a client, pagination, webhook signatures and requests without an SDK.

ClickFix has server SDKs for Node.js, Go and PHP; a DOM-free visitor client for React Native, Node and browsers (@getclickfix/visitor-core); UI-free widget hooks for React; a full Laravel package and a webview package for Flutter. The server SDKs provide authentication, bounded retries (including Retry-After), pagination, user_hash and webhook signature verification. All are MIT licensed; the source lives in the public clickfix-sdks, clickfix-php and clickfix-laravel repositories, and every package has a runnable example app in clickfix-examples. Everything is published under github.com/ClickSolveMainTR.

PackageVersionInstall
widget.js (SDK 2.1)2.1.0<script src="https://getclickfix.com/widget.js" data-widget="WIDGET_ID" async></script>, types at https://getclickfix.com/widget.d.ts
@getclickfix/node0.2.1npm install @getclickfix/node (Node 22+)
@getclickfix/visitor-core0.1.1npm install @getclickfix/visitor-core (React Native, Node 22+, browsers; no DOM)
@getclickfix/react-widget0.1.2npm install @getclickfix/react-widget (React 18+)
Go modulego/v0.2.0go get github.com/ClickSolveMainTR/clickfix-sdks/go@v0.2.0
clickfix/sdk0.1.0composer require clickfix/sdk guzzlehttp/guzzle (PHP 8.1+, any PSR-18 client)
clickfix/laravel0.1.0composer require clickfix/laravel (Laravel 10-13, PHP 8.1+)
clickfix_webview0.1.1flutter pub add clickfix_webview (Flutter 3.38+, webview bridge v1)

Versions are generated from the package files themselves (make sdks); each package keeps its changelog in CHANGELOG.md. In 0.x a minor version may carry a breaking change; the changelog marks it Breaking. Source: clickfix-sdks, clickfix-php, clickfix-laravel. Example apps: clickfix-examples.

Use https://api.getclickfix.com as the base URL (no /v1 suffix). API keys stay on the server; never put them in browser or mobile code. Creating keys, scopes, the response envelope and error codes: API. Every endpoint page of the API reference also carries ready-made requests for curl, Node.js, Go and PHP.

Node.js: @getclickfix/node

A typed Node 22+ fetch client whose paths types come from the API contract. Example app: clickfix-examples/node-express.

npm install @getclickfix/node
import { createClient } from "@getclickfix/node";

const client = createClient({
  baseURL: "https://api.getclickfix.com",
  apiKey: process.env.CLICKFIX_API_KEY!,
});

const created = await client.request(
  "/v1/public-api/conversations",
  "post",
  {
    contact_external_id: "1001",
    inbox_uuid: inboxUUID,
    subject: "Delivery question",
  },
  {
    idempotencyKey: "order-1042-support",
    signal: AbortSignal.timeout(30_000),
  },
);
ExportWhat it does
createClient({ baseURL, apiKey, attempts? })request(path, method, body?, { path?, query?, idempotencyKey?, signal? }); error responses throw an APIError with status, code, request id and details
paginate(fetchPage)An AsyncGenerator that unrolls { items, total, limit, offset } pages into items
verifyWebhook(secret, timestamp, signature, body)The v1= signature over the raw body with a 5 minute tolerance; webhooks
identityHash(secret, userId, companyId?)The widget user_hash; with companyId the company is signed too (identity verification)

GETs and writes with an idempotencyKey are attempted at most three times (exponential backoff, bounded Retry-After; on 429, 502, 503, 504 and network errors); writes without a key are not retried. The API stores completed write responses, errors included: retrying with the same key cannot turn a recorded error into a success; use a new key for corrected input.

Visitor client: @getclickfix/visitor-core

Calls the widget endpoints (/v1/widget/{public_id}/*) without a DOM: through the same-origin proxy in the browser (mode: "browser"), with an app key in a mobile app (mode: "app"). Includes retries, session renewal, a realtime abstraction and the webview bridge types (BridgeCommand, BridgeMessage, commandScript, parseMessage, isSafeLink). Example app: clickfix-examples/react-native-webview.

npm install @getclickfix/visitor-core
import { createVisitorClient, commandScript } from "@getclickfix/visitor-core";

const client = createVisitorClient({
  mode: "app",
  baseURL: "https://getclickfix.com/api",
  publicId: "WIDGET_ID",
  app: { key: APP_KEY, packageName: "com.example.app" },
});
await client.init();
await client.identify({ user_id: user.id, user_hash: userHashFromServer });

// Once the webview is open and clickfix:ready arrived, hand the session over:
webview.injectJavaScript(
  commandScript({
    type: "clickfix:session",
    id: "s1",
    token: client.getToken(),
  }),
);

The app key lives only in the native core's HTTP header; it never reaches the webview or the page.

React: @getclickfix/react-widget

Typed hooks to drive the ready-made messenger (launcher, home, help center) from your own button, and createWidgetClient + useWidget on top of visitor-core to draw your own UI. Example app: clickfix-examples/nextjs-widget.

npm install @getclickfix/react-widget
"use client";
import { useMemo } from "react";
import { useClickFix, useClickFixEvent } from "@getclickfix/react-widget";

export function Support({ consent }: { consent: boolean }) {
  const options = useMemo(
    () =>
      consent
        ? {
            appOrigin: "https://getclickfix.com",
            widgetId: "WIDGET_ID",
            hideLauncher: true,
          }
        : null, // null defers loading until consent is given
    [consent],
  );
  const { ready, unread, open, sdk } = useClickFix(options);
  useClickFixEvent("conversation:ended", (e) => console.log(e.status));

  return (
    <button aria-expanded={open} disabled={!ready} onClick={() => sdk.toggle()}>
      Support {unread || ""}
    </button>
  );
}

With the UI-free client you render conversations and messages yourself:

await client.identify({ user_id: user.id, user_hash: serverProvidedHmac });
const { items } = await client.conversations();
await client.createConversation({
  text: "Hello",
  client_id: crypto.randomUUID(),
});
await client.sendMessage(
  conversationUUID,
  { text: "Photo", client_id: stableMessageUUID },
  file,
);
await client.markRead(conversationUUID);

user_hash is produced on your server; never send the identity secret to the browser. Widget commands and events: Web widget.

Go: github.com/ClickSolveMainTR/clickfix-sdks/go

A client generated from the API contract (oapi-codegen) plus authentication, a 30 second HTTP timeout, bounded retries, pagination and webhook verification. Releases are tagged go/vX.Y.Z. Example app: clickfix-examples/go-server.

go get github.com/ClickSolveMainTR/clickfix-sdks/go@latest
import clickfix "github.com/ClickSolveMainTR/clickfix-sdks/go"

client, err := clickfix.New("https://api.getclickfix.com", os.Getenv("CLICKFIX_API_KEY"))
if err != nil {
	log.Fatal(err)
}
res, err := client.GetV1PublicApiContactsWithResponse(ctx, &clickfix.GetV1PublicApiContactsParams{})
FunctionWhat it does
New(baseURL, key)The generated ClientWithResponses with a retrying transport; every operation is <Name>WithResponse(ctx, …)
Iterate(ctx, fetch, yield) / ListPage[T](ctx, c, resource, limit, offset)Unrolls {items, total, limit, offset} pages into items
VerifyWebhook(secret, timestamp, signature, body, now, tolerance)The v1= signature over the raw body
IdentityHash(secret, userID) / IdentityCompanyHash(secret, userID, companyID)The widget user_hash; the second also signs the company id

PHP: clickfix/sdk

A PSR-18 client for PHP 8.1+ (Guzzle recommended; PSR-17 factories via php-http/discovery) with typed errors, bounded retries, offset pagination, webhook verification and user_hash. Source: clickfix-php. Example app: clickfix-examples/php-plain.

composer require clickfix/sdk guzzlehttp/guzzle
use ClickFix\Client;
use ClickFix\UserHash;
use ClickFix\Webhooks;

$client = new Client('https://api.getclickfix.com', getenv('CLICKFIX_API_KEY'));

$contact = $client->contacts()->upsertByExternalId('1001', [
    'name' => 'Ayşe Yılmaz',
    'email' => 'ayse@example.com',
], idempotencyKey: 'user-1001-v3');

foreach ($client->contacts()->all(['q' => 'acme']) as $item) {
    // every matching contact, page by page
}

$userHash = UserHash::make($identitySecret, $authenticatedUserId); // only this value goes to the widget
$ok = Webhooks::verify($secret, $headers['ClickFix-Webhook-Timestamp'], $headers['ClickFix-Webhook-Signature'], $rawBody);

Resource classes are contacts(), companies(), customFields(), conversations(), events(), webhooks(), triggers() and apiChannel($instance, $secret), with list/all/get/create/update/delete style methods per endpoint. Error responses are subclasses of ClickFix\Exception\ApiException (AuthenticationException 401, ForbiddenException 403, NotFoundException 404, ConflictException 409, ValidationException 400/422, RateLimitException 429 with retryAfter, ServerException 5xx); network failures throw TransportException.

Laravel package

For Laravel 10, 11, 12 and 13 (PHP 8.1+) clickfix/laravel packs the steps of this guide: a service provider and the ClickFix facade, config/clickfix.php, artisan commands, a signature-checking POST /clickfix/webhook route with one Laravel event per ClickFix event, and the @clickfixWidget Blade directive. Package discovery registers the provider. Source and changelog: clickfix-laravel. Example app: clickfix-examples/laravel-app.

Install

composer require clickfix/laravel
php artisan clickfix:install
php artisan clickfix:check

clickfix:install publishes config/clickfix.php, adds the missing CLICKFIX_* keys to .env (generating the webhook secret), and prints the webhook URL to subscribe plus the next steps. Once the keys are filled in, clickfix:check tries the API key against the API and verifies the widget id, the identity secret, the webhook secret and that the route is https; anything missing exits with code 1.

.env keyWhere
CLICKFIX_API_KEYThe API key from the Before you start table
CLICKFIX_WIDGET_IDWeb widget → Install
CLICKFIX_IDENTITY_SECRETWeb widget → Security → Identity verification; stays on the server
CLICKFIX_WEBHOOK_SECRETThe subscription's data.secret, or the value clickfix:install generated (use it in the subscription)
CLICKFIX_WEBHOOK_QUEUEEmpty: events fire inside the request; true or a queue name: from a queued job

Artisan commands

CommandWhat it does
clickfix:install [--force] [--no-env]Publish the config, add the .env keys, generate the webhook secret, print the webhook URL
clickfix:checkAPI key, widget id, identity secret, webhook secret and route; exit code 1 on failure
clickfix:user-hash {user} [--raw] [--json]user_hash for a user (model key), to debug WIDGET_IDENTITY_INVALID
clickfix:webhook:secret [--write]64 hex character secret; --write stores it in .env
clickfix:sync:contacts [--id=*] [--since=] [--chunk=] [--dry-run]Create or update the model in config('clickfix.contacts') as contacts by external id (PUT /contacts/by-external-id, contacts:write)

clickfix:sync:contacts reads the model (App\Models\User), the field map (name, email, phone, job_title, locale, timezone → model attribute or dotted path) and the custom field map from the config. The external id is always the model's primary key, the same value identify sends as user_id, so the synced contact and the widget session are one person (external id).

Widget and identity verification

    @clickfixWidget
</body>

The directive prints the queue snippet, the ClickFix("boot", {...}) call and the widget.js tag. Without arguments it identifies the authenticated user (user_id is the model key, user_hash is computed on the server with the identity secret, name and email come from the model); with nobody logged in the widget opens anonymously. @clickfixWidget($customer) takes a specific model, @clickfixWidget(null) is always anonymous, @clickfixWidget(auth()->user(), ['locale' => 'en']) adds boot options. The fields in the identity come from clickfix.widget.user, or the model implements ProvidesClickFixIdentity (including company and attributes).

For Inertia, Livewire or a SPA that boots the widget itself, build the identity on the server and share it with the page:

use ClickFix\Laravel\Facades\ClickFix;

ClickFix::userHash($user);  // hex HMAC-SHA256(identity secret, model key)
ClickFix::identity($user);  // ['user_id' => '42', 'user_hash' => '…', 'name' => …, 'email' => …]

Call ClickFix("identify", identity) in the browser; the identity secret never leaves the server (identity verification).

Webhook

The package registers POST /clickfix/webhook outside the web group (no session, no CSRF) behind the clickfix.webhook middleware. The middleware verifies ClickFix-Webhook-Timestamp and ClickFix-Webhook-Signature over the raw body (the rule in Webhooks; 5 minute tolerance, constant-time comparison); a bad or stale signature is 401, a missing secret 503. A redelivery with the same ClickFix-Webhook-Id is recognised from the cache and acknowledged with 200 without firing the events twice.

Every verified delivery raises ClickFix\Laravel\Events\WebhookReceived; a known event name also raises its own class: ConversationCreated, ConversationResolved, MessageReceived, MessageSent, ContactCreated, ContactUpdated, CompanyUpdated, SlaBreached, CsatSubmitted … (one class per event; an unknown name raises only WebhookReceived).

use ClickFix\Laravel\Events\MessageReceived;
use Illuminate\Support\Facades\Event;

Event::listen(MessageReceived::class, function (MessageReceived $event) {
    $event->get('conversation_uuid');
    $event->get('contact_external_id'); // your user id
    $event->payload->occurredAt();      // Carbon
});

With CLICKFIX_WEBHOOK_QUEUE=true (or a queue name) the events fire from the ProcessWebhook job instead of the request and the route answers 202. To define the route yourself set CLICKFIX_WEBHOOK_ROUTE=false and attach the clickfix.webhook middleware to your own route. Creating the subscription: Webhooks.

Flutter: clickfix_webview

Mobile apps show the widget in a secure webview that carries the session; for Flutter the ready-made clickfix_webview package implements the bridge contract as ClickFixController + ClickFixWebView. Installation, the method table and samples: Mobile: webview and Flutter. Example app: clickfix-examples/flutter-webview.

Shared behaviour

Pagination

import { paginate } from "@getclickfix/node";

for await (const contact of paginate(async (limit, offset) => {
  const envelope = await client.request(
    "/v1/public-api/contacts",
    "get",
    undefined,
    { query: { limit, offset } },
  );
  // Validate envelope.data against your own schema and return {items, total, limit, offset}.
  return validateContactPage(envelope.data);
})) {
  await consume(contact);
}

Offset pagination is for snapshots; concurrent inserts can shift pages. De-duplicate by UUID when syncing; for incremental sync use the trigger cursor or webhooks.

Webhook signature and identity hash

import { identityHash, verifyWebhook } from "@getclickfix/node";

const valid = verifyWebhook(
  secret,
  headers.timestamp,
  headers.signature,
  rawBody,
);

// user_hash for the signed-in user and their company in the web widget
const userHash = identityHash(identitySecret, authenticatedUser.id, company.id);

Verify the signature over the raw body before parsing JSON, and record and de-duplicate ClickFix-Webhook-Id before any side effect. Verification without an SDK (Node.js, Python, Go): Webhooks.

Without an SDK

Example scripts: clickfix-examples/curl.

curl -X POST "https://api.getclickfix.com/v1/public-api/conversations/CONVERSATION_UUID/messages" \
  -H "Authorization: Bearer $CLICKFIX_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1042-note" \
  -d '{ "content": "The order has shipped.", "private": true }'

On 429 RATE_LIMITED wait for Retry-After and retry with the same Idempotency-Key; do not retry 400, 401 and 403.

Last updated on