Introducción

Laudus dispone de una API para que puedas conectar tus sistemas a Laudus, para obtener y actualizar información desde y hacia nuestro ERP. Con nuestra API podrás, por ejemplo, insertar un pedido de ventas, u obtener los movientos contables filtrados por fecha y cuenta contable, desde un sistema externo.

Una API (Application Programming Interface) es una herramienta para programadores. Si vas a utilizarla, y no eres técnico, necesitarás a alguien que te ayude a realizar tus proyectos. Por este motivo verás que el lenguaje que ocupamos en esta documentación es para desarrolladores.

Conceptos generales

  • Actualmente la API está en versión Beta. Es 100% funcional, pero puede haber pequeños detalles. No se cambiará el modelo de datos, por lo que todos tus desarrollos seguirán vigentes. Estamos agregando nuevos endPoints de manera constante.
  • Todas las peticiones deben hacerse mediante "https", no responde a llamadas no seguras "http".
  • Los nombres de las entidades de los endPoints van en plural. Por ejemplo:
    GET /sales/customers
  • Los id de cada entidad (por ejemplo el id de un cliente) se pasa en la URL, no como un parámetro. Es decir, nuestros endPoints son así:
    GET /sales/customers/{id}
    y para recuperar el cliente 12, haríamos:
  • GET /sales/customers/12
  • Utilizamos los verbos GET, POST, PUT y DELETE:
    • GET: para obtener información. Para obtener el cliente 12:
      GET /sales/customers/12
      Y para obtener sus contactos:
      GET /sales/customers/12/contacts
    • POST: para generar un movimiento nuevo. Nunca llevan un {id}, porque es nuevo.
      POST /sales/customers/
    • PUT: para actualizar una entidad ya existente. Para modificar la información del cliente 12:
      PUT /sales/customers/12
    • DELETE: para borrar información. Si queremos elminiar el cliente 12:
      DELETE /sales/customers/12
  • No utilizamos el verbo PATCH porque la sintaxis puede ser algo complicada, y hemos preferido mantenerla simple admitiendo PUT parciales.
  • Todos los nombres de las propiedades van en inglés, y en camelCase.
  • Se permite enviar y obtener la información en JSON y en XML. Por ejemplo, si estamos enviando un cliente nuevo en un JSON para un POST, y queremos que la respuesta nos venga en XML, debemos incluir los encabezados:
    Content-Type: application/json
    Accept: application/xml
  • POST y PUT retornan la entidad completa que se está creando/modificando
  • Se utiliza compresión de datos del lado del servidor, por lo que deberías incluir el encabezado:
    Accept-Encoding: gzip, deflate
  • Las fechas van con el formato clásico de JSON (ISO8601), por ejemplo: "2021-05-31T09:00:23". No utilizamos zona horaria, asumimos la zona America/Santiago.
  • Cuando hablamos de entidades nos referimos a las diferentes unidades de información del ERP. Por ejemplo: clientes, bodegas, facturas, comprobantes, etc.
  • Dado que la API es una herramienta para programadores, nuestro personal de soporte en Laudus no da soporte para la misma.

Cómo habilitar la API

Te indicamos los primeros pasos para comenzar a utilizar la API :

  1. Tu empresa debe estar en el remoto de Laudus, no se puede utilizar la API si tienes Laudus instalado en local en tus servidores. Es lógico, puesto que todo el acceso a la API son servidores que deben estar donde están sus datos.
  2. Se debe habilitar el acceso a la API, que por defecto está deshabilitado. Se hace en \Herramientas\Opciones:
  3. Debes crear un usuario para que lo utilice la API como login. El login se hace con el RUT de la empresa, y un nombre de usuario y contraseña definidos en la misma (en \Herramientas\Seguridad\Usuarios). NO hay un API key que genere Laudus.
    El usuario se crea en Laudus, no se crea desde la API. Y lo crea la empresa usuaria, no lo crea el personal de Laudus.
    No es necesario crear un usuario específico para la API, podría hacer un login con el nombre de usuario y contraseña de cualquier usuario, pero es buena práctica dedicar un usuario exclusivamente de uso de la API.
    Y así le puede dar a dicho usuario los permisos que estimes necesarios. Por ejemplo, puedes dejarle ver la información de los productos, pero no actualizar la misma. De esta forma, controlas lo que puede hacer el usuario de la API (es decir, el sistema externo que se conectará al ERP).
    Y si tienes varias conexiones de varios sistemas externos, podría incluso definir un usuario para cada uno de ellos, con sus restricciones.

Una vez que se ha habilitado la API y generado un usuario, ya puedes autenticarte y comenzar a funcionar, tal y como se describe en el siguiente apartado.

Autenticación

Lo primero que debes hacer para comenzar a llamar a cualquier endPoint de la API es autenticarte, para obtener un token. Para ello debes llamar al endPoint:
POST https://api.laudus.cl/auth/login

Y en el cuerpo debes poner el nombre de usuario que has creado para la API, así como la contraseña y el RUT de la empresa a la que vas a acceder. Por ejemplo:
{
   "userName": "usuario_creado_para_la_API",
   "password": "contraseña_del_usuario_API",
   "companyVATId": "RUT_de_la_empresa_con_guión_y_dígito_verificador"
}

Como respuesta de este POST obtendrás un token en formato JWT, junto con su fecha de caducidad. Por ejemplo:
{
   "token": "eyJhbGciOiJIUzI1NiIsInR5cCI .......... ifW6_p4IYBrLRNY_Dqgl_1Ms",
   "expiration": "2021-06-25T17:48:45-04:00"
}

Este token debes utilizarlo en todas las llamadas a la API, en un encabezado Authorization, después de la palabra Bearer, y sin comillas. Por ejemplo:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI .......... ifW6_p4IYBrLRNY_Dqgl_1Ms

Códigos de Respuesta

Cuando haces una llamada a la API siempre obtienes una respuesta, ya sea del servidor (porque haces una llamada inválida) o de la API. Siempre deberías comprobar la respuesta después de cada llamada, no des por hecho que todo siempre funciona bien :-)

En esta sección intentamos resumir qué es lo que puedes esperar en las respuestas al llamar a la API

Como norma general recuerda que en HTTP los códigos de estado 2XX son para cuando todo va bien, los 4XX cuando hay un error de quien hace la petición (tu parte del trabajo), y los 5XX cuando el error es de la parte del servidor (nuestra parte del trabajo). Nosotros no utilizamos los códigos 1XX (son informativos), ni 3XX (indican algún tipo de redirección).

Códigos genéricos

Aquí te describimos los códigos en general, y más abajo algunos casos específicos en el caso de los diferentes tipos de verbos utilizados

  • 200: Todo va bien, recibirás la respuesta con un código 200 y en el cuerpo del mensaje la información necesaria.
  • 400: Nos envías un JSON con algún error de forma, o una petición incorrecta: te retornamos un código 400
  • 401: El token ha expirado o existe algún problema con el acceso. Recibirás un código 401, y un mensaje:
    {
       "type": "error",
       "message": "El token ha expirado",
       "status": 401,
       "code": "TOKEN_EXPIRED"
    }

  • 403: El usuario y/o contraseña no son correctos para el RUT de la empresa: recibirás un código 403, y en el cuerpo del mensaje lo siguiente:
    {
       "type": "error",
       "message": "Access denied",
       "status": 403,
       "code": "FORBIDDEN"
    }

    No esperes mucha más información en el mensaje, por ejemplo si lo que está mal es el usuario o la contraseña, o el RUT. Ni siquiera utilizamos el código 401 que habría que utilizar en algunos casos, sería dar más pistas. No querrías que alguien que quiere obtener acceso indebido utilice esta información extra.
  • 404: No existe la entidad (el recurso). Te retornamos un código 404
    Por ejemplo, nos pides la cotización 12034:
    GET /sales/quotes/12034
    y esta cotización no existe (el recurso no existe en el servidor).
    Esto es aplicable a las peticiones GET, PUT, y DELETE. En un POST no tiene sentido, porque lo que haces es crear un nuevo recurso.
  • 422: Error en la validación de los datos. Quieres actualizar algún recurso, pero nos envías información no válida, como por ejemplo un email sin la @, un precio unitario fuera de rango, o una factura a un cliente bloqueado. Hay gente que prefiere un código 409 para las reglas de validación, a nosotros nos encaja más el 422. En el cuerpo del mensaje recibirás información descriptiva de la validación que falla.
  • 500: algo ha ido mal en la parte del servidor. Puede ser un problema del servidor web, o un error en nuestra programación al procesar tu petición.
    Si el problema es del servidor web, recibirás la respuesta propia de Apache, IIS o Nginx (o el servidor que utilicemos en ese momento).
    Si el problema es nuestro, recibirás un mensaje descriptivo con el tipo de error, del estilo:
    {
       "type": "error",
       "message": "Variable not found: miVariableQueNoExiste",
       "status": 500,
       "code": "INTERNAL_SERVER_ERROR"
    }

GET

  • Todo va bien: retornamos un código 200, y en el cuerpo del mensaje la información en formato que nos has pedido en el encabezado Accept
  • No existe la entidad (el recurso). Por ejemplo, nos pides la cotización 12034:
    GET /sales/quotes/12034
    y esta cotización no existe (el recurso no existe en el servidor). Te retornamos un código 404

POST

  • Todo va bien: retornamos un código 200, y en el cuerpo del mensaje la entidad completa que se acaba de crear. No retornamos el id recién creado, retornamos la entidad (recurso) completa. Lo hacemos porque hay ciertas propiedades que sea crean nuevas en el servidor (por ejemplo la fecha y usuario de creación), que no las manejas tú, y es bueno tener la representación completa de vuelta de la petición para poder mostrarla al usuario.
    Se podría retornar un 201 con un link al nuevo recurso, pero la mayoría de las veces eso solo implica forzarte a realizar una petición adicional. Y tu tiempo y el nuestro son valiosos.
  • Como hemos comentado más arriba, si los datos no pasan las reglas de validación de Laudus se devuelve un 422. Y, por último, en un POST no puede hacer 404.

PUT

  • Muy parecido al POST, salvo que sí puede haber un 404 si no existe el recurso que estás intentando modificar.

DELETE

  • Si se pudo borrar el recurso, retornamos un 204 y el cuerpo de la respuesta va vacío (por eso retornamos un 204 en vez de un 200).
  • Si hubo un problema al borrar esta entidad (por ejemplo el cliente que se quiere borrar ya tiene facturas), retornamos un 422 porque se rompen las reglas de eliminación.

EndPoints del tipo "list"

En los endPoints del tipo "list", que devuelven listados de información de las diferentes entidades, retornamos lo siquiente:

  • 200 cuando todo va bien. En el cuerpo del mensaje va la información solicitada, en el formato que se ha especificado en el encabezado Accept de la petición. Recuerda que esos endPoints permiten los formatos JSON, XML y CSV
  • 204 cuando la consulta retorna sin datos. Por ejemplo, quieres todos los clientes de "Pichidangui", pero lamentablemente no tienes ningún cliente en esta localidad.
  • 206 si no podemos retornar todos los registros que se solicitaron, y solo se envía una parte de ellos.

Encabezados a incluir en las llamadas a la API

  • Authorization: es necesario agregar el token obtenido en la autenticación en cada llaamada a la API a través de un encabezado "Authorization: Bearer mi-token", de la forma:

    Authorization: Bearer eyJhbGciOiJIXzI1NiI..............MRjkQyO17GholDyRAjrYSE
  • Content-Type: indica el tipo de datos que van dentro del cupero de la petición. Admitimos XML y JSON, aunque hoy en día lo más utilizado es JSON. Por ejemplo:

    Content-Type: application/json
  • Accept: indica el tipo de datos que se desean en la respuesta. Pueden escoger JSON y XML, y en los endPoints tipo "list" se pueden obtener los datos en CSV también. Por ejemplo:

    Accept: application/json
    Accept: application/xml
    Accept: text/csv

  • Accept-Encoding: lo utilizaremos para pedir los resultados comprimidos. Muy importante en los listados y resultados de informes y estadísticas. Se debe utilizar el encabezado:

    Accept-Encoding: gzip, deflate

Los endPoints de listados

Casi todas las entidades tienen un endPoint que permite obtener listados de información de la misma. Estos endPoints son del estilo (por ejemplo, para obtener cotizaciones):
POST /sales/quotes/list

Utilizamos el verbo POST porque estos endPoints deben incluir un cuerpo con los filtros y condiciones de la consulta. Y el esquema es demasiado completo como para enviarlo en un GET en forma de parámetros.

Hoy en día no hay nada que impida enviar una petición GET con cuerpo, pero el problema está en que algunos lenguajes, notablemente Javascript, no permiten peticiones GET con cuerpo.
En nuestra opinión no se explica que a estas alturas del siglo XXI arrastremos una decisión de 1990. No solo la estructura de parámetros no es adecuada para muchas consultas, sino que se puede chocar con el límite que imponen los navegadores al tamaño de la URL.

Cada vez hay más ejemplos de compañías que admiten GET con body (siendo Elastic una de las más conocidas con su servicio Elastic Search), tal vez lo incorporemos en algún momento.

Estos endPoints llevan siempre un cuerpo en JSON (por lo tanto es obligatorio el encabezado Content-Type: application/json), con las especificaciones de la consulta, y pueden recibir la información en los formatos JSON, XML y CSV, incorporando alguno de los encabezados:
Accept: application/json
Accept: application/xml
Accept: text/csv

JSON de condiciones

El contenido del JSON con la consulta a realizar tiene 4 apartados:

  • options: un objeto de opciones generales, como por ejemplo para la paginación con las propiedades limit y offset.
    • offset: de todos los resultados de la consulta, se devolverán las filas a partir de este valor + 1. Es decir, se ignoran los offset primeros resultados.
    • limit: la cantidad de resultados que queremos retornar a partir de offset.
    Por ejemplo, si ponemos:
    "options": {
       "offset": 10,
       "limit": 50
    }

    Obtendremos 50 resultados, del 11 en adelante (dejamos los 10 primeros atrás por el valor de offset). Es decir, de toda la consulta que hallamos especificado, nos devolverá los resultados del 11 al 60 (50 filas en total). Esto se utiliza para paginar consultas muy grandes, e ir obteniendo las diferentes páginas de resultados.
  • fields: un array de los campos a incluir en el resultado de la consulta. Puede ser cualquiera de los campos del modelo de cada entidad. Los modelos se pueden consultar en la especificación de Swagger.
  • filterBy: una array de filtros, que se encadenan con AND. Abajo incluímos varios ejemplos donde se puede comprender mejor.
    En los filtros se puede incluir cualquier campo del modelo, y los operadores "=", "!=", "<=", ">=", y para las compraciones de strings también se pueden utilizar "contains", "starts", y "ends".
    Las comparaciones se hacen "case insensitive", es decir, si quieres obtener todos los clientes "Carlos", puedes poner "Carlos", "cArLos" ... En cuanto al valor a comparar, puede ser un solo valor, o varios valores en un array. Es decir, si queremos información de todas la cotizaciones hasta la 50, podríamos utilizar:
    "filterBy": [{"quoteId", "operator": "<=", value: 50}]
    Y si queremos información de las cotizaciones de los clientes 34, 56 y 77, podemos hacer cualquiera de las opciones:
    "filterBy": [{"customerId", "operator": "=", value: [34, 56, 77]}]
    "filterBy": [{"customerId", "operator": "=", value: 34},
       {"customerId", "operator": "=", value: 56},
       {"customerId", "operator": "=", value: 77}]

  • orderBy: un array con las formas de ordenar los resultados obtenidos. Pueden especifcarse las propiedades field y direction.
    Y direction puede tener los valores ASC y DESC.

Ejemplos

Con algunos ejemplos se comprenderá mejor las consultas que se pueden realizar.

Vamos primero con un ejemplo del JSON completo, y después ejemplos de las diferentes secciones.
Queremos obtener el id, razón social, RUT, y datos de facturación de todos los clientes de la comuna de Arica, ordenados por RUT de manera creciente:
{
  "options": {
    "offset": 0,
    "limit": 0
  },
  "fields": [
    "customerId", "legalName", "VATId", "addressBilling", "cityBilling", "countyBilling",
    "zipCodeBilling", "stateBilling"
  ],
  "filterBy": [
    {
      "field": "countyBilling",
      "operator": "=",
      "value": "arica"
    }
  ],
  "orderBy": [
    {
      "field": "VATId",
      "direction": "ASC"
    }
  ]
}

Todos los ejemplos que vienen a continuación se ha elaborado para el endPoint de Pedidos de Ventas:
POST /sales/orders/list

  • De los pedidos de ventas, quiero obtener el número del pedido, fecha, nombre del cliente, y el nombre del usuario que lo modificó por última vez:
    "fields": ["salesOrderId", "issuedDate", "customer.name", "modifiedBy.name"]
  • Además de los campos de arriba, quiero también el nombre del producto, la cantidad pedida, y la unidad de medida (se crearán varias líneas por cada pedido, una línea por producto):
    "fields": ["salesOrderId", "issuedDate", "customer.name",
       "modifiedBy.name", "items.product.SKU", "items.quantity",
       "items.product.unitOfMeasure"]
  • Y quiero estos pedidos de ventas filtrados para obtener solo los pedidos realizados a partir del 1-enero-2021:
    "filterBy": [{
      "field": "issuedDate",
      "operator": ">",
      "value": "2020-12-31T23:59:59"
    }]
  • Ahora voy a filtrar estos mismos pedidos además solo por aquellos que tengan productos que contengan "aceite" en el código:
    "filterBy": [{
      "field": "issuedDate",
      "operator": ">",
      "value": "2020-12-31T23:59:59"
    },
    {
      "field": "items.product.SKU",
      "operator": "contains",
      "value": "aceite"
    }]
  • O mejor aún, que contengan "aceite" o "vinagre" en el código:
    "filterBy": [{
      "field": "issuedDate",
      "operator": ">",
      "value": "2020-12-31T23:59:59"
    },
    {
      "field": "items.product.SKU",
      "operator": "contains",
      "value": ["aceite", "vinagre"]
    }]
  • Y ahora todos los pedidos del 1 al 100, menos el 50, 51, 52, 53, y 54:
    "filterBy": [{
      "field": "salesOrderId",
      "operator": "<=",
      "value": 100
    },
    {
      "field": "salesOrderId",
      "operator": "!=",
      "value": [50, 51, 52, 53, 54]
    }]
  • Para terminar con los filtros, todos los pedidos de los clientes de Arica:
    "filterBy": [{
      "field": "customer.countyBilling",
      "operator": "=",
      "value": "Arica"
    }]
  • Vamos a ordenarlos ahora por la fecha de emisión del pedido en orden ascendete:
    "orderBy": [{
      "field": "issuedDate",
      "direction": "ASC"
    }]
  • O también los podemos ordenar por el código del producto, y para aquellas filas con los códigos iguales, según la cantidad pedida en orden descendente:
    "orderBy": [{
      "field": "items.product.SKU",
      "direction": "ASC"
    },
      "field": "items.quantity",
      "direction": "DESC"
    }]

Ejemplos de Código para utilizar la API


En esta sección incluimos ejemplos de programas en algunos lenguajes para acceder a la API. Incluimos la forma de autenticarse (obtener un token e incluirlo en los encabezados), cómo recuperar información de un cliente, moidificarla, y cómo escribir los cambios de vuelta.

C#



// Ejemplo para ejecutar por consola que recupera un cliente a través de la API, realiza unos cambios.
// y graba el cliente modificado. Incluye obtención de token
// Este ejemplo utiliza la librería Newtonsoft.Json que debe estar instalada
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
using System;
using System.Dynamic;
using System.IO;
using System.Net;

namespace LaudusAPI_Ejemplos
{
class Program
{
// Clase para preservar credencial de acceso a la API (token y fecha de expiración del mismo)
public class Credential
{
public string token;
public DateTime expiration;
}

// Clase para representar la entidad Account (Cuenta del plan de cuentas contable)
public class Account
{
public int accountId;
public string accountNumber;
public string name;
}

// Clase para representar la entidad Customer (Cliente)
public class Customer
{
public int customerId;
public string name;
public string legalName;
public string VATId;
public string activityName;
public Account account;
}


static void Main(string[] args)
{
// Url base de la API
string url = "https://api.laudus.cl/";

// Obtener credenciales de acceso a la API
Credential credencial = ObtenerToken(url);
if (credencial != null)
{
Console.WriteLine("-----------------------<< Obtener Token >>-----------------------");
Console.WriteLine("Token = " + credencial.token);
Console.WriteLine("Expiration = " + credencial.expiration.ToString());

// Obtener datos del cliente con el customerId 18
var cliente = GetCliente(url, credencial, 18);
if (cliente != null)
{
Console.WriteLine("-----------------------<< Get Cliente >>-----------------------");
Console.WriteLine("Id = " + cliente.customerId.ToString());
Console.WriteLine("Nombre = " + cliente.name);
Console.WriteLine("LegalName = " + cliente.legalName);
Console.WriteLine("VATId = " + cliente.VATId);
Console.WriteLine("ActivityName = " + cliente.activityName);
Console.WriteLine("account.accountId = " + cliente.account.accountId.ToString());
Console.WriteLine("account.accountNumber = " + cliente.account.accountNumber);
Console.WriteLine("account.name = " + cliente.account.name);

// Cambio algunos datos del cliente obtenido para luego guardar esos cambios
cliente.name = "Nombre Modificado";
cliente.legalName = "Modificado S.A.";

// Body del put para guardar los cambios
string body = JsonConvert.SerializeObject(cliente);

// Guardo los cambios
var clienteGuardado = GuardarCliente(url, credencial, cliente.customerId, body);

if (clienteGuardado != null)
{
Console.WriteLine("-----------------------<< Put Cliente >>-----------------------");
Console.WriteLine("Id = " + clienteGuardado.customerId.ToString());
Console.WriteLine("Nombre = " + clienteGuardado.name);
Console.WriteLine("LegalName = " + cliente.legalName);
Console.WriteLine("VATId = " + cliente.VATId);
Console.WriteLine("ActivityName = " + cliente.activityName);
Console.WriteLine("account.accountId = " + cliente.account.accountId.ToString());
Console.WriteLine("account.accountNumber = " + cliente.account.accountNumber);
Console.WriteLine("account.name = " + cliente.account.name);
}
}

}
Console.WriteLine("----------------------------------------------------------------");
Console.ReadLine();
}

public static Credential ObtenerToken(string url)
{
Credential token = null;
try
{
// Se replica la estructura del JSON a enviar con un objecto expando.
dynamic loginInfo = new ExpandoObject();
loginInfo.userName = "mi-nombre-de-usuario";
loginInfo.password = "mi-contraseña";
// RUT de la empresa con guión. Puede llevar puntos o no
loginInfo.companyVATId = "XXXXXXXXXX-X";
// Se serializa el objeto a JSON
string json_data = JsonConvert.SerializeObject(loginInfo);

// Se configura la petición.
HttpWebRequest request;
request = WebRequest.Create(url + "auth/login") as HttpWebRequest;
request.Headers.Add("Accept", "application/json");
request.Headers.Add("Accept-Encoding", "gzip, deflate");
request.ContentType = "application/json";
request.Method = "POST";

// Body de la petición
using (var streamWriter = new StreamWriter(request.GetRequestStream()))
{
streamWriter.Write(json_data);
streamWriter.Flush();
streamWriter.Close();
}

// Respuesta
using (HttpWebResponse response = request.GetResponse() as HttpWebResponse)
{
var reader = new StreamReader(response.GetResponseStream());
string result = reader.ReadToEnd();
token = JsonConvert.DeserializeObject(result);
}
}
catch (WebException exception)
{
// Se intenta atrapar primero cualquier webExcepcion
using (var stream = exception.Response.GetResponseStream())
{
using (var reader = new StreamReader(stream))
{
Console.WriteLine(reader.ReadToEnd());
}
Console.ReadLine();
}
}
catch (Exception exception)
{
// Cualquier otra excepcion la atrapamos aqui
Console.WriteLine(exception.Message);
Console.ReadLine();
}
return token;
}

public static Customer GetCliente(string url, Credential credencial, int idCliente)
{
Customer retVal = null;
try
{
//Se configura la petición.
HttpWebRequest request;
request = WebRequest.Create(string.Format("{0}sales/customers/{1}", url, idCliente)) as HttpWebRequest;
request.Headers.Add("Authorization", "Bearer " + credencial.token);
request.Headers.Add("Accept", "application/json");
request.Headers.Add("Accept-Encoding", "gzip, deflate");
request.ContentType = "application/json";
request.Method = "GET";

// Respuesta
using (HttpWebResponse response = request.GetResponse() as HttpWebResponse)
{
var reader = new StreamReader(response.GetResponseStream());
string result = reader.ReadToEnd();

// Si se trabaja con un modelo de datos:
// se deserializa la respuesta de la API como un objeto de la clase Customer
retVal = JsonConvert.DeserializeObject(result);

// Si no se trabaja con un modelo de datos:
// se deserializa la respuesta de la API como un objeto dynamic
// para que funcione hay que cambiar también el tipo de retorno del método de
// Customer a dynamic, y lo mismo con la variable retVal
// var expConverter = new ExpandoObjectConverter();
// retVal = JsonConvert.DeserializeObject(result, expConverter);
}
}
catch (WebException exception)
{
// Se intenta atrapar primero cualquier webExcepcion
using (var stream = exception.Response.GetResponseStream())
{
using (var reader = new StreamReader(stream))
{
Console.WriteLine(reader.ReadToEnd());
}
Console.ReadLine();
}
}
catch (Exception exception)
{
// Cualquier otra excepcion la atrapamos aqui
Console.WriteLine(exception.Message);
Console.ReadLine();
}
return retVal;
}

// Si trabajamos con un modelo de datos, la delcaración es "public static Customer". // Pero ni no trabajásemos con un modelo de datos, debería ser "public static dynamic" public static Customer GuardarCliente(string url, Credential credencial, int idCliente, string body)
{
// Si trabajamos con un modelo de datos, creamos la variable de retorno del modelo Customer retVal = null;
// Si NO trabajamos con un modelo de datos, la variable de retorno tiene que ser de tipo dynamic // dynamic retVal = null;
try
{
// Se configura la petición.
HttpWebRequest request;
request = WebRequest.Create(string.Format("{0}sales/customers/{1}", url, idCliente)) as HttpWebRequest;
request.Headers.Add("Authorization", "Bearer " + credencial.token);
request.ContentType = "application/json";
request.Method = "PUT";

// Body de la petición
using (var streamWriter = new StreamWriter(request.GetRequestStream()))
{
streamWriter.Write(body);
streamWriter.Flush();
streamWriter.Close();
}

// Respuesta
using (HttpWebResponse response = request.GetResponse() as HttpWebResponse)
{
var reader = new StreamReader(response.GetResponseStream());
string result = reader.ReadToEnd();

retVal = JsonConvert.DeserializeObject(result);
}
}
catch (WebException exception)
{
// Se intenta atrapar primero cualquier webExcepcion
using (var stream = exception.Response.GetResponseStream())
{
using (var reader = new StreamReader(stream))
{
Console.WriteLine(reader.ReadToEnd());
}
Console.ReadLine();
}
}
catch (Exception exception)
{
// Cualquier otra excepcion la atrapamos aqui
Console.WriteLine(exception.Message);
Console.ReadLine();
}
return retVal;
}
}
}

Python


# Ejemplo para ejecutar por consola que recupera un cliente a través de la API, realiza unos cambios,
# y graba el cliente modificado. Incluye obtención de token

import requests
import json
import sys
from collections import OrderedDict
from datetime import datetime

class laudusAPI_Ejemplos:
    
    #Host 
    hostAPI = "https://api.laudus.cl"

    #objeto para preservar los credenciales de acceso a la API
    credential = {"token": "", "expiration": ""}
    
    #objeto para representar la entidad Account (Cuenta del plan de cuentas contable)
    account = {}

    #objeto para representar la entidad Customer (clientes)
    customer = {}

    ##########################################################################################

    def obtenerToken(self):
        #obtiene un token realizando un request a la API

        vReturn = False

        self.credential = {}

        #esquema de request Login
        requestLoginSchema = {"userName": "", "password": "", "companyVATId": ""}
        #se aplican los valores finales al esquema de request Login
        requestLoginSchema["userName"] = "su nombre de usuario"
        requestLoginSchema["password"] = "su clave de acceso"
        requestLoginSchema["companyVATId"] = "el rut de su empresa"
        
        #se contruye el request body en json
        requestBodyJson = json.dumps(requestLoginSchema)
        #se construyen los headers requeridos para el request
        requestHeaders = {"Content-type": "application/json", "Accept": "application/json"}
        
        print("-----------------------<< Obtener Token >>-----------------------")
        
        try:
            request = requests.post(self.hostAPI + "/auth/login", data = requestBodyJson, headers = requestHeaders)
            respondStatusCode = request.status_code

            #Se verifica al código status de respuesta
            #Requests viene de fábrica con un 'status code lookup object' para una fácil referencia
            if respondStatusCode == requests.codes.ok:
                vReturn = True
                self.credential = json.loads(request.text)
                print("token = " + self.credential["token"])
                print("expiration = " + self.credential["expiration"])
            else:
                vReturn = False
                requestError = json.loads(request.text)
                requestErrorMessage = ""
                if 'message' in requestError:
                    requestErrorMessage = requestError['message']
                print('error login ' + requestErrorMessage)
        except:
            vReturn = False
            print("Unexpected error: ", sys.exc_info()[0])
        
        return vReturn

    ##########################################################################################

    def isValidToken(self):
        #indica si el token almacenado en el objeto credential es válido
        #si el token almacenado no es válido obtiene un nuevo token e igualmente indica su validez

        vReturn = True

        if "expiration" in self.credential and len(self.credential["expiration"]) > 0:
            ltNow = datetime.now()
            ltToken = datetime.fromisoformat(self.credential["expiration"])
            ltToken = ltToken.replace(tzinfo=None)
            if ltToken < ltNow:
                return self.obtenerToken()
            else:
                return vReturn
        else:
            return self.obtenerToken()

    ##########################################################################################

    def getCliente(self, customer_id):

        print("-----------------------<< Obtener Customer >>-----------------------")

        vReturn = False

        #se verifica la validez del token almacenado y si no lo fuera se obtiene uno nuevo
        if not self.isValidToken():
            print("No se pudo obtener un token válido")
            return vReturn

        self.customer = {}

        #se construyen los headers requeridos para el request
        requestHeaders = {'Authorization': 'Bearer ' + self.credential["token"], 'Accept': 'application/json'}
        
        try:
            #request
            request = requests.get(self.hostAPI + "/sales/customers/" + str(customer_id), headers = requestHeaders)
            respondStatusCode = request.status_code

            #Se verifica al código status de respuesta
            if respondStatusCode == requests.codes.ok:
                vReturn = True
                self.customer = json.loads(request.text)
                print(self.customer)
            else:
                vReturn = False
                requestError = json.loads(request.text)
                requestErrorMessage = ""
                if 'message' in requestError:
                    requestErrorMessage = requestError['message']
                print('error get customer ' + requestErrorMessage)
        except:
            vReturn = False
            print("Unexpected error: ", sys.exc_info()[0])
        
        return vReturn        

    ##########################################################################################

    def putCliente(self, customer_id):

        print("-----------------------<< Guardar Customer >>-----------------------")
        
        vReturn = False

        #se verifica la validez del token almacenado y si no lo fuera se obtiene uno nuevo        
        if not self.isValidToken():
            print("No se pudo obtener un token válido")
            return vReturn        

        #se contruye el request body en json
        requestBodyJson = json.dumps(self.customer)

        #se construyen los headers requeridos para el request
        requestHeaders = {'Authorization': 'Bearer ' + self.credential["token"], 'Accept': 'application/json', "Content-type": "application/json"}
        
        try:
            #request
            request = requests.put(self.hostAPI + "/sales/customers/" + str(customer_id), data = requestBodyJson, headers = requestHeaders)
            respondStatusCode = request.status_code

            #Se verifica al código status de respuesta
            if respondStatusCode == requests.codes.ok:
                vReturn = True
                self.customer = json.loads(request.text)
                print(self.customer)
            else:
                vReturn = False
                requestError = json.loads(request.text)
                requestErrorMessage = ""
                if 'message' in requestError:
                    requestErrorMessage = requestError['message']
                print('error put customer ' + requestErrorMessage)
        except:
            vReturn = False
            print("Unexpected error: ", sys.exc_info()[0])
        
        return vReturn          

    ##########################################################################################

if __name__ == '__main__':
    
    ejemplo = laudusAPI_Ejemplos()
    
    if ejemplo.obtenerToken():

        #[opcionalmente] al llamar al ejemplo 'python code.py 606' puede indicarse un customerId '606' con el que usar el ejemplo 
        #si no se pasó ningún customerId se usará por defecto el customerId '18'
        customerId = 18
        if len(sys.argv) > 1:
            if int(sys.argv[1]) > 0:
                customerId = int(sys.argv[1])
        if ejemplo.getCliente(customerId):
            ejemplo.customer["legalName"] = "new legalName"
            if ejemplo.putCliente(customerId):
                pass

PHP


<?php

class laudusAPI_Ejemplos {
    
    public function __construct()
    {
        //host
        $this->hostAPI = "https://api.laudus.cl";
        //objeto para preservar los credenciales de acceso a la API
        $this->credential = array("token" => "", "expiration" => "");  
        //objeto para representar la entidad Account (Cuenta del plan de cuentas contable)
        $this->account = array();  
        //objeto para representar la entidad Customer (clientes)
        $this->customer = array();          
    }

    /////////////////////////////////////////////////////////////////////////////////////////////////////////////

    public function obtenerToken() {
        //obtiene un token realizando un request a la API

        $vReturn = false;

        $this->credential = array();

        //esquema de request Login
        $requestLoginSchema = array("userName" => "", "password" => "", "companyVATId" => "");
        #se aplican los valores finales al esquema de request Login
        $requestLoginSchema["userName"] = "su nombre de usuario";
        $requestLoginSchema["password"] = "su clave de acceso";
        $requestLoginSchema["companyVATId"] = "el RUT de su empresa";

        //se contruye el request body en json
        $requestBodyJson = json_encode($requestLoginSchema); 
        
        echo "-----------------------<< Obtener Token >>-----------------------\n\n";

        try {
            $request = curl_init($this->hostAPI."/auth/login");
            curl_setopt($request, CURLOPT_SSL_VERIFYPEER, false);
            curl_setopt($request, CURLOPT_CUSTOMREQUEST, "POST");
            curl_setopt($request, CURLOPT_POSTFIELDS, $requestBodyJson);
            curl_setopt($request, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($request, CURLOPT_HTTPHEADER, array(
                "Accept: application/json",
                "Content-type: application/json", 
                "Content-Length: ".strlen($requestBodyJson))
            );
            //make post
            $respond = curl_exec($request);    
            //respond status code
            $respondStatusCode = curl_getinfo($request, CURLINFO_HTTP_CODE);  
            curl_close($request); 

            if ($respondStatusCode == 200) {
                $vReturn = true;
                $this->credential = json_decode($respond);
                echo "token = " . $this->credential->{"token"}."\n\n";
                echo "expiration = " . $this->credential->{"expiration"}."\n\n";
            }
            else {
                $vReturn = false;
                $requestError = json_decode($respond);     
                $requestErrorMessage = "";   
                if (isset($requestError->{"message"})) {    
                    $requestErrorMessage = $requestError->{"message"};
                }
                echo "error login ".$requestErrorMessage."\n\n";
            }
        }
        catch (Exception $error) {
            $vReturn = false;
            echo "Unexpected error: ",  $error->getMessage(), "\n";
        }
        
        return $vReturn;
    }  

    /////////////////////////////////////////////////////////////////////////////////////////////////////////////

    public function getCliente($customerId) {
        //se obtiene la información del cliente

        echo "-----------------------<< Obtener Customer >>-----------------------\n\n";        

        $vReturn = false;

        #se verifica la validez del token almacenado y si no lo fuera se obtiene uno nuevo
        if (!$this->isValidToken()) {
            echo "No se pudo obtener un token válido";
            return $vReturn;
        }

        $this->customer = array();

        try {
            $request = curl_init($this->hostAPI."/sales/customers/".$customerId); 
            curl_setopt($request, CURLOPT_SSL_VERIFYPEER, false);
            curl_setopt($request, CURLOPT_CUSTOMREQUEST, "GET");
            curl_setopt($request, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($request, CURLOPT_HTTPHEADER, array(
                "Accept: application/json",
                "Authorization: Bearer ".$this->credential->{"token"})
            );

            //make GET
            $respond = curl_exec($request);    
            //respond status code
            $respondStatusCode = curl_getinfo($request, CURLINFO_HTTP_CODE);  
            curl_close($request); 

            if ($respondStatusCode == 200) {
                $vReturn = true;
                $this->customer = json_decode($respond);
                echo "customerId = " . $this->customer->{"customerId"}."\n\n";
                echo "name = " . $this->customer->{"name"}."\n\n";
                echo "legalName = " . $this->customer->{"legalName"}."\n\n";
            }
            else {
                $vReturn = false;
                $requestError = json_decode($respond);     
                $requestErrorMessage = "";   
                if (isset($requestError->{"message"})) {    
                    $requestErrorMessage = $requestError->{"message"};
                }
                echo "error get customer ".$requestErrorMessage."\n\n";
            }
        }
        catch (Exception $error) {
            $vReturn = false;
            echo "Unexpected error: ",  $error->getMessage(), "\n";
        }        
        
        return $vReturn;            
    }

    /////////////////////////////////////////////////////////////////////////////////////////////////////////////

    public function  putCliente($customerId){
        //se realiza el guardado de datos del cliente

        echo "-----------------------<< Guardar Customer >>-----------------------\n\n";
        
        $vReturn = false;

        #se verifica la validez del token almacenado y si no lo fuera se obtiene uno nuevo
        if (!$this->isValidToken()) {
            echo "No se pudo obtener un token válido";
            return $vReturn;
        }     

        try {
            #se contruye el request body en json
            $requestBodyJson = json_encode($this->customer);
            $request = curl_init($this->hostAPI."/sales/customers/".$customerId); 
            curl_setopt($request, CURLOPT_SSL_VERIFYPEER, false);
            curl_setopt($request, CURLOPT_CUSTOMREQUEST, "PUT");
            curl_setopt($request, CURLOPT_POSTFIELDS, $requestBodyJson);
            curl_setopt($request, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($request, CURLOPT_HTTPHEADER, array(
                "Accept: application/json",
                "Authorization: Bearer ".$this->credential->{"token"},
                "Content-type: application/json", 
                "Content-Length: ".strlen($requestBodyJson))
            );

            //make POST
            $respond = curl_exec($request);    
            //respond status code
            $respondStatusCode = curl_getinfo($request, CURLINFO_HTTP_CODE);  
            curl_close($request); 

            if ($respondStatusCode == 200) {
                $vReturn = true;
                $this->customer = json_decode($respond);
                echo "customerId = " . $this->customer->{"customerId"}."\n\n";
                echo "name = " . $this->customer->{"name"}."\n\n";
                echo "legalName = " . $this->customer->{"legalName"}."\n\n";
            }
            else {
                $vReturn = false;
                $requestError = json_decode($respond);     
                $requestErrorMessage = "";   
                if (isset($requestError->{"message"})) {    
                    $requestErrorMessage = $requestError->{"message"};
                }
                echo "error get customer ".$requestErrorMessage."\n\n";
            }
        }
        catch (Exception $error) {
            $vReturn = false;
            echo "Unexpected error: ",  $error->getMessage(), "\n";
        }        
        
        return $vReturn;

    }

    /////////////////////////////////////////////////////////////////////////////////////////////////////////////

    public function isValidToken() {
        #indica si el token almacenado en el objeto credential es válido
        #si el token almacenado no es válido obtiene un nuevo token e igualmente indica su validez

        $vReturn = true;

        if (isset($this->credential->{"expiration"})) {
            $ltNow = new DateTime("NOW");    
            $ltNow = $ltNow->format('c');
            if ($this->credential->{"expiration"} < $ltNow) {
                return $this->obtenerToken();
            }
            else {
                return $vReturn;
            }            

        }
        else {
            return $this->obtenerToken();
        }

    }

}

/////////////////////////////////////////////////////////////////////////////////////////////////////////////

$ejemplo = new laudusAPI_Ejemplos();
if ($ejemplo->obtenerToken()) {
    //en este ejemplo se usa el customerId 18
    $customerId = 18;
    if ($ejemplo->getCliente($customerId)) {
        //se cambia el valor de la propiedad 'legalName' del cliente
        $ejemplo->customer->{"legalName"} = "new legal Name";
        if ($ejemplo->putCliente($customerId)) {
            //guardado correctamente
        }
    }
}

?>

JavaScript


/*
	Ejemplo para ejecutar por consola que recupera un cliente a través de la API, realiza unos cambios,
	y graba el cliente modificado. Incluye obtención de token.

	No hemos incluido tratamiento de llamadas síncronas/asincrónicas para no complicar el ejemplo.
	Pero en un código en producción habría que tenerlo en cuenta para utilizar callbacks u otro método
	para tener en cuenta los retrasos que se producen entre la llamda a la función y la respuesta del servidor
	
	Por la misma razón no hemos incluido clases como en los otros ejemplos de los otros lenguajes de 
	programación, para no complicar más el código e ir directamente a la funcionalidad que se precisa.
*/

//Host 
let hostAPI = "https://api.laudus.cl";
//objeto para preservar los credenciales de acceso a la API
let credential = {"token": "", "expiration": ""};
//objeto para representar la entidad Account (Cuenta del plan de cuentas contable)
let account = {};
//objeto para representar la entidad Customer (clientes)
let customer = {};	

///////////////////////////////////////////////////////////////////////////////////////////////////////

function obtenerToken() {

	//obtiene un token realizando un request a la API

    let vReturn = false;
    credential = {};

    //esquema de request Login
    let requestLoginSchema = {"userName": "", "password": "", "companyVATId": ""};
    //se aplican los valores finales al esquema de request Login
    requestLoginSchema.userName = "su nombre de usuario";
    requestLoginSchema.password = "su clave de acceso";
    requestLoginSchema.companyVATId = "RUT de sus empresa";
    let requestBodyJson = JSON.stringify(requestLoginSchema);

	console.log("-----------------------<< Obtener Token >>-----------------------");

    let requestAPI = new XMLHttpRequest();

    requestAPI.open("POST", hostAPI + '/auth/login', false);
    requestAPI.onreadystatechange=function() {
        if (requestAPI.readyState==4) {
        	let respondStatusCode = requestAPI.status;
            //se verifica al código status de respuesta
            if (respondStatusCode == 200 && requestAPI.responseText.length > 0) {
                vReturn = true;
                credential = JSON.parse(requestAPI.responseText);
                if (credential.token) {
                    console.log("token = " + credential.token);
                    console.log("expiration = " + credential.expiration);
                }
            }
            else {
                vReturn = false;
                let requestErrorMessage = '';
                if (requestAPI.responseText.length > 0) {
	                let requestError = JSON.parse(requestAPI.responseText);
	                if (requestError.message) {
	                    requestErrorMessage = requestError.message;
	                }	                	
                }
                console.log('error login ' + requestErrorMessage);
            }
            return vReturn;
        }
    }
    //se construyen los headers requeridos para el request
    requestAPI.setRequestHeader("Content-Type", "application/json");
    requestAPI.setRequestHeader("Accept", "application/json");
    requestAPI.send(requestBodyJson);
}

///////////////////////////////////////////////////////////////////////////////////////////////////////

function getCliente(customerId) {
	//obtiene los datos de un cliente realizando un request a la API
    
    let vReturn = false;
    customer = {};

	console.log("-----------------------<< Obtener Customer >>-----------------------");

    let requestAPI =new XMLHttpRequest();
    requestAPI.open("GET", hostAPI + '/sales/customers/' + customerId, false);
    requestAPI.onreadystatechange=function() {
        if (requestAPI.readyState==4) {
        	let respondStatusCode = requestAPI.status;
            //se verifica al código status de respuesta
            if (respondStatusCode == 200 && requestAPI.responseText.length > 0) {
                vReturn = true;
                customer = JSON.parse(requestAPI.responseText);
                if (customer.customerId) {
                    console.log("customerId = " + customer.customerId);
                    console.log("name = " + customer.name);
                    console.log("legalName = " + customer.legalName);
                }
            }
            else {
                vReturn = false;
                let requestErrorMessage = '';
                if (requestAPI.responseText.length > 0) {
	                let requestError = JSON.parse(requestAPI.responseText);
	                if (requestError.message) {
	                    requestErrorMessage = requestError.message;
	                }	                	
                }
                console.log('error get cliente ' + requestErrorMessage);
            }
            return vReturn;
        }
    }
    //se construyen los headers requeridos para el request
    requestAPI.setRequestHeader("Accept", "application/json");
    requestAPI.setRequestHeader("Authorization", "Bearer " + credential.token);
    requestAPI.send();
}

///////////////////////////////////////////////////////////////////////////////////////////////////////

function putCliente(customerId) {
	//modifica y guarda un cliente

    let vReturn = false;

    //se contruye el body del request
    let requestBodyJson = JSON.stringify(customer);

	console.log("-----------------------<< Guardar Customer >>-----------------------");

    //send request
    let requestAPI = new XMLHttpRequest();
    requestAPI.open("PUT", hostAPI + '/sales/customers/' + customerId, false);
    requestAPI.onreadystatechange=function() {
        if (requestAPI.readyState==4) {
        	let respondStatusCode = requestAPI.status;
            //se verifica al código status de respuesta
            if (respondStatusCode == 200 && requestAPI.responseText.length > 0) {
                vReturn = true;
                customer = JSON.parse(requestAPI.responseText);
                if (customer.customerId) {
                    console.log("customerId = " + customer.customerId);
                    console.log("name = " + customer.name);
                    console.log("legalName = " + customer.legalName);
                }
            }
            else {
                vReturn = false;
                let requestErrorMessage = '';
                if (requestAPI.responseText.length > 0) {
	                let requestError = JSON.parse(requestAPI.responseText);
	                if (requestError.message) {
	                    requestErrorMessage = requestError.message;
	                }	                	
                }
                console.log('error put customer ' + requestErrorMessage);
            }
            return vReturn;
        }
    }
    //se construyen los headers requeridos para el request
    requestAPI.setRequestHeader("Content-Type", "application/json");
    requestAPI.setRequestHeader("Accept", "application/json");
    requestAPI.setRequestHeader("Authorization", "Bearer " + credential.token);
    requestAPI.send(requestBodyJson);
}

///////////////////////////////////////////////////////////////////////////////////////////////////////

function isValidToken() {
	//indica si el token es válido
	let vReturn = false;
	let ldNow = new Date();
	ldNow = ldNow.toISOString();
	if (credential.expiration) {
		if (credential.expiration > ldNow) {
			vReturn = true;
		}
	}

	return vReturn;
}

///////////////////////////////////////////////////////////////////////////////////////////////////////

//customerId usado en el ejemplo
let customerId = 18;
obtenerToken();
if (isValidToken())	 {
	getCliente(customerId);
	//se verifica que exista un cliente cargado y que corresponde con el que queremos modificar
	if (customer.customerId && customer.customerId == customerId) {
		//se modifica la propiedad legalName del cliente
		customer.legalName = 'Mi razón social';
		putCliente(customerId);
	}
}

Uso de Swagger