Skip to Content
v1.5Official documentation for the LX CloudPos API · stable in production
v1.5 (current)Endpoint referenceCreate or update product

Create or update product

POST /api/v1/productos

Creates or updates a product in the tenant’s inventory catalog — the same catalog used by POST /facturas when a line carries idProducto. It’s an upsert by codigo: if a product with that code already exists in the tenant, it’s updated; otherwise, it’s created.

It exists because, through v1.2, an integrator could only invoice lines already loaded in the catalog, or ad-hoc lines from the purchase catalog — there was no way to populate the sales catalog before issuing. With this endpoint you can preload the integrator’s own SKU and later invoice by codigoProducto without knowing LX CloudPos’s internal ids.

Authentication and permissions

AttributeValue
SchemeAPI key (X-Api-Key)
Required scopecatalogo:write
IdempotentDoes not accept Idempotency-Key. The upsert by codigo is already retry-safe: resending the same body gives the same result, just as an update (200) instead of a creation (201).
Rate limit120 req/min per API key
Max body64 KB — smaller than the general 256 KB limit

Headers

POST /api/v1/productos HTTP/1.1 Host: api.pos.lxsyscr.com X-Api-Key: lxs_live_a1b2c3d4_001122... Content-Type: application/json Accept: application/json

Request body

See ProductoApiRequestDto for the full schema.

Example — create:

{ "codigo": "API-TEST-001", "nombre": "Producto de prueba API", "descripcion": "Alta desde la API de integradores", "codigoCabys": "2342002000000", "unidadMedida": "Unid", "precioVenta": 1500.00, "precioCosto": 1000.00, "impuestos": [ { "codigo": "01", "porcentaje": 13.00, "codigoTarifa": "08" } ] }

idMarca and idProveedor weren’t sent — see the note about that pair of fields in the business rules.

Example — update (same codigo, price changes):

{ "codigo": "API-TEST-001", "nombre": "Producto de prueba API", "descripcion": "Alta desde la API de integradores", "codigoCabys": "2342002000000", "unidadMedida": "Unid", "precioVenta": 1650.00, "precioCosto": 1000.00, "impuestos": [ { "codigo": "01", "porcentaje": 13.00, "codigoTarifa": "08" } ] }

Important: This update is not partial. The body above repeats every field, not just precioVenta, because whatever is omitted gets overwritten with its default value. See the business rules.

Responses

201 Created — the product was created

HTTP/1.1 201 Created Location: /api/v1/productos/API-TEST-001 Content-Type: application/json
{ "idProducto": 919, "codigo": "API-TEST-001", "nombre": "Producto de prueba API", "descripcion": "Alta desde la API de integradores", "codigoCabys": "2342002000000", "unidadMedida": "Unid", "precioVenta": 1500.0000, "precioCosto": 1000.0000, "idCategoria": null, "idSubcategoria": null, "idMarca": 0, "idProveedor": 0, "estado": true, "impuestos": [ { "codigo": "01", "porcentaje": 13.0000, "codigoTarifa": "08" } ], "advertencia": null }

200 OK — an existing product was updated

Same shape as the 201. The only way to tell a creation and an update apart is the status code: the body carries no indicator of its own.

400 Bad Request

Two possible sources, with two different formats.

a) Model validation — a required field is missing, or precioVenta/precioCosto are not greater than zero (for example if they are omitted from the body, since they default to 0). Detailed format, same as the rest of the API:

{ "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", "title": "One or more validation errors occurred.", "status": 400, "errors": { "Codigo": ["The Codigo field is required."], "CodigoCabys": ["The CodigoCabys field is required."] } }

b) Business rules — simple { "error": "..." } format, in the order they’re validated:

{ "error": "Debe indicar 'codigo'." }
{ "error": "Debe indicar 'nombre'." }
{ "error": "El código CABYS debe tener 13 dígitos." }
{ "error": "Debe indicar 'unidadMedida'." }
{ "error": "Debe indicar 'precioVenta'." }
{ "error": "Debe indicar 'precioCosto'." }
{ "error": "Debe indicar al menos un impuesto en 'impuestos'." }
{ "error": "Cada línea de 'impuestos' debe indicar 'codigo'." }
{ "error": "El campo 'estado' no se puede cambiar al actualizar un producto: activar o desactivar es una operación aparte. Enviá el producto sin 'estado' o con el valor que ya tiene." }
{ "error": "La marca 7 no existe. Consultá GET /api/v1/catalogos/marcas." }
{ "error": "El proveedor 3 no existe." }
{ "error": "El tipo de código '05' no existe. Consultá GET /api/v1/catalogos/tipos-codigo." }

And, after the save attempt, three infrequent edge cases — the second is usually a race with another request creating or editing the same codigo in parallel:

{ "error": "No se pudo guardar el producto." }
{ "error": "El código 'API-TEST-001' ya pertenece a otro producto." }
{ "error": "El producto 'API-TEST-001' se guardó pero no se pudo releer." }

All three arrive as 400, not 500, even though conceptually they correspond more to an internal failure than to an error in the request sent.

401 Unauthorized

{ "error": "API key ausente, inválida o revocada." }

403 Forbidden

Valid key but without catalogo:write:

{ "error": "La API key no tiene el scope requerido para esta operación." }

429 Too Many Requests / 500 Internal Server Error

Same as the rest of the API — see Rate limiting and HTTP status codes. The 500 from this endpoint responds:

{ "error": "Error interno guardando el producto." }

Business rules

  • Upsert by codigo, not by idProducto. The request never includes idProducto — there’s no way to say “update product 919”; resolution always happens by looking up the codigo.

  • The update is NOT partial. Unlike customers, this endpoint rebuilds the entire product from the body on every call. An omitted field does not keep its previous value:

    • descripcion omitted → ends up "" (empty string, not null).
    • idCategoria / idSubcategoria omitted → end up null.
    • porcentajeGanancia omitted → ends up 0.
    • tipoCodigo omitted → falls back to the default "01".
    • impuestos gets replaced entirely by whatever comes in the body — it is not merged with what it already had.
  • The two exceptions, the only fields that keep their previous value when omitted:

    • estado: only applies when creating (if omitted, the product is born active). On an update the product keeps the state it had, and sending a value different from the current one returns 400 — it’s not a silent no-op. Activating or deactivating is a separate operation: in the POS, deactivating a product that was never used deletes it, and an update request must not result in an unrequested deletion.
    • idMarca / idProveedor: keep the brand and supplier the product already had.
  • idMarca / idProveedor are optional, with no arbitrary default. If omitted when creating, the product ends up with no brand and no supplier — the system does not assign one on its own. In the response this shows up as idMarca: 0 / idProveedor: 0, not null: the 0 means “unassigned,” not “brand number 0.” The tenant’s brands are listed with GET /catalogos/marcas and created with POST /marcas. A nonexistent id returns 400 naming the field.

  • tipoCodigo is validated against Hacienda’s catalog (GET /catalogos/tipos-codigo). If omitted, "01" is used (seller’s product code), correct for the vast majority of integrations.

  • codigoCabys and unidadMedida are required because they are tax data: they go straight into the document’s XML when the product is invoiced. The CABYS code must have at least 13 digits — see CABYS codes.

  • impuestos is required, with at least one line — the same minimum the POS’s product form requires. Each line needs codigo (for example "01" for VAT).

  • advertencia doesn’t block anything. It’s populated when the codigoCabys classifies as a service (first digit 6–9) and the unidadMedida sent isn’t a valid service unit: the system autocorrects it to "Os" and reports it in the response, instead of rejecting the request.

    El CABYS {codigoCabys} corresponde a un servicio; la unidad de medida se ajustó de "{anterior}" a "Os" (Otro tipo de servicio). Puede cambiarla por una unidad de servicio más específica si lo prefiere.
  • Audit: the product is recorded with the user "api:" followed by the API key’s name, so you can trace which integration modified each product.

  • Stock is not managed through this endpoint. On creation, the product is created with quantity 0 in every warehouse. On an edit, stock is not modified. To move it, use Entries and Transfers, not this endpoint.