Live Chat Contact Tokens
A contact token tells the SendSeven live chat widget exactly which contact is chatting. Your backend mints the token through the API, and can save data to the contact and pass extra context for the chat in the same call. Your page passes the token to the widget. The chat then starts bound to that contact, and your AI agents treat the data you sent as verified.
Use contact tokens when you want your AI agent to answer with account-specific knowledge, for example the customer's plan, open orders or account status, without asking the visitor to prove who they are.
A signed-in customer opens the help bubble in your app and asks why their last invoice was higher. Your backend mints a contact token with the plan, the billing period and the open invoices, so the agent answers from verified account data in the first reply. See Customer context for how contact tokens combine with the other context sources.
How it works
- A user signs in to your site or app.
- Your backend calls
POST /api/v1/contact-tokenswith the widget, the contact, and optionally data to save and context for the chat. - SendSeven returns a short-lived token that starts with
s7ct_. - Your page sets
window.SendSevenConfig.contactTokenbefore loading the widget. - When the chat starts, the widget redeems the token. The conversation is linked to the contact, and your
contextobject is attached to the chat session as verified data.
The token is opaque: it carries no data itself and is useless on any other widget or workspace.
Contact tokens, identity verification and custom data
| Contact token | Signed identity | SendSevenConfig custom data | |
|---|---|---|---|
| Who creates it | Your backend, via the API | Your backend, signed with the widget secret | The page |
| Links the chat to a contact | Yes | Yes | No |
| Saves data to the contact and shares it with AI agents as verified | Yes, with save and context | No | No (shown as unverified claims) |
| Shares extra context | Yes, context (up to 4 KB) | No | Yes, unverified |
| Secret on your server | An API key | The widget secret | None |
| Revocable | Yes, per token or per contact | Rotate the secret | n/a |
Use a contact token when the data should be trusted. Anything set directly in the browser can be changed by the visitor, so SendSeven never treats it as verified.
SendSevenConfig custom data is visitor-controlledThis data can technically be changed by the visitor. For trusted data, use contact tokens. SendSevenConfig.custom is fine for non-sensitive values such as the visitor's language or the page topic. It is stored as visitor-provided, checked against the field (for example, a dropdown only accepts its options) and never overwrites a value from your team, an import, an integration, a flow or your own system.
By default the widget saves any active custom field passed in custom. To narrow this, set the widget configuration value customDataScope to form_fields (in the app: the widget's Advanced section, Custom data from your website). The widget then only saves the custom fields on its pre-chat form. all_fields is the default, and any other value is treated as form_fields.
Requirements
- An API key with the
contact_tokens:createandcontacts:readscopes. contacts:createas well, if you usecreate_if_missingto create contacts that do not exist yet.contacts:updateas well, if a call changes an existing contact: attaching or verifying a contact method, linking anexternal_id, saving fields withsaveorsave_if_empty, or merging withon_conflict: "merge". A call that only finds a contact and changes nothing does not need it.
contact_tokens:create is a high-privilege scopeA contact token makes the widget treat a visitor as a specific contact, so this scope is treated like a credential that can act as any of your contacts. It can only be added to API keys, never to OAuth apps or third-party integrations. Currently only the Owner role includes it, so an Owner must create the API key. Store the key like any other secret, on your server only.
- A live chat widget id. Find it in Widgets → your live chat widget.
1. Mint a token on your server
Mint a fresh token on every page load for a signed-in user. Never mint tokens in the browser, because that would expose your API key.
curl -X POST "https://api.sendseven.com/api/v1/contact-tokens" \
-H "Authorization: Bearer s7_api_your_api_key" \
-H "X-Tenant-ID: your-tenant-id" \
-H "Content-Type: application/json" \
-d '{
"widget_id": "3f6c1d2e-8a1b-4c5d-9e0f-123456789abc",
"contact": {
"external_id": "user-123",
"name": "Anna Weber",
"methods": [
{ "type": "email", "value": "[email protected]", "verified": true },
{ "type": "whatsapp", "value": "+4917012345678", "verified": false }
],
"create_if_missing": true,
"on_conflict": "error"
},
"context": { "open_orders": 2, "last_order_id": "A-1001" },
"save": { "plan": "Scale" },
"save_if_empty": { "company": "Acme GmbH" },
"ttl_seconds": 600
}'
Node.js
async function mintContactToken(user) {
const methods = [];
if (user.email) {
methods.push({ type: "email", value: user.email, verified: user.emailVerified === true });
}
const res = await fetch("https://api.sendseven.com/api/v1/contact-tokens", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SENDSEVEN_API_KEY}`,
"X-Tenant-ID": process.env.SENDSEVEN_TENANT_ID,
"Content-Type": "application/json",
},
body: JSON.stringify({
widget_id: process.env.SENDSEVEN_WIDGET_ID,
contact: {
external_id: String(user.id),
name: user.name,
methods,
create_if_missing: true,
},
context: { open_orders: user.openOrders },
save: { plan: user.plan },
}),
});
if (!res.ok) return null; // fall back to an anonymous chat
const { token } = await res.json();
return token;
}
Python
import os
import requests
def mint_contact_token(user):
methods = []
if user.email:
methods.append({"type": "email", "value": user.email, "verified": bool(user.email_verified)})
resp = requests.post(
"https://api.sendseven.com/api/v1/contact-tokens",
headers={
"Authorization": f"Bearer {os.environ['SENDSEVEN_API_KEY']}",
"X-Tenant-ID": os.environ["SENDSEVEN_TENANT_ID"],
},
json={
"widget_id": os.environ["SENDSEVEN_WIDGET_ID"],
"contact": {
"external_id": str(user.id),
"name": user.name,
"methods": methods,
"create_if_missing": True,
},
"context": {"open_orders": user.open_orders},
"save": {"plan": user.plan},
},
timeout=5,
)
if resp.status_code != 201:
return None # fall back to an anonymous chat
return resp.json()["token"]
The response (201 Created, sent with Cache-Control: no-store):
{
"id": "9b2f0c4e-1d3a-4e5f-8a7b-0c1d2e3f4a5b",
"token": "s7ct_Q2hhbmdlTWVJbllvdXJPd25Ub2tlbkV4YW1wbGVYWFg",
"token_prefix": "s7ct_Q2hh",
"widget_id": "3f6c1d2e-8a1b-4c5d-9e0f-123456789abc",
"contact_id": "5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9",
"expires_at": "2026-10-07T14:40:00Z",
"grant_expires_in": 86400,
"max_redemptions": 10,
"created": false,
"merged_contact_ids": [],
"skipped": []
}
| Response field | Description |
|---|---|
contact_id | The contact the token is for. |
created | true when this call created the contact. |
merged_contact_ids | Contacts merged into this one because of on_conflict: "merge". |
skipped | Contact methods that were not attached, for example {"type": "whatsapp", "value": "4917012345678", "reason": "unverified_conflict"}. |
The token value is returned only in this response. SendSeven stores a hash of it, so it cannot be shown again. Keep the id if you want to revoke this token later.
The whole call is one transaction. If any part fails (an invalid field, a conflict, a missing scope), nothing is changed and no token is created.
Request fields
| Field | Description |
|---|---|
widget_id | The live chat widget the token can be redeemed on. Required. |
contact.external_id | Your own stable user id. It is linked to the contact once and reused, the same way as a signed identity's user_id. |
contact.contact_id | An existing SendSeven contact id. |
contact.methods | Up to 10 contact methods, each {"type", "value", "verified"}. See Contact methods. |
contact.name | Display name, used only when a contact is created. To change the name of an existing contact, use save. |
contact.create_if_missing | Create the contact when nothing matches. Requires contacts:create. Default false. |
contact.on_conflict | What happens when a method belongs to a different contact: error (default) or merge. See Conflicts. |
context | Any JSON object, up to 4 KB serialized, shared with the chat as verified context. Never stored on the contact. |
save | Fields written to the contact now, replacing the current value. See Saving data to the contact. |
save_if_empty | Like save, but only fills fields that are empty. |
ttl_seconds | How long the token can be redeemed, 30 to 3600 seconds. Default 600. |
grant_ttl_seconds | How long the shared data stays valid in the chat after redeem, 60 to 604800 seconds. Default 86400 (24 hours). |
max_redemptions | How often the token may be redeemed, for page reloads and extra tabs, 1 to 50. Default 10. |
expose | Deprecated. See Legacy request shape. |
Contact methods
Each entry in contact.methods is one email address, phone number or WhatsApp number:
| Key | Description |
|---|---|
type | email, phone or whatsapp. Any other type returns 422 invalid_method_type. |
value | The address or number. Phone numbers are stored in international format. WhatsApp numbers are accepted with or without a leading +, and are stored without it. |
verified | true if your own system has confirmed the method, for example through a confirmation link or an SMS code. Default false. |
verified: truestores the method as verified on the contact. AI agents treat it as confirmed, and it can be used to find the contact.verified: falseattaches the method as an unverified claim. It is never used to find a contact.- A method that is already verified stays verified. Sending it again with
verified: falsenever removes its verification.
Only set verified: true for methods you have really confirmed. A verified method is used to find the existing contact, including its conversation history.
How the contact is found
The first match wins:
- The contact linked to
external_id. - The contact with this
contact_id. An unknowncontact_idreturns404 contact_not_found. - The contact that holds one of your
verified: truemethods as a verified method.
Unverified methods are never used to match. If nothing matches and create_if_missing is true, a new contact is created (this needs contacts:create). Otherwise the request fails with 404 contact_not_found.
When external_id was not linked yet, it is linked to the contact that was found or created.
Conflicts
A method you send is a conflict when it already belongs to a different contact than the one found. SendSeven checks every method before it changes anything.
-
on_conflict: "error"(default). The request fails with409 identifier_conflictand nothing changes:{
"detail": {
"error": "One or more contact methods belong to another contact",
"code": "identifier_conflict",
"conflicts": [
{ "type": "email", "value": "[email protected]", "contact_id": "0a1b2c3d-..." }
]
}
} -
on_conflict: "merge". A contact is merged into the contact found only when you send the method as verified and that contact's own copy of it is verified too. The merge works exactly like a merge in the app. The method then belongs to the contact found, and the token is created. This needscontacts:update.- Any other conflict never causes a merge: a method you send unverified, or a method whose copy on the other contact is unverified (for example, typed in by a chat visitor). That method is not attached, and it is listed in the response as
skippedwith the reasonunverified_conflict. - If your API key is limited to certain inboxes, contacts outside them are not visible to it. Merging such a contact, or saving fields to it, returns
404 contact_not_found. - If the merge cannot be completed, the request fails with
409 merge_failedand nothing changes.
- Any other conflict never causes a merge: a method you send unverified, or a method whose copy on the other contact is unverified (for example, typed in by a chat visitor). That method is not attached, and it is listed in the response as
SendSeven never moves an email address or phone number from one contact to another automatically. Your team decides that by merging contacts. on_conflict: "merge" is the one explicit exception: your server asks for the merge, for a method it has verified. Use it only when your own system is the source of truth for who owns that email address or number.
Saving data to the contact
save and save_if_empty write data to the contact at the time the token is created. The data stays on the contact after the chat.
{
"save": { "plan": "Scale", "language": "de" },
"save_if_empty": { "company": "Acme GmbH" }
}
- Keys are standard fields (
name,first_name,last_name,language) or the key of an active custom field. Any other key returns422 unknown_fieldnaming the key. Email addresses and phone numbers go incontact.methods, not insave. savereplaces the current value.save_if_emptyonly writes a field that is empty.- A key may be in
saveor insave_if_empty, not both. - Values are checked like any other custom field value, for example a dropdown only accepts its options. An invalid value returns
422 invalid_field_valuenaming the key. - Saving needs
contacts:update, orcontacts:createwhen this call created the contact. A contact outside the inbox access of your API key returns404 contact_not_found. - AI agents see saved values as data on record. Each custom field's AI access setting still decides whether an AI agent may change the value later.
save, save_if_empty or context?
save / save_if_empty | context | |
|---|---|---|
| Stored on the contact | Yes, until changed | No |
| Seen by AI agents | Yes, as data on record | Yes, as verified context for this chat |
| Keys | Standard fields and existing custom fields | Anything, up to 4 KB |
| Typical use | Plan, customer type, company | Open orders, cart value, last order id |
Use context for data that changes often or is only relevant to this chat. It is kept only with the token and the chat session, and is removed when they expire.
Legacy request shape (expose)
Requests in the earlier shape keep working:
{
"widget_id": "3f6c1d2e-8a1b-4c5d-9e0f-123456789abc",
"contact": {
"external_id": "user-123",
"email": "[email protected]",
"create_if_missing": true
},
"expose": {
"standard_fields": ["name", "email"],
"custom_fields": ["plan", "customer_since"]
},
"context": { "open_orders": 2 }
}
contact.emailandcontact.phonefind the contact that holds them, in the ordercontact_id,external_id, email, phone, and are attached to it when no other contact holds them.exposeshares fields that are already stored on the contact with the chat.expose.standard_fieldsacceptsname,first_name,last_name,email,phone,language,country,timezone,company,created_atandchannel.expose.custom_fieldsaccepts up to 50 custom field keys. Values are read when the token is redeemed. Only values set by your team, through the API, an import, an integration, a flow or a contact token are shared.
expose is deprecated but still supported. For new integrations, write data with save or save_if_empty, and choose what AI agents read in the agent's contact context settings and each custom field's AI access setting. See How SendSeven uses contact context.
As soon as a request uses contact.methods, contact.on_conflict, save or save_if_empty, the matching and conflict rules above apply. contact.email and contact.phone in such a request are treated as unverified methods.
2. Pass the token to the widget
Set the token before the widget script loads:
<script>
window.SendSevenConfig = {
contactToken: "{{ contact_token }}"
};
</script>
<script
src="https://widget.sendseven.com/widget-support.js"
data-widget-id="YOUR_WIDGET_ID"
data-tenant-id="YOUR_TENANT_ID"
async
></script>
In a single-page app, fetch the token from your own backend after login, set window.SendSevenConfig.contactToken, and only then load the widget. If minting fails, leave contactToken out and the visitor chats anonymously as usual.
contactToken can be combined with a signed identity. Call window.SendSeven.logout() when the user signs out, exactly as described in Logout and shared devices.
Lifetime and redemption
- Redeem window (
ttl_seconds). The widget must redeem the token within this window. Expired tokens are ignored, and the chat starts without verified context. - Redemptions (
max_redemptions). Each page load or tab that starts or resumes a chat with the token uses one redemption. When they are used up, further redeems are ignored. - Shared data (
grant_ttl_seconds). After a successful redeem,context(and, in the legacy shape, theexposefields) stay attached to that chat session for this long. Values are a snapshot taken at redeem time. Mint a new token to refresh them. - Failures are silent for the visitor. An invalid, expired, revoked or exhausted token never blocks the chat. The visitor can always chat, just without verified context.
Revoking tokens
Revoke a single token:
curl -X DELETE "https://api.sendseven.com/api/v1/contact-tokens/9b2f0c4e-1d3a-4e5f-8a7b-0c1d2e3f4a5b" \
-H "Authorization: Bearer s7_api_your_api_key" \
-H "X-Tenant-ID: your-tenant-id"
Revoke every token of a contact, for example on logout or when a user's access changes:
curl -X POST "https://api.sendseven.com/api/v1/contacts/5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9/contact-tokens/revoke" \
-H "Authorization: Bearer s7_api_your_api_key" \
-H "X-Tenant-ID: your-tenant-id"
Revoking takes effect immediately: the token can no longer be redeemed, and chats that already redeemed it stop using its shared data. Revoking an already revoked token succeeds.
Security notes
- Keep your API key on the server. Only the token goes to the page.
- Mint per page load and keep the TTL short. Anyone who copies a token within its redeem window can start a chat as that contact. Never put tokens in URLs, logs or analytics.
- Mark only methods you have confirmed as
verified. Verified methods are used to find the existing contact, including its conversation history. - Use
on_conflict: "merge"deliberately. It merges contacts without a person reviewing them. - Share only what the chat needs. Do not put secrets, passwords or payment data into
contextorsave. - Revoke on logout with the per-contact revoke endpoint, and call
window.SendSeven.logout()in the browser.
API reference
| Method | Endpoint | Required scope | Description |
|---|---|---|---|
POST | /api/v1/contact-tokens | contact_tokens:create + contacts:read (+ contacts:create for create_if_missing, + contacts:update to change an existing contact or to merge) | Mint a token. Returns 201 with the token. |
DELETE | /api/v1/contact-tokens/{token_id} | contact_tokens:create | Revoke one token. Returns {"success": true, "id": "..."}. |
POST | /api/v1/contacts/{contact_id}/contact-tokens/revoke | contact_tokens:create | Revoke all tokens of a contact. Returns {"success": true, "id": "...", "revoked": 2}. |
Error bodies look like {"detail": {"error": "...", "code": "context_too_large"}}. Errors about a specific field also carry field, and identifier_conflict carries conflicts.
| Status | Code | Meaning |
|---|---|---|
403 | insufficient_scope | The API key lacks contacts:read, lacks contacts:create for create_if_missing, or lacks contacts:update while the call would change an existing contact or uses on_conflict: "merge". |
404 | widget_not_found | The widget does not exist in this workspace. |
404 | contact_not_found | No contact matched and create_if_missing is false, the contact_id does not exist, a contact to merge or update is outside your API key's inbox access, or the contact id in a revoke call does not exist. |
404 | token_not_found | The token id in a revoke call does not exist. |
409 | identifier_conflict | A contact method belongs to another contact and on_conflict is error. Nothing was changed. |
409 | merge_failed | The contacts could not be merged. Nothing was changed. |
422 | invalid_contact | No contact_id, external_id, method, email or phone was given, or a value is malformed. |
422 | invalid_method_type / invalid_method_value | A method has an unsupported type or an invalid value. too_many_methods: more than 10 methods. |
422 | invalid_on_conflict | on_conflict is not error or merge. |
422 | unknown_field | A key in save or save_if_empty is not a writable standard field or an active custom field. |
422 | invalid_field_value | A value in save or save_if_empty is not valid for its field. |
422 | invalid_save | save or save_if_empty is not an object, has more than 50 keys, or both contain the same key. |
422 | invalid_expose | expose.standard_fields or expose.custom_fields is not a list. |
422 | unknown_standard_field | expose.standard_fields contains a field that is not in the list above. |
422 | unknown_custom_field | expose.custom_fields contains a key that is not an active custom field. |
422 | too_many_custom_fields | More than 50 custom fields were requested in expose. |
422 | invalid_context / context_too_large | context is not a JSON object, or is larger than 4 KB. |
422 | invalid_ttl / invalid_max_redemptions | A lifetime or redemption value is outside its range. |
429 | rate_limited | More than 120 tokens per minute were requested per API key or per workspace. Retry after the Retry-After header. |
Related
- Live Chat Identity Verification - link chats to your users with a signed identity
- Live Chat - live chat channel overview
- Widget Appearance & Channels - widget configuration options