Create or update customer
POST /api/v1/clientes
Creates or updates a customer by identificacion. Before this endpoint existed, an integrator could only create a customer as a side effect of issuing a document (the Mode B of FacturaApiClienteDto); here you can create, update, or query without issuing any document.
Authentication and permissions
| Attribute | Value |
|---|---|
| Scheme | API key (X-Api-Key) |
| Required scope | catalogo:write |
| Idempotent | Does not accept Idempotency-Key. The upsert by identificacion is already retry-safe, except when it’s a partial update and the second attempt includes fewer fields. |
| Rate limit | 120 req/min per API key |
| Max body | 64 KB |
Headers
POST /api/v1/clientes HTTP/1.1
Host: api.pos.lxsyscr.com
X-Api-Key: lxs_live_a1b2c3d4_001122...
Content-Type: application/json
Accept: application/jsonRequest body
See ClienteApiRequestDto for the full schema.
Example — create:
{
"identificacion": "117890123",
"tipoIdentificacion": "01",
"nombre": "Cliente Prueba",
"apellidos": "Integracion API",
"correo": "cliente.prueba@ejemplo.cr",
"telefono": "88888888"
}Example — partial update (only the phone changes):
{
"identificacion": "117890123",
"telefono": "87001234"
}This only overwrites telefono. nombre, apellidos, correo, and the rest stay exactly as they were — there’s no need to repeat them.
Responses
201 Created — the customer was created
HTTP/1.1 201 Created
Location: /api/v1/clientes/117890123
Content-Type: application/json{
"idCliente": 46,
"identificacion": "117890123",
"tipoIdentificacion": "01",
"nombre": "Cliente Prueba",
"apellidos": "Integracion API",
"nombreInterno": null,
"correo": "cliente.prueba@ejemplo.cr",
"telefono": "8888-8888",
"direccion": null,
"comentario": null,
"estado": true,
"exonerado": false,
"pagaTimbre": false,
"limiteCredito": null,
"exoneraciones": []
}Note "telefono": "8888-8888": "88888888" was sent and the system normalized it. exoneraciones comes back empty because this endpoint doesn’t manage them — they’re registered from the POS; see ClienteApiResponseDto and Tax exemptions.
200 OK — an existing customer was updated
Same shape as the 201, reflecting the customer’s final state. Fields that weren’t sent keep their previous value. exoneraciones reflects whatever the customer already had registered in the POS, if anything.
400 Bad Request
a) Model validation — same detailed format as the rest of the API. In practice this is rare: the DTO doesn’t mark any field as required at the framework level, so almost every 400 from this endpoint comes from business rules.
b) Business rules — simple format:
{ "error": "La identificación es requerida." }When creating (new identification):
{ "error": "El nombre es requerido para dar de alta el cliente." }{ "error": "El tipo de identificación es requerido para dar de alta el cliente." }{ "error": "Debe ingresar al menos un número de teléfono." }{ "error": "Teléfono inválido: \"12345\". Debe tener 8 dígitos (formato 506 0000-0000)." }{ "error": "Debe ingresar al menos un correo electrónico." }{ "error": "Correo inválido: \"no-es-un-correo\". Use un formato como nombre@dominio.com." }
correoandtelefonoare required when creating, even though the schema doesn’t mark them as such: validation rejects an empty string. If the integration doesn’t yet have the customer’s contact details, you won’t be able to create it through this endpoint without registering a placeholder value; use this endpoint only once you have real data.
When updating, the same phone and email errors apply if those fields are sent. See the semantics table below.
Rare edge cases that also arrive as 400:
{ "error": "No se pudo dar de alta el cliente 117890123." }{ "error": "El cliente 46 recién creado no se pudo leer." }{ "error": "No se pudo actualizar el cliente 117890123." }{ "error": "El cliente 46 actualizado no se pudo releer." }And, on creation, if there’s a race with another request creating the same customer in parallel:
{ "error": "Ya existe un cliente con identificación 117890123." }401 Unauthorized
{ "error": "API key ausente, inválida o revocada." }403 Forbidden
{ "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 cliente." }Business rules
- Upsert by
identificacion. If a customer with that identification already exists in the tenant, it’s updated; otherwise, it’s created. The identification is the lookup key, not an editable field: there’s no way to change a customer’s identification through this endpoint. - The update IS partial — unlike products. It starts from the existing record and only overwrites the fields present in the body. That said, the exact semantics of “present” vary by field: the
null= “don’t change” equivalence is not applied uniformly.
| Field | Absent or null | "" (explicit empty string) |
|---|---|---|
tipoIdentificacion | No change | Ignored — no error, no change |
nombre | No change | Ignored — no error, no change |
apellidos | No change | Applied — ends up "" |
nombreInterno | No change | Applied — ends up null |
telefono | No change | 400 — cannot be cleared, only replaced with a valid one |
correo | No change | 400 — same case as phone |
direccion | No change | Applied — ends up null |
comentario | No change | Applied — ends up null |
exonerado / pagaTimbre | No change. If the field is sent, false applies just like true. | — (they are booleans) |
limiteCredito | No change | — (it is numeric) |
In practice: to clear direccion, comentario, or nombreInterno, send "". To change telefono or correo you need to send a valid value. For nombre or tipoIdentificacion, sending "" has no effect — if you need to correct them, send the real value.
- The phone is normalized. If you send
"88888888", it’s stored and returned as"8888-8888". You can send several separated by;and all of them are validated. Currently only Costa Rica is supported (8 digits, prefix506); any other digit count produces400. pagaTimbre(vending-machine stamp tax) is assumedfalseon creation if not sent explicitly. This is the same convention used by the automatic creation that happens during issuance.exoneradois just a boolean flag. Tax exemptions themselves — documents, validity period, percentage — are not managed through this endpoint: they’re created from the POS and queried (with theiridExoneracion) in the response’sexoneracionesarray. See Tax exemptions.