How it works
- Contacts are your users, keyed by your own user ID. They carry a name, email, phone number and any attributes you like, such as a plan or a verification status.
- Events are things that happened to a contact, like “Transfer failed”. They appear on the customer’s timeline, and as a card in any chat with them from the last 24 hours.
- A contact’s phone number links them to their WhatsApp chats, even ones that start later. A signed identify() links them to their website chats.
There are two ways in. Your server can call the REST API, which agents can trust. Your website can use the chat widget, where identity is signed by your server and everything else is shown as unverified.
Before you start
- In Frictionless, open Settings and find Developers. Only workspace owners see it.
- Copy your Workspace ID. It’s public, and goes in the website snippet.
- Create a secret key (
fk_live_…) and store it as an environment variable on your server. It’s shown once. Never put it in a web page or a mobile app.
From your server (REST API)
Base URL https://api.usefrictionless.com/api/v1. Send JSON with your secret key in the Authorization header. The API only accepts calls from servers; browsers are blocked so the key can’t leak through a web page.
Create or update a contact
PUT /contacts/:userId. Only the fields you send change. Set an attribute to null to remove it.
curl -X PUT https://api.usefrictionless.com/api/v1/contacts/user_42 \
-H "Authorization: Bearer $FRICTIONLESS_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Ada Obi",
"email": "ada@example.com",
"phone": "+2348012345678",
"attributes": { "plan": "Business", "kycVerified": true, "walletBalance": 45000 }
}'Returns the contact, and linkedChats: how many chats it’s now linked to.
Send an event
POST /events. Say who it happened to with userId (best: saved even before they ever chat, and creates the contact if needed), with phone (for someone already chatting on WhatsApp), or both. amount and reference get their own rows on the card.
curl -X POST https://api.usefrictionless.com/api/v1/events \
-H "Authorization: Bearer $FRICTIONLESS_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"userId": "user_42",
"event": "Transfer failed",
"properties": { "amount": "NGN 45,000", "reference": "TRX_0001", "reason": "Insufficient funds" }
}'Returns 201 with the event’s id and postedToConversations: how many active chats received a card. occurredAt (ISO 8601) is optional and defaults to now.
In Node.js
async function sendEvent(userId, event, properties) {
const res = await fetch("https://api.usefrictionless.com/api/v1/events", {
method: "POST",
headers: {
Authorization: "Bearer " + process.env.FRICTIONLESS_SECRET_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({ userId, event, properties }),
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(error.code + ": " + error.message);
}
return res.json();
}
await sendEvent("user_42", "Transfer failed", { amount: "NGN 45,000", reference: "TRX_0001" });Read or delete a contact
GET /contacts/:userId returns the contact and its latest 20 events, which helps when checking an integration. DELETE /contacts/:userId removes the contact and its events, for example when a user deletes their account with you. Chats and messages stay.
On your website (chat widget)
Add the chat snippet if you haven’t, then tell it who is signed in.
<script src="https://usefrictionless.com/widget/v1.js"></script>
<script>
Frictionless.init({ company_id: "YOUR_WORKSPACE_ID" });
// Only for signed-in users. userHash comes from your server (below).
Frictionless.identify({
userId: "user_42",
userHash: "HASH_FROM_YOUR_SERVER",
name: "Ada Obi",
email: "ada@example.com",
});
// Something that just happened on the page
Frictionless.track("Checkout failed", { amount: "NGN 45,000", reference: "ORD_981" });
</script>Computing userHash
userHash is the HMAC-SHA256 of the user ID, keyed with your secret key, as lowercase hex. Compute it on your server when you render the page, so nobody can pretend to be another user by editing it.
// Node.js
const crypto = require("crypto");
const userHash = crypto.createHmac("sha256", process.env.FRICTIONLESS_SECRET_KEY).update(user.id).digest("hex");# Python
import hmac, hashlib, os
user_hash = hmac.new(os.environ["FRICTIONLESS_SECRET_KEY"].encode(), user_id.encode(), hashlib.sha256).hexdigest()// PHP
$userHash = hash_hmac('sha256', $user->id, getenv('FRICTIONLESS_SECRET_KEY'));identify() accepts a name and email but not a phone number. Send phone numbers from your server, so a visitor can’t attach themselves to someone else’s WhatsApp chat.
track() and setContext() run in the visitor’s browser, where anyone can change them, so agents see them marked “not verified”. Send anything agents must rely on, such as balances or payment outcomes, from your server.
Limits
| What | Limit |
|---|---|
| userId | 1 to 128 letters, numbers or _ . : @ + - |
| Attribute and property keys | Up to 40 letters, numbers, spaces, dots, dashes or underscores |
| Values | Text up to 500 characters, numbers, or true/false. No nested objects. |
| Attributes per contact | 50 per request, 100 in total |
| Properties per event | 20 |
| Event name | 120 characters |
| Phone | 7 to 15 digits with the country code, like +2348012345678 |
| Requests | 300 a minute per secret key |
Errors
Errors come back as { "error": { "code": "...", "message": "..." } }. The message says what to fix.
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request, invalid_user_id | Something in the request is missing or the wrong shape. |
| 401 | missing_key, invalid_key | No secret key, or one that was rotated. |
| 404 | contact_not_found, customer_not_found | No contact with that userId, or nobody with that phone has chatted yet. |
| 429 | rate_limited | Over 300 requests a minute. Wait and retry. |
| 500 | server_error | Our side. Retry with a short backoff. |
Keeping it safe
- Keep the secret key on your server. If it ever leaks, rotate it in Settings; the old key stops working at once.
- Rotating also changes every userHash, so deploy the new key to your server straight away.
- Send only what agents need to help. You are responsible for having a lawful basis to share your users’ data with Frictionless; see our privacy policy for how we handle it.