Customers
The account that signs the contract, company or individual: kind, the tax id in document, legal name and registrations for a company, id document for an individual. With contacts, linked products, custom fields and contact history.
Everything worth automating is here: customers, people, products, release notes, knowledge base and AI agents. The spec is generated from the code itself — what the docs say is what the server does.
Start from what your integration does: talk to the API from your server, or put the widget inside your app.
The public API in OpenAPI, generated from the code: routes, scopes, stable error codes and idempotency.
Open openapi.jsonSeven official SDKs, each with a README, examples and open source code on GitHub.
See the seven SDKsNine packages for apps — Flutter, iOS, Android, React Native, .NET, Java, Python, Delphi and Lazarus — with the install command for each one.
See the native appsThe snippet for your website or web system, which also covers Electron, Tauri, Capacitor and Ionic.
See the web widgetNine packages that send your system’s errors to bFocus — browser, Node, Python, PHP, .NET, Java, Go, React Native and Flutter — each with its install command.
See the nine packagesThe role of whoever uses your product arrives through the widget role and is synced through the SDKs — and it is a statement, not a permission.
See the three operationsHow your server signs user and company so a customer never sees another one’s ticket — and how the signature renews itself.
See how to signThree steps in the panel and one call from your terminal.
Under Integrations › API keys, checking only the scopes the integration needs.
An environment variable on your server. The server key never reaches the browser.
Every request carries Authorization: Bearer. Writes accept Idempotency-Key.
# the spec comes from the server, not from a hand-written file curl -s https://api.bfocus.com.br/api/v1/integration/openapi.json \ | jq '.info, (.paths | keys | length)' # every authenticated call carries the key in the header curl -s https://api.bfocus.com.br/api/v1/integration/products \ -H "Authorization: Bearer $BFOCUS_API_KEY"
The spec is served by the API itself, generated from the code
$ pip install bfocus import os from bfocus import sign_widget_identity_v2 # the signature carries the moment it was made and lasts 7 days signature = sign_widget_identity_v2( os.environ["BFOCUS_WIDGET_SECRET"], user_external_id="app-77", # no ":" customer_external_id="erp-1042", ) # "v2.<ts>.<hex>" — hex = HMAC-SHA256 of "v2:<ts>:app-77:erp-1042" # Once expired, the widget asks the page for a new one (Renewal tab)
The identity is signed on your server and lasts 7 days
// the signature lasts 7 days. Once expired, the widget asks the page for a new one // and carries on where it was — no reload, nothing typed is lost bFocus.init({ user: { externalId: "app-77" }, customer: { externalId: "erp-1042" }, userHash: seal, // gets { reason, identify }. Return the new seal: string, object, // promise — or nothing, and answer later with bFocus.identify onIdentityExpired: () => fetch("/bfocus/seal").then((r) => r.text()), }); // the other two routes, if the callback does not fit: window.addEventListener("bfocus:identity-expired", () => { bFocus.identify({ userHash: seal }); // partial fields }); // on login, send the new user together with the new seal bFocus.identify({ user: { externalId: "app-99" }, userHash: seal }); // bFocus.isOpen() and the onOpen/onClose notices tell you when to re-identify
An expired signature renews itself; the page has 8 seconds to answer
$ npm install @bfocus/sdk import { Bfocus } from "@bfocus/sdk"; const bf = new Bfocus({ apiKey: process.env.BFOCUS_API_KEY! }); // creates or updates by the code in YOUR system — running it again does not duplicate const cliente = await bf.customers.upsert("ERP 1042", { name: "Padaria Estrela", email: "[email protected]", });
@bfocus/sdk package 0.2.5, published on npm
$ pip install bfocus import os from bfocus import Bfocus bf = Bfocus(os.environ["BFOCUS_API_KEY"]) cliente = bf.customers.upsert("ERP 1042", name="Padaria Estrela") print(cliente["id"])
bfocus package 0.2.5, published on PyPI
$ composer require bfocus/bfocus <?php require __DIR__ . '/vendor/autoload.php'; $bf = new \Bfocus\Bfocus(getenv('BFOCUS_API_KEY')); $cliente = $bf->customers->upsert('ERP 1042', [ 'name' => 'Padaria Estrela', ]);
bfocus/bfocus package 0.2.5, published on Packagist
All of them go through the same set of 74 conformance cases: same names, same errors with a stable code and the same retry policy.
npm install @bfocus/sdk README and source on GitHub pip install bfocus README and source on GitHub composer require bfocus/bfocus README and source on GitHub dotnet add package Bfocus README and source on GitHub implementation("br.com.bernisoftware:bfocus:0.2.5") README and source on GitHub go get github.com/bernisoftware/bfocus-go README and source on GitHub gem install bfocus README and source on GitHub All seven are published on each language registry, at version 0.2.5, with the source in a public repository. For any other language, the REST API works the same way.
Packages for the Monitoring add-on: one init call with the agent’s ingest key and your system’s errors arrive grouped and scrubbed, and are born as tasks in Projects. Same route, same event shape and the same version everywhere — none has a runtime dependency and none makes a network call on import.
npm install @bfocus/monitor README and code on GitHub npm install @bfocus/monitor-node README and code on GitHub npm install @bfocus/monitor-react-native README and code on GitHub pip install bfocus-monitor README and code on GitHub composer require bfocus/monitor README and code on GitHub dotnet add package Bfocus.Monitor README and code on GitHub implementation("br.com.bernisoftware:bfocus-monitor:0.1.2") README and code on GitHub go get github.com/bernisoftware/[email protected] README and code on GitHub flutter pub add bfocus_monitor README and code on GitHub No build step on the front end? A script tag does it: <script src="https://widget.bfocus.com.br/v1/monitor.js" data-bf-monitor-key="bf_mon_…" async></script>
All nine are on each language’s registry, at version 0.1.2. A monitor failure never takes your system down: the handler you already had keeps working, the queue is bounded and every error of ours is swallowed.
The role says who that person is in that product — admin, billing, counter clerk. Your page declares role: { key, label } on the widget init and, if that role does not exist yet, it is created and linked on its own. These three operations are the other path: syncing, on every deploy, the roles your system already knows, with the two instructions the AI agent uses.
GET /products/{slug}/roles The roles of that product, by name, with how many people are in each one.
PUT /products/{slug}/roles/{key} Idempotent: safe to run on every deploy. Only what comes in the body changes, and an empty string clears an instruction.
DELETE /products/{slug}/roles/{key} Whoever had the role is left without one and release notes aimed at it go back to everybody. If your page keeps declaring the key, the role is created again on the next session.
In all seven languages, under the same name. The key is immutable — it is how your page refers to the role; the label and the two instructions change freely. A role is a statement, not a permission: it is declared by your page and travels OUTSIDE the signature (userHash covers user and customer), so the access limits are conversation instructions for the agent — what the person is allowed to see is still decided by bFocus.
Nobody types a customer in twice. Your ERP, your SaaS or your desktop system sends what it already has — using the id it already uses — and bFocus learns who is who, who uses which product and what shipped in each version.
PUT /customers/{your_id}: creating and updating are the same call, with the code your system already uses. The same goes for people, contacts, extra identifiers, the product link, products, release notes and knowledge base articles.
Up to 500 customers or 500 people per call (100 articles). Each item is independent: whatever fails comes back with its own error code, in the same order, without taking the others down.
Public API writes accept Idempotency-Key: for 24 hours, a repeat returns the original response with Idempotent-Replayed, without running again. The SDKs send the key themselves on every retry — a timeout followed by a retry returns the same response instead of creating the record twice.
linked when the same person now counts for one more customer, merged_into when your id became an alias of a record that already existed, unlinked when they leave this customer and stay in the others. The 409 for a contact already in use says whose contact it is.
What syncing wires up on the other side
What the API does not do
We would rather have a small API that does not change than a big one that breaks.
The account that signs the contract, company or individual: kind, the tax id in document, legal name and registrations for a company, id document for an individual. With contacts, linked products, custom fields and contact history.
The person lives inside the customer (batch loading and her identifiers have their own routes), with a tax id, custom fields and portal access. The answer says what happened: linked when the same person now counts in one more customer, merged_into when your id became an alias of a record that already existed, unlinked when the person leaves this customer and stays in the others.
A person or a customer is found by several ids. The list comes back readable — external_id, label and who linked it — so you find out that your system’s id became an alias of another system’s record.
Up to 500 customers or people in one call. Every item is independent: a failing one comes back with its own error code, in the same order, and does not take the rest down.
The catalog of what your company sells, which organizes the knowledge base, the agents and the release notes.
List, create/update and delete the role of whoever uses each product: GET, PUT and DELETE on /products/{slug}/roles, with the products:read and products:write scopes. The PUT is idempotent, made to run inside your deploy. The role also arrives on its own through the widget — and it guides the AI agent and targets internal release notes, never as a permission.
Idempotent publishing by product and version, designed to run inside your deploy.
Creation and updates by external code, in batches of up to 100, with publishing and search.
Read the configuration and preview an answer, so you can test the agent before releasing it.
Several per company, scoped by resource, with an optional IP list, revocation and a record of the last use. A refused address lands in the Authorizations queue, to be allowed with one click.
House rules
With verified identity turned on, it is what stops one customer from seeing another's ticket. It holds in the web widget, in the native packages and in browser mode. A widget created today starts with it already on.
The verified identity guide shows how to compute the v2 signature in Node, Python, PHP and the other SDK languages, with an example to copy and a test to check that your implementation matches ours.
Through the widget, with the publishable key, through email and through WhatsApp. The integration API handles the records around them: customers, contacts, products, identifiers, bulk import and customer interactions.
You can. Every SDK and every native package has a public repository at github.com/bernisoftware, with the README and the examples shown on this page.
Each SDK release states whether the public surface changed. We recommend pinning the exact package version and upgrading after reading that line.
You can. Plenty of teams start by syncing customers from the ERP and publishing release notes from the deploy, and only add the widget later.
The browser reports the page address, so the check runs on the real domain — that list is what protects the publishable key, which is public. Register the root domain and the subdomains come with it: https://customer.com already allows app.customer.com and shop.customer.com. That is the recommended path, because it leaves less room for mistakes. An explicit wildcard works too, as an alternative: https://*.customer.com. The scheme has to match (https does not cover http) and so does the port, when you declare one. In the native packages the origin is app:// followed by the app id, and there the host is an identifier: app://com.customer does not allow app://com.customer.other, so register each id.
Without typing anything. A domain refused by the widget, or an address refused on an API key, becomes a row under Company › Integrations › Authorizations: the address exactly as it arrived, how many attempts and when the last one was. Allowing it is one click, and the value goes into the right list — the widget domains or that key’s IPs. That ends the mistake of typing www instead of the root domain, or http instead of https. Only an admin can allow it. Dismissing drops the row without authorizing anything.
No. The widget asks the page for a new identity and carries on where it was, with no reload. The page answers through onIdentityExpired in init, through the bfocus:identity-expired event on window, or by calling bFocus.identify at any time — and the answer can be a promise. The deadline is 8 seconds; past that, the identity refused screen shows up. In the native packages the renewal uses the userHashProvider you already set up.
No, and that is on purpose: a server call carries no Origin, and a domain declared by the client would be forgeable — it would look like protection without being one. The credential here is the secret key, with per-resource scopes, a per-minute limit, revocation and a record of the last use. There is also an optional restriction by origin address: IP, CIDR range or hostname resolved by DNS at call time. It works for anyone calling from a server with a fixed outbound IP, such as a VPS or your own machine. On a platform without a fixed outbound IP — App Platform, Lambda, Cloud Run, CI — it does not help, because the IP that leaves there has no relation to your app’s domain. The secret key never goes to the browser. When your front end needs the data, your company’s backend is the one that calls the API; only the widget’s publishable key goes to the browser.
The trial account already comes with access to the API and the full spec.