Theme
Authentication
The widget works with four identity modes. Signing in is never required: visitor mode is always available, and the other modes optionally upgrade a visitor to a member identity. Chat and calls work the same way whichever mode is used — a member identity only makes it clear who a conversation belongs to.
| Mode | Identity source | What it takes |
|---|---|---|
| 1. Visitor (default) | Automatic, anonymous | Nothing |
2. Custom login (getToken) | Your own backend | A socket token signed with secret_key |
| 3. Social login — Webfon keys | Webfon's OAuth applications | Enabling the provider in the admin panel |
| 4. Social login — your own keys | Your own OAuth applications | client_id / client_secret in the admin panel |
1. Visitor mode (default)
Requires no configuration. On first open the widget silently generates an anonymous visitor identity and connects with it. The visitor is never asked for a name or a login; chat and calls start straight away. Every mode below is layered on top of this base — when a member signs out, the widget returns to visitor mode.
The visitor identity is created in the browser and stays stable across page loads and sessions, so a returning visitor picks up where they left off. If they clear their browser data, a new identity is generated and the previous history is no longer reachable from that browser.
2. Custom login — getToken
You identify the user signed in on your site to the widget yourself. Your backend directly signs the socket token the widget will connect with — a JWT signed with the provider's secret_key (HS256) and valid for about 1 hour. The widget uses that token as-is; no Webfon endpoint sits in between.
json
{
"pid": "PROVIDER_ID",
"uid": "<user-id>",
"type": "user",
"cnf": { "jkt": "<widget-jkt>" },
"name": "...",
"email": "...",
"avatar": "https://.../avatar.jpg",
"language": "tr",
"iat": 1735689600,
"exp": 1735693200
}pid(required) — the provider ID.uid(required) — your site's own user ID.type(required) — must be"user".cnf.jkt(required) — the value the widget hands to yourgetTokencallback (see below). Copy it in verbatim; without it the connection is rejected. It ties the token to this one browser, so a stolen token cannot be used anywhere else.name/email/avatar(optional) — shown in the widget as the member's name and avatar (avataris a URL).language(optional) — the language the member wants to be answered in ("tr","en", …). Sending it skips the pre-chat sheet: that sheet's only question for a signed-in member is the language, so a token that answers it lets the member start writing right away.exp— about 1 hour; the widget renews it automatically throughgetTokenbefore it expires.
secret_key is never sent to the browser
Never send the signing secret (secret_key) to the browser. The token is signed by your own backend; the widget only carries the signed token. secret_key stays on the server.
Signing the token on your backend
The widget calls your getToken callback with a jkt value. Your backend copies it into cnf.jkt and signs the socket token with any JWT library:
php
<?php
// routes/api.php — the widget calls getToken({ jkt }); body: { "jkt": "..." }.
use Firebase\JWT\JWT; // firebase/php-jwt
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::post('/webfon-token', function (Request $request) {
$user = $request->user(); // the member in session; null → visitor
if (!$user) {
return response()->json(['token' => null]);
}
$now = time();
$payload = [
'pid' => 'PROVIDER_ID',
'uid' => (string) $user->id,
'type' => 'user',
'cnf' => ['jkt' => (string) $request->input('jkt')], // copy getToken's jkt (required)
'name' => $user->name, // optional
'email' => $user->email, // optional
'avatar' => $user->avatar_url, // optional (URL)
'iat' => $now,
'exp' => $now + 3600, // ~1 hour; the widget renews via getToken
];
// secret_key stays SERVER-SIDE ONLY: config/services.php → 'webfon' => ['secret' => env('WEBFON_SECRET')]
return response()->json([
'token' => JWT::encode($payload, config('services.webfon.secret'), 'HS256'),
]);
})->middleware('auth');php
<?php
namespace App\Controller;
use Firebase\JWT\JWT; // firebase/php-jwt
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route;
class WebfonTokenController extends AbstractController
{
// the widget calls getToken({ jkt }); body: { "jkt": "..." }.
#[Route('/api/webfon-token', methods: ['POST'])]
public function __invoke(Request $request): JsonResponse
{
$user = $this->getUser(); // the member in session; null → visitor
if (!$user) {
return $this->json(['token' => null]);
}
$jkt = (string) ($request->toArray()['jkt'] ?? '');
$now = time();
$payload = [
'pid' => 'PROVIDER_ID',
'uid' => (string) $user->getUserIdentifier(),
'type' => 'user',
'cnf' => ['jkt' => $jkt], // copy getToken's jkt (required)
'name' => $user->getName(), // optional
'email' => $user->getEmail(), // optional
'avatar' => $user->getAvatarUrl(), // optional (URL)
'iat' => $now,
'exp' => $now + 3600, // ~1 hour; the widget renews via getToken
];
// secret_key stays SERVER-SIDE ONLY: %env(WEBFON_SECRET)%
return $this->json([
'token' => JWT::encode($payload, $_ENV['WEBFON_SECRET'], 'HS256'),
]);
}
}js
// app/api/webfon-token/route.js (App Router)
import jwt from 'jsonwebtoken';
import { auth } from '@/auth'; // your own session solution (e.g. NextAuth v5)
// the widget calls getToken({ jkt }); body: { jkt }.
export async function POST(req) {
const session = await auth();
if (!session?.user) return Response.json({ token: null }); // → visitor
const { jkt } = await req.json();
const now = Math.floor(Date.now() / 1000);
const token = jwt.sign(
{
pid: 'PROVIDER_ID',
uid: String(session.user.id),
type: 'user',
cnf: { jkt }, // copy getToken's jkt (required)
name: session.user.name, // optional
email: session.user.email, // optional
avatar: session.user.image, // optional (URL)
iat: now,
exp: now + 3600, // ~1 hour; the widget renews via getToken
},
process.env.WEBFON_SECRET, // stays on the server; never sent to the browser
{ algorithm: 'HS256' },
);
return Response.json({ token });
}js
import jwt from 'jsonwebtoken';
// POST /api/webfon-token — the widget calls getToken({ jkt }); body: { jkt }.
app.post('/api/webfon-token', (req, res) => {
const user = req.user; // your own session
if (!user) return res.json({ token: null }); // → the widget stays a visitor
const now = Math.floor(Date.now() / 1000);
const token = jwt.sign(
{
pid: 'PROVIDER_ID',
uid: String(user.id),
type: 'user',
cnf: { jkt: req.body.jkt }, // copy getToken's jkt (required)
name: user.name, // optional
email: user.email, // optional
avatar: user.avatarUrl, // optional (URL)
iat: now,
exp: now + 3600, // ~1 hour; the widget renews via getToken
},
process.env.WEBFON_SECRET, // stays on the server; never sent to the browser
{ algorithm: 'HS256' }, // exp is in the payload; do NOT use expiresIn
);
res.json({ token });
});php
<?php
use Firebase\JWT\JWT; // firebase/php-jwt
// POST /api/webfon-token — the widget calls getToken({ jkt }); body: { "jkt": "..." }.
// secret_key stays SERVER-SIDE ONLY; it is never sent to the browser.
$body = json_decode(file_get_contents('php://input'), true) ?? [];
$jkt = (string) ($body['jkt'] ?? '');
$user = current_member(); // your own session; null → visitor
if (!$user) { echo json_encode(['token' => null]); exit; }
$now = time();
$payload = [
'pid' => 'PROVIDER_ID',
'uid' => (string) $user->id,
'type' => 'user',
'cnf' => ['jkt' => $jkt], // copy getToken's jkt (required)
'name' => $user->name, // optional
'email' => $user->email, // optional
'avatar' => $user->avatarUrl, // optional (URL)
'iat' => $now,
'exp' => $now + 3600, // ~1 hour; the widget renews via getToken
];
echo json_encode(['token' => JWT::encode($payload, SECRET_KEY, 'HS256')]);Binding with getToken (recommended)
You define a callback and the widget pulls the token itself whenever it needs one — on first open and automatically renewed when it expires. The callback takes the connection key's jkt and forwards it to the endpoint above; if it returns null, the widget runs as a visitor. Because the request goes to the same origin as the widget host page, no CORS setup is needed.
html
<script>
window.WebfonConfig = {
providerId: 'PROVIDER_ID',
getToken: async ({ jkt }) => {
const res = await fetch('/api/webfon-token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ jkt }),
});
return res.ok ? (await res.json()).token : null; // null → visitor
},
};
</script>
<script src="https://assets.webfon.io/sdk.js" async></script>js
initWebfon({
providerId: 'PROVIDER_ID',
getToken: async ({ jkt }) => {
const res = await fetch('/api/webfon-token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ jkt }),
});
return res.ok ? (await res.json()).token : null;
},
});The token you return is cached in the browser and reused until it expires, so getToken is not called again on every page transition. logout() clears it and returns the widget to visitor mode.
Why getToken?
Each token is tied to the one browser it was issued for, and getToken is what hands your backend the jkt it needs to do that — so it is the only way to do custom login. A token cannot be signed ahead of time, shipped with the page, or reused in another browser.
3. Social login — Webfon keys
Member login without writing code: the visitor signs in through the social buttons (Google, GitHub…) on the widget's login page. Webfon's own applications are used as the OAuth application — all you have to do is leave the key source as Webfon Application Keys in the Member Login section of the admin panel and enable the providers you want (see Admin Panel).
Only the providers you enable appear on the widget's login page.
Supported providers
Google, Facebook, Apple, GitHub, X.
4. Social login — your own OAuth applications
If you want your own brand to appear on the consent screen instead of Webfon, create your own OAuth applications at the providers and enter their keys into the admin panel. In the Member Login section:
- Key Source → choose
Own Application Keys. - An enable checkbox per provider + Client ID / Client Secret.
- Apple additionally requires Team ID, Key ID, and the p8 private key.
Register the following as the redirect URI in your OAuth application, substituting the provider name for {social}. The URL is the same for every business, so one URI per provider is enough:
https://api.webfon.io/v1/public/oauth/{social}/callbackExamples: …/oauth/google/callback, …/oauth/github/callback, …/oauth/apple/callback.
Everything else works exactly as in mode 3; only the application keys change.
What social login looks like
Both key sources follow the same three steps, and the widget handles all of them:
- The visitor clicks a social button on the widget's login page; a popup opens on the provider's consent page.
- The visitor approves.
- The popup closes itself and the member session starts in the widget.
Nothing to implement
Social login needs no code on your side — enabling the providers in the admin panel is the whole setup. It also never starts on its own: only a click on the login page begins it, and logout() ends the session the same way it does for custom login.
HTTPS is required
The widget only runs in a secure context — HTTPS in production, or localhost while developing. A page served over plain HTTP cannot open a session. This is not configurable.
Member records
Members who arrive through social login (modes 3/4) are kept as a permanent member record: the agent panel shows their name, email, and which provider they signed in with, and later logins update the same record. With custom login (mode 2) the member identity comes from the fields in your token, and the record is created the first time that member sends a message or places a call.
In both cases, history from the anonymous visitor session is not linked to the member record after signing in — they stay separate.