Sign users in
The guest API, and the Flutter SDK, need to know which of your users is asking. Your backend proves it with a short token, signed with your app’s identity secret. Nothing has to be published and PurrCRM fetches nothing from you.
1. Make a secret
Section titled “1. Make a secret”On Settings › Connect › Sign-in, press Make a secret. It is shown once (crm_is_…). Keep it in your backend’s secrets, never in an app or a web page. A test app has its own secrets (crm_is_test_…), for your staging backend.
2. Sign a token for the signed-in user
Section titled “2. Sign a token for the signed-in user”The token is a JWT signed with HS256:
| Claim | Value |
|---|---|
sub | The user’s id: your external id for them, as in Identify. |
iat | When you signed it. |
exp | When it expires: minutes or hours away. More than 30 days is refused. |
iss, aud, knd | Only if your Sign-in tab names an issuer, an audience or a kind. |
Each example signs a token for the user user-123, reading the secret from an environment variable of your own, CRM_IDENTITY_SECRET, and prints it.
// npm install jsonwebtokenimport jwt from "jsonwebtoken";
const userId = "user-123";const token = jwt.sign({ sub: userId }, process.env.CRM_IDENTITY_SECRET, { algorithm: "HS256", expiresIn: "1h",});console.log(token);# pip install pyjwtimport osfrom datetime import datetime, timedelta, timezone
import jwt
user_id = "user-123"now = datetime.now(timezone.utc)token = jwt.encode( {"sub": user_id, "iat": now, "exp": now + timedelta(hours=1)}, os.environ["CRM_IDENTITY_SECRET"], algorithm="HS256",)print(token)// Standard library only: an HS256 token is two base64url JSON parts and// their HMAC-SHA256.package main
import ( "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/json" "fmt" "os" "time")
func sign(userID, secret string, ttl time.Duration) string { enc := base64.RawURLEncoding header := enc.EncodeToString([]byte(`{"alg":"HS256","typ":"JWT"}`)) now := time.Now() claims, _ := json.Marshal(map[string]any{"sub": userID, "iat": now.Unix(), "exp": now.Add(ttl).Unix()}) unsigned := header + "." + enc.EncodeToString(claims) mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(unsigned)) return unsigned + "." + enc.EncodeToString(mac.Sum(nil))}
func main() { fmt.Println(sign("user-123", os.Getenv("CRM_IDENTITY_SECRET"), time.Hour))}<?php// composer require firebase/php-jwtrequire 'vendor/autoload.php';
use Firebase\JWT\JWT;
$userId = 'user-123';$token = JWT::encode( ['sub' => $userId, 'iat' => time(), 'exp' => time() + 3600], getenv('CRM_IDENTITY_SECRET'), 'HS256');echo $token, PHP_EOL;# gem install jwtrequire "jwt"
user_id = "user-123"token = JWT.encode( { "sub" => user_id, "iat" => Time.now.to_i, "exp" => Time.now.to_i + 3600 }, ENV.fetch("CRM_IDENTITY_SECRET"), "HS256")puts tokenTo try a token, paste it into Test a token on the Sign-in tab (below).
3. Serve it to your app
Section titled “3. Serve it to your app”Give your signed-in user their token from a route of your own, behind your own sign-in. With Express, where requireSignedIn is your own sign-in’s check and req.user the user it found:
import express from "express";import jwt from "jsonwebtoken";
const app = express();
app.get("/crm-token", requireSignedIn, (req, res) => { const token = jwt.sign({ sub: String(req.user.id) }, process.env.CRM_IDENTITY_SECRET, { algorithm: "HS256", expiresIn: "1h", }); res.set("Cache-Control", "no-store").json({ token });});Your web app sends it as Authorization: Bearer <token> (From a website or web app). The Flutter SDK fetches it with CrmTokens.fetched (Sign users in).
Check a token
Section titled “Check a token”Paste a token into Test a token on the Sign-in tab. It says which contact the token signs in, or exactly why it is refused.
When PurrCRM refuses a token:
401with codetoken_expired: the signature or the time is wrong. Get a new token and try once more.401with codeunauthorized: the token can never work as it is (signed for the other mode, too long, nosub). Thedetailsays why; asking again will not help.
Replacing the secret
Section titled “Replacing the secret”Two secrets work at once. Make a new one, deploy your backend with it, wait until the old one’s Last used stops moving, then Revoke it. Revoking takes effect within 30 seconds.
Other ways
Section titled “Other ways”- Your own sign-in’s key set: if your sign-in already issues tokens and publishes its keys at a public https address (JWKS, with EdDSA, RS256 or ES256), name it on the Sign-in tab instead. Your tokens are then checked against it.
- Unverified sign-in: for a prototype only, the user’s id is sent with no token, as
X-CRM-User-Id. Anyone who knows an id can act as that user.