asssistive
Public API

Authentication

Why the Public API uses two credentials — a publishable key for your server, and a visitor token for the device.

The Public API uses two credentials, and the split is the whole design. Understanding why there are two — rather than one key that does everything — tells you where each one belongs in your architecture.

The publishable key (pk_)

A publishable key identifies your organization. It authorizes exactly two things:

  • Starting a conversation.
  • Reading help-center content — articles and categories.

That's the complete list. It cannot read or write any conversation that already exists, including ones it started itself. Once a conversation exists, the publishable key is done with it forever.

Send it as:

Authorization: Bearer pk_example_k3Yb9wQ2rTfA8sLdN1vH

Creating one

Go to agent.useassistive.com/settings/api-keysOwners and Admins only — and click New key. Give it a name, then choose a Type.

Type defaults to Secret — you must actively choose Publishable

The dialog offers two types, Secret (sk_) and Publishable (pk_), and Secret is selected by default. Clicking straight through gives you an sk_ key, which the Public API rejects outright — and it fails with the same opaque 401 as a key that doesn't exist, so nothing tells you that's what happened.

If you're building against the Public API, pick Publishable before you hit Create key.

The plaintext then appears once, on a Copy your API key step that tells you plainly: "This secret is shown only once. Store it somewhere safe — you won't be able to see it again." Copy it into your secrets manager before you press Done.

Despite the name, don't publish a publishable key

"Publishable" is about what it can't read, not about it being safe to hand out. On this API a pk_ key can start conversations on your organization's behalf, and every key your organization holds shares one rate budget — so a leaked key can be used to open junk conversations in your inbox and burn the whole organization's quota in the process.

Treat it as semi-trusted: keep it on your server, exactly as you would any other API credential.

How keys are stored

A key is 256 bits of entropy, base64url-encoded, and stored only as a SHA-256 hash. The plaintext is shown once, at creation, and is unrecoverable afterwards — not by you, not by support, not by anyone. Copy it into your secrets manager the moment you see it.

The secret key kind is rejected here

Assistive also has a secret key kind (sk_). The Public API does not accept it. It isn't a lesser-privileged fallback or an alternative — it simply doesn't authenticate on this surface.

And notice how the failure looks. A revoked key, an unknown key, and an sk_ key all fail identically:

{
  "success": false,
  "request_id": "6d7979ce52075b24979cd5b415319339",
  "error": {
    "code": "UNAUTHORIZED",
    "message": "the api key is invalid or has been revoked"
  }
}

The API never tells you which of the three it was — that would confirm to an attacker that a key they found is real but revoked, or real but the wrong kind. When you're debugging a 401, check the key's prefix and its status in the dashboard; the response won't narrow it down for you.

The visitor token (vt_)

A visitor token identifies one conversation — not a user, not a session, one conversation.

It's minted by POST /conversations and returned exactly once, in that response. It is the credential for reading and posting messages on that conversation, and it's sent the same way as the key:

Authorization: Bearer vt_example_7Jq4Xm2Pz9Ln6Rd3Wb5T

Same construction as a publishable key: 256 bits, base64url, stored only as a SHA-256 hash.

It is returned once, and there is no recovery

If you lose a visitor token, you cannot get it back — not from an endpoint, not from the dashboard. The only remedy is to start a new conversation, which gives the user an empty thread and leaves their old one stranded where only an agent can see it.

Persist it server-side, against the user it belongs to, in the same transaction that handles the response.

Lifetime: 30 days of disuse

A visitor token expires after 30 days of disuse, and the window slides: every successful read or post pushes the expiry out again.

So the token for a conversation your user is still having never ages out. A token nobody has touched for 30 days goes cold. In practice this means an active support thread stays live indefinitely, and abandoned ones clean themselves up.

There is no refresh flow. If a token does expire, you don't renew it — your server starts a new conversation to get a new one. Plan for that: a user coming back after a long silence gets a fresh thread rather than a resurrected one.

Revocation

An agent can revoke a conversation's token from the dashboard. That cuts the client off immediately and permanently — there's no grace period and no way to un-revoke.

It's the escape hatch for a token you believe has leaked. Note that it's your agents who have this power, not your integration: there is no API to revoke a token.

Expired, revoked, and unknown all fail identically

All three cases return the same thing:

401 UNAUTHORIZED — the visitor token is invalid

This is deliberate — a token's state cannot be probed. You cannot tell from the response whether a token was never real, has gone cold, or was cut off by an agent, and neither can anyone who steals one.

The consequence for your code: don't write three branches. There's one — the token no longer works, so start a new conversation.

Why two credentials

Here is the reasoning, because it dictates how you build:

The publishable key belongs on your server. The visitor token is what may reach an end user's device.

A visitor token is scoped to a single conversation. So if one leaks — off a stolen phone, out of a logged network request, through a compromised client — the blast radius is that one conversation. The leaked token cannot read any other conversation, cannot post to one, and cannot start new ones. It is the smallest useful credential the API could hand a client, which is precisely why it's the one that's safe to hand them.

A publishable key, handed to the same client, would let them start unlimited conversations against your organization's shared quota.

Presenting a token for a conversation it doesn't own returns 404

Not 403. This is deliberate:

404 NOT_FOUND — conversation not found

A 403 would be an admission that the conversation exists and the caller simply isn't allowed to see it. That turns a valid visitor token into an oracle for enumerating which conversation IDs are real. A 404 reveals nothing, so there's nothing to probe for.

The practical consequence when debugging: a 404 on a conversation you know exists almost always means you're using the wrong token, not that the conversation is gone.

The integration shape this implies

Put together, the two credentials point at one architecture:

  1. Your backend holds the pk_ key and calls POST /conversations.
  2. Your backend stores the returned conversation_id and visitor_token in your own database, against the signed-in user.
  3. Your backend hands only the visitor token to the client.
  4. The client polls for messages and posts replies directly, using that token.

Your server stays in the loop for the one privileged operation (starting a conversation, where you decide who the user is) and stays out of the loop for the chatty ones (every message, in both directions).

The full walkthrough is in Embedding support chat.

Rate limits

The two credentials are limited on different axes, which is the point — a chatty end user can't eat the budget your backend needs.

EndpointsCredentialLimitedDefault
Start a conversation; articles; categoriespk_Per organization120/min
Read and post messagesvt_Per token60/min

Publishable-key endpoints share one organization-wide budget. Every key your organization holds draws on it. Minting a second key for a second integration buys you no extra headroom, and one misbehaving integration can starve the others.

Visitor-token endpoints are limited per token — each conversation gets its own budget, so one client hammering its own conversation cannot starve anybody else's. This is what makes it safe to hand the token to a device and let it talk to us directly.

60/min is one request per second — polling faster will hit it

A poll every second is exactly the limit, so any jitter or retry puts you over it. Poll every 3–5 seconds while the chat is open and back off when it isn't. See the polling strategy.

Limits are GCRA-based, so they refill smoothly rather than resetting on a hard window boundary, and they fail open — if the limiter backend is unavailable, requests are allowed through rather than rejected. Read X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset off the response; they are the source of truth, not any number written here.

Key hygiene

  • One key per integration. Sharing a key across systems means one leak, or one revocation, hits all of them at once.
  • Keep them out of source control. Environment variables or a secrets manager.
  • Rotate by swapping, not by breaking. There's no in-place rotate: create the new key, deploy it everywhere, then revoke the old one. Revocation takes effect immediately, with no grace period, so doing it in that order is what gives you zero downtime.

On this page