Introducción
Bienvenido a la documentación de referencia de la API de Apunto (v1).
Aquí encontrarás cómo conectar tu cuenta de freight forwarding con sistemas externos: autenticación, recursos (operaciones, servicios, centro de costos, contactos, facturas, documentos) y manejo de errores.
Empieza por Información general para entender el contexto del producto, el modelo de datos y los pasos para tu primera integración. Después continúa con Autenticación y el recurso que necesites en el menú lateral.
Información general
La API de Apunto es el canal programático hacia tu cuenta en Apunto: la plataforma donde tu equipo de freight forwarding gestiona operaciones, servicios, contactos, documentos y la economía de cada embarque (centro de costos, facturas y gastos).
Con la API puedes leer y actualizar la misma información que ves en la aplicación web, conectar sistemas externos y automatizar flujos sin depender de captura manual.
Qué es Apunto (contexto del sistema)
Apunto está pensado para forwarders y operadores logísticos que coordinan importaciones, exportaciones y movimientos domésticos. En la plataforma web el trabajo se organiza así:
- Cuenta (account) — Es tu organización. Todos los datos de la API pertenecen a la cuenta asociada al usuario del token; no hay acceso cruzado entre cuentas.
- Operación — El expediente del embarque o proyecto logístico (cliente, moneda, agente operativo, estado, márgenes).
- Servicio — Cada tramo o actividad dentro de la operación (marítimo, aéreo, terrestre, aduanas, etc.).
- Centro de costos (
CostCenter) — Líneas de ingreso y gasto ligadas a un servicio; son la base para rentabilidad y para facturar o registrar compras. - Contactos y direcciones — Catálogo de clientes, proveedores, carriers y ubicaciones (puertos, plantas, domicilios fiscales).
- Documentos comerciales — Facturas de cliente (
Invoice) y facturas o gastos de proveedor (Bill), con opción de vincular líneas al centro de costos o generar borradores desde una operación. - Colaboración — Comentarios (
messages), tareas (to_dos) y carpetas de archivos en operaciones, servicios y contactos.
La API refleja ese mismo modelo: casi siempre navegarás operación → servicios → centro de costos, y usarás contactos y direcciones como catálogos de referencia.
Cuenta (tenant)
├── Operaciones
│ ├── Servicios
│ │ └── Centros de costo (líneas ingreso/gasto)
│ ├── Comentarios, tareas, carpetas de archivos
│ └── Agregado de centros de costo (solo lectura)
├── Contactos (+ direcciones anidadas, mensajes, tareas)
├── Direcciones (nivel cuenta)
├── Facturas y bills
└── Adjuntos (p. ej. extensión Chrome)
Qué puedes construir con la API
Algunos ejemplos habituales de integración:
- Sincronizar operaciones con un ERP, WMS o portal del cliente (estado, referencias, fechas clave).
- Crear o actualizar servicios cuando un carrier, aduana o sistema de tracking confirme un hito.
- Registrar ingresos y gastos en el centro de costos desde herramientas de compras o conciliación.
- Generar borradores de factura o bill a partir de líneas de centro de costos ya capturadas en la operación.
- Mantener contactos y direcciones alineados con tu CRM o directorio maestro.
- Publicar comentarios o tareas desde bots, correo o tickets internos.
- Subir documentos (BL, pedimentos, POD) a la carpeta correcta de una operación o servicio.
Principios REST y formato de datos
La API sigue REST: recursos con URLs predecibles y verbos HTTP estándar.
| Método | Uso típico |
|---|---|
GET |
Leer uno o listar (con paginación) |
POST |
Crear |
PUT / PATCH |
Actualizar |
DELETE |
Eliminar (cuando el recurso lo permite) |
El cuerpo de las peticiones y todas las respuestas usan JSON. Envía Content-Type: application/json y Accept: application/json en operaciones con cuerpo o cuando quieras dejar explícito el formato esperado.
URL base (producción): https://control.apunto.io/api/v1
Respuesta exitosa (HTTP 200)
Ejemplo simplificado de una operación (GET /api/v1/operations/:id):
{
"operation": {
"id": 1042,
"identification": "IMP-2024-0847",
"kind": "importation",
"mode": "maritime",
"status": "active",
"client_ref": "PO-7781",
"contact": {
"id": 88,
"name": "Acme Logistics",
"alias": "ACME"
},
"currency": {
"id": 1,
"name": "USD"
},
"profit_amount": "1250.00",
"profit_percentage": "18.5",
"services_count": 3,
"created_at": "2024-03-15T10:30:00.000Z",
"updated_at": "2024-03-20T14:22:00.000Z"
}
}
Las creaciones exitosas suelen responder 201 Created con el recurso en el cuerpo.
Respuesta de error
Los errores combinan el código HTTP y un cuerpo JSON. La forma exacta depende del tipo de fallo:
Autenticación (401) — sin cuerpo en algunos casos, o:
{
"error": "Invalid email or password."
}
Validación (422) — campos con mensajes:
{
"errors": {
"contact_code": ["can't be blank"],
"kind": ["is invalid"]
}
}
Recurso no encontrado (404):
{
"error": "Operation not found."
}
Consulta la sección Errores para el catálogo de códigos y buenas prácticas de reintento.
Alcance de la cuenta y permisos
- Cada token de API pertenece a un usuario. Las peticiones se ejecutan con su sesión y permisos (roles y políticas de la cuenta).
- Los listados y búsquedas solo devuelven registros de
Current.account— la cuenta activa de ese usuario. - Un usuario bloqueado recibe 403 aunque el token siga siendo válido.
- Si tu integración necesita acciones que el usuario no puede hacer en la web, la API tampoco las permitirá.
Identificadores: ID numérico y códigos (*_code)
En lecturas (GET) recibirás id numérico en JSON.
En altas y cambios (POST / PATCH / PUT) conviene usar códigos legibles que ya manejas en Apunto, por ejemplo:
| Parámetro | Resuelve |
|---|---|
contact_code |
Alias del contacto (cliente/proveedor) |
supplier_code |
Alias del proveedor |
currency_code |
Moneda (MXN, USD, …) |
operational_agent_email |
Usuario agente operativo |
address_code |
Alias de dirección |
Así evitas hardcodear IDs internos entre entornos. Si un código no existe en la cuenta, obtendrás error de validación o 404 según el endpoint.
Paginación
Los listados están paginados. Por defecto page=1 y per_page=25 (máximo 100).
GET /api/v1/operations?page=2&per_page=50
{
"operations": [],
"pagination": {
"page": 2,
"per_page": 50,
"total": 150
}
}
Evita bucles que recorran “toda la cuenta” sin paginar; usa total y page hasta agotar resultados.
Versionado
La versión actual es v1 (prefijo /api/v1/). Dentro de una misma versión mayor intentamos no romper contratos existentes; cambios incompatibles implicarán una nueva versión (v2, etc.).
Cómo empezar (checklist)
- Acceso — Usuario activo en tu cuenta Apunto con permisos para los módulos que integrarás.
- Token — En la web: Configuración → Tokens de API → crea un token y guárdalo de forma segura (solo se muestra al crearlo).
- Primera petición — Prueba
GET /api/v1/operationsconAuthorization: Bearer TU_TOKEN. - Explora recursos — Lee una operación (
show), luego sus servicios y centros de costo anidados o vía endpoints dedicados. - Escrituras — Empieza con contactos o comentarios de bajo riesgo antes de tocar facturación o centro de costos.
- Errores y límites — Implementa logging del status HTTP y del cuerpo JSON; respeta paginación y reintentos prudentes en 5xx.
Siguiente paso
Antes de implementar integraciones completas, revisa:
- Autenticación — Bearer token y login alternativo (
POST /api/v1/auth) - Operaciones y Servicios — núcleo del dominio
- Centro de costos (CostCenter) — ingresos, gastos y vínculo con facturas
- Errores — códigos HTTP y formatos de error
Soporte: soporte@apunto.com · Manuales de producto: https://docs.apunto.com
Autenticación
Descripción General
La API de Apunto utiliza autenticación mediante Bearer token. Para acceder a la API, necesitas:
- Crear un token de API en la configuración de tu cuenta
- Incluir el token en el header
Authorizationde cada petición
Crear un Token de API
Los tokens de API se pueden crear a través de la interfaz web:
- Navega a Configuración → Tokens de API
- Haz clic en Nuevo Token de API
- Dale un nombre descriptivo a tu token
- Copia el token generado (solo se mostrará una vez)
Usar el Token de API
Para autenticarte, incluye el token en el header Authorization:
curl "https://control.apunto.io/api/v1/operations" \
-H "Authorization: Bearer TU_TOKEN_API"
require 'net/http'
require 'uri'
uri = URI.parse("https://control.apunto.io/api/v1/operations")
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer TU_TOKEN_API"
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(request)
end
import requests
headers = {
'Authorization': 'Bearer TU_TOKEN_API',
'Content-Type': 'application/json'
}
response = requests.get(
'https://control.apunto.io/api/v1/operations',
headers=headers
)
const axios = require('axios');
const config = {
headers: {
'Authorization': 'Bearer TU_TOKEN_API',
'Content-Type': 'application/json'
}
};
axios.get('https://control.apunto.io/api/v1/operations', config)
.then(response => console.log(response.data))
.catch(error => console.error(error));
Incluye tu token de API en el header Authorization de todas las peticiones:
Authorization: Bearer TU_TOKEN_API
Login (Autenticación Alternativa)
Si necesitas autenticarte programáticamente y obtener un token:
curl -X POST "https://control.apunto.io/api/v1/auth" \
-H "Content-Type: application/json" \
-d '{
"email": "usuario@ejemplo.com",
"password": "tu_contraseña"
}'
require 'net/http'
require 'uri'
require 'json'
uri = URI.parse("https://control.apunto.io/api/v1/auth")
request = Net::HTTP::Post.new(uri)
request.content_type = "application/json"
request.body = JSON.dump({
"email" => "usuario@ejemplo.com",
"password" => "tu_contraseña"
})
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(request)
end
import requests
import json
url = 'https://control.apunto.io/api/v1/auth'
payload = {
'email': 'usuario@ejemplo.com',
'password': 'tu_contraseña'
}
response = requests.post(url, json=payload)
token = response.json()['token']
const axios = require('axios');
axios.post('https://control.apunto.io/api/v1/auth', {
email: 'usuario@ejemplo.com',
password: 'tu_contraseña'
})
.then(response => {
const token = response.data.token;
console.log('Token:', token);
})
.catch(error => console.error(error));
Respuesta:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Petición HTTP
POST /api/v1/auth
Parámetros del Body
| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
| string | Sí | Correo electrónico del usuario | |
| password | string | Sí | Contraseña del usuario |
| otp_attempt | string | No | Código OTP (si 2FA está activado) |
Respuesta
Retorna un objeto que contiene el token de API.
Operaciones
Las operaciones representan procesos completos de freight forwarding (importación, exportación, transporte doméstico, etc.).
Objeto Operation
Atributos Principales
| Atributo | Tipo | Descripción |
|---|---|---|
| id | integer | Identificador único |
| identification | string | Identificador legible (ej: "IMP-001-2024") |
| kind | string | Tipo: importation, exportation, domestic, crosstrade, transportation, consulting, export_trading_company, import_trading_company |
| mode | string | Modo de transporte: land, aerial, maritime |
| status | string | Estado: confirmed, active, finished, closed, canceled |
| client_ref | string | Referencia del cliente |
| contact | object | Cliente de la operación |
| currency | object | Moneda de la operación |
| operational_agent | object | Agente operativo asignado |
| profit_amount | decimal | Monto de ganancia |
| profit_percentage | decimal | Porcentaje de ganancia |
| services_count | integer | Número de servicios asociados |
| comments_count | integer | Número de comentarios |
| tasks_count | integer | Número de tareas |
| folders_count | integer | Número de carpetas de documentos |
| goods_description | string | Descripción de la mercancía |
| incoterm | string | INCOTERM aplicable |
| service_scope | string | Alcance del servicio (enum; ej: door_to_door, port_cy_to_port_cy, airport_to_airport) |
| economic_month | date | Mes económico (primer día del mes; acepta fecha parseable) |
| regime | string | Régimen aduanero (texto libre) |
| income_amount | decimal | Ingresos |
| expense_amount | decimal | Gastos |
| quote_external_id | string | ID externo de cotización |
| nomenclature | string | Nomenclatura / fracción arancelaria |
| tags | array | Etiquetas (tag_list al crear/actualizar) |
| services | array | Servicios completos (solo en show); cada servicio incluye cost_centers |
| created_at | datetime | Fecha de creación |
| updated_at | datetime | Fecha de última actualización |
Listar Operaciones GET
Definición
GET /api/v1/operations
Ejemplo de llamada
curl "https://control.apunto.io/api/v1/operations" \
-H "Authorization: Bearer TU_TOKEN" \
-H "Content-Type: application/json"
require 'uri'
require 'net/http'
uri = URI('https://control.apunto.io/api/v1/operations')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Get.new(uri)
request['Authorization'] = 'Bearer TU_TOKEN'
request['Content-Type'] = 'application/json'
response = http.request(request)
puts response.body
import requests
url = "https://control.apunto.io/api/v1/operations"
headers = {
"Authorization": "Bearer TU_TOKEN",
"Content-Type": "application/json"
}
response = requests.get(url, headers=headers)
print(response.json())
fetch('https://control.apunto.io/api/v1/operations', {
method: 'GET',
headers: {
'Authorization': 'Bearer TU_TOKEN',
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => console.log(data));
Respuesta JSON
{
"operations": [
{
"id": 123,
"identification": "IMP-001-2024",
"kind": "importation",
"mode": "maritime",
"status": "active",
"client_ref": "REF-001",
"contact": {
"alias": "ACME",
"name": "ACME SA DE CV"
},
"currency": {
"code": "MXN",
"name": "Peso Mexicano"
},
"operational_agent": {
"email": "agente@apunto.com",
"name": "Juan Pérez"
},
"profit_amount": 5000.00,
"profit_percentage": 15.5,
"services_count": 3,
"comments_count": 8,
"tasks_count": 5,
"folders_count": 2,
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:00Z"
}
],
"pagination": {
"page": 1,
"per_page": 25,
"total": 150
}
}
Retorna una lista paginada de operaciones de la cuenta.
Parámetros Query
| Parámetro | Descripción |
|---|---|
| page | Número de página (default: 1) |
| per_page | Registros por página (default: 25, max: 100) |
Buscar Operaciones GET
Definición
GET /api/v1/operations/search
Búsqueda ligera (máximo 20 resultados) por identification, client_ref, goods_description o number. Usada por integraciones (p. ej. extensión de Chrome) para elegir destino al subir adjuntos.
Parámetros Query
| Parámetro | Descripción |
|---|---|
| q | Texto de búsqueda (opcional; sin q devuelve las 20 operaciones más recientes por updated_at) |
Respuesta JSON
{
"operations": [
{
"id": 123,
"identification": "IMP-001-2024",
"number": 1,
"client_ref": "REF-001",
"kind": "importation",
"mode": "maritime",
"status": "active",
"goods_description": "Maquinaria",
"contact_name": "ACME",
"updated_at": "2024-01-15T10:30:00Z",
"services": [
{ "id": 789, "identification": "SRV-001", "mode": "maritime", "status": "active" }
]
}
]
}
Obtener una Operación GET
Definición
GET /api/v1/operations/:id
Ejemplo de llamada
curl "https://control.apunto.io/api/v1/operations/123" \
-H "Authorization: Bearer TU_TOKEN"
Respuesta JSON
{
"operation": {
"id": 123,
"identification": "IMP-001-2024",
"kind": "importation",
"mode": "maritime",
"status": "active",
"client_ref": "REF-001",
"contact": {
"alias": "ACME",
"name": "ACME SA DE CV"
},
"currency": {
"code": "MXN",
"name": "Peso Mexicano"
},
"goods_description": "Maquinaria industrial",
"incoterm": "FOB",
"service_scope": "door_to_door",
"quote_external_id": "QT-12345",
"nomenclature": "8479.89.99",
"profit_amount": 5000.00,
"profit_percentage": 15.5,
"services_count": 3,
"comments_count": 8,
"tasks_count": 5,
"folders_count": 2,
"tags": ["urgente", "cliente-vip"],
"to_dos": [
{
"id": 55,
"title": "Revisar documentación",
"completed": false,
"required": true,
"start_at": null,
"end_at": "2024-01-20T18:00:00Z"
}
],
"folders": [
{
"id": 10,
"name": "BL",
"parent_id": null,
"files_count": 1,
"attachments": [
{
"id": 501,
"filename": "bl.pdf",
"byte_size": 245000,
"content_type": "application/pdf",
"url": "https://control.apunto.io/rails/active_storage/blobs/redirect/..."
}
],
"children": []
}
],
"services": [
{
"id": 789,
"identification": "SRV-001-2024",
"mode": "maritime",
"status": "active",
"shipment_type": "fcl",
"shipment_kind": "international",
"supplier": {
"alias": "MAERSK",
"name": "Maersk Line"
},
"eta_date": "2024-02-15",
"etd_date": "2024-01-20",
"bl": "BL123456",
"booking": "BOOK789",
"cost_centers": [
{
"id": 901,
"concept": "Flete marítimo",
"quantity": 1,
"income_amount": 15000,
"expense_amount": 9000,
"profit_amount": 6000,
"profit_percentage": 40.0,
"currency": { "code": "MXN", "name": "Peso Mexicano" },
"supplier": { "id": 111, "alias": "MAERSK", "name": "Maersk Line" },
"linked": {
"quote_line_item_id": null,
"invoice_line_item_id": null,
"bill_line_item_id": null
}
}
]
}
],
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:00Z"
}
}
Retorna los detalles completos de una operación específica.
Crear Operación POST
Definición
POST /api/v1/operations
Ejemplo de llamada
curl -X POST "https://control.apunto.io/api/v1/operations" \
-H "Authorization: Bearer TU_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"operation": {
"contact_code": "ACME",
"currency_code": "MXN",
"operational_agent_email": "agente@apunto.com",
"kind": "importation",
"mode": "maritime",
"client_ref": "REF-001",
"goods_description": "Maquinaria industrial",
"incoterm": "FOB"
}
}'
require 'uri'
require 'net/http'
require 'json'
uri = URI('https://control.apunto.io/api/v1/operations')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Post.new(uri)
request['Authorization'] = 'Bearer TU_TOKEN'
request['Content-Type'] = 'application/json'
request.body = {
operation: {
contact_code: 'ACME',
currency_code: 'MXN',
operational_agent_email: 'agente@apunto.com',
kind: 'importation',
mode: 'maritime'
}
}.to_json
response = http.request(request)
puts response.body
import requests
import json
url = "https://control.apunto.io/api/v1/operations"
headers = {
"Authorization": "Bearer TU_TOKEN",
"Content-Type": "application/json"
}
data = {
"operation": {
"contact_code": "ACME",
"currency_code": "MXN",
"operational_agent_email": "agente@apunto.com",
"kind": "importation",
"mode": "maritime"
}
}
response = requests.post(url, headers=headers, data=json.dumps(data))
print(response.json())
fetch('https://control.apunto.io/api/v1/operations', {
method: 'POST',
headers: {
'Authorization': 'Bearer TU_TOKEN',
'Content-Type': 'application/json'
},
body: JSON.stringify({
operation: {
contact_code: 'ACME',
currency_code: 'MXN',
operational_agent_email: 'agente@apunto.com',
kind: 'importation',
mode: 'maritime'
}
})
})
.then(response => response.json())
.then(data => console.log(data));
Respuesta JSON (201 Created)
{
"operation": {
"id": 124,
"identification": "IMP-002-2024",
"kind": "importation",
"mode": "maritime",
"status": "confirmed",
"client_ref": "REF-001",
"goods_description": "Maquinaria industrial",
"incoterm": "FOB",
"created_at": "2024-01-16T09:15:00Z"
},
"message": "Operación creada exitosamente"
}
Crea una nueva operación.
Parámetros
| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
| contact_code | string | Sí | Código (alias) del contacto |
| currency_code | string | No | Código de moneda (default: MXN) |
| operational_agent_email | string | No | Email del agente operativo |
| kind | string | Sí | Tipo de operación |
| mode | string | Sí | Modo de transporte |
| client_ref | string | No | Referencia del cliente |
| goods_description | string | No | Descripción de mercancía |
| incoterm | string | No | INCOTERM |
| service_scope | string | No | Alcance (door_to_door, port_cy_to_port_cy, etc.) |
| status | string | No | Estado inicial (default del modelo: confirmed) |
| profit_amount | decimal | No | Ganancia |
| profit_percentage | decimal | No | Margen % |
| income_amount | decimal | No | Ingresos |
| expense_amount | decimal | No | Gastos |
| regime | string | No | Régimen |
| quote_external_id | string | No | Referencia externa de cotización |
| nomenclature | string | No | Nomenclatura |
| economic_month | string | No | Mes económico (fecha parseable → primer día del mes) |
| tag_list | array | No | Etiquetas |
Valores Permitidos
kind: importation, exportation, domestic, crosstrade, transportation, consulting, export_trading_company, import_trading_company
mode: land, aerial, maritime
status: confirmed, active, finished, closed, canceled
service_scope: valores dependen del modo de transporte (p. ej. door_to_door, door_to_port_cy, port_cy_to_port_cy, airport_to_airport, door_to_door). Ver enum completo en el modelo Operation.
Actualizar Operación PUT
Definición
PUT /api/v1/operations/:id
PATCH /api/v1/operations/:id
Ejemplo de llamada
curl -X PUT "https://control.apunto.io/api/v1/operations/123" \
-H "Authorization": Bearer TU_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"operation": {
"status": "active",
"client_ref": "REF-001-UPDATED"
}
}'
require 'uri'
require 'net/http'
require 'json'
uri = URI('https://control.apunto.io/api/v1/operations/123')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Put.new(uri)
request['Authorization'] = 'Bearer TU_TOKEN'
request['Content-Type'] = 'application/json'
request.body = {
operation: {
status: 'active',
client_ref: 'REF-001-UPDATED'
}
}.to_json
response = http.request(request)
puts response.body
import requests
import json
url = "https://control.apunto.io/api/v1/operations/123"
headers = {
"Authorization": "Bearer TU_TOKEN",
"Content-Type": "application/json"
}
data = {
"operation": {
"status": "active",
"client_ref": "REF-001-UPDATED"
}
}
response = requests.put(url, headers=headers, data=json.dumps(data))
print(response.json())
fetch('https://control.apunto.io/api/v1/operations/123', {
method: 'PUT',
headers: {
'Authorization': 'Bearer TU_TOKEN',
'Content-Type': 'application/json'
},
body: JSON.stringify({
operation: {
status: 'active',
client_ref: 'REF-001-UPDATED'
}
})
})
.then(response => response.json())
.then(data => console.log(data));
Respuesta JSON
{
"operation": {
"id": 123,
"identification": "IMP-001-2024",
"status": "active",
"client_ref": "REF-001-UPDATED",
"updated_at": "2024-01-16T10:45:00Z"
},
"message": "Operación actualizada exitosamente"
}
Actualiza una operación existente.
Eliminar Operación DELETE
Definición
DELETE /api/v1/operations/:id
Ejemplo de llamada
curl -X DELETE "https://control.apunto.io/api/v1/operations/123" \
-H "Authorization: Bearer TU_TOKEN"
Respuesta JSON
{
"message": "Operación eliminada exitosamente"
}
Elimina la operación y marca sus servicios asociados como eliminados (deleted_at). Requiere permisos de cuenta.
Cerrar operación POST
Definición
POST /api/v1/operations/:id/close
Cierra la operación cuando ningún servicio está en estado active o finished (todos deben estar closed o canceled). Equivalente a la acción web de cierre.
Respuesta JSON
{
"operation": { "id": 123, "status": "closed" },
"message": "Operación cerrada exitosamente"
}
Si aún hay servicios activos o en proceso, responde 422 con errors y message.
Reabrir operación POST
Definición
POST /api/v1/operations/:id/reopen
Reabre una operación en estado finished o closed y la regresa a active (evento AASM reopen).
Cancelar operación POST
Definición
POST /api/v1/operations/:id/cancel
Cancela la operación solo si todos sus servicios ya están en estado canceled.
Consulta los motivos disponibles con GET /api/v1/operation_cancellation_reasons (ver Motivos de cancelación).
Parámetros (body JSON, raíz)
| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
| canceled_at | date | No | Fecha de cancelación (default: hoy; en la web el formulario la marca como obligatoria) |
| operation_cancellation_reason_id | integer | Condicional | ID del motivo — obligatorio si la cuenta tiene motivos activos |
| operation_cancellation_reason_name | string | Condicional | Alternativa al id: nombre del motivo (ej. Cliente canceló) |
| cancellation_notes | string | No | Notas adicionales |
Si la cuenta tiene motivos configurados, debes enviar id o name. Si no hay catálogo, el motivo es opcional.
Respuesta JSON
{
"operation": { "id": 123, "status": "canceled" },
"message": "Operación cancelada exitosamente"
}
Centro de costos agregado GET
GET /api/v1/operations/:operation_id/cost_centers
Listado paginado de todas las líneas de ingreso/gasto de los servicios de la operación. CRUD completo bajo /services/:service_id/cost_centers (ver Centro de costos).
Generar factura o factura de proveedor POST
POST /api/v1/operations/:id/generate_invoice
POST /api/v1/operations/:id/generate_bill
Body: { "cost_center_ids": [1, 2], ... }. Para generate_invoice, incluye códigos CFDI en cuentas MX. Detalle en Facturas y facturas de proveedor.
Carpetas de documentos
Listar árbol con archivos:
GET /api/v1/operations/:operation_id/folders
POST /api/v1/operations/:operation_id/folders
Subir / actualizar / eliminar archivos: ver sección Documentos en la documentación.
Comentarios de Operación
Los comentarios están anidados bajo las operaciones. Ver Comentarios para más detalles.
GET /api/v1/operations/:operation_id/messages
POST /api/v1/operations/:operation_id/messages
PUT /api/v1/operations/:operation_id/messages/:id
DELETE /api/v1/operations/:operation_id/messages/:id
Tareas de Operación
Las tareas están anidadas bajo las operaciones. Ver Tareas para CRUD completo.
GET /api/v1/operations/:operation_id/to_dos
POST /api/v1/operations/:operation_id/to_dos
PATCH /api/v1/operations/:operation_id/to_dos/:id
POST /api/v1/operations/:operation_id/to_dos/:id/complete
DELETE /api/v1/operations/:operation_id/to_dos/:id
Servicios
Los servicios representan los componentes logísticos individuales dentro de una operación (transporte marítimo, aéreo, terrestre, aduanas, etc.).
Objeto Service
Atributos Principales
| Atributo | Tipo | Descripción |
|---|---|---|
| id | integer | Identificador único |
| identification | string | Identificador legible (ej: "SRV-001-2024") |
| mode | string | Modo: land, aerial, maritime, customs |
| status | string | Estado: active, finished, closed, canceled |
| shipment_type | string | Tipo de envío (ej: fcl, lcl) |
| shipment_kind | string | Clase: national, international |
| operation | object | Operación padre (anidada) |
| supplier | object | Proveedor del servicio (anidado) |
| service_agent | object | Agente de servicio (anidado) |
| eta_date | date | Fecha estimada de arribo |
| etd_date | date | Fecha estimada de salida |
| pickup_date | date | Fecha de recolección |
| delivery_date | date | Fecha de entrega |
| comments_count | integer | Número de comentarios |
| tasks_count | integer | Número de tareas |
| folders_count | integer | Número de carpetas de documentos |
| cost_centers | array | Líneas del centro de costos (solo en show) |
| bl | string | Bill of Lading |
| booking | string | Número de reserva |
| created_at | datetime | Fecha de creación |
| updated_at | datetime | Fecha de última actualización |
Listar Servicios GET
Definición
GET /api/v1/services
Ejemplo de llamada
curl "https://control.apunto.io/api/v1/services" \
-H "Authorization: Bearer TU_TOKEN" \
-H "Content-Type: application/json"
require 'uri'
require 'net/http'
uri = URI('https://control.apunto.io/api/v1/services')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Get.new(uri)
request['Authorization'] = 'Bearer TU_TOKEN'
request['Content-Type'] = 'application/json'
response = http.request(request)
puts response.body
import requests
url = "https://control.apunto.io/api/v1/services"
headers = {
"Authorization": "Bearer TU_TOKEN",
"Content-Type": "application/json"
}
response = requests.get(url, headers=headers)
print(response.json())
fetch('https://control.apunto.io/api/v1/services', {
method: 'GET',
headers: {
'Authorization': 'Bearer TU_TOKEN',
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => console.log(data));
Respuesta JSON
{
"services": [
{
"id": 789,
"identification": "SRV-001-2024",
"mode": "maritime",
"status": "active",
"shipment_type": "fcl",
"shipment_kind": "international",
"operation": {
"id": 123,
"identification": "IMP-001-2024",
"kind": "importation",
"client_ref": "REF-001"
},
"supplier": {
"id": 111,
"alias": "MAERSK",
"name": "Maersk Line"
},
"service_agent": {
"id": 222,
"email": "agente@apunto.com",
"name": "María López"
},
"eta_date": "2024-02-15",
"etd_date": "2024-01-20",
"pickup_date": "2024-01-18",
"delivery_date": "2024-02-17",
"comments_count": 3,
"tasks_count": 2,
"folders_count": 1,
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:00Z"
}
],
"pagination": {
"page": 1,
"per_page": 25,
"total": 85
}
}
Retorna una lista paginada de servicios de la cuenta.
Parámetros Query
| Parámetro | Descripción |
|---|---|
| page | Número de página (default: 1) |
| per_page | Registros por página (default: 25, max: 100) |
| operation_id | Filtrar servicios de una operación |
Obtener un Servicio GET
Definición
GET /api/v1/services/:id
Ejemplo de llamada
curl "https://control.apunto.io/api/v1/services/789" \
-H "Authorization: Bearer TU_TOKEN"
Respuesta JSON
{
"service": {
"id": 789,
"identification": "SRV-001-2024",
"mode": "maritime",
"status": "active",
"shipment_type": "fcl",
"shipment_kind": "international",
"operation": {
"id": 123,
"identification": "IMP-001-2024",
"kind": "importation",
"client_ref": "REF-001"
},
"supplier": {
"id": 111,
"alias": "MAERSK",
"name": "Maersk Line"
},
"service_agent": {
"id": 222,
"email": "agente@apunto.com",
"name": "María López"
},
"bl": "BL123456",
"booking": "BOOK789",
"guide_number": null,
"flight_number": null,
"awb_number": null,
"airline_name": null,
"shipping_line_name": "Maersk",
"customs_agent": {
"id": 333,
"alias": "ADUANAS-MX",
"name": "Aduanas México SA"
},
"customs_address": {
"id": 444,
"alias": "ADUANA-VERACRUZ",
"name": "Aduana Veracruz",
"address_type": "customs",
"full_address": "Puerto de Veracruz, Veracruz, México"
},
"customs_reference": "REF-ADU-001",
"dispatch_appointment_at": "2024-02-16T09:00:00Z",
"observations": "Requiere inspección especial",
"eta_date": "2024-02-15",
"etd_date": "2024-01-20",
"pickup_date": "2024-01-18",
"delivery_date": "2024-02-17",
"mbl": "MBL123",
"hbl": "HBL456",
"mawb": null,
"hawb": null,
"pedimento": "24-01-1234-5678901",
"carta_porte": null,
"manifiesto_carga": "MAN-001",
"comments_count": 3,
"tasks_count": 2,
"folders_count": 1,
"tags": ["urgente", "refrigerado"],
"cost_centers": [
{
"id": 901,
"concept": "Flete marítimo",
"quantity": 1,
"income_amount": 15000,
"expense_amount": 9000,
"profit_amount": 6000,
"profit_percentage": 40.0,
"currency": { "code": "MXN", "name": "Peso Mexicano" },
"expense_currency": { "code": "MXN", "name": "Peso Mexicano" },
"supplier": { "id": 111, "alias": "MAERSK", "name": "Maersk Line" },
"notes": null,
"linked": {
"quote_line_item_id": null,
"invoice_line_item_id": null,
"bill_line_item_id": null
}
}
],
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:00Z"
}
}
Retorna los detalles completos de un servicio específico. Incluye arrays cost_centers (centro de costos), to_dos y folders (igual que operaciones). En index solo hay contadores.
Crear Servicio POST
Definición
POST /api/v1/services
Ejemplo de llamada
curl -X POST "https://control.apunto.io/api/v1/services" \
-H "Authorization: Bearer TU_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"service": {
"operation_id": 123,
"supplier_code": "MAERSK",
"service_agent_email": "agente@apunto.com",
"mode": "maritime",
"shipment_type": "fcl",
"shipment_kind": "international",
"bl": "BL123456",
"booking": "BOOK789"
}
}'
require 'uri'
require 'net/http'
require 'json'
uri = URI('https://control.apunto.io/api/v1/services')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Post.new(uri)
request['Authorization'] = 'Bearer TU_TOKEN'
request['Content-Type'] = 'application/json'
request.body = {
service: {
operation_id: 123,
supplier_code: 'MAERSK',
service_agent_email: 'agente@apunto.com',
mode: 'maritime'
}
}.to_json
response = http.request(request)
puts response.body
import requests
import json
url = "https://control.apunto.io/api/v1/services"
headers = {
"Authorization": "Bearer TU_TOKEN",
"Content-Type": "application/json"
}
data = {
"service": {
"operation_id": 123,
"supplier_code": "MAERSK",
"service_agent_email": "agente@apunto.com",
"mode": "maritime"
}
}
response = requests.post(url, headers=headers, data=json.dumps(data))
print(response.json())
fetch('https://control.apunto.io/api/v1/services', {
method: 'POST',
headers: {
'Authorization': 'Bearer TU_TOKEN',
'Content-Type': 'application/json'
},
body: JSON.stringify({
service: {
operation_id: 123,
supplier_code: 'MAERSK',
service_agent_email: 'agente@apunto.com',
mode: 'maritime'
}
})
})
.then(response => response.json())
.then(data => console.log(data));
Respuesta JSON (201 Created)
{
"service": {
"id": 790,
"identification": "SRV-002-2024",
"mode": "maritime",
"status": "active",
"operation": {
"id": 123,
"identification": "IMP-001-2024",
"kind": "importation",
"client_ref": "REF-001"
},
"supplier": {
"id": 111,
"alias": "MAERSK",
"name": "Maersk Line"
},
"bl": "BL123456",
"booking": "BOOK789",
"created_at": "2024-01-16T09:15:00Z"
},
"message": "Servicio creado exitosamente"
}
Crea un nuevo servicio dentro de una operación.
Parámetros
| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
| operation_id | integer | Sí | ID de la operación padre |
| supplier_code | string | No | Código (alias) del proveedor |
| service_agent_email | string | No | Email del agente de servicio |
| mode | string | Sí | Modo de transporte |
| shipment_type | string | No | Tipo de envío |
| shipment_kind | string | No | Clase de envío |
| bl | string | No | Bill of Lading |
| booking | string | No | Número de reserva |
| eta_date | date | No | Fecha estimada de arribo |
| etd_date | date | No | Fecha estimada de salida |
| pickup_date | date | No | Recolección |
| delivery_date | date | No | Entrega |
| customs_agent_code | string | No | Alias del agente aduanal |
| customs_address_code | string | No | Alias de dirección aduanal (cuenta) |
| customs_reference | string | No | Referencia aduanal |
| dispatch_appointment_at | datetime | No | Cita de despacho |
| observations | string | No | Observaciones |
| guide_number | string | No | Guía |
| flight_number | string | No | Vuelo |
| awb_number | string | No | AWB |
| airline_name | string | No | Aerolínea |
| shipping_line_name | string | No | Naviera |
| mbl, hbl, mawb, hawb | string | No | Documentos de transporte |
| pedimento, carta_porte, manifiesto_carga | string | No | Campos aduana/terrestre |
| status | string | No | Estado |
| tag_list | array | No | Etiquetas |
Valores Permitidos
mode: land, aerial, maritime, customs
status: active, finished, closed, canceled
shipment_kind: national, international
shipment_type: cadena según modo (marítimo: fcl, lcl, …; terrestre: ltl, ftl, …; aéreo: std, eco, …). No es un enum cerrado de cuatro valores; debe coincidir con los tipos configurados en la aplicación.
Actualizar Servicio PUT
Definición
PUT /api/v1/services/:id
PATCH /api/v1/services/:id
Ejemplo de llamada
curl -X PUT "https://control.apunto.io/api/v1/services/789" \
-H "Authorization: Bearer TU_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"service": {
"status": "finished",
"eta_date": "2024-02-14"
}
}'
require 'uri'
require 'net/http'
require 'json'
uri = URI('https://control.apunto.io/api/v1/services/789')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Put.new(uri)
request['Authorization'] = 'Bearer TU_TOKEN'
request['Content-Type'] = 'application/json'
request.body = {
service: {
status: 'finished',
eta_date: '2024-02-14'
}
}.to_json
response = http.request(request)
puts response.body
import requests
import json
url = "https://control.apunto.io/api/v1/services/789"
headers = {
"Authorization": "Bearer TU_TOKEN",
"Content-Type": "application/json"
}
data = {
"service": {
"status": "finished",
"eta_date": "2024-02-14"
}
}
response = requests.put(url, headers=headers, data=json.dumps(data))
print(response.json())
fetch('https://control.apunto.io/api/v1/services/789', {
method: 'PUT',
headers: {
'Authorization': 'Bearer TU_TOKEN',
'Content-Type': 'application/json'
},
body: JSON.stringify({
service: {
status: 'finished',
eta_date: '2024-02-14'
}
})
})
.then(response => response.json())
.then(data => console.log(data));
Respuesta JSON
{
"service": {
"id": 789,
"identification": "SRV-001-2024",
"status": "finished",
"eta_date": "2024-02-14",
"updated_at": "2024-01-16T10:45:00Z"
},
"message": "Servicio actualizado exitosamente"
}
Actualiza un servicio existente.
Eliminar Servicio DELETE
Definición
DELETE /api/v1/services/:id
Ejemplo de llamada
curl -X DELETE "https://control.apunto.io/api/v1/services/789" \
-H "Authorization: Bearer TU_TOKEN"
Respuesta JSON
{
"message": "Servicio eliminado exitosamente"
}
Marca el servicio como eliminado (deleted_at); no borra físicamente el registro.
Finalizar servicio POST
Definición
POST /api/v1/services/:id/finish
Pasa el servicio de active a finished (misma acción que Finalizar en la web). Es el paso previo habitual antes de cerrar.
Cerrar servicio POST
Definición
POST /api/v1/services/:id/close
Cierra un servicio en estado finished (finished → closed). En servicios terrestres (mode: land) puede requerir tarea POD aprobada; si no, responde 422 con mensaje de POD.
Reabrir servicio POST
Definición
POST /api/v1/services/:id/reopen
Reabre un servicio en finished o closed y lo regresa a active.
Cancelar servicio POST
Definición
POST /api/v1/services/:id/cancel
Cancela un servicio (active o finished → canceled). Recalcula utilidad de la operación padre. Falla con 422 si hay facturas vinculadas que bloquean la cancelación.
Consulta motivos con GET /api/v1/service_cancellation_reasons (ver Motivos de cancelación).
Parámetros (body JSON, raíz)
| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
| canceled_at | date | No | Fecha de cancelación (default: hoy) |
| service_cancellation_reason_id | integer | Condicional | ID del motivo — obligatorio si la cuenta tiene motivos activos |
| service_cancellation_reason_name | string | Condicional | Alternativa al id: nombre del motivo |
| cancellation_notes | string | No | Notas adicionales |
Si la cuenta tiene motivos configurados, debes enviar id o name.
Respuesta JSON
{
"service": { "id": 789, "status": "canceled" },
"message": "Servicio cancelado exitosamente"
}
Carpetas de documentos
GET /api/v1/services/:service_id/folders
Comentarios de Servicio
Los comentarios están anidados bajo los servicios. Ver Comentarios para más detalles.
GET /api/v1/services/:service_id/messages
POST /api/v1/services/:service_id/messages
PUT /api/v1/services/:service_id/messages/:id
DELETE /api/v1/services/:service_id/messages/:id
Tareas de Servicio
Las tareas están anidadas bajo los servicios. Ver Tareas para más detalles.
GET /api/v1/services/:service_id/to_dos
POST /api/v1/services/:service_id/to_dos
PUT /api/v1/services/:service_id/to_dos/:id
POST /api/v1/services/:service_id/to_dos/:id/complete
DELETE /api/v1/services/:service_id/to_dos/:id
Centro de costos (CostCenter)
Las líneas del centro de costos registran ingresos y gastos por servicio. En el API se identifican como CostCenter (respuestas y rutas). Internamente Apunto las persiste como registros de ingreso/gasto por servicio.
Cada línea puede vincularse opcionalmente a cotización, factura de cliente o factura de proveedor.
Objeto CostCenter
| Atributo | Tipo | Descripción |
|---|---|---|
| id | integer | Identificador único |
| concept | string | Concepto / descripción |
| quantity | decimal | Cantidad |
| income_amount | decimal | Ingreso (subtotal de venta) |
| expense_amount | decimal | Gasto (subtotal de costo) |
| profit_amount | decimal | Utilidad (calculada) |
| profit_percentage | decimal | Margen % (calculado) |
| currency | object | Moneda del ingreso |
| expense_currency | object | Moneda del gasto |
| exchange_rate | decimal | Tipo de cambio ingreso |
| expense_exchange_rate | decimal | Tipo de cambio gasto |
| supplier | object | Proveedor del gasto |
| item | object | Producto/servicio SAT (opcional) |
| service | object | Servicio padre (en listados agregados) |
| linked | object | quote_line_item_id, invoice_line_item_id, bill_line_item_id |
| notes | string | Notas (detalle) |
| created_at / updated_at | datetime | Auditoría |
Listados devuelven { "cost_centers": [...], "pagination": {...} }. Show/create/update devuelven { "cost_center": { ... } }.
Listar líneas de un servicio GET
GET /api/v1/services/:service_id/cost_centers
Parámetros de consulta: page, per_page (máx. 100).
Listar líneas de una operación (agregado) GET
GET /api/v1/operations/:operation_id/cost_centers
Solo lectura: incluye todas las líneas de los servicios de la operación.
Crear línea POST
POST /api/v1/services/:service_id/cost_centers
{
"cost_center": {
"concept": "Flete marítimo",
"supplier_code": "MAERSK",
"currency_code": "MXN",
"income_amount": 15000,
"expense_amount": 9000,
"quantity": 1
}
}
También puedes usar supplier_id, currency_id, expense_currency_code, item_id, income_unit_price, expense_unit_price, notes.
Ver / actualizar / eliminar
GET /api/v1/services/:service_id/cost_centers/:id
PATCH /api/v1/services/:service_id/cost_centers/:id
DELETE /api/v1/services/:service_id/cost_centers/:id
DELETE es eliminación lógica (soft-delete).
Facturas y facturas de proveedor
Facturas de cliente (Invoice)
Captura básica para integraciones. No incluye timbrado CFDI por API; en cuentas mexicanas la creación exige campos CFDI para dejar el borrador listo para timbrar desde la web.
Endpoints principales
| Método | Ruta | Descripción |
|---|---|---|
| GET | /invoices |
Listado |
| GET | /invoices/:id |
Detalle (incluye line_items, CFDI, totales) |
| POST | /invoices |
Crear con line_items_attributes |
| PATCH | /invoices/:id |
Actualizar |
| DELETE | /invoices/:id |
Eliminar (libera vínculos del centro de costos) |
| POST | /invoices/:id/update_documents |
PDF/XML externos |
En cada line_item: link_cost_center_id, linked_to_cost_center.
Crear factura (cuenta MX)
{
"invoice": {
"contact_code": "ACME",
"currency_code": "MXN",
"description": "Servicios de flete",
"cfdi_use_code": "G03",
"payment_method_code": "01",
"payment_type_code": "PUE",
"line_items_attributes": [
{
"item_id": 456,
"description": "Flete marítimo",
"quantity": 1,
"price": 15000
}
]
}
}
Códigos alternativos: cfdi_use_id, payment_method_id, payment_type_id.
Vincular líneas de factura ↔ centro de costos
GET /invoices/:id/cost_center_link_options?operation_id=123— operaciones candidatas, líneas de venta disponibles (cost_centers) y líneas de factura sin vincular.POST /invoices/:id/link_cost_centers— el importe de la línea de factura debe coincidir con el ingreso de la línea del centro de costos (tolerancia $0.01).
{
"invoice_cost_center_link_form": {
"operation_id": 789,
"links": {
"321": "654"
}
}
}
links mapea line_item_id → cost_center_id.
DELETE /invoices/:id/line_items/:line_item_id/unlink_cost_center— desvincular.
Facturas de proveedor (Bill)
contact_code / contact_id es el proveedor. El API fuerza kind: expense. Sin CFDI ni validación XML por API.
| Método | Ruta | Descripción |
|---|---|---|
| GET | /bills |
Listado paginado |
| GET | /bills/:id |
Detalle con line_items |
| POST | /bills |
Crear |
| PATCH | /bills/:id |
Actualizar |
| DELETE | /bills/:id |
Eliminar |
Vinculación análoga a facturas de cliente, con bill_cost_center_link_form, endpoints cost_center_link_options / link_cost_centers / unlink_cost_center, y comparación contra el gasto (expense_amount) de la línea del centro de costos.
Generar borrador desde el centro de costos
Desde una operación, puedes crear un documento a partir de líneas seleccionadas (equivalente al botón «Generar factura» en la web). La API persiste el borrador de inmediato (a diferencia de la web, que solo abre el formulario de revisión).
Generar factura de cliente
POST /api/v1/operations/:id/generate_invoice
{
"cost_center_ids": [654, 655],
"cfdi_use_code": "G03",
"payment_method_code": "01",
"payment_type_code": "PUE"
}
Respuesta 201 con resumen de la factura en borrador (status: drafted).
Generar factura de proveedor
POST /api/v1/operations/:id/generate_bill
{
"cost_center_ids": [654]
}
Las bills creadas por API suelen quedar en estado opened tras el guardado (regla de negocio del modelo Bill).
Documentos (carpetas y archivos)
Los documentos de una operación o servicio se organizan en carpetas (AttachmentFolder) con archivos adjuntos (Active Storage). Puedes listarlos, subirlos, renombrarlos y eliminarlos vía API.
Objeto Folder (árbol)
| Atributo | Tipo | Descripción |
|---|---|---|
| id | integer | ID de la carpeta |
| name | string | Nombre |
| parent_id | integer | Carpeta padre (null = raíz) |
| files_count | integer | Número de archivos directos |
| attachments | array | Archivos en esta carpeta (ver abajo) |
| children | array | Subcarpetas (misma estructura, recursivo) |
Objeto Attachment (archivo)
| Atributo | Tipo | Descripción |
|---|---|---|
| id | integer | ID del adjunto (ActiveStorage::Attachment) — usar para PATCH/DELETE |
| blob_id | integer | ID del blob |
| filename | string | Nombre del archivo |
| byte_size | integer | Tamaño en bytes |
| content_type | string | MIME type |
| created_at | datetime | Fecha de subida |
| url | string | URL firmada/temporal para descarga (requiere mismo token/host) |
Listar carpetas y archivos GET
Definición
GET /api/v1/operations/:operation_id/folders
GET /api/v1/services/:service_id/folders
Ejemplo
curl "https://control.apunto.io/api/v1/operations/123/folders" \
-H "Authorization: Bearer TU_TOKEN"
Respuesta JSON
{
"folderable": { "type": "Operation", "id": 123 },
"folders": [
{
"id": 10,
"name": "BL",
"parent_id": null,
"files_count": 1,
"attachments": [
{
"id": 501,
"blob_id": 9001,
"filename": "bl.pdf",
"byte_size": 245000,
"content_type": "application/pdf",
"created_at": "2024-01-15T12:00:00Z",
"url": "https://control.apunto.io/rails/active_storage/blobs/redirect/..."
}
],
"children": []
}
]
}
Crear carpeta POST
Definición
POST /api/v1/operations/:operation_id/folders
POST /api/v1/services/:service_id/folders
Parámetros (folder)
| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
| name | string | Sí | Nombre de la carpeta |
| parent_id | integer | No | ID de carpeta padre (misma operación/servicio) |
{
"folder": {
"name": "Documentos aduana",
"parent_id": 10
}
}
Subir archivo POST
Definición
POST /api/v1/attachments
Content-Type: multipart/form-data
Campos multipart
| Campo | Requerido | Descripción |
|---|---|---|
| file | Sí | Archivo (máx. 25 MB) |
| folderable_type | Sí | Operation o Service |
| folderable_id | Sí | ID de la operación o servicio |
| parent_folder_id | No | ID de carpeta destino; si se omite, usa carpeta raíz Gmail |
| source_url | No | URL de origen (metadata) |
| source_message | No | Contexto (metadata) |
Ejemplo cURL
curl -X POST "https://control.apunto.io/api/v1/attachments" \
-H "Authorization: Bearer TU_TOKEN" \
-F "folderable_type=Operation" \
-F "folderable_id=123" \
-F "parent_folder_id=10" \
-F "file=@/ruta/al/archivo.pdf"
Respuesta (201)
{
"success": true,
"attachment": {
"id": 501,
"filename": "archivo.pdf",
"folder_id": 10,
"folder_name": "BL",
"folderable_type": "Operation",
"folderable_id": 123
},
"message": "Archivo subido exitosamente"
}
Renombrar archivo PATCH
Definición
PATCH /api/v1/attachments/:id
{
"filename": "bl-final.pdf"
}
Eliminar archivo DELETE
Definición
DELETE /api/v1/attachments/:id
Elimina el adjunto del storage (purge). El :id es el id del attachment devuelto en listados, no el blob_id.
Tareas en operaciones y servicios
Las tareas tienen CRUD anidado. Ver Tareas (To-Dos).
Resumen de rutas bajo operación:
GET /api/v1/operations/:operation_id/to_dos
POST /api/v1/operations/:operation_id/to_dos
GET /api/v1/operations/:operation_id/to_dos/:id
PATCH /api/v1/operations/:operation_id/to_dos/:id
POST /api/v1/operations/:operation_id/to_dos/:id/complete
DELETE /api/v1/operations/:operation_id/to_dos/:id
El show de operación incluye hasta 100 tareas recientes en to_dos[]; use el listado paginado para conjuntos grandes.
Motivos de cancelación
Catálogos configurados por cuenta (mismos valores que el modal web de cancelación). Úsalos para obtener el id o enviar el name al cancelar operaciones o servicios.
Objeto CancellationReason
| Atributo | Tipo | Descripción |
|---|---|---|
| id | integer | ID interno (usar en *_cancellation_reason_id) |
| name | string | Etiqueta visible (usar en *_cancellation_reason_name) |
| description | string | Descripción opcional |
| status | string | active (solo se listan activos) |
Listar motivos — operaciones GET
GET /api/v1/operation_cancellation_reasons
Ejemplo
curl "https://control.apunto.io/api/v1/operation_cancellation_reasons" \
-H "Authorization: Bearer TU_TOKEN"
Respuesta JSON
{
"operation_cancellation_reasons": [
{
"id": 3,
"name": "Cliente canceló",
"description": null,
"status": "active"
},
{
"id": 5,
"name": "Ya no se requiere el servicio",
"description": null,
"status": "active"
}
]
}
Listar motivos — servicios GET
GET /api/v1/service_cancellation_reasons
Respuesta JSON
{
"service_cancellation_reasons": [
{
"id": 2,
"name": "Proveedor no disponible",
"description": null,
"status": "active"
}
]
}
Uso al cancelar
Operación — POST /api/v1/operations/:id/cancel
| Parámetro | Requerido | Descripción |
|---|---|---|
| canceled_at | No | Fecha (default: hoy). En la web es obligatoria en el formulario. |
| operation_cancellation_reason_id | Condicional | ID del catálogo (ver GET arriba) |
| operation_cancellation_reason_name | Condicional | Nombre exacto del motivo (alternativa al id; no sensible a mayúsculas) |
| cancellation_notes | No | Notas adicionales |
Enviar id o name, no ambos obligatorios. Si envías un id/name inválido → 422.
{
"operation_cancellation_reason_name": "Cliente canceló",
"cancellation_notes": "El cliente desistió del embarque"
}
Servicio — POST /api/v1/services/:id/cancel
| Parámetro | Requerido | Descripción |
|---|---|---|
| canceled_at | No | Fecha (default: hoy) |
| service_cancellation_reason_id | Condicional | ID del catálogo |
| service_cancellation_reason_name | Condicional | Nombre del motivo |
| cancellation_notes | No | Notas adicionales |
{
"service_cancellation_reason_id": 2,
"cancellation_notes": "Proveedor declinó"
}
Flujo recomendado para integraciones
GET /api/v1/operation_cancellation_reasons(o servicios) al iniciar sesión o cachear por cuenta.- Mostrar al usuario la lista de
name(como el modal web). - Al cancelar, enviar
operation_cancellation_reason_namecon el texto elegido o elidobtenido del listado.
Los motivos se administran en la web en Configuración → Motivos de cancelación (operación / servicio); la API solo lee el catálogo activo.
Contactos
Los contactos representan clientes, proveedores, prospectos y otros participantes en las operaciones de freight forwarding.
Objeto Contact
{
"id": 456,
"name": "ABC Trading Company",
"alias": "ABC",
"identification": "ABC1234567890",
"legal_name": "ABC Trading Company S.A. de C.V.",
"kind": ["client", "supplier"],
"services": ["maritime", "aerial"],
"email": "contacto@abc.com",
"phone": "+52 55 1234 5678",
"status": "active",
"billing_address": {
"id": 101,
"street": "Av. Reforma 123",
"city": "Ciudad de México",
"postal_code": "06600",
"country": "MX"
},
"tags": ["prioritario"],
"created_at": "2024-01-10T09:00:00Z"
}
Atributos
| Atributo | Tipo | Descripción |
|---|---|---|
| id | integer | Identificador único |
| name | string | Nombre del contacto |
| alias | string | Alias corto (sin espacios) |
| identification | string | RFC o identificación fiscal |
| legal_name | string | Razón social legal |
| kind | array | Tipos: client, supplier, prospect, carrier, customs_agent |
| services | array | Servicios: maritime, aerial, land, customs |
| string | Correo electrónico | |
| phone | string | Teléfono |
| status | string | Estado: active, inactive |
| billing_address | object | Dirección de facturación |
| tags | array | Etiquetas del contacto |
Listar Contactos GET
curl "https://control.apunto.io/api/v1/contacts" \
-H "Authorization: Bearer TU_TOKEN_API"
uri = URI.parse("https://control.apunto.io/api/v1/contacts")
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer TU_TOKEN_API"
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(request)
end
import requests
headers = {'Authorization': 'Bearer TU_TOKEN_API'}
response = requests.get(
'https://control.apunto.io/api/v1/contacts',
headers=headers
)
axios.get('https://control.apunto.io/api/v1/contacts', {
headers: { 'Authorization': 'Bearer TU_TOKEN_API' }
})
.then(response => console.log(response.data));
Respuesta:
{
"contacts": [
{
"id": 456,
"name": "ABC Trading Company",
"alias": "ABC",
"kind": ["client"],
"status": "active"
}
],
"pagination": {
"current_page": 1,
"total_pages": 3,
"total_count": 30
}
}
Obtiene una lista de todos los contactos de la cuenta autenticada.
Petición HTTP
GET /api/v1/contacts
Parámetros Query
| Parámetro | Tipo | Por Defecto | Descripción |
|---|---|---|---|
| page | integer | 1 | Número de página |
| per_page | integer | 25 | Registros por página |
| kind | string | null | Filtrar por tipo: client, supplier, prospect |
| status | string | all | Filtrar por estado |
| search | string | null | Búsqueda por nombre, alias o identificación |
Obtener un Contacto Específico
curl "https://control.apunto.io/api/v1/contacts/456" \
-H "Authorization: Bearer TU_TOKEN_API"
Respuesta:
{
"id": 456,
"name": "ABC Trading Company",
"alias": "ABC",
"identification": "ABC1234567890",
"kind": ["client", "supplier"],
"services": ["maritime", "aerial"],
"email": "contacto@abc.com",
"phone": "+52 55 1234 5678",
"billing_address": {
"street": "Av. Reforma 123",
"city": "Ciudad de México"
}
}
Obtiene los detalles de un contacto específico.
Petición HTTP
GET /api/v1/contacts/:id
Crear un Contacto POST
curl -X POST "https://control.apunto.io/api/v1/contacts" \
-H "Authorization: Bearer TU_TOKEN_API" \
-H "Content-Type: application/json" \
-d '{
"contact": {
"name": "XYZ Logistics",
"alias": "XYZ",
"kind": ["client"],
"email": "info@xyz.com",
"phone": "+52 55 9876 5432",
"billing_address_attributes": {
"street": "Calle Principal 456",
"city": "Monterrey",
"state": "Nuevo León",
"postal_code": "64000",
"country": "MX",
"address_type": "billing"
}
}
}'
require 'uri'
require 'net/http'
require 'json'
uri = URI('https://control.apunto.io/api/v1/contacts')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Post.new(uri)
request['Authorization'] = 'Bearer TU_TOKEN_API'
request['Content-Type'] = 'application/json'
request.body = {
contact: {
name: 'XYZ Logistics',
alias: 'XYZ',
kind: ['client'],
email: 'info@xyz.com'
}
}.to_json
response = http.request(request)
puts response.body
payload = {
'contact': {
'name': 'XYZ Logistics',
'alias': 'XYZ',
'kind': ['client'],
'email': 'info@xyz.com',
'phone': '+52 55 9876 5432',
'billing_address_attributes': {
'street': 'Calle Principal 456',
'city': 'Monterrey',
'postal_code': '64000',
'country': 'MX'
}
}
}
response = requests.post(
'https://control.apunto.io/api/v1/contacts',
headers=headers,
json=payload
)
fetch('https://control.apunto.io/api/v1/contacts', {
method: 'POST',
headers: {
'Authorization': 'Bearer TU_TOKEN_API',
'Content-Type': 'application/json'
},
body: JSON.stringify({
contact: {
name: 'XYZ Logistics',
alias: 'XYZ',
kind: ['client'],
email: 'info@xyz.com'
}
})
})
.then(response => response.json())
.then(data => console.log(data));
Respuesta:
{
"id": 457,
"name": "XYZ Logistics",
"alias": "XYZ",
"kind": ["client"],
"status": "active",
"created_at": "2024-01-16T10:00:00Z"
}
Crea un nuevo contacto.
Petición HTTP
POST /api/v1/contacts
Parámetros del Body
| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
| name | string | Sí | Nombre del contacto |
| alias | string | Sí | Alias corto (sin espacios) |
| kind | array | Sí | Tipos de contacto |
| identification | string | No | RFC o identificación fiscal |
| legal_name | string | No | Razón social |
| string | No | Correo electrónico | |
| phone | string | No | Teléfono |
| services | array | No | Servicios que ofrece/requiere |
| billing_address_attributes | object | No | Dirección de facturación |
Actualizar un Contacto PATCH
curl -X PATCH "https://control.apunto.io/api/v1/contacts/456" \
-H "Authorization: Bearer TU_TOKEN_API" \
-H "Content-Type: application/json" \
-d '{
"contact": {
"email": "nuevo@abc.com",
"phone": "+52 55 1111 2222",
"tags": ["vip", "prioritario"]
}
}'
require 'uri'
require 'net/http'
require 'json'
uri = URI('https://control.apunto.io/api/v1/contacts/456')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Patch.new(uri)
request['Authorization'] = 'Bearer TU_TOKEN_API'
request['Content-Type'] = 'application/json'
request.body = {
contact: {
email: 'nuevo@abc.com',
phone: '+52 55 1111 2222'
}
}.to_json
response = http.request(request)
puts response.body
import requests
import json
url = "https://control.apunto.io/api/v1/contacts/456"
headers = {
"Authorization": "Bearer TU_TOKEN_API",
"Content-Type": "application/json"
}
data = {
"contact": {
"email": "nuevo@abc.com",
"phone": "+52 55 1111 2222"
}
}
response = requests.patch(url, headers=headers, data=json.dumps(data))
print(response.json())
fetch('https://control.apunto.io/api/v1/contacts/456', {
method: 'PATCH',
headers: {
'Authorization': 'Bearer TU_TOKEN_API',
'Content-Type': 'application/json'
},
body: JSON.stringify({
contact: {
email: 'nuevo@abc.com',
phone: '+52 55 1111 2222'
}
})
})
.then(response => response.json())
.then(data => console.log(data));
Respuesta:
{
"id": 456,
"name": "ABC Trading Company",
"email": "nuevo@abc.com",
"phone": "+52 55 1111 2222",
"updated_at": "2024-01-16T11:00:00Z"
}
Actualiza un contacto existente.
Petición HTTP
PATCH /api/v1/contacts/:id
Parámetros URL
| Parámetro | Descripción |
|---|---|
| id | El ID del contacto a actualizar |
Búsqueda Rápida de Contactos GET
curl "https://control.apunto.io/api/v1/contacts/search?q=ABC" \
-H "Authorization: Bearer TU_TOKEN_API"
Busca contactos por nombre, alias o identificación.
Petición HTTP
GET /api/v1/contacts/search
Parámetros Query
| Parámetro | Tipo | Descripción |
|---|---|---|
| q | string | Término de búsqueda |
| kind | string | Filtrar por tipo |
| limit | integer | Número máximo de resultados (por defecto: 10) |
Direcciones
Las direcciones representan ubicaciones físicas para embarques, facturación, aduanas, puertos y aeropuertos.
Objeto Address
{
"id": 101,
"name": "Almacén Principal CDMX",
"alias": "ALM-CDMX",
"address_type": "shipping",
"status": "active",
"street": "Av. Insurgentes Sur 1234",
"outdoor_number": "1234",
"internal_number": "Int. 5",
"neighborhood": "Col. Del Valle",
"city": "Ciudad de México",
"state": "Ciudad de México",
"postal_code": "03100",
"country": "MX",
"description": "Almacén de recepción y distribución",
"contact_information": "Juan Pérez - Tel: 55-1234-5678",
"created_at": "2024-01-10T09:00:00Z"
}
Atributos
| Atributo | Tipo | Descripción |
|---|---|---|
| id | integer | Identificador único |
| name | string | Nombre descriptivo de la ubicación |
| alias | string | Alias corto para referencia rápida |
| address_type | string | Tipo: billing, shipping, port, customs, airport, general |
| status | string | Estado: active, inactive |
| street | string | Calle |
| outdoor_number | string | Número exterior |
| internal_number | string | Número interior |
| neighborhood | string | Colonia o barrio |
| city | string | Ciudad |
| state | string | Estado o provincia |
| postal_code | string | Código postal |
| country | string | Código de país (ISO 3166-1 alpha-2) |
| description | string | Descripción adicional |
| contact_information | string | Información de contacto en la ubicación |
Listar Direcciones GET
curl "https://control.apunto.io/api/v1/addresses" \
-H "Authorization: Bearer TU_TOKEN_API"
uri = URI.parse("https://control.apunto.io/api/v1/addresses")
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer TU_TOKEN_API"
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(request)
end
import requests
headers = {'Authorization': 'Bearer TU_TOKEN_API'}
response = requests.get(
'https://control.apunto.io/api/v1/addresses',
headers=headers
)
axios.get('https://control.apunto.io/api/v1/addresses', {
headers: { 'Authorization': 'Bearer TU_TOKEN_API' }
})
.then(response => console.log(response.data));
Respuesta:
{
"addresses": [
{
"id": 101,
"name": "Almacén Principal CDMX",
"alias": "ALM-CDMX",
"address_type": "shipping",
"city": "Ciudad de México",
"status": "active"
}
],
"pagination": {
"current_page": 1,
"total_pages": 2,
"total_count": 20
}
}
Obtiene una lista de todas las direcciones de la cuenta autenticada.
Petición HTTP
GET /api/v1/addresses
Parámetros Query
| Parámetro | Tipo | Por Defecto | Descripción |
|---|---|---|---|
| page | integer | 1 | Número de página |
| per_page | integer | 25 | Registros por página |
| address_type | string | all | Filtrar por tipo de dirección |
| status | string | all | Filtrar por estado |
| city | string | null | Filtrar por ciudad |
| country | string | null | Filtrar por país |
| search | string | null | Búsqueda por nombre o alias |
Obtener una Dirección Específica GET
curl "https://control.apunto.io/api/v1/addresses/101" \
-H "Authorization: Bearer TU_TOKEN_API"
Respuesta:
{
"id": 101,
"name": "Almacén Principal CDMX",
"alias": "ALM-CDMX",
"address_type": "shipping",
"street": "Av. Insurgentes Sur 1234",
"outdoor_number": "1234",
"neighborhood": "Col. Del Valle",
"city": "Ciudad de México",
"state": "Ciudad de México",
"postal_code": "03100",
"country": "MX",
"full_address": "Av. Insurgentes Sur 1234, Col. Del Valle, 03100, Ciudad de México, MX"
}
Obtiene los detalles de una dirección específica.
Petición HTTP
GET /api/v1/addresses/:id
Crear una Dirección POST
curl -X POST "https://control.apunto.io/api/v1/addresses" \
-H "Authorization: Bearer TU_TOKEN_API" \
-H "Content-Type: application/json" \
-d '{
"address": {
"name": "Puerto de Veracruz",
"alias": "VER-PORT",
"address_type": "port",
"street": "Av. Marina Mercante",
"outdoor_number": "S/N",
"neighborhood": "Puerto de Veracruz",
"city": "Veracruz",
"state": "Veracruz",
"postal_code": "91700",
"country": "MX",
"description": "Terminal de contenedores"
}
}'
payload = {
'address': {
'name': 'Puerto de Veracruz',
'alias': 'VER-PORT',
'address_type': 'port',
'street': 'Av. Marina Mercante',
'city': 'Veracruz',
'state': 'Veracruz',
'postal_code': '91700',
'country': 'MX'
}
}
response = requests.post(
'https://control.apunto.io/api/v1/addresses',
headers=headers,
json=payload
)
const data = {
address: {
name: 'Puerto de Veracruz',
alias: 'VER-PORT',
address_type: 'port',
street: 'Av. Marina Mercante',
city: 'Veracruz',
postal_code: '91700',
country: 'MX'
}
};
axios.post('https://control.apunto.io/api/v1/addresses', data, {
headers: {
'Authorization': 'Bearer TU_TOKEN_API',
'Content-Type': 'application/json'
}
});
Respuesta:
{
"id": 102,
"name": "Puerto de Veracruz",
"alias": "VER-PORT",
"address_type": "port",
"city": "Veracruz",
"status": "active",
"created_at": "2024-01-16T10:30:00Z"
}
Crea una nueva dirección.
Petición HTTP
POST /api/v1/addresses
Parámetros del Body
| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
| name | string | Sí | Nombre de la ubicación |
| address_type | string | Sí | Tipo de dirección |
| street | string | Sí | Calle |
| neighborhood | string | Sí | Colonia |
| city | string | Sí | Ciudad |
| state | string | Sí | Estado |
| postal_code | string | Sí | Código postal |
| country | string | Sí | Código de país (2 letras, ej: MX, US) |
| alias | string | No | Alias corto |
| outdoor_number | string | No | Número exterior |
| internal_number | string | No | Número interior |
| description | string | No | Descripción (máx. 255 caracteres) |
| contact_information | string | No | Contacto (máx. 255 caracteres) |
Actualizar una Dirección PATCH
curl -X PATCH "https://control.apunto.io/api/v1/addresses/101" \
-H "Authorization: Bearer TU_TOKEN_API" \
-H "Content-Type: application/json" \
-d '{
"address": {
"contact_information": "Ana García - Tel: 55-9999-8888",
"description": "Almacén actualizado con nuevo contacto",
"status": "active"
}
}'
require 'uri'
require 'net/http'
require 'json'
uri = URI('https://control.apunto.io/api/v1/addresses/101')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Patch.new(uri)
request['Authorization'] = 'Bearer TU_TOKEN_API'
request['Content-Type'] = 'application/json'
request.body = {
address: {
contact_information: 'Ana García - Tel: 55-9999-8888',
description: 'Almacén actualizado con nuevo contacto'
}
}.to_json
response = http.request(request)
puts response.body
import requests
import json
url = "https://control.apunto.io/api/v1/addresses/101"
headers = {
"Authorization": "Bearer TU_TOKEN_API",
"Content-Type": "application/json"
}
data = {
"address": {
"contact_information": "Ana García - Tel: 55-9999-8888",
"description": "Almacén actualizado con nuevo contacto"
}
}
response = requests.patch(url, headers=headers, data=json.dumps(data))
print(response.json())
fetch('https://control.apunto.io/api/v1/addresses/101', {
method: 'PATCH',
headers: {
'Authorization': 'Bearer TU_TOKEN_API',
'Content-Type': 'application/json'
},
body: JSON.stringify({
address: {
contact_information: 'Ana García - Tel: 55-9999-8888',
description: 'Almacén actualizado con nuevo contacto'
}
})
})
.then(response => response.json())
.then(data => console.log(data));
Respuesta:
{
"id": 101,
"name": "Almacén Principal CDMX",
"contact_information": "Ana García - Tel: 55-9999-8888",
"updated_at": "2024-01-16T11:30:00Z"
}
Actualiza una dirección existente.
Petición HTTP
PATCH /api/v1/addresses/:id
Parámetros URL
| Parámetro | Descripción |
|---|---|
| id | El ID de la dirección a actualizar |
Tipos de Dirección
| Tipo | Descripción | Uso Común |
|---|---|---|
| billing | Facturación | Dirección fiscal del contacto |
| shipping | Embarque | Origen/destino de mercancía |
| port | Puerto | Terminal marítima |
| customs | Aduana | Recinto aduanero |
| airport | Aeropuerto | Terminal aérea |
| general | General | Uso múltiple |
Búsqueda de Direcciones
curl "https://control.apunto.io/api/v1/addresses/search?q=Puerto&type=port" \
-H "Authorization: Bearer TU_TOKEN_API"
Busca direcciones por nombre, alias o ciudad.
Petición HTTP
GET /api/v1/addresses/search
Parámetros Query
| Parámetro | Tipo | Descripción |
|---|---|---|
| q | string | Término de búsqueda |
| type | string | Filtrar por tipo de dirección |
| country | string | Filtrar por país |
| limit | integer | Número máximo de resultados (por defecto: 10) |
Comentarios (Messages)
Los comentarios permiten agregar notas, comunicaciones y actualizaciones a operaciones, servicios y contactos. Están anidados bajo sus recursos padre.
Objeto Message
Atributos Principales
| Atributo | Tipo | Descripción |
|---|---|---|
| id | integer | Identificador único |
| content | text | Contenido del comentario |
| owner | object | Usuario que creó el comentario |
| edited | boolean | Indica si el comentario fue editado |
| created_at | datetime | Fecha de creación |
| updated_at | datetime | Fecha de última actualización |
Listar Comentarios GET
Definición
GET /api/v1/operations/:operation_id/messages
GET /api/v1/services/:service_id/messages
GET /api/v1/contacts/:contact_id/messages
Ejemplo de llamada
# Comentarios de una operación
curl "https://control.apunto.io/api/v1/operations/123/messages" \
-H "Authorization: Bearer TU_TOKEN"
# Comentarios de un servicio
curl "https://control.apunto.io/api/v1/services/789/messages" \
-H "Authorization: Bearer TU_TOKEN"
require 'uri'
require 'net/http'
uri = URI('https://control.apunto.io/api/v1/operations/123/messages')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Get.new(uri)
request['Authorization'] = 'Bearer TU_TOKEN'
response = http.request(request)
puts response.body
import requests
url = "https://control.apunto.io/api/v1/operations/123/messages"
headers = {"Authorization": "Bearer TU_TOKEN"}
response = requests.get(url, headers=headers)
print(response.json())
fetch('https://control.apunto.io/api/v1/operations/123/messages', {
headers: {'Authorization': 'Bearer TU_TOKEN'}
})
.then(response => response.json())
.then(data => console.log(data));
Respuesta JSON
{
"messages": [
{
"id": 456,
"content": "Cliente confirmó recepción de mercancía",
"owner": {
"email": "usuario@apunto.com",
"name": "Juan Pérez"
},
"edited": false,
"created_at": "2024-01-16T14:30:00Z",
"updated_at": "2024-01-16T14:30:00Z"
}
],
"pagination": {
"page": 1,
"per_page": 25,
"total": 12
}
}
Retorna todos los comentarios de un recurso específico.
Parámetros Query
| Parámetro | Descripción |
|---|---|
| page | Número de página (default: 1) |
| per_page | Registros por página (default: 25, max: 100) |
Obtener un Comentario GET
Definición
GET /api/v1/operations/:operation_id/messages/:id
GET /api/v1/services/:service_id/messages/:id
GET /api/v1/contacts/:contact_id/messages/:id
Ejemplo de llamada
curl "https://control.apunto.io/api/v1/operations/123/messages/456" \
-H "Authorization: Bearer TU_TOKEN"
Respuesta JSON
{
"message": {
"id": 456,
"content": "Cliente confirmó recepción de mercancía",
"owner": {
"email": "usuario@apunto.com",
"name": "Juan Pérez"
},
"edited": false,
"created_at": "2024-01-16T14:30:00Z",
"updated_at": "2024-01-16T14:30:00Z"
}
}
Retorna un comentario específico.
Crear Comentario POST
Definición
POST /api/v1/operations/:operation_id/messages
POST /api/v1/services/:service_id/messages
POST /api/v1/contacts/:contact_id/messages
Ejemplo de llamada
curl -X POST "https://control.apunto.io/api/v1/operations/123/messages" \
-H "Authorization: Bearer TU_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"message": {
"content": "Cliente solicitó actualización de ETA"
}
}'
require 'uri'
require 'net/http'
require 'json'
uri = URI('https://control.apunto.io/api/v1/operations/123/messages')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Post.new(uri)
request['Authorization'] = 'Bearer TU_TOKEN'
request['Content-Type'] = 'application/json'
request.body = {
message: {
content: 'Cliente solicitó actualización de ETA'
}
}.to_json
response = http.request(request)
puts response.body
import requests
import json
url = "https://control.apunto.io/api/v1/operations/123/messages"
headers = {
"Authorization": "Bearer TU_TOKEN",
"Content-Type": "application/json"
}
data = {
"message": {
"content": "Cliente solicitó actualización de ETA"
}
}
response = requests.post(url, headers=headers, data=json.dumps(data))
print(response.json())
fetch('https://control.apunto.io/api/v1/operations/123/messages', {
method: 'POST',
headers: {
'Authorization': 'Bearer TU_TOKEN',
'Content-Type': 'application/json'
},
body: JSON.stringify({
message: {
content: 'Cliente solicitó actualización de ETA'
}
})
})
.then(response => response.json())
.then(data => console.log(data));
Respuesta JSON (201 Created)
{
"message": {
"id": 457,
"content": "Cliente solicitó actualización de ETA",
"owner": {
"email": "usuario@apunto.com",
"name": "Juan Pérez"
},
"edited": false,
"created_at": "2024-01-16T15:00:00Z",
"updated_at": "2024-01-16T15:00:00Z"
}
}
Crea un nuevo comentario en el recurso especificado.
Parámetros
| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
| content | text | Sí | Contenido del comentario |
Actualizar Comentario PUT
Definición
PUT /api/v1/operations/:operation_id/messages/:id
PUT /api/v1/services/:service_id/messages/:id
PUT /api/v1/contacts/:contact_id/messages/:id
Ejemplo de llamada
curl -X PUT "https://control.apunto.io/api/v1/operations/123/messages/456" \
-H "Authorization: Bearer TU_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"message": {
"content": "Cliente confirmó recepción de mercancía [EDITADO]"
}
}'
Respuesta JSON
{
"message": {
"id": 456,
"content": "Cliente confirmó recepción de mercancía [EDITADO]",
"edited": true,
"updated_at": "2024-01-16T15:30:00Z"
}
}
Actualiza un comentario existente.
Eliminar Comentario DELETE
Definición
DELETE /api/v1/operations/:operation_id/messages/:id
DELETE /api/v1/services/:service_id/messages/:id
DELETE /api/v1/contacts/:contact_id/messages/:id
Ejemplo de llamada
curl -X DELETE "https://control.apunto.io/api/v1/operations/123/messages/456" \
-H "Authorization: Bearer TU_TOKEN"
Respuesta JSON
{
"message": "Comentario eliminado exitosamente"
}
Elimina un comentario.
Ejemplo de Contexto por URL
Cuando llamas a GET /api/v1/operations/123/messages, ya sabes que:
- messageable_type = "Operation"
- messageable_id = 123
Por lo tanto, estos campos no se incluyen en la respuesta JSON.
Tareas (To-Dos)
Las tareas permiten crear seguimiento y recordatorios asociados a operaciones, servicios y contactos. Están anidados bajo sus recursos padre.
Objeto ToDo
Atributos Principales
| Atributo | Tipo | Descripción |
|---|---|---|
| id | integer | Identificador único |
| title | string | Título de la tarea |
| description | text | Descripción detallada |
| start_at | datetime | Fecha de inicio |
| end_at | datetime | Fecha de fin |
| completed | boolean | Estado de completado |
| rejected | boolean | Si la tarea fue rechazada |
| required | boolean | Si la tarea es requerida |
| completed_at | datetime | Fecha de completado |
| owner | object | Usuario asignado a la tarea |
| completed_by | object | Usuario que completó la tarea |
| created_at | datetime | Fecha de creación |
| updated_at | datetime | Fecha de última actualización |
Listar Tareas GET
Definición
GET /api/v1/operations/:operation_id/to_dos
GET /api/v1/services/:service_id/to_dos
GET /api/v1/contacts/:contact_id/to_dos
Ejemplo de llamada
# Tareas de una operación
curl "https://control.apunto.io/api/v1/operations/123/to_dos" \
-H "Authorization: Bearer TU_TOKEN"
# Tareas de un servicio
curl "https://control.apunto.io/api/v1/services/789/to_dos" \
-H "Authorization: Bearer TU_TOKEN"
require 'uri'
require 'net/http'
uri = URI('https://control.apunto.io/api/v1/operations/123/to_dos')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Get.new(uri)
request['Authorization'] = 'Bearer TU_TOKEN'
response = http.request(request)
puts response.body
import requests
url = "https://control.apunto.io/api/v1/operations/123/to_dos"
headers = {"Authorization": "Bearer TU_TOKEN"}
response = requests.get(url, headers=headers)
print(response.json())
fetch('https://control.apunto.io/api/v1/operations/123/to_dos', {
headers: {'Authorization': 'Bearer TU_TOKEN'}
})
.then(response => response.json())
.then(data => console.log(data));
Respuesta JSON
{
"to_dos": [
{
"id": 321,
"title": "Solicitar documentos al cliente",
"start_at": "2024-01-15T10:00:00Z",
"end_at": "2024-01-20T18:00:00Z",
"completed": false,
"rejected": false,
"required": true,
"completed_at": null,
"owner": {
"email": "operador@apunto.com",
"name": "Carlos Ramírez"
},
"completed_by": null,
"created_at": "2024-01-15T10:00:00Z",
"updated_at": "2024-01-15T10:00:00Z"
}
],
"pagination": {
"page": 1,
"per_page": 25,
"total": 8
}
}
Retorna todas las tareas de un recurso específico.
Parámetros Query
| Parámetro | Descripción |
|---|---|
| page | Número de página (default: 1) |
| per_page | Registros por página (default: 25, max: 100) |
| completed | Filtrar por estado: true, false |
Obtener una Tarea GET
Definición
GET /api/v1/operations/:operation_id/to_dos/:id
GET /api/v1/services/:service_id/to_dos/:id
GET /api/v1/contacts/:contact_id/to_dos/:id
Ejemplo de llamada
curl "https://control.apunto.io/api/v1/operations/123/to_dos/321" \
-H "Authorization: Bearer TU_TOKEN"
Respuesta JSON
{
"to_do": {
"id": 321,
"title": "Solicitar documentos al cliente",
"start_at": "2024-01-15T10:00:00Z",
"end_at": "2024-01-20T18:00:00Z",
"completed": false,
"rejected": false,
"required": true,
"completed_at": null,
"owner": {
"email": "operador@apunto.com",
"name": "Carlos Ramírez"
},
"completed_by": null,
"created_at": "2024-01-15T10:00:00Z",
"updated_at": "2024-01-15T10:00:00Z"
}
}
Retorna una tarea específica.
Crear Tarea POST
Definición
POST /api/v1/operations/:operation_id/to_dos
POST /api/v1/services/:service_id/to_dos
POST /api/v1/contacts/:contact_id/to_dos
Ejemplo de llamada
curl -X POST "https://control.apunto.io/api/v1/operations/123/to_dos" \
-H "Authorization: Bearer TU_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"to_do": {
"title": "Revisar documentación aduanal",
"description": "Verificar que todos los documentos estén completos",
"start_at": "2024-01-20T09:00:00Z",
"end_at": "2024-01-25T18:00:00Z",
"required": true
}
}'
require 'uri'
require 'net/http'
require 'json'
uri = URI('https://control.apunto.io/api/v1/operations/123/to_dos')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Post.new(uri)
request['Authorization'] = 'Bearer TU_TOKEN'
request['Content-Type'] = 'application/json'
request.body = {
to_do: {
title: 'Revisar documentación aduanal',
description: 'Verificar que todos los documentos estén completos',
start_at: '2024-01-20T09:00:00Z',
end_at: '2024-01-25T18:00:00Z',
required: true
}
}.to_json
response = http.request(request)
puts response.body
import requests
import json
url = "https://control.apunto.io/api/v1/operations/123/to_dos"
headers = {
"Authorization": "Bearer TU_TOKEN",
"Content-Type": "application/json"
}
data = {
"to_do": {
"title": "Revisar documentación aduanal",
"description": "Verificar que todos los documentos estén completos",
"start_at": "2024-01-20T09:00:00Z",
"end_at": "2024-01-25T18:00:00Z",
"required": true
}
}
response = requests.post(url, headers=headers, data=json.dumps(data))
print(response.json())
fetch('https://control.apunto.io/api/v1/operations/123/to_dos', {
method: 'POST',
headers: {
'Authorization': 'Bearer TU_TOKEN',
'Content-Type': 'application/json'
},
body: JSON.stringify({
to_do: {
title: 'Revisar documentación aduanal',
description: 'Verificar que todos los documentos estén completos',
start_at: '2024-01-20T09:00:00Z',
end_at: '2024-01-25T18:00:00Z',
required: true
}
})
})
.then(response => response.json())
.then(data => console.log(data));
Respuesta JSON (201 Created)
{
"to_do": {
"id": 322,
"title": "Revisar documentación aduanal",
"start_at": "2024-01-20T09:00:00Z",
"end_at": "2024-01-25T18:00:00Z",
"completed": false,
"rejected": false,
"required": true,
"completed_at": null,
"owner": {
"email": "operador@apunto.com",
"name": "Carlos Ramírez"
},
"completed_by": null,
"created_at": "2024-01-16T11:00:00Z"
}
}
Crea una nueva tarea en el recurso especificado.
Parámetros
| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
| title | string | Sí | Título de la tarea |
| description | text | No | Descripción detallada |
| start_at | datetime | No | Fecha de inicio |
| end_at | datetime | No | Fecha de fin |
| required | boolean | No | Si la tarea es requerida |
Actualizar Tarea PUT
Definición
PUT /api/v1/operations/:operation_id/to_dos/:id
PUT /api/v1/services/:service_id/to_dos/:id
PUT /api/v1/contacts/:contact_id/to_dos/:id
Ejemplo de llamada
curl -X PUT "https://control.apunto.io/api/v1/operations/123/to_dos/321" \
-H "Authorization: Bearer TU_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"to_do": {
"title": "Solicitar documentos al cliente [URGENTE]",
"end_at": "2024-01-18T18:00:00Z",
"required": true
}
}'
Respuesta JSON
{
"to_do": {
"id": 321,
"title": "Solicitar documentos al cliente [URGENTE]",
"end_at": "2024-01-18T18:00:00Z",
"required": true,
"updated_at": "2024-01-16T11:30:00Z"
}
}
Actualiza una tarea existente.
Marcar como Completada POST
Definición
POST /api/v1/operations/:operation_id/to_dos/:id/complete
POST /api/v1/services/:service_id/to_dos/:id/complete
POST /api/v1/contacts/:contact_id/to_dos/:id/complete
Ejemplo de llamada
curl -X POST "https://control.apunto.io/api/v1/operations/123/to_dos/321/complete" \
-H "Authorization: Bearer TU_TOKEN"
Respuesta JSON
{
"to_do": {
"id": 321,
"title": "Solicitar documentos al cliente",
"completed": true,
"completed_at": "2024-01-16T12:00:00Z"
},
"message": "Tarea marcada como completada"
}
Marca una tarea como completada.
Eliminar Tarea DELETE
Definición
DELETE /api/v1/operations/:operation_id/to_dos/:id
DELETE /api/v1/services/:service_id/to_dos/:id
DELETE /api/v1/contacts/:contact_id/to_dos/:id
Ejemplo de llamada
curl -X DELETE "https://control.apunto.io/api/v1/operations/123/to_dos/321" \
-H "Authorization: Bearer TU_TOKEN"
Respuesta JSON
{
"message": "Tarea eliminada exitosamente"
}
Elimina una tarea.
Ejemplo de Contexto por URL
Cuando llamas a GET /api/v1/operations/123/to_dos, ya sabes que:
- todoable_type = "Operation"
- todoable_id = 123
Por lo tanto, estos campos no se incluyen en la respuesta JSON.
Errores
La API usa códigos de estado HTTP convencionales para indicar éxito o fallo.
Códigos de Estado HTTP
| Código | Significado | Descripción |
|---|---|---|
| 200 | OK | Petición exitosa |
| 201 | Created | Recurso creado exitosamente |
| 204 | No Content | Petición exitosa sin contenido (típicamente DELETE) |
| 400 | Bad Request | Petición inválida |
| 401 | Unauthorized | Autenticación fallida o token inválido |
| 403 | Forbidden | No tienes permiso para este recurso |
| 404 | Not Found | Recurso no encontrado |
| 422 | Unprocessable Entity | Error de validación |
| 429 | Too Many Requests | Límite de tasa excedido |
| 500 | Internal Server Error | Error en el servidor |
| 503 | Service Unavailable | Problema temporal del servidor |
Formato de Respuesta de Error
Ejemplo de error:
{
"error": "Validation failed",
"message": "No se pudo guardar el recurso",
"details": {
"contact_id": ["no puede estar en blanco"],
"kind": ["no está incluido en la lista"],
"mode": ["no puede estar en blanco"]
},
"status": 422
}
Todos los errores siguen este formato:
| Campo | Tipo | Descripción |
|---|---|---|
| error | string | Tipo de error corto |
| message | string | Mensaje de error legible |
| details | object | Detalles de validación (para errores 422) |
| status | integer | Código de estado HTTP |
Errores Comunes
Error de Autenticación
{
"error": "Unauthorized",
"message": "Token de API inválido o faltante",
"status": 401
}
Causa: Header Authorization faltante o inválido.
Solución: Asegúrate de enviar un token válido en formato Bearer TU_TOKEN_API.
Error de Validación
{
"error": "Validation failed",
"message": "No se pudo guardar el recurso",
"details": {
"name": ["no puede estar en blanco"],
"email": ["no es válido"]
},
"status": 422
}
Causa: Campos requeridos faltantes o datos que no cumplen validaciones.
Solución: Revisa el objeto details para errores específicos.
Recurso No Encontrado
{
"error": "Not Found",
"message": "No se encontró la operación solicitada",
"status": 404
}
Causa: El ID del recurso no existe o no tienes acceso.
Solución: Verifica el ID y tus permisos.
Límite de Tasa Excedido
{
"error": "Rate limit exceeded",
"message": "Has excedido el límite. Intenta más tarde.",
"retry_after": 3600,
"status": 429
}
Causa: Demasiadas peticiones en poco tiempo.
Solución: Espera el tiempo en retry_after (segundos). Implementa backoff exponencial.
Permiso Denegado
{
"error": "Forbidden",
"message": "No tienes permiso para esta acción",
"status": 403
}
Causa: Tu cuenta o token no tiene los permisos necesarios.
Solución: Contacta al administrador para solicitar permisos.
Error del Servidor
{
"error": "Internal Server Error",
"message": "Ocurrió un error inesperado. Intenta más tarde.",
"status": 500
}
Causa: Error inesperado en el servidor.
Solución: Reintenta la petición. Si persiste, contacta soporte.
Mejores Prácticas
- Verifica códigos de estado - No te bases solo en el body
- Implementa reintentos - Para errores 5xx y rate limits (429)
- Usa backoff exponencial - Al reintentar peticiones fallidas
- Registra errores - Guarda respuestas de error para debugging
- Valida antes de enviar - Reduce errores 422 validando en cliente
- Maneja expiración de tokens - Prepárate para refrescar tokens
- Mensajes al usuario - Muestra errores significativos a usuarios finales
Ejemplo de Manejo de Errores
def make_api_request(url, method = :get, body = nil)
# ... código de petición ...
case response.code.to_i
when 200..299
JSON.parse(response.body)
when 401
raise 'Error de autenticación. Verifica tu token.'
when 404
raise 'Recurso no encontrado.'
when 422
errors = JSON.parse(response.body)
raise "Errores de validación: #{errors['details']}"
when 429
retry_after = response['Retry-After'].to_i
sleep(retry_after)
make_api_request(url, method, body) # Reintentar
when 500..599
raise 'Error del servidor. Intenta más tarde.'
end
end
def make_api_request(url, method='GET', body=None):
try:
response = requests.request(method, url, json=body, headers=headers)
if 200 <= response.status_code < 300:
return response.json()
elif response.status_code == 401:
raise Exception('Error de autenticación')
elif response.status_code == 404:
raise Exception('Recurso no encontrado')
elif response.status_code == 422:
errors = response.json()
raise Exception(f'Validación: {errors.get("details")}')
elif response.status_code == 429:
retry_after = int(response.headers.get('Retry-After', 60))
time.sleep(retry_after)
return make_api_request(url, method, body)
elif response.status_code >= 500:
raise Exception('Error del servidor')
except requests.exceptions.RequestException as e:
raise Exception(f'Petición fallida: {str(e)}')
async function makeApiRequest(url, method = 'GET', body = null) {
try {
const response = await axios({ method, url, data: body, headers });
return response.data;
} catch (error) {
const status = error.response?.status;
const data = error.response?.data;
switch (status) {
case 401:
throw new Error('Error de autenticación');
case 404:
throw new Error('Recurso no encontrado');
case 422:
throw new Error(`Validación: ${JSON.stringify(data.details)}`);
case 429:
const retryAfter = parseInt(error.response.headers['retry-after']) || 60;
await new Promise(r => setTimeout(r, retryAfter * 1000));
return makeApiRequest(url, method, body);
case 500:
case 502:
case 503:
throw new Error('Error del servidor');
default:
throw new Error(`Error: ${status}`);
}
}
}