BrightshoalDocsOpen console
MenuAuthentication

Get started

Authentication

Which credential to use where, what a key can reach, and what the API says when it refuses one.

Three credentials

CredentialLooks likeUse it fromIt can
Secret API keyvak_ + 32 hex charactersYour servers onlyCall everything under /api
Publishable keypk_live_ (site), pk_test_ (test) or pk_preview_ (preview link) + 32 hex charactersBrowsers, page sourceRead one assistant's chat settings and start conversations with it
Conversation tokenct_ + 48 hex charactersThe browser that started the conversationSend messages to, read, talk in and end that one conversation

A secret key goes in the Authorization header as a bearer token:

Terminal
curl https://api.myleadsfactory.tech/api/assistants \
  -H "Authorization: Bearer $MLF_API_KEY"

The API does not answer browsers on /api (there is no CORS policy for it), so a secret key in front-end code fails as well as leaking. For anything a visitor's browser does, use a publishable key.

Create a secret key

In the console, open Settings > API keys and create one. Only organization owners can. The full key is shown once; afterwards the console shows its first characters so you can tell keys apart. Revoking a key there takes effect on the next request.

Two choices shape what the key can do:

Org-wide or one project. A project key reads and writes only inside its project, and every resource it creates lands there. An org-wide key reaches every project, so creating something (an assistant, a number, a webhook) needs a project_id in the body, and lists accept ?project_id= to narrow down. A few areas need an org-wide key: knowledge, WhatsApp, creating projects and listing members.

Full access or read-only. A read-only key can make GET (and HEAD) requests, plus knowledge search, a POST that only reads, and nothing else. Use one for dashboards and exports, so a leaked key cannot change anything.

Keys can't create other keys: /api has no API-key endpoints, so a leaked key cannot mint its own successor.

When a key is refused

Every refusal is a status code and a JSON body whose error is a string code, with any details beside it. Errors lists the codes.

StatusBodyWhy
401{"error":"unauthorized","message":"Missing or malformed Authorization header."}No Authorization: Bearer ... header
401{"error":"unauthorized","message":"Invalid or revoked API key."}Wrong, mistyped or revoked key
403{"error":"read_only_key"}A read-only key tried to write
403{"error":"project_key_not_allowed","message":"This route needs an org-wide API key."}A project key on an org-wide route, such as listing members
403{"error":"invalid_request","message":"A project-scoped key cannot create projects."}A project key creating a project
403{"error":"insufficient_role","required":"owner","actual":"admin"}An action the admin role cannot take
404{"error":"not_found"}It does not exist, or it is in a project the key cannot see

A resource outside your key's project answers 404, not 403, so a key never learns what exists elsewhere.

Publishable keys

A publishable key is created with your secret key and pinned to one assistant (or to a project, in which case each request names the assistant):

curl -X POST https://api.myleadsfactory.tech/api/publishable-keys \
  -H "Authorization: Bearer $MLF_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "assistant_id": "asst_7d2e9b14c6a04f3e8b5d1a9c0e7f6b23" }'

What protects a site key is the assistant's allowed origins (config.channels.web_chat.allowed_origins). With origins listed, a conversation only starts from those exact origins (scheme, host and port; no wildcards) and anything else gets 403 {"error":"origin_not_allowed"}. When your own page calls the API, the origin checked is the one the browser sends in its Origin header, which a page cannot fake, so another site cannot borrow yours. The widget, which runs in a frame served by the API, names its host page in parent_origin, and so may a request with no Origin header at all. Allowed origins keep other sites' pages out; they do not authenticate a server, which can send any headers it likes. With the list empty, any origin may start a chat, which is handy while you build and wrong for production.

A test key carries its own allowed_origins (at least one, up to 20), set when you create it, and its chats run the assistant's draft (or live config when there is no draft). Web chat must still be on. A preview link loads from any site and works while web chat is off; turn it off or let it expire to stop it. See live and test.

Revoke a publishable key with DELETE/api/publishable-keys/{id}. Pages that still carry it stop loading the chat, so publish the replacement first.

Conversation tokens

Starting a conversation returns a ct_... token, valid for up to 30 days. A token from a test key stops working when that key is revoked, and one from a preview link when the link is turned off or expires. Send it as Authorization: Bearer ct_... on that conversation's messages, updates, attachments, voice sessions and end. It reaches nothing else. Build your own chat UI walks through the full exchange.

Keep keys safe

  • Keep secret keys in your server's environment or secret manager, never in source control.
  • One key per system, named after it, so you can revoke one integration without breaking the rest.
  • Prefer project keys and read-only keys: give each system the least it needs.
  • Rotate by creating the new key, deploying it, then revoking the old one.

Stuck on authentication?