Skip to content

Latest commit

 

History

6 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

NerdResolve Inventory API: real-time ERP stock, inside your chatbot

Inventory API

Real-time ERP stock, inside your chatbot. The customer asks "do you have this set in size S?", and the answer comes from the real Bling v3 balance — not from a spreadsheet, not from a guess.

License Node Stack Endpoints

The problem  ·  How it works  ·  Decisions  ·  Run it  ·  License


The problem

A sales chatbot knows how to hold a conversation, but it doesn't know what's in stock. The usual ways out are all bad: a spreadsheet updated by hand, an ERP export that goes stale the same day, or — worst of all — the bot guessing at availability while the store sells something that already ran out.

This API is the bridge that was missing. It takes the question in natural language, searches the real Bling catalog, decides which product it is, looks up the balance and returns JSON the bot can use without inventing anything.

"do you have the black set in P?"
        │
        ▼
  GET /estoque?q=conjunto preto P
        │
        ├─ extracts the product terms (color and size drop out of the search)
        ├─ searches Bling and ranks by match score
        ├─ resolves the right variant — or asks back
        ▼
  { "found": true, "stock": 5, "available": true }

How it works

The full path of a question, from WhatsApp to the balance:

flowchart TD
  A["Customer asks on WhatsApp"] --> B["Chatbot (HTTP Action)"]
  B --> C["GET /estoque?q=..."]
  C --> D{"X-API-Key valid?"}
  D -- "no" --> E["401"]
  D -- "yes" --> F{"query has enough terms?"}
  F -- "no" --> G["found=false, asks for more detail"]
  F -- "yes" --> H["extracts terms and searches Bling"]
  H --> I{"access_token expired?"}
  I -- "yes" --> J["renews via refresh_token"]
  J --> H
  I -- "no" --> K["ranks the products by score"]
  K --> L{"found anything?"}
  L -- "no" --> M["found=false"]
  L -- "yes" --> N{"parent product with no clear variant?"}
  N -- "yes" --> O["ambiguous=true with the options"]
  N -- "no" --> P["looks up the variant balance"]
  P --> Q{"stock greater than zero?"}
  Q -- "yes" --> R["available=true with the quantity"]
  Q -- "no" --> S["available=false"]
  O --> T["Bot answers the customer"]
  R --> T
  S --> T
  M --> T
  G --> T
Loading

The decisions worth recording

Color and size drop out of the search, but come back in the ranking. Bling searches by name, and sending conjunto preto P as the query finds nothing: the product is registered as CONJUNTO ... COR:PRETO;TAMANHO:P. The API separates the two roles — it searches by the product terms, and uses color and size only to choose among the variants that came back.

When the question is ambiguous, the API asks back. If the customer didn't say the size, five existing variants are no reason to guess at the first one. The response comes with ambiguous: true and the list of options, and the bot is what drives the conversation.

The balance has two paths. Bling's stock endpoint doesn't always return the product's row. When it doesn't, the API falls back to the product's saldoVirtualTotal — rather than reporting zero and killing a sale.

Token renewal is protected against a race. Two requests hitting a 401 at the same time would renew the token twice, and the second would invalidate the first. Renewal sits behind a single promise, and before calling the API it re-reads the token file: if another process already renewed, it doesn't spend the call.

Color aliases become a canonical form. Nobody types AZUL MARINHO. The API expands marinho, terracota, oliva and company before comparing.


The endpoints

Method Route What for
GET /estoque?q= The main query: a natural-language question, returns the balance
GET /estoque/variantes?nome= Every variant of a product, with the balance of each
GET /health Availability probe
GET /oauth/url Generates the Bling authorization URL
GET /oauth/callback Exchanges the Bling code for tokens

The three responses from /estoque

In stock, with a balance:

{
  "found": true,
  "id": 900000001,
  "name": "CONJUNTO EXEMPLO COR:PRETO;TAMANHO:P",
  "price": 160.0,
  "stock": 5,
  "available": true,
  "message": "Sim, temos 5 unidade(s) disponivel(is)."
}

The variant is missing — the bot needs to ask:

{
  "found": false,
  "ambiguous": true,
  "message": "Encontrei algumas opcoes para este produto. Qual delas voce quer verificar?",
  "options": [
    { "id": 900000001, "name": "CONJUNTO EXEMPLO COR:PRETO;TAMANHO:P" },
    { "id": 900000002, "name": "CONJUNTO EXEMPLO COR:PRETO;TAMANHO:M" }
  ]
}

Not found:

{
  "found": false,
  "message": "Produto nao encontrado para \"xyz\". Verifique nome, cor e tamanho."
}

The data in these examples is fictitious. The real catalog belongs to the integration's client and isn't in this repository. The message field ships in Portuguese because it's the text the bot relays to the end customer.


Under the hood

src/
  app.js                    builds the Express app, without opening a port
  main.js                   runtime entry point
  bin/oauth.js              CLI for the initial authorization
  config/env.js             every process.env value passes through here
  controllers/              HTTP handlers: estoque, health, oauth
  http/middleware/          X-API-Key and the global error handler
  routes/                   the routes of the stock feature
  services/
    bling.service.js        OAuth2 client and calls to Bling
    estoque.service.js      the rule: search, rank, resolve the variant
  utils/matcher.js          normalization, aliases and match score
docs/                       integration guide and architecture map

The business rules live in services and utils/matcher.js, with no knowledge that HTTP exists. The controllers only translate a request into a call and a response into JSON — that's what makes the matcher testable without starting a server.

Decision Why
No database Bling is the source of truth. Caching the balance is how a store gets a sale wrong.
Tokens in a file and in env On a host with ephemeral disk the file disappears; the refresh_token from env guarantees the boot.
app.js separate from main.js The app builds without listening on a port, so it can be tested without starting a server.
Rate limit in front of everything The Bling quota belongs to the client. 120 req/min per IP protects it.

Run it

npm install
cp .env.example .env     # fill in your Bling app credentials
npm run dev              # http://localhost:3000

Authorize the app in Bling (one time only):

npm run oauth:url                        # open the URL, authorize, copy the code
npm run oauth:exchange -- YOUR_CODE      # exchanges the code for tokens

Query it:

curl http://localhost:3000/health

curl "http://localhost:3000/estoque?q=conjunto+preto+P" \
  -H "X-API-Key: your_key"

Deploy

Any Node host works. The Procfile is ready for Railway or Render: push the repository, configure the environment variables and point the Bling app's redirect URL at https://your-domain/oauth/callback.


Configuration

No .env goes into the repository — .env.example is the template.

Variable What for
BLING_CLIENT_ID / BLING_CLIENT_SECRET Your Bling app credentials. They are secrets
BLING_REFRESH_TOKEN Renews access without intervention. It is a secret
API_SECRET_KEY The key the bot sends in the X-API-Key header. It is a secret
BLING_SYNC_ENV false stops the API from rewriting the local .env on renewal
PORT Railway and Render inject it automatically

The Bling access_token expires in 1 hour and the API renews it on its own. On a host with ephemeral disk, .tokens.json doesn't survive a restart: update BLING_REFRESH_TOKEN in the environment variables whenever you reauthorize.


Known gaps

  • No test suite. matcher.js is a pure function and the obvious candidate for the first one — score, aliases and term extraction.
  • No cache. Every query hits Bling. At high volume, a short TTL per search term would save quota, at the cost of a slightly stale balance.
  • Search limited to 100 products per term. A large catalog with a very generic name may not surface the right variant on the first page.

License

© 2026 NerdResolve. All rights reserved. See LICENSE.md.

The repository is public for technical evaluation and portfolio demonstration. The code may be read and studied; there is no license to use, copy or redistribute it. Bling and BotConversa are trademarks of their respective owners; this project only consumes the public APIs of both.


Inventory API  ·  built by NerdResolve

Want your bot answering with real stock levels? contact@nerdresolve.com

Made by Matheus Mariath · NerdResolve

About

Real-time ERP stock, inside your chatbot. Node.js + Express middleware that queries Bling v3 in natural language and answers with the actual balance.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages