Skip to main content

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.

Pro use case

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​

  1. A user signs in to your site or app.
  2. Your backend calls POST /api/v1/contact-tokens with the widget, the contact, and optionally data to save and context for the chat.
  3. SendSeven returns a short-lived token that starts with s7ct_.
  4. Your page sets window.SendSevenConfig.contactToken before loading the widget.
  5. When the chat starts, the widget redeems the token. The conversation is linked to the contact, and your context object 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 tokenSigned identitySendSevenConfig custom data
Who creates itYour backend, via the APIYour backend, signed with the widget secretThe page
Links the chat to a contactYesYesNo
Saves data to the contact and shares it with AI agents as verifiedYes, with save and contextNoNo (shown as unverified claims)
Shares extra contextYes, context (up to 4 KB)NoYes, unverified
Secret on your serverAn API keyThe widget secretNone
RevocableYes, per token or per contactRotate the secretn/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-controlled

This 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:create and contacts:read scopes.
  • contacts:create as well, if you use create_if_missing to create contacts that do not exist yet.
  • contacts:update as well, if a call changes an existing contact: attaching or verifying a contact method, linking an external_id, saving fields with save or save_if_empty, or merging with on_conflict: "merge". A call that only finds a contact and changes nothing does not need it.
contact_tokens:create is a high-privilege scope

A 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 fieldDescription
contact_idThe contact the token is for.
createdtrue when this call created the contact.
merged_contact_idsContacts merged into this one because of on_conflict: "merge".
skippedContact 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​

FieldDescription
widget_idThe live chat widget the token can be redeemed on. Required.
contact.external_idYour 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_idAn existing SendSeven contact id.
contact.methodsUp to 10 contact methods, each {"type", "value", "verified"}. See Contact methods.
contact.nameDisplay name, used only when a contact is created. To change the name of an existing contact, use save.
contact.create_if_missingCreate the contact when nothing matches. Requires contacts:create. Default false.
contact.on_conflictWhat happens when a method belongs to a different contact: error (default) or merge. See Conflicts.
contextAny JSON object, up to 4 KB serialized, shared with the chat as verified context. Never stored on the contact.
saveFields written to the contact now, replacing the current value. See Saving data to the contact.
save_if_emptyLike save, but only fills fields that are empty.
ttl_secondsHow long the token can be redeemed, 30 to 3600 seconds. Default 600.
grant_ttl_secondsHow long the shared data stays valid in the chat after redeem, 60 to 604800 seconds. Default 86400 (24 hours).
max_redemptionsHow often the token may be redeemed, for page reloads and extra tabs, 1 to 50. Default 10.
exposeDeprecated. See Legacy request shape.

Contact methods​

Each entry in contact.methods is one email address, phone number or WhatsApp number:

KeyDescription
typeemail, phone or whatsapp. Any other type returns 422 invalid_method_type.
valueThe address or number. Phone numbers are stored in international format. WhatsApp numbers are accepted with or without a leading +, and are stored without it.
verifiedtrue if your own system has confirmed the method, for example through a confirmation link or an SMS code. Default false.
  • verified: true stores the method as verified on the contact. AI agents treat it as confirmed, and it can be used to find the contact.
  • verified: false attaches 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: false never 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:

  1. The contact linked to external_id.
  2. The contact with this contact_id. An unknown contact_id returns 404 contact_not_found.
  3. The contact that holds one of your verified: true methods 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 with 409 identifier_conflict and 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 needs contacts: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 skipped with the reason unverified_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_failed and nothing changes.
Methods never move on their own

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 returns 422 unknown_field naming the key. Email addresses and phone numbers go in contact.methods, not in save.
  • save replaces the current value. save_if_empty only writes a field that is empty.
  • A key may be in save or in save_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_value naming the key.
  • Saving needs contacts:update, or contacts:create when this call created the contact. A contact outside the inbox access of your API key returns 404 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_emptycontext
Stored on the contactYes, until changedNo
Seen by AI agentsYes, as data on recordYes, as verified context for this chat
KeysStandard fields and existing custom fieldsAnything, up to 4 KB
Typical usePlan, customer type, companyOpen 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.email and contact.phone find the contact that holds them, in the order contact_id, external_id, email, phone, and are attached to it when no other contact holds them.
  • expose shares fields that are already stored on the contact with the chat. expose.standard_fields accepts name, first_name, last_name, email, phone, language, country, timezone, company, created_at and channel. expose.custom_fields accepts 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, the expose fields) 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 context or save.
  • Revoke on logout with the per-contact revoke endpoint, and call window.SendSeven.logout() in the browser.

API reference​

MethodEndpointRequired scopeDescription
POST/api/v1/contact-tokenscontact_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:createRevoke one token. Returns {"success": true, "id": "..."}.
POST/api/v1/contacts/{contact_id}/contact-tokens/revokecontact_tokens:createRevoke 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.

StatusCodeMeaning
403insufficient_scopeThe 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".
404widget_not_foundThe widget does not exist in this workspace.
404contact_not_foundNo 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.
404token_not_foundThe token id in a revoke call does not exist.
409identifier_conflictA contact method belongs to another contact and on_conflict is error. Nothing was changed.
409merge_failedThe contacts could not be merged. Nothing was changed.
422invalid_contactNo contact_id, external_id, method, email or phone was given, or a value is malformed.
422invalid_method_type / invalid_method_valueA method has an unsupported type or an invalid value. too_many_methods: more than 10 methods.
422invalid_on_conflicton_conflict is not error or merge.
422unknown_fieldA key in save or save_if_empty is not a writable standard field or an active custom field.
422invalid_field_valueA value in save or save_if_empty is not valid for its field.
422invalid_savesave or save_if_empty is not an object, has more than 50 keys, or both contain the same key.
422invalid_exposeexpose.standard_fields or expose.custom_fields is not a list.
422unknown_standard_fieldexpose.standard_fields contains a field that is not in the list above.
422unknown_custom_fieldexpose.custom_fields contains a key that is not an active custom field.
422too_many_custom_fieldsMore than 50 custom fields were requested in expose.
422invalid_context / context_too_largecontext is not a JSON object, or is larger than 4 KB.
422invalid_ttl / invalid_max_redemptionsA lifetime or redemption value is outside its range.
429rate_limitedMore than 120 tokens per minute were requested per API key or per workspace. Retry after the Retry-After header.