Developers
The Botender API
Read your dispensaries, their leads, Insights and knowledge from your own tools, ask the assistant, and get events as they happen. A JSON API over HTTPS, with keys you make and limit yourself. It comes with a paid plan.
Start
- Sign in, open Settings and make a key under API keys. Copy it then: it is shown once.
- Send it with every request as "Authorization: Bearer <key>". Keep it on your server, never in a web page.
- Ask who you are: GET /api/v1/account answers with the account and what the key may do.
curl https://botender.com/api/v1/account \
-H "Authorization: Bearer btk_..."Keys
- A key starts with "btk_". We keep only a fingerprint of it, so a lost key cannot be shown again: revoke it and make another.
- Limit a key to some dispensaries, or give it all of them, including ones added later.
- Limit it to some parts of a dispensary, the same parts a team member can be given: Inbox, Leads, Widget, Knowledge, Rules, Settings, Insights. Each endpoint below names the part it needs.
- A key reads only, unless you let it write: change a lead's status, subscribe webhooks, ask the assistant and search the knowledge. A key that only reads never spends credits.
- A key that can write may also add dispensaries (a key with every dispensary and every part), change a dispensary's name and status, and add, change and delete its typed documents and questions and answers. A key with the Insights part reads a dispensary's monthly reports.
- Of a dispensary's settings, a key changes words and looks only, each with the part it is on: ai_name, ai_tone, business_address, services, knowledge_glossary, response_language, business_name, hours, widget_title, widget_color, widget_secondary_color, widget_theme, widget_font_family, widget_position, widget_avatar, widget_launcher_label, widget_offset_x, widget_offset_y, widget_greeting, widget_greeting_b, widget_labels, widget_prompt_rules, widget_standing_rewards. Of widget_labels, only the chat window's, feedback, error, product button and "Help me choose" lines, one by one. Texts are held shorter than in the dashboard, and none may hold a web address, an email address or a phone number but the dispensary's own website and phone number. The OpenAPI document gives each one's limits.
- No key deletes a dispensary or changes its persona, instructions, phone numbers, menu, keys, links, where shoppers' details or reports go, emails, credits, the age check, disclaimer or other compliance settings, the plan's settings, or the team: those stay in the dashboard.
- A key can expire in 30, 90, 365 days, or never. An account holds up to 20 keys.
- Owners and admins with the whole account make keys. A key stops working when it is revoked, when it expires, and when the person who made it leaves the account or loses access to all of it; then it is revoked for good. Settings shows who made each key and which no longer works.
Limits
- 120 calls a minute per key, and 300 a minute across an account's keys.
- A key's own limit comes first: a key over it never uses up the account's. Calls a key may not make (a part, a write or a dispensary it was not given) count only against 30 a minute of their own.
- Asking the assistant: 60 a minute per key, with the chat's usual reply limits. Each reply uses the account's credits, as a shopper's does.
- Searching the knowledge: 60 a minute per key, 600 an hour per dispensary and 2,000 an hour per account. Each search takes 1 credit before it runs, and gives it back if it could not.
- Insights for a dispensary: 20 a minute per key.
- Changes to dispensaries and knowledge: 30 a minute per key, counted only for a change that is made. A dispensary's rules texts: 5 changes an hour. A dispensary is added only within the plan's locations.
- Knowledge: 200 changes an hour per key and 600 per account, and 100,000 characters of text an hour per key and 250,000 per account. Through the API a dispensary holds up to 500 documents and 500,000 characters of its own.
- 30 requests a minute without a working key from one address, then 429 for that address's failing requests.
- Over a limit, the answer is 429 with a Retry-After header, in seconds.
Endpoints
Every dispensary id in an address must be one of your account's, and one the key may use; any other answers 404.
get/api/v1/account
The account and this key
The account the key belongs to, and what the key may do.
Needs: Any key
get/api/v1/dispensaries
List dispensaries
The dispensaries this key may use, oldest first.
Needs: Any key
post/api/v1/dispensaries
Add a dispensary
Adds a dispensary to the account, as Add a dispensary does: it starts in setup, with the starter settings. The plan's locations are counted before and after; past them the answer is 403 plan_limit. A slug already in the account answers 409 conflict. A dispensary is never deleted through the API.
Needs: Every dispensary and part, write · Limit: 30 changes a minute per key; the plan's locations
get/api/v1/dispensaries/{id}
Get a dispensary
One dispensary: its name, status and the addresses of its chat.
Needs: Any key
patch/api/v1/dispensaries/{id}
Change a dispensary
Changes its name and status (the Settings part) and some of its settings: words and looks, each checked as the dashboard checks it and needing the part it is on (see DispensaryConfiguration for the list and the limits). No text may hold a web address, an email address or a phone number but the dispensary's own. Send only what should change: settings left out are kept, and the chat's own lines change one by one. Anything else (the persona and instructions, phone numbers, menus, keys, addresses, where shoppers' details go, emails, credits, compliance, team members and invitations) stays in the dashboard. A change made in the dashboard at the same moment is never undone: the save is made over what was read, or read again.
Needs: Any key, write · Limit: 30 changes a minute per key; the rules texts 5 changes an hour per dispensary
get/api/v1/dispensaries/{id}/stats
Insights for a dispensary
The numbers Insights and the monthly report show for a period: chats, resolved, handoffs, ratings, time saved and topics. No shopper's words.
Needs: Insights · Limit: 20 a minute per key
get/api/v1/stats
A month across the dispensaries
A month's totals across the dispensaries this key may use, from the monthly reports kept for each.
Needs: Insights
get/api/v1/dispensaries/{id}/reports
Monthly reports
The dispensary's monthly report as data: the numbers kept for the month and the paragraph of the emailed report, never computed again, and the months kept. Not who it is emailed to.
Needs: Insights
get/api/v1/dispensaries/{id}/leads
List leads
The dispensary's leads, newest first.
Needs: Leads
patch/api/v1/dispensaries/{id}/leads/{leadId}
Set a lead's status
Moves a lead along: new, contacted, qualified, converted or lost.
Needs: Leads, write
get/api/v1/dispensaries/{id}/knowledge/{docId}
Get a knowledge document
One document the assistant answers from, with its text: the dispensary's own, or one every dispensary in the account reads (only for a key with every dispensary).
Needs: Knowledge
post/api/v1/dispensaries/{id}/knowledge
Add knowledge
Adds a typed document or a question and its answer for the dispensary, for every store or some, as the Knowledge tab does: 201 for a new document, 200 when it replaced the answer to the same question for the same stores. The assistant reads it once it is embedded, usually within a minute. Past the dispensary's documents or characters the answer is 409 knowledge_limit. Pages and files by address are added in the dashboard.
Needs: Knowledge, write · Limit: 30 changes a minute per key; knowledge changes 200 an hour per key and 600 per account; text written 100,000 characters an hour per key and 250,000 per account; at most 500 documents and 500,000 characters of a dispensary's own
patch/api/v1/dispensaries/{id}/knowledge/{docId}
Change knowledge
Changes one of the dispensary's own documents: its title and text (a question and answer: both together), whether the assistant reads it, and its stores. A document every dispensary reads answers 404.
Needs: Knowledge, write · Limit: 30 changes a minute per key; knowledge changes 200 an hour per key and 600 per account; text written 100,000 characters an hour per key and 250,000 per account; at most 500 documents and 500,000 characters of a dispensary's own
delete/api/v1/dispensaries/{id}/knowledge/{docId}
Delete knowledge
Deletes one of the dispensary's own documents. A document every dispensary reads answers 404.
Needs: Knowledge, write · Limit: 30 changes a minute per key; knowledge changes 200 an hour per key and 600 per account; text written 100,000 characters an hour per key and 250,000 per account; at most 500 documents and 500,000 characters of a dispensary's own
post/api/v1/dispensaries/{id}/knowledge/search
Search the knowledge
What the assistant would read to answer a question: the documents and the text it would be given. Each search uses a credit, taken before it runs (402 when the account has none), so it needs a key that can write.
Needs: Knowledge, write · Limit: 60 a minute per key, 600 an hour per dispensary and 2,000 an hour per account; 1 credit a search
post/api/v1/dispensaries/{id}/chat
Ask the assistant
Sends a message to the dispensary's assistant, as a shopper would in the chat, and answers with its reply and the chat's sessionId. Send the sessionId back to continue the chat; only the key that started a chat can. Send "Accept: text/event-stream" for the reply as it is written. The chat shows in the Inbox, and its replies use the account's credits. When your team takes the chat over, the answer is empty with "paused": true: read their replies from .../chat/{sessionId}/replies. A store whose menu is searched through Jane's DM API answers without products here: Jane needs every product it returns reported as seen by the shopper, which only the chat on a website does.
Needs: Widget, write · Limit: 60 a minute per key; the chat's usual reply caps and credits
get/api/v1/dispensaries/{id}/chat/{sessionId}/replies
Read the team's replies
The replies your team wrote in an API chat from the Inbox, oldest first, and whether a person has the chat now. Only the key that started the chat can read it. Ask every 10 seconds or more while a chat is paused.
Needs: Widget
get/api/v1/dispensaries/{id}/webhooks
List webhooks
The dispensary's webhook subscriptions. Their addresses only for a key that can change them.
Needs: Rules
post/api/v1/dispensaries/{id}/webhooks
Subscribe a webhook
Sends the events named (lead.created, conversation.escalated, conversation.rated) to an address, signed. Needs the Leads part too, since events carry shoppers' details. At most 10 per dispensary. The subscription belongs to this key: it is removed when the key is revoked or its maker leaves the account or loses access to all of it, and nothing is sent to it while the key is expired.
Needs: Rules, write · Limit: 30 changes an hour per dispensary
delete/api/v1/dispensaries/{id}/webhooks/{subId}
Unsubscribe a webhook
Removes a subscription this key made. Needs the Leads part too. One made in the dashboard or by another key answers 404.
Needs: Rules, write · Limit: 30 changes an hour per dispensary
get/api/v1/dispensaries/{id}/webhooks/sample
A sample event
A made-up event of the kind, as a webhook receives it, marked "test": true.
Needs: Rules
get/api/v1/openapi.json
This document
The OpenAPI 3.1 description of the API. No key needed.
Needs: No key
Ask the assistant
The reply comes from the same assistant your shoppers chat with, with your menu, knowledge and rules. Keep the sessionId it answers with and send it back to continue the chat: a chat belongs to the key that started it. Send "Accept: text/event-stream" to get the reply as it is written. The chat shows in the Inbox, where your team can take it over: the answer is then empty, with "paused": true, and GET /api/v1/dispensaries/<id>/chat/<sessionId>/replies gives you what they write (ask every 10 seconds or more, sending back its cursor as since).
An answer that is not the assistant's reply says so in "error": credits_depleted or monthly_cap (the store is out of replies), ai_timeout (the assistant took too long), ai_unavailable (it could not answer) or empty_response. Its "response" is still the words a shopper would see. In a stream, the error frame carries the same code.
A store whose menu is searched through Jane's DM API answers here without products: Jane needs every product it returns reported as seen by the shopper, which only the chat on a website does. Its answers come from what the assistant knows.
curl https://botender.com/api/v1/dispensaries/<id>/chat \
-H "Authorization: Bearer btk_..." \
-H "Content-Type: application/json" \
-d '{"message":"What are your hours today?"}'{
"response": "We are open until 9 tonight. Anything I can help you find?",
"sessionId": "api_5d41402abc4b_3f2a1b4c5d6e4f708a9b0c1d2e3f4a5b"
}Webhooks
- Events: lead.created, conversation.escalated, conversation.rated. Each is POSTed as JSON to the address you subscribe.
- Subscribe with POST /api/v1/dispensaries/<id>/webhooks and unsubscribe with DELETE on the id it answers: the REST hooks pattern tools such as Zapier use. GET .../webhooks/sample shows an event before one happens.
- Subscriptions you make here show on the dispensary's Rules tab, up to 10 per dispensary. Each belongs to the key that made it: only that key unsubscribes it here, and it is removed when the key is revoked or its maker leaves the account or loses access to all of it. Nothing is sent to it while the key is expired.
- Every request carries X-Webhook-Signature: sha256=<hex>, an HMAC-SHA256 of the raw body with the dispensary's signing secret, which owners and admins find on its Rules tab. Check it on the raw bytes, before parsing.
import { createHmac, timingSafeEqual } from 'node:crypto';
function verifySignature(rawBody, header, secret) {
const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex');
return header.length === expected.length
&& timingSafeEqual(Buffer.from(header), Buffer.from(expected));
}import hashlib, hmac
def verify_signature(raw_body: bytes, header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(header, expected)Errors
An error answers with its status and { "error": { "code": "...", "message": "..." } }.
- 400 bad_request: The request is not valid: the message says what.
- 401 missing_key, invalid_key: No key, or one that is unknown, revoked or expired.
- 402 insufficient_credits: The account is out of credits.
- 403 plan_required, forbidden, read_only, plan_limit: The plan has no API, or the key may not use that part or may not write, or the plan has no room for another location.
- 404 not_found: Not there, or not one this key may use.
- 409 conflict, knowledge_limit: A dispensary with that slug is already in the account, the dispensary changed while it was saved (try again), or its knowledge is full for the API.
- 429 rate_limited: Too many requests: wait the Retry-After seconds.
- 503 not_available, unavailable: Try again shortly.
Build on it
Start free, connect your menu and put the chat on your site. The API is there when your plan includes it.