Skip to content
Developers

A small API, documented and predictable

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.

  • OpenAPI spec with 42 operations
  • Idempotency on writes
  • 600 requests per minute
Quick start

From the key to the first call

Three steps in the panel and one call from your terminal.

  1. 1

    Generate the key

    Under Integrations › API keys, checking only the scopes the integration needs.

  2. 2

    Keep it out of the code

    An environment variable on your server. The server key never reaches the browser.

  3. 3

    Call it

    Every request carries Authorization: Bearer. Writes accept Idempotency-Key.

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

Official SDKs

Seven languages, the same behavior

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.

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.

All repositories at github.com/bernisoftware

Monitoring

Nine capture packages, the same event in all of them

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.

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.

See what Monitoring does

Product roles

The role arrives from your page, and you sync it through the SDK

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.

  • products.roles.list products:read
    GET /products/{slug}/roles

    The roles of that product, by name, with how many people are in each one.

  • products.roles.upsert products:write
    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.

  • products.roles.delete products:write
    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.

See how the widget declares the role

Staying in sync

Your system and bFocus, on one shared record

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.

  1. 1

    The id is yours

    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.

  2. 2

    The initial load goes in batches

    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.

  3. 3

    Retrying is safe

    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.

  4. 4

    The registry tells you what happened

    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

  • Whoever you synced is who signs into the widget, with access decided per link and your server’s signature
  • The customer↔product link decides what each one sees of the release notes and what the AI uses to answer
  • The release note published by your deploy, idempotent per product and version, lights the badge inside the customer’s app and records each acknowledgement
  • Knowledge base articles come in by external code and become sources for the AI agent, searchable through the same API
  • The customer’s history accepts interactions from your system, and they show up in the dossier beside Mesa
  • A CPF merges records on its own; with no CPF, name and contact go to the possible-duplicates queue instead of being forced together

What the API does not do

  • It does not open tickets: a ticket is born from the widget, email, chat, phone or WhatsApp — on purpose, so every case has a conversation it came from.
  • It does not create leads: a lead is born at your site’s front door or by hand, on the Leads screen.
What the API covers

A few resources, properly resolved

We would rather have a small API that does not change than a big one that breaks.

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.

People

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.

Extra identifiers

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.

Batch load

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.

Products

The catalog of what your company sells, which organizes the knowledge base, the agents and the release notes.

Product roles

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.

Release notes

Idempotent publishing by product and version, designed to run inside your deploy.

Knowledge base

Creation and updates by external code, in batches of up to 100, with publishing and search.

AI agents

Read the configuration and preview an answer, so you can test the agent before releasing it.

Keys

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

  • Bearer authentication, with separate read and write scopes
  • Idempotency-Key on this API’s writes: for 24 hours, repeating the call returns the same response instead of creating it again
  • X-Request-Id on every response, so you can match it with your own log
  • A limit of 600 requests per minute per key
  • Server to server: the integration API validates no domain and uses no CORS — the secret key is what authenticates
  • Optional origin restriction by IP, CIDR range or hostname resolved at call time
  • Errors with a stable code, so your handling does not break when the text changes
  • Clearing a contact is explicit: clear: ["email", "phone"] wipes it; null or a missing field means "leave it alone"
  • The 409 for a contact already in use says whose contact it is, so you decide without opening the database
  • An OpenAPI spec with 42 operations, generated from the code and served by the API itself
Verified identity

Your server says who is on the other side

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.

  1. 1 Your backend signs the user + company pair with the secret key of your account
  2. 2 The app passes that signature when it initializes the widget
  3. 3 bFocus checks the signature. With verified identity switched on, bFocus turns away anyone without a valid signature
  4. 4 The signature lasts 7 days and carries the moment it was made. When it expires, the widget asks the page for a new one and carries on where it was, with no reload — the visitor does not lose what they typed
  5. 5 The page answers through one of these routes: onIdentityExpired in the init options, the bfocus:identity-expired event on window, or bFocus.identify({ user, customer, userHash }) at any time, with whichever fields you want
  6. 6 The answer has to arrive within 8 seconds. Without it, the identity refused screen shows up, as before
  7. 7 In the nine native packages there is nothing to do: the renewal uses the userHashProvider you already set up
  8. 8 When you rotate the secret key, the previous one still works for 7 days, so the rotation does not cut off anyone holding the old signature

Ready-made examples

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.

Technical questions

How do tickets come in?

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.

Can I read the SDK source?

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.

How do you version the API?

Each SDK release states whether the public surface changed. We recommend pinning the exact package version and upgrading after reading that line.

Can I use the API alone, without the widget?

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.

How does the widget decide which domains it opens on?

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.

A domain (or an IP) was refused. How do I allow it?

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.

The signature expired with the page open. Does the user lose what they typed?

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.

Can I restrict the integration API by domain?

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.

Generate a key and test it in ten minutes

The trial account already comes with access to the API and the full spec.