> ## Documentation Index
> Fetch the complete documentation index at: https://brosbots.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Integrations overview

> Connect Brosbots to external services and platforms.

## 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](/en/voice-agent/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](/en/integrations/custom) and [Webhooks](/en/integrations/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).

<Card title="Google Calendar and Outlook" href="/en/integrations/calendar/google-outlook">
  The agent sees free time in your calendar and books into it - you sign in once, no API keys
</Card>

<Card title="Cal.com bookings" href="/en/integrations/calendar/calcom">
  Let callers book appointments directly during the call via Cal.com
</Card>

### Leads and callbacks

The agent can call a new customer on its own: someone who filled in the
[website form](/en/integrations/leads/lead-webhook) or a [Google Ads lead
form](/en/integrations/leads/ad-leads), left a number in a Facebook, LinkedIn
or TikTok ad ([Make and Zapier templates](/en/integrations/leads/make-zapier)),
showed up as a [new contact in HubSpot or
Pipedrive](/en/integrations/leads/crm-leads), or [could not get
through](/en/integrations/leads/missed-calls) 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](/en/integrations/n8n) or a
[**Make** scenario](/en/integrations/make) (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](#reading-a-sheet-during-the-call). It can also
check something nobody wrote down for it - see [Searching the web
mid-call](#searching-the-web-mid-call). A shop's own orders and stock are read
from BaseLinker - see [BaseLinker: orders and
stock](/en/integrations/ecommerce/baselinker) - from Shopify, see [Shopify: orders and
product availability](/en/integrations/ecommerce/shopify), from
WooCommerce, see [WooCommerce: orders and product
availability](/en/integrations/ecommerce/woocommerce) - or straight
from an Allegro account, see [Allegro: orders and offers](/en/integrations/ecommerce/allegro).

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](#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.

<Warning>
  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\.
</Warning>

#### 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](/en/features/contacts#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.

<Note>
  **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**.
</Note>

## 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

| Integration | What is checked |
| - | - |
| **Google Sheets** | We sign with the key, open the spreadsheet and **ask for write permission** - by appending zero rows, so nothing lands in the sheet. A spreadsheet shared as *Viewer* is refused here rather than on the first real call. A Sheets API disabled in the Google Cloud project is reported as its own problem. |
| **Cal.com** | The key, and then the event type's free slots - the same question the in-call booking asks. Event types owned by a **team** pass exactly like personal ones. |
| **Email (SMTP)** | Signing in, sending nothing. Both port **587** (STARTTLS) and **465** (SSL/TLS) work; hosting providers hand out either. |
| **HubSpot** | The token's scopes, read back from HubSpot - missing ones are named, see [Connecting HubSpot](#connecting-hubspot). |
| **Pipedrive** | Reading the account with the same request the agent makes during a call. |
| **Daktela** | Who the token's API user is, then the list of ticket categories - the message names them, because a connection takes the category by name. A configured category that does not exist is reported here rather than after a call. |
| **Google Calendar, Outlook** | Reads the next 7 days from the connected account, and checks the booking hours and timezone. An expired sign-in is named as such. An older Google connection through a webhook: the address only, because a real test would create a real event. |
| **Slack, Zapier, n8n, Make, webhooks** | Delivering a test message, see below. For n8n and Make with the secret header, and a test address (`/webhook-test/`) is named as such. |

<Note>
  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.
</Note>

## 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:

| Scope | What it is for |
| - | - |
| `crm.objects.contacts.read` | The in-call lookup, so the agent recognises the caller |
| `crm.objects.contacts.write` | The after-call push, so the contact is created or updated |
| `crm.schemas.contacts.read` | The field list in the agent's mappings, so it can offer your own contact properties |
| `crm.objects.calls.write` | Logging the call on the contact's timeline. Older private apps cover it with `crm.objects.contacts.write`, so if the scope is not on your list, add nothing |

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**.

<Warning>
  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.
</Warning>

## 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:

| Part | Meaning |
| - | - |
| **Source** | The variable name, from the call or from the initial context. |
| **Context** | `gathered` - collected by the agent during the call; `initial` - known before the call (agent variables, `phone_number`, `campaign_id`, campaign columns); `call` - what the platform works out **after** the call, see below. |
| **Field / column** | The CRM contact property or the spreadsheet column. |

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](/en/voice-agent/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.

<Note>
  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.
</Note>

### 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:

| Source | What lands in the cell |
| - | - |
| `summary` | The AI call summary |
| `action_items` | Follow-ups, one per line |
| `intents` | Caller intents, comma separated |
| `topics` | Topics discussed, comma separated |
| `sentiment` / `sentiment_score` | Sentiment and its -1 to 1 score |
| `success` / `success_reason` | Whether the call met its goal (TRUE/FALSE) and why |
| `call_date` / `call_time` | When the call happened |
| `duration_seconds` | Length in seconds |
| `phone_number` / `disposition` | Caller number and call disposition |
| `recording_url` / `transcript_url` | Links to the recording and transcript. They don't expire; they stop working once the call is deleted or a retention policy removes the file. Anyone holding the link can open the file |
| `call_id` / `agent_name` | Call id and agent name |

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.

<Note>
  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.
</Note>

### 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.

<Note>
  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.
</Note>

### 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.

<Note>
  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.
</Note>

### 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.

<Note>
  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.
</Note>

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.

| Field | What it does |
| - | - |
| **Spreadsheet tab** | Where the notes go. No tab for them yet? **Create tab** adds one with its header row (date and time, phone, name, note) and maps the columns in the same click. |
| **What the note must contain** | In your own words, e.g. "Note who is asking to be called back, what about, and when they are available." It goes straight to the agent - the more specific it is, the fewer notes that just say "customer called". |
| **Phrases that make the agent write a note** | Examples such as "ask someone to call me back". The agent matches the **meaning**, not the literal words. Without them it writes a note on some calls and not others, and nobody can tell which. |
| **Collect contact details before saving** | The agent asks for a first and last name and a callback number, and reads the number back to confirm it. |
| **Spreadsheet columns** | One line per real column of the tab. For each one you pick what goes in it: the note, the caller's name, the phone, the subject, the date and time, or a variable collected during the call. **Match automatically** fills in whatever can be recognised from the names. |

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

| Field | What it does |
| - | - |
| **Connection** | Which Google Sheets connection to read. |
| **Sheet tab to search** | The tab to look in. Leave it empty to use the tab set on the connection - useful when the agent writes to `Calls` but reads from `Orders`. |
| **What this sheet holds** | One line, in your own words, e.g. "Order statuses. Look up by the order number the caller reads out." It is passed to the agent so it knows *when* to check the sheet. |

<Note>
  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.
</Note>

### 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.

| What the agent says | What it means | What to do |
| - | - | - |
| "I could not read that sheet" | Wrong Spreadsheet ID, or the sheet is not shared with the service account | Share it with the Service Account Email as **Editor**, check the ID |
| Nothing matching, but the row is clearly there | The tab searched is not the tab you are looking at | Set **Sheet tab to search** explicitly |
| It answers from its own knowledge instead of looking | The agent was never told what the sheet is for | Fill in **What this sheet holds** |
| It finds the row but reads out the wrong field | Two columns have the same header | Give them distinct headers - duplicates come back as `Phone` and `Phone (2)` |

## 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](/en/integrations/n8n) and
[Make](/en/integrations/make).

| Field | What it does |
| - | - |
| **When to use it, and what to say** | In your own words, e.g. "Post a notification when a caller is interested in a deal above 10k. Say what it is about and when they expect an answer." It goes to the agent along with the tool. |
| **Phrases that should trigger it** | Examples such as "I'd like to speak to sales". The agent matches the **meaning**, not the literal words. |
| **Include call details** (Slack, email) | The caller's number, the agent's name and the call id are appended to the message. The agent never supplies them, so it cannot invent them. On by default for Slack, off for email. |
| **Default recipient** (email) | Where mail goes when no address is said during the call. The agent then stops asking for one. |
| **Default subject** (email) | Used when the agent names no subject of its own. Without it the subject is invented anew on every call. |

<Warning>
  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.
</Warning>

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](mailto:jan@company.com)"), turn on **Allow sending to
addresses given during the call** on the action. The per-call limit still
applies.

<Note>
  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.
</Note>

## 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.

| What you see | What it means | What to do |
| - | - | - |
| HubSpot `403` “This app hasn't been granted all required scopes” | The private app token is missing a scope | Add `crm.objects.contacts.write` (and the other two above) in HubSpot, save the app, then **Test connection** |
| `403` / no access to this spreadsheet | The sheet was never shared with the service account | Share it with the Service Account Email as **Editor** |
| "The service account private key stored on this Google Sheets connection cannot be read" | An older service account connection holds a damaged key | Edit the connection and click **Sign in with Google**, or paste the key again from the JSON file |
| HubSpot / Pipedrive / Daktela `429` or `5xx` after 3 attempts | The vendor was unavailable for longer than the retries | Click **Retry** next to the push in the call's Integrations panel |
| "BrosBots cannot open this spreadsheet" (signed in with Google) | The spreadsheet was not chosen from the list (e.g. a pasted address) | Click **Choose spreadsheet** and pick it. If it is not in the list, that account cannot edit it - ask the owner to share it |
| `403` mentioning the Sheets API | The Sheets API is off in that Google Cloud project | Enable "Google Sheets API" in the project |
| `404` / no spreadsheet with this ID | Wrong Spreadsheet ID | Copy the part of the sheet URL between `/d/` and `/edit` |
| The tab does not exist | The **Sheet name** does not match a tab | Correct the tab name in the agent's Integrations tab |
| No column mappings configured | The connection is fine, the agent has no mappings | Add at least one mapping in the agent's Integrations tab |
| Every mapped field was empty | The mappings point at names nothing filled on this call | Map something that always exists, e.g. the `call` context's `summary` or `call_date` |
| Skipped: trigger / outcome | The filter excluded this call | Change **Trigger** or the outcome list, or accept it |
| Written, but the row is nowhere to be seen | It went to another file, or far below the data | Follow the link in the delivery's summary - it opens the spreadsheet, and the cell it wrote is named next to it, e.g. `Rozmowy!A2` |
| Nothing at all in either list | The integration is switched off for this agent | Turn it on in the agent's Integrations tab |

<Note>
  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.
</Note>

<Note>
  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.
</Note>

## 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](https://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.

| Field | What it does |
| - | - |
| **Connection** | Which Brave Search connection to use. |
| **What it should look up** | One sentence in your own words, e.g. "Check the current grant calls on gov.pl before answering about deadlines." It goes into the prompt, so the agent knows what is worth searching for. |

<Note>
  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.
</Note>

### 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](https://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

| Use | Before | With AgentMail |
| - | - | - |
| Sending e-mail mid-call | SMTP host, port, login and app password | an API key |
| Mail in the daily briefing | IMAP server, port, login and password | an API key plus a forwarding rule |

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.

<Note>
  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.
</Note>

### 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:

| What it says | What to do |
| - | - |
| The key works, but it has no inbox `x` - it does have: … | Paste one of the listed addresses, or clear the field |
| The key works, but it has no inboxes at all | Clear the **Inbox** field and save - one is created for the agent |

If the key really is refused, check it from a terminal - this is the same call
the test makes:

```bash theme={null}
curl -i -H "Authorization: Bearer YOUR_API_KEY" https://api.agentmail.to/v0/inboxes
```

`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](/en/integrations/ecommerce/baselinker) - order status, tracking
  number, and stock and prices from your BaseLinker catalogue.
* [Shopify](/en/integrations/ecommerce/shopify) - fulfillment status, tracking
  number and variant availability through a custom app in your own admin.
* [WooCommerce](/en/integrations/ecommerce/woocommerce) - orders and product
  availability from a WordPress shop through read-only REST API keys.
* [Allegro](/en/integrations/ecommerce/allegro) - orders and active offers from
  your seller account, connected with a single sign-in and no key to paste.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.