Custom Integration
A guide covering BusinessChat's ETL tool, Custom Integration: scheduling syncs, enabling webhooks, and setting up live API requests against any third-party system.
Overview
Custom Integration connects BusinessChat to any third-party application and moves data between the two. Its modes:
- Sync: pull data from the external system and store it in BusinessChat, on a schedule or on demand.
- API request: call the external system in real time, for fetching data or trigger actions, such as checking availability or creating an appointment. Each API request is exposed as a capability.
- Webhooks: expose a URL for the external system to push events to, keeping stored data fresh without polling.
A business can have multiple integrations. Each integration has a mode (sync, API request, or webhook) and calls an external system through a hierarchy of steps that fetch data, transform it, and insert it as custom objects or template objects.
Integration modes
An integration runs in one of three modes. The mode is what triggers it and what happens to the result.
Pulls data in bulk and stores it in BusinessChat. It runs on a schedule (hourly, every 12 hours, or every 24 hours) or on demand. Sync is for reference data a business needs to keep on hand.
Triggered by: a schedule or a manual run. Produces: records created, updated, or deleted. One sync runs at a time.
Calls the external system synchronously, the moment a bot or flow needs it, to fetch fresh data or trigger an action like creating a booking. The response comes straight back to the caller; nothing is stored. Each API request is exposed as a capability.
Triggered by: a capability call from a bot or flow. Produces: the system's live response, mapped and returned to the caller.
Provides a URL the external system can push events to.
Triggered by: the external system. Produces: records created, updated, or deleted.
Capabilities
A capability is the named operation an API request integration offers to the rest of BusinessChat. Bots and flows don't reach into an integration directly; they call a capability by its key, and BusinessChat routes it to the integration behind it. Every capability is one of two kinds: a template capability or a custom one.
Template capabilities
These are the ones BusinessChat ships as a fixed catalog. It defines GET_PRODUCT_BY_ID, GET_ORDER_BY_ID, UPDATE_ORDER_STATUS, and others (the full list is in the template reference). Their key, name, parameters, and output are already fixed; applying one drops a ready-made definition into your integration, and you fill in only how to fulfil it against your API. Because the rest of BusinessChat already understands these keys, the moment you fulfil one, existing bots and flows can use it with no extra wiring.
Custom capabilities
These are ones a business defines itself, for an operation no template covers, CREATE_BOOKING or LIST_AVAILABILITIES for a clinic, say. You give the API request integration a name and declare its parameters yourself; whatever the integration returns is the capability's output. Reach for a custom capability whenever the template catalog doesn't already have the operation you need.
Output
A capability doesn't just run, it returns something, and that return shape is part of its definition. A template capability names the object types it produces (GET_PRODUCT_BY_ID produces a Product and its Variants), and that's what filters which templates a MAP step can target. A custom capability's output is simply whatever its mapping produces. Either way, the caller gets back a mapped, predictable result rather than the provider's raw response.
Parameters
A capability carries parameters, the inputs it needs to do its job. Each parameter comes from one of two places:
- Standalone: the caller provides the value, such as a preferred appointment time. Template parameters are always standalone.
- From entity: the value comes from a field of a stored object, such as the chosen doctor's ID. This binding is per-business, so it's something you declare on a custom capability rather than something a template can ship with.
Graph building
Every integration fetches its data through a graph: a hierarchy of steps that runs from the top down. Each step does one job, and steps nest so an integration can follow the shape of whatever the external system returns.
You need to create the entities before starting with sync.
Step types
- FETCH: makes an HTTP call to the external system (URL, method, headers, authentication, and pagination if the endpoint pages). Its response is held under an alias for later steps to use.
- ITERATE: loops over a list inside a response, running its child steps once for each item.
- MAP: turns whatever is in scope into a stored record for one object type.
- Webhook trigger: the starting point of a webhook integration; it holds the pushed event so the rest of the graph can treat it like any fetched response.
Hierarchy
Because steps nest, a child step runs once for each record its parent produced, and it can see everything above it. Fetch all doctors, then for each doctor fetch their schedule, then map each slot: while mapping a slot, both the doctor and the slot are still in reach, so a slot's record can carry its doctor's ID.
Aliases
Each step's output carries an alias, the name later steps use to refer to it, in URLs like /doctors/{{doctor.id}}/slots and in mappings. Two aliases are always available: params (the inputs from a capability call or webhook) and steps (the results of earlier steps). A step can only reach the aliases above it in the hierarchy, not those beside it.
Pagination
When an endpoint returns its data across many pages, a FETCH step walks all of them automatically and feeds every page through the steps below it. It supports the common paging styles, cursor, offset, page number, and next-link, so a single sync brings back the full set rather than just the first page.
Testing steps
You build the graph one step at a time, and each step is tested against the live system as you go. A step can't be saved until its test passes, and because a step runs on its parent's output, a step whose parent hasn't been tested yet can't be tested either, so you configure and test top-down. Every blocking step must be in a passed state before the integration can be activated.
Good to know
- A graph is generously sized but not unlimited: very deep nesting or a huge number of nested calls in a single run will hit a ceiling, so keep the hierarchy no deeper than the data needs.
- Every call an integration makes to the external system is logged, with secrets hidden, so it's easy to see exactly what was sent and what came back.
Google Sheets
A spreadsheet can be an integration's external system, without writing a request by hand. Choose Google Sheets as the step's type and the step asks for a connected Google account, a spreadsheet, a tab, and what to do there; BusinessChat builds the call. Everything else works as usual: the rows a step reads carry an alias, later steps filter and map them, and a step that changes the sheet is tested before it can be saved.
Connecting a sheet
A step needs a connected Google account. Connect one once, from the step or from the connected-accounts list, and the same account is offered to every later step. Then pick the spreadsheet through Google's own file picker and the tab inside it.
- BusinessChat is given access to the files you pick, not to your whole Drive. A spreadsheet nobody picked with that account cannot be opened, even if you know its address.
- The tab is remembered by its identity rather than its name, so renaming a tab never breaks a step. Deleting the tab does, and the step says so.
- Anyone who can see the integration can see the data the step reads, so connect an account that only reaches the sheets you want shared.
What a step can do
One step does one thing. Reading works in every mode; the three that change the sheet run only in API request integrations, because they act on what a caller sends, and a sync or a webhook would repeat that on every run.
Reads the tab and returns one record per row, keyed by your column names. Empty rows are skipped, and each row carries its row number as _row.
Runs in: any mode.
Adds one row below the last filled row of the columns it writes, filling only those columns. It returns the row it added, with its row number.
Runs in: API request. A call that gives none of its columns a value is refused rather than adding a blank row.
Changes the columns you list on rows an earlier step returned. Other columns are left alone. It returns each row with the columns it wrote.
Runs in: API request.
Empties every cell of rows an earlier step returned. Formatting stays and the rows stay where they are, so nothing below them moves. It returns the rows as it received them.
Runs in: API request.
Testing a step that writes never touches the sheet. The test shows the row it would add, the rows it would change and with what, or the rows it would clear.
Columns and rows
A Get rows step needs names for its columns, because those names become the fields you map from. Two ways to give them:
- A header row: say which row holds the headers, and the names come from it. Every row below is data. Each name becomes a key, so "Doctor name" is read as
doctor_name; you can rename any key, and a column with no header still gets one. - Typed names: name the columns yourself, left to right, when the sheet has no headers. Then every row from the first is data.
You can also list rows to skip, as single rows or ranges (2, 10-12, 50-), for notes and totals that aren't records. Skipping never renumbers anything: a row keeps the _row it has in the sheet.
The steps that write name their columns by letter instead, since they don't read the sheet's headers. Each column you write also gets an alias, which is the name it comes back under in the step's result.
Choosing rows to change
Update row and Clear row don't search the sheet. They work on rows an earlier step already returned, so choosing rows is ordinary graph building:
- Get rows reads the tab.
- A Filter keeps the rows to act on, for example the booking whose id the caller sent. A Sort or an Iterate can come between.
- The write step picks that step's output under Rows to change (or Rows to clear) and acts on those rows.
The list offers every alias the steps above it publish. Activating the integration refuses anything that isn't rows from a Get rows step on the same tab, directly or through a Filter, Sort or Iterate, so a step can't write to row numbers that came from somewhere unrelated.
When more than one row arrives, each step has a choice: Update row changes all of them by default, while Clear row stops with an error that names the rows, because clearing the wrong rows is not something you can undo from here. Either can be switched to the other behaviour. Behind an Iterate the step sees one row at a time, so that choice never comes up.
Two things worth knowing when you write the Filter's condition:
- It compares exactly:
bk-1is notBK-1, spaces count, and a number in a cell is not the text of that number. Wrap both sides intrimanduppercase, or the cell intoString, to match the way a person would read them. - A bare name in a condition is read from the row first. If a parameter has the same name as one of your column keys, the condition compares the row with itself and keeps far more rows than you meant. Rename the column key in the Get rows step to keep them apart.
The values written
Each column a step writes takes its value from one of two places: a parameter of the capability call, or a static value you type once, which is written on every call.
- A parameter the caller leaves out keeps the cell as it is on an update, and leaves it empty on a new row.
- A parameter sent empty, or a static value left blank, clears the cell.
- Values are written as if typed into the sheet, so numbers stay numbers and dates stay dates.
- Text that the sheet would run as a formula, anything starting with
=,+,-or@, is stored as plain text, so a caller cannot plant a formula in your sheet.
Good to know
- Row numbers are positions, not identities. Nothing BusinessChat does moves a row, but a person inserting, deleting or sorting rows between the read and the write moves the target, so the write lands on whatever row now has that number.
- A cleared row is empty, so later reads skip it. Its number stays free until someone fills it again.
- One write at a time per tab: two calls that touch the same tab queue behind each other rather than overwriting one another.
- A column past the last column of the tab is refused with the fix, as is a tab that was deleted or an account that lost access to the sheet.
- Google limits how often a spreadsheet can be read. A sync waits and tries again; an API request reports the limit to its caller rather than holding the call open.
- A read fetches the whole tab, so keep sheets to the rows you need: a long tab makes every run slower.
Transformation
Between fetching and storing, each MAP step runs the object's mapping: one pipeline per field that turns the provider's shape into the object's fields. You build them field by field, with autocomplete and a live preview against sampled data. Paths are case-sensitive, so a field the response returns as Speciality must be mapped with that exact casing, even next to a lowercase one like qualifications.
Shortcuts
Most fields don't need any transform; a bare dotted path is a complete mapping. doctor.clinic.name extracts the value directly, resolving first against the MAP's source and falling back to any ancestor alias. Array indexing works inline (tags[0]), and typing @ suggests every alias in scope with its sampled fields.
Field operations
When a field needs more than extraction, a pipeline chains operations in order, each feeding the next. The available operations are:
- Extract a value by path, Concat several inputs together, or supply a Literal constant.
- Text: trim, lowercase, uppercase, prefix, suffix, replace.
- Casting: to string, to number, to boolean, to fit the field's declared type.
- Default if null: fall back to a value when the source is missing.
Each field declares an expected type, and its final value is coerced or rejected against it. A timestamp field also declares its date format, and a value that doesn't match is caught rather than stored wrong.
Deduplication & aggregation
Reach for this only when the graph produces the same record more than once, which happens whenever a MAP sits under an iteration that repeats its subject, e.g. mapping a doctor once per branch they work at. Without aggregation you'd store the doctor several times; with it, you flag one field as the identifier (the key records are merged on) and any field marked aggregated collects its values into a distinct array across every occurrence. The result is one Doctor whose branches field lists every branch encountered. If a record only ever appears once, you don't need this.
The Operations tab
Field mappings say how a record is shaped; the Operations tab says what a sync is allowed to do with it. Each object picks its supported operations: Upsert (create or update, almost always on) and Delete. Deletes only happen if Delete is enabled here, and enabling it reveals the delete strategy, how a removal on the provider's side is detected:
- Absence in sync: a record missing from a full sync run is a delete candidate. A grace runs count guards against flaky pages: the record must be absent that many consecutive runs before it's removed, and the count resets the moment it reappears.
- Status field: a field value signals the delete. A small pipeline extracts the status and the record is removed when it matches any configured value, e.g.
deletedorarchived. - Event operation: for webhooks, the pushed event's own type says it's a delete.
Custom objects
Entities
An entity is a data model defined per business: a named type (normalized to uppercase, e.g. DOCTOR) with typed fields, text, number, boolean, timestamp (with a declared date format), enum, or array, one of which is the primary key that ties every record back to its source. A custom entity is shaped purely to match the provider's data; you define its fields yourself.
Relations
Relations link entities so synced data keeps its structure. A relation has participants (fields that each point at a specific entity and the field identifying it; a DOCTOR_BRANCH relation's participants point at Doctor and Branch) plus its own attributes for data that belongs to the link itself, like the doctor's working hours at that branch. Relations are populated by MAP steps like entities are, and related data can be traversed in flows without extra provider calls.
Template objects
Alongside the entities you define, BusinessChat ships a catalog of template objects: ready-made types with fixed fields and a fixed primary key. Applying one copies its definition into your business as a starting entity you then map to, so a Product you sync lands in the same shape the native e-commerce integrations (Shopify, Salla, Zid) use, and the rest of the platform recognises it. The e-commerce catalog covers Product, Variant, Order, Customer, Cart, Coupon, Currency, and Store URL (full field lists in the template reference). Anything the catalog doesn't cover, you model as a custom entity.
The published schema
Everything a business builds here, its entities, its relations, and its live capabilities, is published together as one schema. The schema is the contract the rest of BusinessChat reads: it's what bots and flows see and route against, and it's the only view they have of an integration. They never touch a graph or a mapping directly.
What it contains
For each business, the schema lists the stored object types and their fields, the relations between them, and the capabilities currently on offer with their parameters and output. From-entity parameters are where the two halves meet: a capability can draw an input from a stored entity's field, so create booking can take the chosen doctor's ID straight from the Doctor entity.
How it stays current
The schema republishes automatically whenever something changes it, when you add an object, activate a definition, or an integration is paused or restored. so a consumer that receives an older snapshot can discard it rather than act on stale routing.
A capability appears in the schema only while its integration is healthy. If an integration keeps failing and is paused, the capability it backs is dropped from the schema until it recovers, so bots and flows are never routed to an operation that's currently down.
Errors & recovery
Integrations run against systems you don't control, so failures are expected and handled rather than fatal. What happens depends on whose problem it is.
Who each failure goes to
Every error is classified and routed, so the right person is told and noise is kept down:
- Configuration and mapping problems go to the operator with specific guidance, e.g. a mapping that expected a number but got text, so you can fix the setup or follow up with the upstream system.
- Fetch failures (the external system erroring, timing out, or rejecting auth) are surfaced with their cause and count against the integration's health.
- Platform problems are ours; they page our on-call and don't bother the operator.
- For API requests, a bad caller input comes back to the caller as an error and doesn't count against health.
Auto-pause
A single failure doesn't stop anything. But when a sync fails repeatedly (by default, three runs in a row) the integration is paused by the system so it stops hammering a broken endpoint, and its failing step is flagged. If that integration also backs a capability, the capability is taken off offer while it's paused, so bots and flows aren't routed to something that's down. API-request integrations pause on a sustained failure rate rather than a run count.
Getting back to healthy
Recovery is deliberate, not automatic: a paused integration stays paused until you fix it. Open it, retest the flagged step (and any others still failing) until they pass, then activate. Activation re-runs the readiness check, clears the failure count, and puts any capability it backs back on offer. Health changes show up in the interface as they happen, without a refresh.
Import / export
Integrations are portable between businesses and environments. Two transfers, run in order:
- Custom objects first: exports the catalog (entities with their fields, relations with participants and attributes), whole or a named selection; a selection automatically pulls in its dependencies. Import into the target business creates them there.
- Definitions second: exports definition bundles (steps, edges, mappings, capability wiring) together with a manifest of every custom object they reference. Import validates that manifest against the target's catalog; it never creates entities or fields itself, which is why objects go first.
Imports are validated all-or-nothing: any invalid item rejects the whole payload with each failure listed, and nothing is written. Items that already exist in the target are skipped and counted, so re-running an import is safe. Payload sizes are capped; version-stamped files guard against format drift, and a timestamp/format mismatch on a field is caught at import or blocks activation, not discovered in production.
Use it to move a tested integration from staging to production, reuse a proven setup for a new customer on the same platform, or keep a backup before risky changes. Imported definitions arrive as drafts; retest steps against the new environment's provider and activate.
Template reference
The full shipped catalog. All template capabilities are API request mode; applying any of them copies a ready-made definition into your business. Template objects and capabilities keep the same names as the native e-commerce integrations, so synced data is interchangeable with them.
Template capabilities
| Key | What it does | Output |
|---|---|---|
GET_PRODUCT_BY_ID | Fetch one product by its ID | Product, Variant |
GET_VARIANTS | List a product's variants | Variant |
GET_ORDER_BY_ID | Fetch one order by its ID | Order |
GET_ORDER_BY_CONTACT | List a contact's orders | Order |
GET_CONTACT_BY_ID | Fetch one customer by ID | Customer |
UPDATE_ORDER_STATUS | Write a new status to an order | Order |
UPSERT_COUPON | Create or update a coupon | Coupon |
CREATE_COUPON_CODE | Issue a coupon code for a contact | Coupon |
GET_CURRENCY | Read the store's current currency | Currency |
GET_STORE_URL | Read the storefront URL | Store URL |
Template objects
| Object | Holds | Primary key |
|---|---|---|
| Product | Catalog item: name, price, quantity, availability, categories | productId |
| Variant | A product's sellable variant: SKU, price, attributes | providerVariantId |
| Order | Order with amount, status, customer and shipping details | orderId |
| Customer | Contact: phone, name, email, language, opt-in | phoneNumber |
| Cart | Abandoned cart: checkout URL, amount, customer | cartId |
| Coupon | Discount: type, amount, usage limits, validity window | code |
| Currency | Single-field carrier for the store's ISO currency | currency |
| Store URL | Single-field carrier for the storefront URL | storeUrl |
Glossary
The core terms used across this guide, in one place.
| Custom Integration | BusinessChat's ETL tool: connects to any third-party system to sync, request, and receive data. |
|---|---|
| Integration | One configured connection to an external system, running in a single mode. |
| Mode | What triggers an integration and what it does with the result: sync, API request, or webhook. |
| Sync | A bulk pull, on a schedule or on demand, that stores records in BusinessChat. |
| API request | A synchronous call to the external system made the moment a bot or flow needs it; nothing is stored. |
| Webhook | A URL the external system pushes events to, so records update without polling. |
| Capability | A named operation an API request integration exposes to bots and flows; template or custom. |
| Graph | The hierarchy of steps that fetches and shapes an integration's data. |
| Step | One node in the graph: FETCH, ITERATE, MAP, or a webhook trigger. |
| Alias | The name a step's output is referenced by in later URLs and mappings. |
| Mapping | The per-field pipelines a MAP step runs to turn a response into a stored record. |
| Entity | A per-business object type with typed fields and a primary key. |
| Relation | A link between entities, with participants and its own attributes. |
| Template object | A ready-made object type BusinessChat ships, with fixed fields and primary key. |
| Schema | The published contract of a business's objects, relations, and live capabilities. |