Skip to main content
The ecommerce surface exposes what Keebai knows about your catalog, the carts your customers built (including the ones they abandoned), and the sales that closed. On top of that, it lets your own point of sale or ERP manage stock, record sales, and issue their electronic tax documents (DTE) — everything under the /v1/ecommerce prefix, authenticated with a PAT and gated by scope. Products and carts stay read-only: creating or editing a product goes through the portal or the platform integration, not this API.

Data model

Products

The catalog: price, stock, variants, images. Includes both products created in Keebai and those synced from an integration.

Carts

What a customer put together, whether or not they finished. Abandoned carts carry a recovery status and a link back to checkout.

Sales

Closed transactions with their line items, amount breakdown, and payment state.

Inventory

Stock per variant, split between the warehouse and branches, with adjustments, transfers, and the movement history.

Electronic invoicing

Boletas and facturas for your sales, with their folio, status, and PDF.

Sale events

Three webhooks fire as a sale is created, paid, and cancelled.
A cart is not the draft of a sale. They are separate records with no foreign key between them: a cart is what a customer assembled on a storefront, a sale is what your team or the agent closed. Correlate on the customer’s phone or email, or on customer_id, which both carry when Keebai could resolve the contact.

Scopes

The read scopes are separate because they leak different things. Carts and sales carry customer contact details and money; the catalog carries neither. A pricing widget on your website needs ecommerce:products:read and nothing else.
The scopes carry the role’s permissions. On every call the API passes the ecommerce service the permissions that match the token’s scopes, and those scopes were already limited to the user’s role when the token was minted. Minting a token with ecommerce:stock:write or ecommerce:invoicing:issue requires those permissions in your role — adjusting and transferring stock, or issuing DTE — unless you are the company owner or a superuser.
Reads are company-wide. Like CRM, these endpoints return records from every project in your company, not just the project your token belongs to.

Products come from two places

stock is only meaningful when stock_managed is true. On integration products, stock is whatever the last sync brought over — not a live read of the platform.

Inventory

A variant’s stock has one master total. Part of it is allocated to branches and the rest is available in the warehouse. GET /v1/ecommerce/products/{id}/stock returns all three, plus the quantity per branch. Every change leaves an entry in the stock movements, and adjustments are synced to the stores you have connected. To find the product_id and variant_id from a scanned code, use GET /v1/ecommerce/products/find.

Recording sales and invoicing

POST /v1/ecommerce/sales records a sale from your own point of sale: it deducts stock, calculates commissions, and, with dte, issues the receipt (boleta) or invoice (factura) right after. Lines can be catalog products or free lines, such as a service outside the catalog, with their own name. Each line takes a price with VAT (price) or a net price (net_price), and the document shows that net exactly. For an invoice to a customer you invoiced before, the RUT alone is enough. A type: "internal" sale records internal consumption instead, with no payments and no DTE. Send an Idempotency-Key header so a retry after a timeout does not create the sale twice. A DTE that fails does not undo the sale. It comes back in dte.failed with its reason, and you issue it later with POST /v1/ecommerce/sales/{id}/dte. Check which documents the company can issue today with GET /v1/ecommerce/invoicing/types. A receipt always needs a paid sale. An invoice can go on credit: when the company enables credit invoices in Sales › Settings, an invoice on a sale that is not fully paid is issued with a due date (dte.due_date, or today plus the company’s credit days).

Abandoned carts

A cart with abandoned_at set and completed_at absent is the recoverable case. recovery_status tracks what has been done about it: recovery_url is the link that puts the customer back in checkout with the cart intact. Sending it over WhatsApp with a cta_url message is the intended pairing.
recovery_status is not advanced by this API. It is set by whoever does the following up — the portal, the agent, or an integration writing through the platform. Reading pending does not mean nobody has acted; it means nothing has recorded that they did.If your own outreach chases abandoned carts, you are responsible for keeping your side’s state, because you cannot write this field back.
A cart becomes abandoned on a timer, not on an explicit action, so abandoned_at trails the customer’s last activity. Do not treat a cart as fresh just because abandoned_at is recent.

Identifiers

All ids are MongoDB ObjectIds — 24 lowercase hex characters. That covers products, carts, sales, and the customer_id that carts and sales carry when Keebai could resolve the buyer to a contact. external_id is different: it is the id the record has in the source platform (Shopify, WooCommerce, Justo), in that platform’s own format. Use it to join back to your storefront; never pass it where Keebai expects an ObjectId.

Tenancy is implicit

Company comes from your token and is never sent. Sending company returns 400. As the warning above notes, reads span the whole company rather than one project — a token minted for project A lists products, carts, and sales created under project B.

Correlating with the rest of the platform

Ecommerce records are deliberately loosely coupled, which means joining them is your job: If you need a hard link between a sale and a CRM ticket, emit a CRM event carrying your order id in a ticket-scoped custom field. That writes the id onto the ticket, which is the closest thing to a foreign key the platform offers.

Money is stored in minor units

Every amount — price, unit_price, subtotal, total, and the whole amounts block on a sale — is an integer in the currency’s smallest unit. Read currency alongside it, and never assume two decimals: JPY and CLP have none.

Pagination

List endpoints take limit and offset and return the same envelope as the rest of the API:
limit defaults to 50 and caps at 200.
The catalog paginates by page internally, so an offset that is not a multiple of limit is rounded down to the page containing it. Keep offset a multiple of limit — the standard offset += limit loop is always correct.

Errors

Branch on error.code, not on the message text. A 4xx keeps the business code the ecommerce service returned, such as STOCK_INSUFFICIENT_IN_BRANCH or SKU_NOT_FOUND.

Rate limits

These endpoints draw from the shared per-project quota: 1,000 requests a day across the whole public API. That is the binding constraint for anyone mirroring a catalog. A 4,000-product catalog at limit=200 is 20 requests — fine. The same catalog re-synced every 15 minutes is 1,920 a day — not fine.

Sync on a webhook, not a timer

ecommerce.sale.created / .paid / .cancelled tell you when something changed. Polling the sales list to discover it costs a request every time nothing happened.

Page at the ceiling

limit=200 is four times cheaper than the default 50 for the same data.

Next steps

Browse the catalog

Start with products — the simplest of the three.

React to sales

Subscribe to ecommerce.sale.paid and push it into your accounting system.

Chase abandoned carts

Filter for abandoned_at and send recovery_url over WhatsApp.

Record sales from your POS

Create the sale, deduct stock, and issue the boleta in one call.

Keep stock in step

Adjust and transfer stock from your ERP and audit the movements.

Record it on the CRM

Emit a CRM event so the order shows up on the contact’s timeline and the agent knows about it.