Overview
BridgesAgents brings together products, prices and stock from stores in any retail segment (grocery, electronics, pharmacy, fashion...) in a format ready for AI shopping agents.
There are three ways to query the catalog, all with the same agent key:
- MCP, for AI agents (Claude, ChatGPT, your own agents): the agent discovers and calls the tools on its own.
- REST API, for traditional systems and integrations.
- UCP (Universal Commerce Protocol), for agents that already speak that standard.
Start here: create a developer account, generate an agent key under Agent keys in the dashboard and send it with every request in the Authorization: Bearer abd_… header (details in Authentication).
| Resource | URL |
|---|---|
| MCP server (Streamable HTTP) | https://api.bridgesagents.com/mcp |
| REST API | https://api.bridgesagents.com/api/v1 |
| Interactive reference (Swagger) | https://api.bridgesagents.com/swagger-ui.html |
Concepts
| Concept | What it is |
|---|---|
| Merchant | The company (identified by its CNPJ, the Brazilian company tax ID) that sells. |
| Store | A merchant's physical location, with address, coordinates, how it serves customers (pickup, delivery by radius or by CEP ranges — CEP is the Brazilian ZIP code) and opening hours. Price and stock vary by store. The merchant sets up its stores. |
| Product | The item itself, the same in every store: name, brand, category, GTIN/EAN and unit. It has no price. |
| Offer | Product × store: price, promotional price and stock. This is what the agent compares to decide where to buy. |
Details the agent should take into account:
effectivePriceis the price to pay: the promotional price when there is one, otherwise the regular price.updatedAttells when price and stock were last updated.stale: trueflags an offer not updated for more than 7 days (the price may have changed); after 30 days it no longer shows up for agents.soldByWeightmeans the item is sold by weight (e.g. fresh produce): the price is per kg and the final amount is only known when the order is picked.- Units (
unit):UN(each),KG,G,L,ML. requiresPrescription: a medicine that requires a prescription. The agent must not complete the purchase; tell the consumer to present the prescription at the pharmacy.adult: true: 18+ product (alcoholic beverages, tobacco). BridgesAgents only flags it; the agent is the one who checks the age.inStoreOnly: true: sold on site only (fuel). It can't go into a cart and has no pickup or delivery: show the price, the distance andmapsUrl(directions to the store). To track gas prices, search withradiusKm.storeVerified: the store's company has an active CNPJ at the Receita Federal (Brazil's tax authority).storeRatingandstoreRatingCount: average rating (1 to 5) and how many ratings from people who bought through BridgesAgents.kind:GOOD(physical product),FUEL,SERVICE,BOOKABLE(travel, reservations) orDIGITAL(course, gift card). OnlyGOODgoes into a cart; the others are bought through the store'sbuyUrl.currencyis the price currency (ISO 4217, e.g.BRL).priceTypesays how to read the price:FIXED,FROM(starting at),PER_NIGHT,PER_PERSON.fulfillmentsays how it arrives:DELIVERY,PICKUP,IN_STORE(on site),AT_CUSTOMER(at the customer's place),BOOKINGorDIGITAL. Services and bookings have no stock (stockis null); digital can be unlimited. Bookings and digital items work from anywhere.- On the product:
taxonomyId(Google Product Taxonomy category),mpn(manufacturer part number) andattributes(type-specific data:color,size,duration_minutes,max_guests...).
Consumer location
Search and offers accept the consumer's location. With it, only stores that deliver there or that are close enough for pickup (up to 30 km) show up, and each offer says how the store serves the consumer.
- Prefer
latitudeandlongitude: they are precise. With only thepostalCode(CEP), the coordinates come from a CEP lookup service and are often just the city center; in that case the distance comes withdistanceApproximate: true. - Delivery applies if the consumer is within the store's radius or in one of the CEP ranges it serves. CEP ranges work even without coordinates, so always send the CEP when you have it.
Connect via MCP
The server uses the Streamable HTTP transport at https://api.bridgesagents.com/mcp.
Claude Code
claude mcp add --transport http bridgesagents https://api.bridgesagents.com/mcp \
--header "Authorization: Bearer abd_SUA_CHAVE"Clients that only accept local servers (stdio)
Use mcp-remote as a bridge. Example configuration (Claude Desktop, Cursor and similar):
{
"mcpServers": {
"bridgesagents": {
"command": "npx",
"args": ["mcp-remote", "https://api.bridgesagents.com/mcp", "--header", "Authorization:Bearer ${BRIDGESAGENTS_KEY}"],
"env": { "BRIDGESAGENTS_KEY": "abd_SUA_CHAVE" }
}
}
}Test without an agent
MCP Inspector lists and runs the tools from the browser. Choose Streamable HTTP, enter the URL above and add the Authorization header with Bearer abd_….
Clients that support OAuth (such as Claude and MCP Inspector) don't need a header: on first access they get a 401 with WWW-Authenticate, discover the authorization server, register themselves and open the page where the consumer signs in (or creates an account) and allows access. See Authentication.
npx @modelcontextprotocol/inspectorMCP tools
search_products
Searches products by name or brand, in any segment. Ignores accents and case, matches word prefixes (arro → arroz) and synonyms, including regional Brazilian ones (bateria → pilha, bolacha → biscoito, macaxeira → mandioca). Tolerates typos (arros → arroz) and understands English and Spanish terms (rice → arroz, frijoles → feijão, haircut → corte).
| Parameter | Type | Required | Description |
|---|---|---|---|
| query | string | yes | Search text, in Portuguese. E.g. "fone bluetooth" (Bluetooth headphones). |
| limit | number | no | Maximum number of results, from 1 to 50. Default: 10. |
| postalCode | string | no | Consumer's CEP (Brazilian ZIP code). With a location, only products some store serves there. |
| latitude, longitude | number | no | Consumer's coordinates (more precise than the CEP). |
| radiusKm | number | no | Only stores up to this distance from the consumer, in km (up to 100). Requires a location. E.g. gas stations within 5 km. |
[
{
"id": "00000000-0000-0000-0000-0000000000b8",
"gtin": "7890000000103",
"name": "Amoxicilina 500mg 21 cápsulas",
"brand": "Marca F",
"category": "Medicamentos",
"unit": "UN",
"requiresPrescription": true,
"imageUrl": "https://img.example.com/amoxicilina.png"
}
]get_offers
Lists a product's offers by store, from cheapest to most expensive (tie: the closest one first).
| Parameter | Type | Required | Description |
|---|---|---|---|
| productId | UUID | yes | id returned by search_products. |
| postalCode | string | no | Consumer's postal code (in Brazil, the CEP; cep is still accepted). |
| country | string | no | Consumer's country, 2 letters (BR, US, PT). Default: BR. Outside Brazil, the postal code becomes approximate coordinates (OpenStreetMap). |
| latitude, longitude | number | no | Consumer's coordinates. |
| radiusKm | number | no | Only stores up to this distance from the consumer, in km (up to 100). Requires a location. E.g. gas stations within 5 km. |
[
{
"storeId": "00000000-0000-0000-0000-0000000000a4",
"storeName": "Farmácia Exemplo Savassi",
"city": "Belo Horizonte",
"region": "MG",
"price": 32.90,
"promoPrice": null,
"effectivePrice": 32.90,
"stock": 20.000,
"inStock": true,
"soldByWeight": false,
"requiresPrescription": true,
"updatedAt": "2026-10-25T21:51:47Z",
"stale": false,
"distanceKm": 0.2,
"distanceApproximate": false,
"pickup": true,
"delivery": { "fee": 0.00, "etaMinutes": 40, "minOrder": null, "coveredBy": "radius" },
"openNow": true,
"buyUrl": "https://api.bridgesagents.com/r/00000000-0000-0000-0000-0000000000cb"
}
]| Field | What it means |
|---|---|
| distanceKm | Straight-line distance to the store. Null when either side has no coordinates. |
| pickup | Pickup at the store is available (pickup enabled and the store is close to the consumer). |
| delivery | Delivery terms (fee, lead time in minutes, minimum order) if the store delivers there; null if it doesn't. coveredBy: cep_range (certain), radius (exact coordinates) or radius_approximate (computed from the CEP's coordinates: it may not apply, check with the store). Without a location, it shows the terms with coveredBy null. |
| openNow | Whether the store is open now, in its own time zone. Null if the store hasn't provided opening hours. |
| buyUrl | Link for the consumer to buy directly from the store: it opens the store's WhatsApp (with a ready-made message including product and price) or its website. Null if the store hasn't provided a sales channel. Show it to the consumer once they pick the offer. |
create_cart
Builds a cart at one store and returns confirmUrl: the link the consumer opens to review it, enter their name and WhatsApp (and address, for delivery) and confirm. The agent doesn't close the purchase on its own and never receives personal data. The order goes to the store's dashboard and email; payment is with the store. If the store set a Pix key, the link shows a QR Code and a copy-and-paste code with the order amount.
| Parameter | Type | Required | Description |
|---|---|---|---|
| storeId | UUID | yes | Store (from get_offers). A cart belongs to a single store. |
| items | list | yes | 1 to 50 items { productId, quantity }. Fractional quantities only for products sold by weight. |
| fulfillment | string | yes | Products: pickup or delivery. Services: in_store or at_customer (at home). Bookings: booking. Digital: digital. Use what the offer accepts (fulfillment in get_offers). |
| postalCode | string | for delivery | Consumer's postal code (CEP in Brazil); the store must deliver there. |
| note | string | no | Note for the store (shown on the order). |
| scheduledFor | string | for services | Requested date and time, with time zone (e.g. 2026-11-05T14:00-03:00). Use a free slot from check_availability; times outside store hours or already full are rejected. |
| checkIn / checkOut | string | for bookings | YYYY-MM-DD dates. checkOut is required with a per-night price (the line multiplies by the nights). |
| guests | number | for bookings | People. Required with a per-person price (the line multiplies by the guests). |
| externalReference | string | no | Your id for this cart (conversation or user in your system), up to 100 characters. Returned in get_cart, Agent orders and developer webhooks. |
{
"id": "f4e2add5-0103-4fc2-aebb-77dbabff2241",
"status": "OPEN",
"store": { "id": "00000000-0000-0000-0000-0000000000a4", "name": "Farmácia Exemplo Savassi", … },
"fulfillment": "PICKUP",
"items": [
{ "productId": "00000000-0000-0000-0000-0000000000b6", "name": "Protetor Solar FPS 50 200ml",
"unit": "UN", "quantity": 2, "unitPrice": 52.90, "lineTotal": 105.80 }
],
"subtotal": 105.80,
"deliveryFee": null,
"total": 105.80,
"expiresAt": "2026-10-27T02:39:54Z",
"confirmUrl": "https://bridgesagents.com/compras/c/abc_…",
"order": null
}Prescription products can't go in a cart. If any item has a problem (out of stock, quantity, store doesn't sell it), nothing is created and the response lists each problem. On confirmation, price and stock are checked again: if the price changed, the consumer sees the new amount before confirming.
check_availability
Real-time schedule, before create_cart. Service: slots with the free times within store hours (every duration_minutes) and the spots left (free, when the store sets capacity); use a start as scheduledFor. Booking: nights with each night and the free units (free, when the store sets units; 0 = full). The cart rejects times outside store hours or already full and suggests the next free ones.
| Parameter | Type | Required | Description |
|---|---|---|---|
| storeId | UUID | yes | Store (from get_offers). |
| productId | UUID | yes | Service or booking (from search_products). |
| from | YYYY-MM-DD | no | First day; today if empty. |
| days | integer | no | How many days to look at, 1 to 31 (default 7). |
get_cart
Status of a cart created with your key: OPEN, CONFIRMED (with the order number and status) or EXPIRED. The link lasts 24 hours. While the order is NEW, order.respondBy says until when the store can accept it (2 h; 24 h for bookings and digital, counted from when the store opens): without a response it is cancelled automatically with the reason A loja não respondeu a tempo.
| Parameter | Type | Required | Description |
|---|---|---|---|
| cartId | UUID | yes | id returned by create_cart. |
Upcoming tools: quote_delivery and payment through the agent.
UCP and ACP
The same catalog is also available in Google's and OpenAI's agentic commerce formats. Each product (the same in every store, by GTIN) carries the offers from all stores. The purchase still happens at the store: there is no checkout through the agent yet.
UCP (Universal Commerce Protocol)
The profile at https://api.bridgesagents.com/.well-known/ucp advertises the dev.ucp.shopping.catalog.search and dev.ucp.shopping.catalog.lookup capabilities (version 2026-08-25). Each store offer is a variant: the store is in seller, the price in cents ({"amount": 2490, "currency": "BRL"}), and url is the purchase link. Distance, pickup, delivery and opening hours are in metadata. Pass context.postal_code to see only the stores that serve the consumer.
POST/ucp/v1/catalog/search
Text search, with categories and price filters (min/max in cents) and cursor pagination.
curl -X POST "https://api.bridgesagents.com/ucp/v1/catalog/search" -H "Authorization: Bearer abd_SUA_CHAVE" \
-H "Content-Type: application/json" \
-d '{"query": "pilha aa", "context": {"postal_code": "30140-010"}, "pagination": {"limit": 5}}'POST/ucp/v1/catalog/lookup
Products by ids; the ones that don't exist come back in messages with not_found.
POST/ucp/v1/catalog/product
A single product by id, with all its offers.
ACP (Agentic Commerce Protocol, OpenAI)
ChatGPT product feed: one JSON object per line, one line per store offer, with price in the 19.90 BRL format and is_eligible_checkout: false. Offers without a brand, an image or a sales channel are left out (the format requires those fields), as are prescription products. The X-BridgesAgents-Skipped header tells how many were left out.
The feed is for internal use: BridgesAgents itself generates it at /feeds/acp/products.jsonl.gz and sends it to OpenAI. Agents don't need to download it; to query the catalog, use MCP, REST or UCP with the agent key.
Import the catalog
The merchant uploads the spreadsheet they already use, in Excel (.xlsx) or CSV, through the dashboard or the API. Importing again updates price and stock without duplicating products.
- The first row is the header. Column order doesn't matter, and unknown columns are ignored.
- Names are compared ignoring accents, case and punctuation:
Preço de Vendaandpreco_vendaare the same column. - Numbers accept the Brazilian format (
1.234,56,R$ 27,90— R$ is the Brazilian real) and also27.90. - CSV can use semicolon, comma or tab as the separator, in UTF-8 or in Excel's default format for Portuguese.
- Rows with problems don't block the others: the result lists each one with its row number and the reason.
- Fuel (gasoline, ethanol, diesel, CNG) is automatically flagged as sold on site only: no cart, it shows up by distance to the station. Weapons, ammunition, explosives, e-cigarettes and drugs are rejected.
- Limits: 5 MB and 10,000 rows per file.
Column headers are recognized by their Portuguese names (the ones in the table and in the template). Yes/no columns also accept yes/no and true/false.
| Column | Accepted names | Required | Notes |
|---|---|---|---|
| Name | nome, produto, descrição, item | yes | Up to 300 characters. |
| Price | preço, valor, preço de venda, valor unitário | yes | Greater than zero. |
| EAN | ean, gtin, código de barras | no | 8, 12, 13 or 14 digits. It's what links your product to the same product in other stores. |
| Brand | marca, fabricante | no | Without an EAN, name + brand identify the product. |
| Category | categoria, departamento, seção | no | |
| Unit | unidade, un, und | no | UN (default), KG, G, L or ML. |
| Promotional price | preço promocional, promoção, preço oferta | no | Lower than the price. |
| Stock | estoque, quantidade, qtd, saldo | no | Default 0. |
| Sold by weight | vendido por peso, por peso, granel | no | yes/no. Without the column, unit KG counts as sold by weight. |
| Requires prescription | exige receita, receita, tarja, venda sob prescrição | no | yes/no, or "tarja vermelha"/"tarja preta" (Brazilian red/black label prescription drugs). Agents don't complete the purchase of these products. |
| 18+ | maior de 18, maiores de 18, 18, adulto | no | yes/no. Alcoholic beverages and tobacco are flagged automatically; the agent checks the age. |
| Image | imagem, foto, url da imagem, image_link | no | Public https:// URL of the product photo. |
| Item type | tipo de item, kind | no | product, fuel, service, booking or digital. Default: product. |
| Currency | moeda, currency | no | 3-letter code (BRL, USD, EUR). Default: BRL. In the Google feed format, the price can carry the currency (27.90 BRL). |
| Price type | tipo de preço, price_type | no | fixed, from, per night or per person. Default: fixed. |
| Fulfillment | atendimento, fulfillment | no | delivery, pickup, in_store, at_customer, booking or digital (several separated by commas). Default: the type's. |
| MPN and SKU | mpn / sku | no | Manufacturer part number (identifies the item without an EAN, together with the brand) and the code in your system. |
| Google category | google_product_category | no | Google Product Taxonomy id or path (e.g. 2271 or Apparel & Accessories > Clothing). |
| Attributes | atributo_<name> (e.g. atributo_duration_minutes), color, size | no | Type-specific data: product color and size, service duration, booking guests. Google Merchant Center feed spreadsheets are accepted too (title, price, sale_price, image_link...). |
Store setup
The merchant is responsible for setting up each store: address, location, how it serves customers and opening hours. That's how agents know whether the store delivers to the consumer, whether pickup is possible nearby and whether it's open. The dashboard shows what's missing in each store.
- Coordinates: enter the exact ones (in Google Maps, right-click the store and click the numbers). If left blank, we use an approximation from the CEP, which may be just the city center.
- Delivery: by radius (km from the store), by CEP ranges or both. Fee, lead time in minutes and minimum order are set per store.
- Sales channel: WhatsApp for orders and/or a website. That's where the “buy at this store” link (
buyUrl) takes the customer. On the website,{produto}in the URL is replaced with the product name. - Opening hours: per day of the week, in the time zone of the store's state (UF). Two shifts separated by a comma,
24horfechado(closed). Closing after midnight (22:00-02:00) also works.
Store spreadsheet (chains)
In Stores → Import spreadsheet, a chain creates and updates all its stores at once. The code identifies each store: if it exists, it's updated; otherwise, it's created.
As in the catalog, column headers are recognized by their Portuguese names. Days are seg, ter, qua, qui, sex, sab, dom (Monday to Sunday).
| Column | Accepted names | Required | Example |
|---|---|---|---|
| Code | código, código da loja, filial | yes | BH-SAVASSI |
| Name | nome, loja, unidade | yes | Farmácia Centro |
| CEP / postal code | cep, codigo postal, zip, postal_code | yes | 30140-010 |
| Address | rua/logradouro, número, complemento, bairro, cidade, UF | no* | Rua Pernambuco, 1200 |
| Coordinates | latitude, longitude | no | -19.937410 / -43.934170 |
| Service | retirada, entrega | no | sim / não (yes / no) |
| Delivery | raio km, taxa de entrega, prazo (min), pedido mínimo | no | 4 · 6,90 · 45 · 30,00 |
| CEP ranges | faixas cep, ceps atendidos | no | 30100-000 a 30199-999; 30210-000 |
| Opening hours | seg, ter, qua, qui, sex, sab, dom | no | 08:00-12:00, 14:00-20:00 · 24h · fechado |
| Sales channel | whatsapp, site | no | (31) 99999-0000 · https://minhaloja.com.br/busca?q={produto} |
| Pix key | chave_pix, pix | no | Brazil only: CPF, CNPJ, email, mobile number or random key. The consumer pays through the order link. |
* City and UF (state) are filled in from the CEP when left blank. Download the store template spreadsheet
Connect Bling
Merchants who use Bling (a Brazilian ERP) need neither a spreadsheet nor any coding: under Integrations in the dashboard, the admin connects the Bling account, authorizes BridgesAgents (read-only access to products, stock and warehouses) and chooses which warehouse supplies each store.
- Price, photo, barcode, brand and category come from the product record in Bling.
- Each store's stock is the sum of the available balance of the warehouses linked to it.
- Price and stock changes arrive within seconds; a full sync runs every day overnight and shows up under Imports with Bling as the source.
- A product deactivated or deleted in Bling gets zero stock.
- Products with variations (color, size) aren't supported yet: for now, send those through the spreadsheet.
Connect Nuvemshop
Merchants who sell on a Nuvemshop online store (Tiendanube outside Brazil) don't need a spreadsheet either: under Integrations in the dashboard, the admin connects the store, authorizes BridgesAgents (read-only access to products and stock) and chooses which BridgesAgents store receives the stock of each Nuvemshop location (with a single store, this is automatic).
- Each variant becomes an item, named with the product name and the variant values (e.g. "Camiseta Básica – Azul / M").
- Price, sale price, photo, barcode (when it's a valid GTIN), brand and category come from Nuvemshop.
- A variant without stock tracking in Nuvemshop shows as available, with 999 units.
- Product changes arrive within seconds; a full sync runs every day overnight and shows up under Imports with Nuvemshop as the source.
- A product that is hidden, not listed or deleted in Nuvemshop gets zero stock. A variant without a price ("ask for a quote") isn't imported.
Order webhooks
To have the order land straight in the store's system (ERP, POS, scheduling, n8n, Zapier): under Integrations, the account admin or the company developer registers an https address and chooses the store (or all of them) and the events. Admins get an email for every new webhook.
order.created: the consumer confirmed the order (same moment as the email to the store).order.status_changed: the store accepted, got it ready, completed or cancelled it (withcancelReason).order.paid: the store marked the order as paid (or undid it:paidAtgoes back to empty).- Each delivery is a
POSTwith the full order indata.order: items with the store'sgtinandsku, customer, address, booking or service schedule, totals and currency. New fields may appear; none is renamed. - Customer data only goes to the order's store. To send the status back, use
POST /api/v1/me/orders/{id}/statuswith the store key. - Order number: when registering, choose what your system accepts. Digits only:
"number": 1042. Letters and digits:"number": "AB1042". Thereferencefield ("AB1042", the same identifier as the Pix and the consumer's link) is always sent. - Access header (optional): if your system requires a fixed token (
Authorization: Bearer …,X-Api-Key...), register its name and value: it goes in every delivery, along with the signature. The value is encrypted and the dashboard only shows its end. - Developer account: under Webhooks, the agent developer gets the same events for orders placed with their keys, with
data.cart(id,externalReference,agentKeyId) and the order without the buyer's personal data (customeranddeliveryAddressempty). That's how the bot notifies the user without pollingget_cart.
Example
POST https://seusistema.com.br/agentbridge/pedidos
Content-Type: application/json
X-BridgesAgents-Event: order.created
X-BridgesAgents-Event-Id: 6f1c0d2e-…
X-BridgesAgents-Delivery: 0b9a7c41-…
X-BridgesAgents-Signature: t=1793000000,v1=5d2c…
X-Api-Key: (o cabeçalho de acesso, se você cadastrou um)
{
"id": "6f1c0d2e-…",
"event": "order.created",
"createdAt": "2026-10-27T14:03:11Z",
"data": {
"order": {
"id": "…", "number": 1042, "reference": "AB1042", "status": "NEW",
"storeId": "…", "storeCode": "CENTRO", "storeName": "Exemplo Centro",
"fulfillment": "DELIVERY",
"customer": { "name": "Ana Souza", "phone": "+5511988887777", "email": "ana@exemplo.com", "note": null },
"deliveryAddress": { "postalCode": "01310100", "street": "Av. Paulista", "number": "1000", "complement": null,
"district": "Bela Vista", "city": "São Paulo", "region": "SP" },
"items": [
{ "productId": "…", "gtin": "7890000000011", "sku": "ARZ-5", "name": "Arroz Branco Tipo 1 5kg", "unit": "UN",
"quantity": 2, "unitPrice": 24.90, "multiplier": 1, "lineTotal": 49.80, "adult": false }
],
"subtotal": 49.80, "deliveryFee": 8.00, "total": 57.80, "currency": "BRL", "estimated": false,
"schedule": { "scheduledFor": null, "checkIn": null, "checkOut": null, "guests": null },
"paidAt": null, "cancelReason": null, "agentNote": null,
"createdAt": "2026-10-27T14:03:10Z", "updatedAt": "2026-10-27T14:03:10Z",
"payment": { "status": "PENDING", "method": "PIX" }
}
}
}Verifying the signature
When you register, the dashboard shows the secret (whsec_…) once. Each delivery carries X-BridgesAgents-Signature: t=<seconds>,v1=<hex>, where v1 is the HMAC-SHA256, keyed with the secret, of t + "." + body (the body exactly as received). Reject it if it doesn't match or if t is older than 5 minutes.
// Node.js (Express): use o corpo cru, antes de qualquer JSON.parse
import crypto from "node:crypto";
app.post("/agentbridge/pedidos", express.raw({ type: "application/json" }), (req, res) => {
const header = req.get("X-BridgesAgents-Signature") ?? "";
const t = header.match(/t=(\d+)/)?.[1];
const v1 = header.match(/v1=([0-9a-f]+)/)?.[1] ?? "";
const expected = crypto.createHmac("sha256", process.env.BRIDGESAGENTS_WEBHOOK_SECRET)
.update(`${t}.${req.body}`).digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300;
if (!fresh || v1.length !== expected.length
|| !crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected))) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body);
// Guarde event.id: se chegar de novo (nova tentativa ou reenvio), ignore
res.sendStatus(200);
});# Python (Flask)
import hashlib, hmac, os, re, time
from flask import Flask, request, abort
app = Flask(__name__)
@app.post("/agentbridge/pedidos")
def pedidos():
header = request.headers.get("X-BridgesAgents-Signature", "")
t = re.search(r"t=(\d+)", header)
v1 = re.search(r"v1=([0-9a-f]+)", header)
if not t or not v1 or abs(time.time() - int(t[1])) > 300:
abort(401)
body = request.get_data(as_text=True)
expected = hmac.new(os.environ["BRIDGESAGENTS_WEBHOOK_SECRET"].encode(), f"{t[1]}.{body}".encode(),
hashlib.sha256).hexdigest()
if not hmac.compare_digest(v1[1], expected):
abort(401)
event = request.get_json()
return "", 200Responses, retries and resending
- Answer 2xx within 10 seconds and process later if it takes long. Redirects are not followed.
- Without a 2xx we retry after 1 min, 5 min, 30 min, 2 h and 12 h. Once exhausted (about 15 h), the webhook is paused and admins get an email; orders stay in the dashboard.
- The same event can arrive more than once (retry or resend): use
X-BridgesAgents-Event-Id(same as the body'sid) to skip duplicates. Order isn't guaranteed after a failure: comparedata.order.updatedAt. - The dashboard keeps recent deliveries (30 days) with each error, plus Send test (fake order,
webhook.testevent), Resend, Pause/Resume and Rotate secret.
Testing in the sandbox
In the sandbox you test your agent's whole flow without a real store: stores, products and orders are fictitious, and you play the store and the payment checkout.
- In the dashboard: under Agent orders, open an order and use the buttons to accept, mark ready, complete or cancel it, and to report the payment (paid with Pix, card or boleto, refused, refunded, disputed).
- Through the API, with the agent key (for automated tests):
POST /api/v1/carts/{id}/sandbox/statuswith{"status": "ACCEPTED"}(ACCEPTED, READY, COMPLETED, CANCELLED) andPOST /api/v1/carts/{id}/sandbox/paymentwith{"status": "paid", "method": "pix"}(paid, refused, refunded, chargeback, pending; pix, card, boleto). - Each simulation really changes the order: the consumer is notified, your webhooks get
order.status_changedandorder.paid, andget_cartshowsorder.status,order.paymentStatus,order.paymentMethodandorder.paidAt. - Only orders built by your keys. In production these routes don't exist: the store responds.
# Aceitar o pedido e avisar o pagamento aprovado (sandbox, com a chave de agente)
curl -X POST https://api.sandbox.bridgesagents.com/api/v1/carts/$CART_ID/sandbox/status \
-H "Authorization: Bearer $BRIDGESAGENTS_KEY" -H "Content-Type: application/json" \
-d '{"status": "ACCEPTED"}'
curl -X POST https://api.sandbox.bridgesagents.com/api/v1/carts/$CART_ID/sandbox/payment \
-H "Authorization: Bearer $BRIDGESAGENTS_KEY" -H "Content-Type: application/json" \
-d '{"status": "paid", "method": "pix"}'Full toolkit: risk and areas
Developer accounts can see the risk level of the orders their agents generated (GET /api/v1/me/agent-orders/{cartId}/risk) and the numbers by area from the network stores (GET /api/v1/me/insights/regions), and search orders by area (GET /api/v1/me/insights/orders?q=04001, by the start of the postal code or by city or neighborhood name): products and category, payment, refusals and the reason, and the contact (name, email, phone) only from orders with consent: the bot operator collects the buyer's consent, under the clause in its contract with BridgesAgents. What shows depends on what the contract allows, field by field, and every search is logged. Everything is open in the sandbox; in production, under a contract with BridgesAgents.
Send the catalog via API
For the store's system (ERP, online store, script) to send prices and stock automatically, without a spreadsheet. Same rules and messages as the spreadsheet import.
1. Generate the key
There are two kinds:
- Company key (
POST /api/v1/me/api-keys): works for all stores. Each item setsstoreCode, the store's code in your system. Ideal for chains. - Single-store key (
POST /api/v1/stores/{storeId}/api-keys): everything goes to that store andstoreCodeis ignored.
In the dashboard, under API keys, or via the API with the merchant session (see Authentication). The full key only appears in that response: store it securely.
curl -X POST "https://api.bridgesagents.com/api/v1/stores/{storeId}/api-keys" \
-H "Authorization: Bearer abs_…" \
-H "Content-Type: application/json" -d '{"name": "ERP"}'{
"apiKey": { "id": "…", "name": "ERP", "keyPrefix": "abk_Xy12Ab34", "createdAt": "…", "lastUsedAt": null, "revokedAt": null },
"key": "abk_Xy12Ab34…"
}2. Send the items with the key
The key goes in the X-Api-Key header. With the company key, each item's store comes from storeCode; with a single-store key, it's always that store. Each item creates or updates the store's offer; items not sent stay as they are (to take one off sale, send stock 0). Up to 1,000 items per request.
curl -X POST "https://api.bridgesagents.com/api/v1/catalog/items" \
-H "X-Api-Key: abk_…" -H "Content-Type: application/json" \
-d '{
"items": [
{ "storeCode": "BH-SAVASSI", "gtin": "7890000000066", "name": "Pilha Alcalina AA com 4", "price": 17.50, "stock": 60 },
{ "storeCode": "BH-PAMPULHA", "gtin": "7890000000103", "name": "Amoxicilina 500mg 21 cápsulas",
"price": 29.90, "stock": 8, "requiresPrescription": true }
]
}'| Field | Type | Required | Notes |
|---|---|---|---|
| name | text | yes | Up to 300 characters. |
| price | number | yes | Greater than zero, with a decimal point (17.50). |
| gtin | text | no | EAN with 8, 12, 13 or 14 digits. Links the item to the same product in other stores. |
| brand | text | no | Without a GTIN, name + brand identify the product. |
| category | text | no | |
| unit | text | no | UN (default), KG, G, L or ML. |
| promoPrice | number | no | Lower than price. |
| stock | number | no | Default 0. |
| soldByWeight | true/false | no | Without the field, unit KG counts as sold by weight. |
| requiresPrescription | true/false | no | Prescription medicine. Agents don't complete the purchase. |
| adult | true/false | no | 18+ only. Alcoholic beverages and tobacco are flagged automatically from the name and category. |
| storeCode | text | with a company key | The store's code in your system (set up under Stores). |
| kind | text | no | GOOD, FUEL, SERVICE, BOOKABLE or DIGITAL. Default: GOOD. |
| currency | text | no | ISO 4217 (BRL, USD, EUR). Default: BRL. |
| priceType | text | no | FIXED, FROM, PER_NIGHT or PER_PERSON (each type accepts some). |
| fulfillment | list | no | DELIVERY, PICKUP, IN_STORE, AT_CUSTOMER, BOOKING, DIGITAL. Default: the type's. |
| mpn / sku | text | no | Manufacturer part number and the code in your system. |
| googleCategory | text | no | Google Product Taxonomy id or path. |
| attributes | object | no | Type-specific data, e.g. {"duration_minutes": 60, "capacity": 2} (service: duration and simultaneous appointments) or {"max_guests": 2, "check_in": "14:00", "units": 3} (booking: identical units per night). |
| imageUrl | text | no | Public https:// URL of the product photo. The first one provided for the product is used. |
The response tells what got in. Items with problems don't block the others and show up in errors with their position in items (starting at 0):
{
"received": 2, "imported": 1, "failed": 1,
"productsCreated": 0, "offersCreated": 0, "offersUpdated": 1,
"errors": [ { "index": 1, "gtin": null, "name": "Banana Prata", "message": "Preço em branco." } ]
}To replace a leaked key, generate a new one and revoke the old one in the dashboard (or with DELETE /api/v1/stores/{storeId}/api-keys/{keyId}).
REST API
Responses in JSON. Product and offer fields are the same as in the MCP tools.
GET/api/v1/products?q={busca}&limit={1-50}&postalCode=&lat=&lng=
Searches products. limit is optional (default 20); postalCode, lat and lng filter by the stores that serve the consumer (see Consumer location).
curl -H "Authorization: Bearer abd_SUA_CHAVE" "https://api.bridgesagents.com/api/v1/products?q=pilha%20aa"GET/r/{offerId}
The offers' buyUrl: counts the click and redirects to the store's WhatsApp or website.
GET/api/v1/products/{id}/offers
The product's offers by store, from cheapest to most expensive.
curl -H "Authorization: Bearer abd_SUA_CHAVE" \
"https://api.bridgesagents.com/api/v1/products/00000000-0000-0000-0000-0000000000b7/offers?postalCode=30140-010"GET/api/v1/products/{id}/availability
Same as the check_availability tool: ?storeId=...&from=YYYY-MM-DD&days=7.
POST/api/v1/carts
Same as the create_cart tool. Returns 201 with the cart and confirmUrl; items with problems come back in problems (HTTP 400).
curl -X POST "https://api.bridgesagents.com/api/v1/carts" -H "Authorization: Bearer abd_SUA_CHAVE" \
-H "Content-Type: application/json" \
-d '{"storeId": "00000000-0000-0000-0000-0000000000a4", "fulfillment": "pickup",
"items": [{"productId": "00000000-0000-0000-0000-0000000000b6", "quantity": 2}]}'GET/api/v1/carts/{id}
Same as the get_cart tool.
With a merchant session (header Authorization: Bearer abs_…)
GET/api/v1/me
The signed-in merchant.
GET/api/v1/me/stores
The signed-in merchant's stores, with the full setup and what's missing in each one (pending).
POST/api/v1/me/stores
Creates a store. PUT /api/v1/me/stores/{id} updates it.
POST/api/v1/me/stores/import
Store spreadsheet (multipart/form-data, field file). See Store setup.
GET/api/v1/me/offers?q=&storeId=&stale=&page=&size=
Offers from the stores you can see (store manager: only theirs), by product name: price, stock, when it was updated, stale and hidden. q searches name, brand and barcode; stale=true returns only those not updated for more than 7 days. Paginated (size up to 100).
GET/api/v1/me/offers/freshness
Outdated offers by store: total, not updated for more than 7 days (stale) and for more than 30 (hidden, already hidden from agents).
GET/api/v1/me/performance?days={1-90}
Performance of the stores the user can see: impressions to agents, times with the lowest price and clicks on the purchase link, by store and by product.
GET/api/v1/me/price-insights?days={1-90}&storeId=&limit=
Price intelligence: for each of your products, how your price compares with other stores in the same city (without naming them) and how many times you stopped being the lowest price in agent queries. days from 1 to 90.
GET/api/v1/me/agent-orders?status=&keyId=
Developer account: carts and orders from your agent keys, without buyer data (cartStatus OPEN, CONFIRMED or EXPIRED; orderStatus, respondBy, paidAt, externalReference). status: open, confirmed, expired or cancelled. GET .../summary?days=30 returns carts, confirmed, conversion and orders the store didn't respond to.
GET/api/v1/me/orders?status=&storeId=
Orders from the stores you can see. status: novos (new), andamento (in progress), concluidos (completed) or cancelados (cancelled); without it, all. GET .../summary returns how many are new and in progress; GET .../{id}, the order with the customer's details.
POST/api/v1/me/orders/{id}/status
Changes the status: ACCEPTED, READY, COMPLETED or CANCELLED (with reason, which the customer sees). It only moves forward; the customer is notified by email if they gave one.
POST/api/v1/me/api-keys
Generates the company key (all stores). GET lists and DELETE .../{keyId} revokes.
POST/api/v1/stores/{storeId}/catalog/import
Imports the spreadsheet for one of your stores. Send it as multipart/form-data in the file field.
curl -H "Authorization: Bearer abs_…" -F "file=@catalogo.xlsx" \
"https://api.bridgesagents.com/api/v1/stores/{storeId}/catalog/import"GET/api/v1/me/imports?problems={true|false}&limit={1-100}
Import history (spreadsheet and API) for your stores, newest first. problems=true returns only the ones with rejected items or that were refused.
GET/api/v1/me/imports/{id}
A single import, with the rejected items (errors): spreadsheet row or position in items, and the reason.
With a store API key (header X-Api-Key: abk_…)
POST/api/v1/catalog/items
Sends items as JSON. See Send the catalog via API.
POST/api/v1/stores/{storeId}/api-keys
With a merchant session: generates a key for one of your stores. GET on the same path lists the keys (only the beginning of each) and DELETE .../api-keys/{keyId} revokes.
{
"rows": 120,
"imported": 118,
"failed": 2,
"productsCreated": 40,
"offersCreated": 100,
"offersUpdated": 18,
"ignoredColumns": ["Observações"],
"errors": [
{ "line": 7, "message": "Preço inválido: \"abc\"." },
{ "line": 31, "message": "Nome do produto em branco." }
]
}To try the endpoints right in the browser, use the interactive reference (Swagger).
Errors and limits
| Situation | Result |
|---|---|
| Empty search | Empty list. |
| limit outside 1 to 50 | Clamped to the nearest limit. |
| Product without offers or nonexistent id | Empty list. |
| id that isn't a UUID (REST) | HTTP 400. |
| Missing q parameter (REST) | HTTP 400. |
| Import: empty file, unsupported format, no name and price columns, or more than 10,000 rows | HTTP 400. |
| Import: file larger than 5 MB | HTTP 413. |
| Catalog query (REST, MCP or UCP) without an agent key, or with an invalid or revoked key | HTTP 401. |
| More than 120 requests per minute with the same key | HTTP 429, with Retry-After. |
| No session or expired session (dashboard routes) | HTTP 401. |
| Merchant account creating an agent key, or developer account creating a store | HTTP 403. |
| Store or import that isn't yours (or doesn't exist) | HTTP 404. |
| Sending via API: no X-Api-Key, invalid or revoked key | HTTP 401. |
| Search: invalid CEP, only latitude or only longitude, or coordinates outside Brazil | HTTP 400. |
| Store setup with invalid data (CEP, UF, coordinates, opening hours, CEP range) | HTTP 400, with the reason. |
| Sending via API: malformed JSON, no items or more than 1,000 items | HTTP 400. |
Errors use the application/problem+json format (RFC 9457). The detail field carries a message in Portuguese that can be shown to the merchant.
The API's detail messages are always in Portuguese; the BridgesAgents dashboard translates them. If you show them to users in another language, map them by the HTTP status or translate them on your side.
Authentication
| Who | How | What for |
|---|---|---|
| Agent (developer account) | Agent key: Authorization: Bearer abd_… | Search products and offers: REST, MCP and UCP. Required. |
| Consumer's AI assistant | OAuth 2.1: Authorization: Bearer abo_… | Search products and offers on behalf of a consumer who connected the assistant. Scope catalog. |
| Merchant | Session: Authorization: Bearer abs_… | Dashboard, spreadsheet import, key generation, history. Only sees its own stores. |
| Merchant's system | Store key: X-Api-Key: abk_… | Send the catalog as JSON to that store. |
Agent key
No catalog query is anonymous. Create a developer account (no CNPJ needed) and generate keys under Agent keys in the dashboard. The key is shown only once; the database stores only its hash. Use one key per agent or environment: if one leaks, revoke just that one. The dashboard shows how many requests each key made per day, and the limit is 120 per minute per key.
curl -X POST "https://api.bridgesagents.com/api/v1/me/agent-keys" -H "Authorization: Bearer abs_…" \
-H "Content-Type: application/json" -d '{"name": "Agente em produção"}'
# → { "apiKey": { "id": "…", "keyPrefix": "abd_Xk3…" }, "key": "abd_…" } (só desta vez)Plans and quota
Each request authenticated with an agent key counts as one request (search, offers, cart or MCP call). The Free plan includes 10,000 requests per month; once you reach them, the API returns 429 with the explanation in detail until the 1st of the next month. Paid plans (Pro and Scale) don't block: overage is billed per 1,000 requests on the end-of-month invoice. We email you at 80% and 100% of the quota, and this month's usage shows under Plan and billing in the dashboard. The sandbox has no quota.
Sandbox
To test without touching real stores, use the sandbox: site at https://sandbox.bridgesagents.com and API at https://api.sandbox.bridgesagents.com (MCP at /mcp). It runs the same version as production (every change is published to both), but with its own database of sample stores and products. Create a developer account there: keys start with abd_test_ and don't work in production (nor do production keys work there). Carts and orders work end to end, only at the sample stores; emails arrive with [Sandbox] in the subject.
OAuth 2.1 (MCP server)
We follow the MCP specification's authorization: metadata at /.well-known/oauth-protected-resource and /.well-known/oauth-authorization-server, dynamic registration at POST /oauth/register (public clients only), authorization code with mandatory PKCE S256 at /oauth/authorize and POST /oauth/token. The access token (abo_) lasts 1 hour and the refresh token (abr_) 30 days, rotated on every use; reusing a refresh token or a code drops the connection. POST /oauth/revoke revokes. Consumers see and disconnect apps under My purchases › Connected apps.
Only discovery (/.well-known/ucp), this documentation and the purchase link /r/…, which the consumer opens, are public.
Dashboard session
The session comes from login and lasts 30 days:
curl -X POST "https://api.bridgesagents.com/api/v1/auth/login" -H "Content-Type: application/json" \
-d '{"email": "voce@sualoja.com.br", "password": "…"}'
# → { "token": "abs_…", "expiresAt": "…", "user": { … } }Merchant sign-up (POST /api/v1/auth/signup) creates the company, the first store and the user at once; developer sign-up (POST /api/v1/auth/signup/developer) asks only for the company or project name, your name, email and password. Both return the session right away. POST /api/v1/auth/logout ends the session.
The agent key still works for agents that don't use OAuth.