Skip to main content

Introduction

Brosbots offers a flexible architecture for integrating with external services. You configure integrations once at the organization level, then enable them in individual agents. The catalogue in the Integrations tab is one continuous grid of tiles. You narrow it with the search box, the category filter and the All / Connected / Not connected filter. The agent tools - transfer, end call and your own HTTP API - sit at the front of the grid, because nearly every agent needs them; see Tools. Next to the catalogue is the Custom tab: a wizard for systems the catalogue does not cover. It builds calls to your API during the conversation, event webhooks and lead addresses for your forms; see Custom integrations and Webhooks.

Integration categories

Phone numbers

Numbers are managed by the platform - you buy them and assign them to agents in the dashboard, in the Numbers tab. You don’t need an account with an external carrier.

Calendar and bookings

In-call bookings for appointment-based businesses (salons, clinics, restaurants).

Google Calendar and Outlook

The agent sees free time in your calendar and books into it - you sign in once, no API keys

Cal.com bookings

Let callers book appointments directly during the call via Cal.com

Leads and callbacks

The agent can call a new customer on its own: someone who filled in the website form or a Google Ads lead form, left a number in a Facebook, LinkedIn or TikTok ad (Make and Zapier templates), showed up as a new contact in HubSpot or Pipedrive, or could not get through to your number.

CRM and automations

During or after a call, the agent can: save leads to HubSpot, Pipedrive or Daktela, send follow-up SMS and emails, notify on Slack, trigger Zapier, run an n8n workflow or a Make scenario (and hear what they answer), and send webhooks to any system (your own backend). It can also read: a Google Sheets lookup during the call answers questions from a spreadsheet you already keep - see Reading a sheet during the call. It can also check something nobody wrote down for it - see Searching the web mid-call. A shop’s own orders and stock are read from BaseLinker - see BaseLinker: orders and stock - from Shopify, see Shopify: orders and product availability, from WooCommerce, see WooCommerce: orders and product availability - or straight from an Allegro account, see Allegro: orders and offers. A Slack notification, an email, a Zapier trigger and a calendar entry can also happen while the caller is still on the line - see Slack, email, Zapier and the calendar during the call. You’ll find the configuration in the dashboard under the Integrations tab and in the settings of individual agents.

The after-call SMS

The message is written in the agent’s Integrations tab. The field has an Insert variable menu listing what this particular agent collects during a call, the call’s own facts ({{caller_number}}, {{call_date}}, {{call_time}}, {{call_duration}}, {{call_disposition}}, {{agent_name}}) and what is known before the call - so no name has to be remembered. Below the box you see how many paid SMS the text costs, and a warning once it needs more than one - the carrier bills each. A single SMS is 160 characters, but one non-GSM character cuts that to 70, which is otherwise something you learn from the invoice. The counter measures the template; filling the variables in makes it longer.
A call summary cannot be inserted into an SMS. It is one variable in the template and a paragraph once sent - three paid messages on every call. The finished message is also trimmed to 320 characters, and any single variable to 80.

10 SMS an hour from a plfon number

The plfon carrier accepts 10 SMS an hour from one number. The rest are not lost: the platform keeps to the limit itself and queues the overflow, and an SMS goes out once the number may send again (within about an hour). On the SMS tab such a message carries a Queued badge with the time it goes out, and the call screen shows it as “queued”. Once an hour per number you get a notification (bell and e-mail, as you set them) that SMS are waiting. The SMS is charged when it is sent, not when it is queued. If the recipient replies STOP in the meantime, or lands on the Do not call list, the queued SMS does not go out and is marked “not sent”. plfon sends no delivery reports, so the last status of an SMS from a plfon number is sent: the carrier accepted it. Whether the phone received it is not known. SMS from Telnyx numbers get delivered or not delivered from the carrier.

SMS to a fixed number (alert)

The other direction: a message to you, not to the caller. In the same section turn on SMS to a fixed number, give a number (several separated by a comma) and write the alert - e.g. Call at {{call_time}} from {{caller_number}}, outcome: {{call_disposition}}. You also choose when it goes out: on every attempt, on answered calls only, or on unanswered ones only. {{call_summary_short}} - a summary trimmed to 140 characters - resolves in the alert only. The alert is sent from the agent’s number and billed like any other SMS, so several recipients means several messages. Suggest a text reads the agent’s system prompt and proposes a few ready messages for different call goals - confirming what was agreed, a summary with the next step, a follow-up after an unanswered call. Every suggestion fits in one SMS - longer drafts are dropped before you see them. A suggestion only enters the field when you pick it, and you can still edit it. Suggestions use only variables that will actually resolve.
Google Sheets and the calendars (Google, Outlook, Cal.com) work on the Free plan - your agent writes every call into your own spreadsheet and books appointments without paying for anything. Email, Zapier, n8n, Make and webhooks start on the Start plan, Slack and WhatsApp on Pro. The CRM integrations - HubSpot, Pipedrive and Daktela - start on Business.

Finding an integration

The Integrations page has a search box next to the category tabs and an All / Connected / Not connected filter. Search matches the provider name, the description and the category label, so “calendar” or “sheet” finds the right tile without knowing the brand. While either is active the category headings are dropped and the matches render as one grid. The same pair - search, and an All / On / Off filter - sits above the action cards in an agent’s own Integrations tab. Cards that are off sit two to a row; the one you switch on spans the full width, where its settings appear.

Saving checks the connection

Save connection verifies the credentials with the provider before it writes anything. If the check fails nothing is saved, and the form tells you exactly what to fix - a truncated private key, say, or a spreadsheet that was never shared with the service account. The usual Google Sheets mistake is how the private key was copied out of the JSON file. Literal \n, wrapping quotes, a trailing comma, even the whole JSON file pasted into the key field are all handled for you - but a key that was cut short is refused at save time rather than on the first real call. If you know the check cannot pass yet (the spreadsheet gets shared later, for instance), use Save anyway. Save anyway does not get past a Google service account key that cannot be read at all (damaged, cut short, not a private key): the connection is not saved and the form says what is wrong with the key. Such a key never gets a token, so saving it would only move the failure to every call. Renaming an existing connection or changing its default tab does not check the key again.

What each check proves

Slack, Zapier, n8n, Make and webhooks have no read side, so the only way to check them is to deliver something. Saving a new connection, or a changed URL, puts one test message in the channel; renaming a connection sends nothing.

Connecting HubSpot

HubSpot has retired its old API keys, so the connection uses a private app token. Create the app in HubSpot under Settings > Integrations > Private Apps, and on the Scopes tab tick all three of these: Save the app, copy its access token (it starts with pat-) and paste it into the connection. Save connection reads the token’s scopes from HubSpot and names the ones that are missing, so a token that can only read contacts is refused here rather than at the end of the first real call. Adding a scope later needs no new token
  • tick it in HubSpot, save the app, then press Test connection.
Without crm.objects.contacts.write every push comes back as 403 This app hasn't been granted all required scopes and no contact is ever created. This is the most common HubSpot problem, and the call’s Integrations panel is where it shows up.

Field mappings after a call

CRM and Google Sheets write data after a call using the mappings you set in the agent configuration, in the Integrations tab. Each mapping has three parts: You don’t have to retype the names - click the arrow in the Source field to see the variables available for the chosen context (gathered ones come with the description of what the agent finds out). The list of collected data lives in the Variables tab - see Template variables. None of these names has to be remembered. The arrow in Source lists the variables available in the chosen context, and the arrow in CRM field lists the real contact fields of your HubSpot, Pipedrive or Daktela, labels and custom properties included. Match the CRM’s fields fills in everything that can be recognised by name or label in one go. A field name one character off is accepted by the form and dropped by the CRM after the call, in silence - which is why it is worth picking from the list.

A new contact, or an update to an existing one

The HubSpot write never creates a second contact for somebody already in the CRM. Before anything is created, the contact is looked up in this order:
  1. Phone number - first as the call delivered it, then in E.164 form (+48537666648) and as bare digits, so a number typed into HubSpot with spaces or dashes is still found. The phone, mobilephone and hs_calculated_phone_number properties are all searched, the last being HubSpot’s own normalised copy.
  2. Email address - only once the number has found nobody, or when the call has no caller id at all (the website widget).
The number goes first on purpose: it comes from the phone network, while the email address was heard during the call and written down from the transcript, so a single misheard letter would create a duplicate. A contact found this way is updated - only the properties you mapped, and that were not empty on this call, are overwritten; everything else is left alone. When nothing matches, a new contact is created.
Matching needs the crm.objects.contacts.read scope. A token without it still writes, but cannot recognise an existing contact and creates a new one every time.

The call on the contact’s timeline

Writing properties says what changed, not what the call was about. So after the contact is saved, a call activity is added to it - the one HubSpot shows under Recent activities on the contact record:
  • the call summary,
  • the next steps from the action-item list,
  • the outcome, sentiment, topics and the agent’s name,
  • a link to the recording,
  • the duration and direction, in the fields HubSpot displays itself.
The activity is always attached to the contact the push just wrote to. If HubSpot accepts it but refuses the association, it is deleted again - a note nobody can see from the contact is litter. The Log the call on the contact switch in the agent’s Integrations tab turns this off. Logging never breaks the contact write: if the token lacks access to activities, the contact is still saved and the refusal shows in the Integrations panel of that call.

When the write fires

Every after-call card has two filters:
  • When to send - After every call attempt (including no answer) or Only when the call actually happened. No answer and busy only occur on outbound campaigns; on inbound calls the two options mean the same thing. In a campaign with retries the first option records several entries for the same number.
  • Only on a chosen call outcome - the list holds the outcomes this agent has already recorded on real calls (“appointment booked”, “not interested”). They are not tools or script steps. An outcome that has not happened yet can be typed in by hand.

Call analysis data (the call context)

Everything you see in the call history - the summary, action items, intents, sentiment - can be written to a spreadsheet or a CRM like any other field. Pick the call context and the picker offers: You will find the call id in the app in the ID column of the call history, and in the header of the call itself, where clicking it copies it to the clipboard.
The summary and analysis are produced in the background after the call ends. When a mapping uses them the platform waits for the result before writing, so the columns do not arrive empty. When no mapping uses them the analysis is not forced and costs you nothing.

Google Sheets

Once a connection is selected the platform reads the spreadsheet itself, and the mapping form becomes the sheet: one line per real column, each with a single choice - what goes in it, or nothing. There is no column name to type, so there is none to get wrong. Add a column in the spreadsheet and Refresh columns brings it in; Match the sheet’s columns fills every line it can work out from the column names in one go. Columns are matched to what the agent collects by name or by the question behind them, so a column headed email or Adres e-mail finds the same variable. A mapping left pointing at a column the sheet does not have is flagged rather than dropped. Those mappings collect in a “columns are missing from the sheet” box, where each one shows the header to add (with a copy button - Copy all headers copies them tab-separated, to paste into the sheet’s first row in one go) and, in words, what will land in it - e.g. “What lands here: Call date (Call summary). This is a system variable - the platform works it out after the call.” Add the header to the sheet, click Refresh columns, and the mapping joins the ordinary column list.
If the read fails (wrong service account or spreadsheet ID, or the sheet is not shared with the service account), the fields still work as plain text - type the names manually.

How a row finds its columns

The mappings are a list, and by default the row is appended in that order: the first mapping lands in column A, the second in B, and so on. The column name you type is then only a label for you. That breaks the moment the sheet is not in the same order - an inserted column, two swapped, or a sheet built by hand from the variable names in the Variables tab. So the platform reads the header row first and, when it recognises every titled column, writes each value under its own header instead, in any order. A column is recognised when its header is:
  • the column name given in a mapping, or
  • the source of a mapping (name, email, call_date, …), or
  • the name of a variable the call collected - a column headed jaki_ma_wolumen_polaczen fills itself, no mapping needed.
A mapping with no column of its own is not written: the sheet has no place for it. One unrecognised column and the whole row goes back to mapping order, so a sheet that works today keeps working.
Mappings are not created from your variables automatically after the agent exists. The default list is built once, when the agent is created, from the fields chosen in the wizard. Add a variable later and it reaches the sheet either by adding a mapping for it, or by heading a column with its exact name.

Signing in with Google

The simplest way to connect a spreadsheet is to sign in: Integrations → Google Sheets → Sign in with Google, and pick the account that can edit the spreadsheet. Right after you come back from Google, a list of that account’s spreadsheets opens - choose the one the agent should write to, and its name shows next to the connection. No Google Cloud project to create and nothing to share. BrosBots can open only the spreadsheets chosen in that list, not your whole Google Drive. That is why a spreadsheet is chosen with Choose spreadsheet rather than by pasting its address: a pasted address works only for a spreadsheet that was chosen from the list before. The same button is:
  • next to the connection under Integrations → Google Sheets (the default spreadsheet),
  • in the agent editor: Integrations → the spreadsheet tile, the note or the lookup → This agent’s spreadsheet (another spreadsheet for this agent; empty = the connection’s),
  • in the agent wizard next to the spreadsheet address,
  • in a new campaign with a Google Sheet source.
The editor names the spreadsheet it reads the columns from, so you can tell it is the right one. Under the chosen spreadsheet (on the connection and in the agent editor) there is Show spreadsheet preview: the headers and the last 5 rows of the chosen tab, with a tab switcher and a link to Google Sheets. The newest rows are the ones the agent wrote, so this is where you see that calls land in the right sheet. After you choose a spreadsheet from the list the preview opens by itself. When the sign-in expires or access is revoked, you get a notification with a Sign in again button.
A connection signed in before the spreadsheet list existed says “This connection was made before spreadsheets could be chosen from a list”. Click Sign in again and choose the spreadsheets from the list once more - on the connection and in every agent that has its own spreadsheet.
A new Google Sheets connection is made only by signing in with Google - the service account form (email and private key) is no longer offered, neither on the Integrations page nor in the agent wizard. A pasted key was the one way of connecting that kept failing on real calls. Service account connections made before keep working, and their form still opens when you edit one; a key you change there is checked on save. To move one over, edit it and click Sign in with Google - the connection keeps its id, so every agent using it carries on.

Share the spreadsheet with the service account

For a service account connection made before sign-in existed. A service account is a Google user of its own, and it starts with access to nothing. Creating the key is not enough: open the spreadsheet, click Share, paste the Service Account Email (...@....iam.gserviceaccount.com) and give it the Editor role. This is the step that is missed most often, and until it is done every write is refused with 403 PERMISSION_DENIED.

Taking a note during the call

Save a note to Google Sheets, in the agent’s Integrations tab, appends a row while the caller is still on the line - a callback request, a message to pass on, anything someone has to act on later. It uses the same connection as the post-call push. The columns also decide what the agent asks for: map one to the caller’s name and the tool grows an argument the model has to fill; leave it out and it never asks. The date, the time and the caller’s own number fill themselves from the call (a model asked for the time invents one). The row is laid out by the sheet’s own headers, so reordering columns in Google Sheets breaks nothing, and a column that is configured but missing from the tab is reported back to the agent and the log instead of vanishing silently.

Reading a sheet during the call

Everything above writes. Look up row in Google Sheets, in the agent’s Integrations tab, is the other direction: the agent reads the spreadsheet while the caller is still on the line - an order status, a booking, a customer record, a price. It uses the same connection as the write, so there is nothing new to authorise. Until this existed the only way round it was to stand an n8n or Make webhook in front of the same spreadsheet, because a custom HTTP tool cannot mint a Google service-account token on its own. That still works, and is still the answer for anything a plain row lookup cannot do (writing back, joining two sheets, filtering on more than one column) - but for “find the row and tell me what is in it” the webhook is no longer needed.

What to configure

Fill in What this sheet holds. Switching the tool on is not enough on its own - an agent that has not been told the sheet exists answers “I don’t have that information” instead of looking. This is the field that makes it check.

How the row is found

Row 1 is the header row. Every row below it is data, and the agent searches for the value the caller gave - an order number, an email, a code - across every column, or in one named column if it asks for one. The matching row comes back as column name / value pairs, so the agent can answer with any field in it. Two things are deliberately forgiving:
  • Case and spacing are ignored, so ANNA KOWALSKA finds Anna Kowalska.
  • Phone numbers match on their last nine digits. Caller ID arrives as +48601234567, and the sheet has whatever the person who typed it felt like - 601-234-567, 0048 601 234 567. All of those find the same row.
Called with no value at all, the lookup searches for the caller’s own phone number, so an inbound agent can recognise a returning customer before asking anything.

Limits

A spreadsheet has no query engine, so the lookup reads the tab and scans it. That is fine for the sheets people actually run agents against - up to a few thousand rows - and each lookup adds roughly half a second to the call. The scan stops at 5000 rows. Past that the agent is told the sheet was only read in part rather than being allowed to answer “not found” from half of it. A sheet that large is a sign the data belongs in a database, with the agent reaching it through an HTTP API tool.

Slack, email, Zapier and the calendar during the call

Six actions in the agent’s Integrations tab run while the caller is still on the line: Slack notification, Send email, Zapier trigger, n8n workflow, Make scenario and Schedule meeting. You set them up the way you set up the note - besides picking a connection, you tell the agent when to use the action and what it should say. The n8n workflow and the Make scenario are the only ones that can answer: whatever they answer, the agent passes on to the caller - see n8n and Make.
Leave the first two fields empty on Send email, Zapier trigger, n8n workflow, Make scenario or Schedule meeting and the tool behaves as it always did: the agent decides for itself whether to use it - and in practice it rarely does. A model handed a tool with no word on what it is for tends to say “I’ll let the team know” and send nothing. When those in-call actions do not fire, this is the first thing to check.The Slack notification is the exception. Switched on with the two fields left empty, the agent is told to post a short note about what the caller wanted and what was agreed, before the call ends. Fill the fields in and your own wording replaces that default.
The call does not hang up until an action started in the same turn has finished, so a Slack note the agent fires off as the caller says goodbye is not cut off halfway. A Slack notification or an email sent during the call is recorded in the Integrations panel of that call, just like the post-call pushes - so a message the agent never sent and one Slack rejected show up as different things. The post-call notification is a separate integration, configured in the same tab (summary, outcome and recording to a channel). Both can run at once: the in-call action raises what is happening on the line, the post-call one sums it up.

Who the agent may text, WhatsApp and e-mail

The model picks the recipient of an SMS, a WhatsApp message, a payment link and an e-mail sent during the call. So that a text uploaded to the knowledge base, a page found on the web or the caller themselves cannot talk the agent into sending somewhere else, two rules apply:
  • Recipient. SMS, WhatsApp and payment links go only to the caller’s number and to numbers listed in Other allowed recipients. An e-mail goes only to the Default recipient, the addresses on the same list and the contact’s address from the call data (a lead, a campaign contact). Any other recipient is refused, and the agent tells the caller the message did not go out.
  • Per-call limit. At most 3 SMS (payment links included), 3 WhatsApp messages and 3 e-mails in one call.
If the agent should send to a number or address dictated during the call (e.g. “please send the offer to jan@company.com”), turn on Allow sending to addresses given during the call on the action. The per-call limit still applies.
Agents set up before 19 Oct 2026 that e-mailed an address the caller gave need this switch after the change - otherwise the e-mail is refused.

When nothing was written

Two places answer this, and they answer different questions:
  • A specific call - open it in the call history. The Integrations panel next to the transcript lists what each integration did with that call, and why it did nothing when it wrote no row. A successful write names where it landed (“Row written to ‘Leads’ (row 42)”), and Show what was sent underneath expands every value that went into each column or CRM field - the same way the call’s own contexts are shown above it.
  • An integration that is switched on but left no outcome at all is flagged in amber. It means the after-call job never reached it (an error, or it ran out of time) - not that nothing was configured.
  • The connection in general - Recent deliveries on the Integrations page lists the whole organization’s attempts, newest first.
Every attempt ends in one of three states: written, failed (with the error), or skipped - the integration is on, but the call did not match its trigger or outcome filter, so nothing was meant to be written.

Retries and the Retry button

The HubSpot, Pipedrive, Daktela and Google Sheets pushes retry on their own when the vendor is briefly unavailable: an answer 429 (too many requests), 500, 502, 503, 504, a connection that could not be opened, or a read that timed out. Each request is tried at most three times, a few seconds apart; a Retry-After longer than 10 seconds is not waited for. When the push needed more than one attempt, the call’s Integrations panel says how many. Errors that will not fix themselves - a revoked key (401), missing permissions (403), data the CRM rejects (400) - are not retried. Neither is a contact or row write that timed out halfway: it may have landed, and sending it again would make a duplicate. A push that still failed has a Retry button next to it in the call’s Integrations panel. It runs that one push again - not the SMS, WhatsApp, Slack or webhooks of the same call - and the outcome appears as a new row in the panel a few seconds later. The CRM push looks the contact up first, so a retry updates the contact an earlier attempt may have created rather than adding a second one. The button is only on the newest outcome of a push: an older failure that a later attempt already fixed cannot be repeated.
The row goes directly under the tab’s last filled row - blank rows below the data (which is what a sheet built on Google Tables, or one from an import, looks like) are skipped. It used to go through Google’s own “append”, which on such a sheet lands past the table’s whole reserved area, hundreds of rows below the header. If older calls are missing from under the header, scroll down: they are most likely there.
Browser (WebRTC) calls write rows exactly like phone calls do. The only difference is call.phone_number, which stays empty because there is no number to record.

Searching the web mid-call

Everything else the agent knows is something you put there: the prompt, the uploaded documents, a spreadsheet. That is fine for prices and opening hours and useless for anything that changed this week - which application windows are open, what a public register says today. Search the web, on the agent’s Integrations tab, is the one tool whose answer is not in our database. Underneath it is the Brave Search API: a paid API licensed to return results, answering in one round trip. The free plan covers the odd lookup per call.

Setting it up

  1. Go to brave.com/search/api, register and pick a plan.
  2. Copy the subscription token from the API Keys tab.
  3. In the app: Integrations → Brave Search → paste the key. Optionally set a country (PL) and query language (pl) to narrow results to your market.
  4. On the agent: Integrations tab → Search the web → pick the connection.
Leave What it should look up empty and the agent still searches, but only when it decides it does not know. The field moves that line: it tells the agent which subjects it should not trust its own memory on.

What the agent gets

A title, a description snippet and the source, up to three results. Page bodies are deliberately not fetched: reading and summarising three articles is seconds of silence on a phone call, and the answer has to fit in two sentences anyway. The agent reads out the gist, says where it comes from, and offers to text or email the link if you have those tools switched on. A failed search does not derail the call. An exhausted quota, a rejected key and a timeout all come back as a sentence the agent says out loud (“I could not look that up”), rather than as silence or an invented answer.

AgentMail: an address of the agent’s own

Sending over SMTP and reading over IMAP both need a server, a port and an app password. That is the most common thing to get wrong when setting an agent up, and it cannot be checked until something fails. AgentMail replaces those credentials with one API key and gives the agent an address of its own.

Setting it up

  1. Go to agentmail.to, register and copy the API key. The free plan is three inboxes and 3,000 messages a month.
  2. In the app: Integrations → AgentMail → paste the key. Leave the inbox field empty: saving creates an address for the agent on agentmail.to, or reuses the one the key already has.
  3. A custom domain (agent@yourcompany.com) is added in the AgentMail dashboard, along with its DKIM, SPF and DMARC records.

What changes

On the agent’s Integrations tab the e-mail tool’s connection picker now lists both SMTP and AgentMail connections - the agent sends through whichever one you choose.

A briefing with no IMAP

The Daily briefing section gained a Where to read mail from field: your own mailbox over IMAP, or the agent’s inbox. With the agent’s inbox you set a forwarding rule in your own mail (Gmail: Settings → Forwarding) and the briefing reads whatever arrives - without giving us any password of yours.
AgentMail does not read your Gmail. It hands out new inboxes, so existing mail reaches the agent by forwarding. Reading a Google account directly means either IMAP with an app password or an OAuth app with Google’s annual security assessment - hence the choice.

When the test says the key was rejected

Test connection checks the key against your inbox list first, so “AgentMail rejected this API key” now means the key, and nothing else. Two other answers are about the Inbox field instead: If the key really is refused, check it from a terminal - this is the same call the test makes:
200 means the key is fine and the problem is the inbox. 401 means AgentMail does not know the key: generate a new one in the AgentMail dashboard and paste it without the surrounding spaces.

Online stores

The agent answers the two questions shop customers call about: “where is my order” and “do you have it in stock”. It reads the answer live from the system you sell through:
  • BaseLinker - order status, tracking number, and stock and prices from your BaseLinker catalogue.
  • Shopify - fulfillment status, tracking number and variant availability through a custom app in your own admin.
  • WooCommerce - orders and product availability from a WordPress shop through read-only REST API keys.
  • Allegro - orders and active offers from your seller account, connected with a single sign-in and no key to paste.