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, el ciclo comercial (prospectos, oportunidades, solicitudes y cotizaciones), 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).
- Ciclo comercial — Prospecto, oportunidad, solicitud comercial y cotización. La cotización abierta se convierte en operación.
- 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. El ciclo comercial va de prospecto a oportunidad, solicitud y cotización. En la operación navegas operación → servicios → centro de costos, y usas contactos y direcciones como catálogos de referencia.
Cuenta (tenant)
├── Prospectos
├── Oportunidades
│ └── Solicitudes comerciales
│ ├── Columnas del flujo (list_id)
│ ├── Motivos de ganado y perdido
│ ├── Precios
│ └── Cotizaciones → Operaciones
├── 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.
- Llevar el ciclo comercial desde un prospecto hasta la cotización abierta y 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.
Prospectos
Un prospecto es un contacto con kind que incluye prospect. El alta lo crea así. El ciclo comercial sigue de aquí a una oportunidad.
Objeto Lead
| Atributo | Tipo | Descripción |
|---|---|---|
| id | integer | ID interno |
| alias | string | Alias del contacto |
| name | string | Nombre |
| legal_name | string | Razón social |
| identification | string | Identificación |
| id_fiscal | string | RFC u otra identificación fiscal |
| kind | array | En el alta queda prospect |
| status | string | active o inactive |
| lead_status | string | Estatus comercial |
| lead_status_label | string | Etiqueta del estatus |
| prospect_type | string | direct o origin_agent |
| lead_source | string | Origen del prospecto |
| seller | object | { id, name, email } o null |
| created_at | datetime | Alta |
| updated_at | datetime | Último cambio |
El detalle (GET /leads/:id) agrega description, services, credit_days y billing_address.
lead_status: new_lead, qualified, contacted, negotiation, closed, discarded, bad_timing, revalue
lead_source: referral, website, linkedin, event, cold_call, trade_show, inbound, other
Listar prospectos GET
GET /api/v1/leads
curl "https://control.apunto.io/api/v1/leads?lead_status=new_lead" \
-H "Authorization: Bearer TU_TOKEN"
Respuesta
{
"leads": [
{
"id": 120,
"alias": "ACME",
"name": "Acme Logistics",
"kind": ["prospect"],
"status": "active",
"lead_status": "new_lead",
"lead_status_label": "Nuevo",
"prospect_type": "direct",
"lead_source": "inbound",
"seller": { "id": 8, "name": "Ana Pérez", "email": "ana@apunto.com" }
}
],
"pagination": { "page": 1, "per_page": 25, "total": 1 }
}
| Parámetro | Descripción |
|---|---|
| lead_status | Filtra por estatus comercial |
| prospect_type | direct o origin_agent |
| lead_source | Origen |
| page | Página, default 1 |
| per_page | Default 25, máximo 100 |
Obtener un prospecto GET
GET /api/v1/leads/:id
La respuesta envuelve el objeto en lead.
Crear un prospecto POST
POST /api/v1/leads
curl -X POST "https://control.apunto.io/api/v1/leads" \
-H "Authorization: Bearer TU_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"lead": {
"name": "Acme Logistics",
"alias": "ACME",
"lead_status": "new_lead",
"lead_source": "inbound",
"prospect_type": "direct",
"seller_email": "ana@apunto.com"
}
}'
uri = URI("https://control.apunto.io/api/v1/leads")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer TU_TOKEN"
request["Content-Type"] = "application/json"
request.body = {
lead: {
name: "Acme Logistics",
alias: "ACME",
lead_status: "new_lead",
lead_source: "inbound",
prospect_type: "direct",
seller_email: "ana@apunto.com"
}
}.to_json
import requests
requests.post(
"https://control.apunto.io/api/v1/leads",
headers={"Authorization": "Bearer TU_TOKEN"},
json={
"lead": {
"name": "Acme Logistics",
"alias": "ACME",
"lead_status": "new_lead",
"lead_source": "inbound",
"prospect_type": "direct",
"seller_email": "ana@apunto.com",
}
},
)
await fetch("https://control.apunto.io/api/v1/leads", {
method: "POST",
headers: {
Authorization: "Bearer TU_TOKEN",
"Content-Type": "application/json"
},
body: JSON.stringify({
lead: {
name: "Acme Logistics",
alias: "ACME",
lead_status: "new_lead",
lead_source: "inbound",
prospect_type: "direct",
seller_email: "ana@apunto.com"
}
})
});
name es obligatorio. seller_email es el correo de un usuario de la cuenta; también puedes mandar seller_id.
| Campo | Requerido | Descripción |
|---|---|---|
| name | Sí | Nombre |
| alias | No | Alias |
| legal_name | No | Razón social |
| identification | No | Identificación |
| id_fiscal | No | Identificación fiscal |
| trade_identification | No | Identificación comercial |
| description | No | Notas |
| status | No | active o inactive |
| nationality | No | Nacionalidad |
| credit_days | No | Días de crédito |
| lead_status | No | Estatus comercial |
| prospect_type | No | direct o origin_agent |
| lead_source | No | Origen |
| seller_id | No | ID del vendedor |
| seller_email | No | Correo del vendedor en la cuenta |
| kind | No | Arreglo; el alta usa prospect |
| services | No | maritime, aerial, land, customs |
Respuesta 201 con { "lead": { ... }, "message": "..." }. El detalle incluye description, services, credit_days y billing_address.
Actualizar un prospecto PATCH
PATCH /api/v1/leads/:id
Mismo cuerpo { "lead": { ... } } que en el alta. Los campos que no envías se quedan como están.
Cambiar estatus PATCH
PATCH /api/v1/leads/:id/transition
{ "lead_status": "qualified" }
También se acepta anidado en lead. El valor tiene que estar en el catálogo de la cuenta, o ser el estatus actual.
Eliminar un prospecto DELETE
DELETE /api/v1/leads/:id
Si el contacto no se puede borrar, la respuesta trae errors, blocking_counts y can_archive.
Oportunidades
Una oportunidad es un negocio sobre un contacto. Para marcarla won tiene que tener al menos una solicitud comercial ligada.
Objeto Opportunity
| Atributo | Tipo | Descripción |
|---|---|---|
| id | integer | ID interno |
| identification | string | Folio, por ejemplo OP-00012 |
| title | string | Título |
| status | string | Estatus |
| status_label | string | Etiqueta |
| frequency | string | Frecuencia esperada |
| frequency_label | string | Etiqueta de la frecuencia |
| operations_volume | integer | Volumen de operaciones |
| detail | string | Detalle |
| contact | object | { id, alias, name } o null |
| owner | object | { id, name, email } o null |
| tags | array | Etiquetas |
| created_at | datetime | Alta |
| updated_at | datetime | Último cambio |
El detalle agrega quote_requests_count y opportunity_actions_count.
status: created, in_progress, in_negotiation, won, lost
frequency: daily, weekly, monthly, semiannual, annual
Listar oportunidades GET
GET /api/v1/opportunities
curl "https://control.apunto.io/api/v1/opportunities?status=in_progress" \
-H "Authorization: Bearer TU_TOKEN"
| Parámetro | Descripción |
|---|---|
| status | Filtra por estatus |
| page | Página, default 1 |
| per_page | Default 25, máximo 100 |
La respuesta es { "opportunities": [ ... ], "pagination": { "page", "per_page", "total" } }.
Obtener una oportunidad GET
GET /api/v1/opportunities/:id
También puedes buscar por folio en otros recursos con opportunity_identification (el campo identification).
Crear una oportunidad POST
POST /api/v1/opportunities
curl -X POST "https://control.apunto.io/api/v1/opportunities" \
-H "Authorization: Bearer TU_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"opportunity": {
"title": "Importación FCL Manzanillo",
"contact_code": "ACME",
"owner_email": "ana@apunto.com",
"frequency": "monthly",
"operations_volume": 4
}
}'
request.body = {
opportunity: {
title: "Importación FCL Manzanillo",
contact_code: "ACME",
owner_email: "ana@apunto.com",
frequency: "monthly",
operations_volume: 4
}
}.to_json
requests.post(
"https://control.apunto.io/api/v1/opportunities",
headers={"Authorization": "Bearer TU_TOKEN"},
json={
"opportunity": {
"title": "Importación FCL Manzanillo",
"contact_code": "ACME",
"frequency": "monthly",
"operations_volume": 4,
}
},
)
await fetch("https://control.apunto.io/api/v1/opportunities", {
method: "POST",
headers: {
Authorization: "Bearer TU_TOKEN",
"Content-Type": "application/json"
},
body: JSON.stringify({
opportunity: {
title: "Importación FCL Manzanillo",
contact_code: "ACME",
frequency: "monthly",
operations_volume: 4
}
})
});
title va en el cuerpo. El contacto es obligatorio: contact_id o contact_code (el alias del contacto, en mayúsculas). Si no mandas dueño, queda el usuario del token.
| Campo | Requerido | Descripción |
|---|---|---|
| title | Sí | Título |
| contact_id | Sí, o contact_code |
ID del contacto |
| contact_code | Sí, o contact_id |
Alias del contacto |
| owner_id | No | ID del dueño |
| owner_email | No | Correo de un usuario de la cuenta |
| frequency | No | Frecuencia |
| operations_volume | No | Volumen |
| detail | No | Detalle |
| status | No | Estatus inicial |
| tag_list | No | Arreglo de etiquetas |
Respuesta 201 con { "opportunity": { ... }, "message": "..." }.
Actualizar una oportunidad PATCH
PATCH /api/v1/opportunities/:id
Mismo cuerpo { "opportunity": { ... } }.
Cambiar estatus PATCH
PATCH /api/v1/opportunities/:id/transition
{ "status": "in_progress" }
También se acepta { "opportunity": { "status": "in_progress" } }. Pasar a won sin una solicitud comercial ligada responde 422.
Eliminar una oportunidad DELETE
DELETE /api/v1/opportunities/:id
Respuesta con { "message": "..." }.
Solicitudes comerciales
La solicitud comercial es el pedido de cotización: ruta, carga y precios. Vive en una columna del flujo (list_id). De aquí sale la cotización.
Antes de crear una solicitud, lee el flujo y, cuando vayas a cerrarla, los motivos de ganado o perdido.
Columnas del flujo GET
GET /api/v1/quotation_pipelines
curl "https://control.apunto.io/api/v1/quotation_pipelines" \
-H "Authorization: Bearer TU_TOKEN"
Respuesta
{
"quotation_pipelines": [
{
"id": 4,
"name": "Comercial",
"status": "active",
"default_pipeline": true,
"lists": [
{
"id": 10,
"name": "Solicitado",
"position": 1,
"target_quote_request_status": "requested"
}
]
}
]
}
El id de cada elemento de lists es el list_id que pide el alta. default_pipeline marca el flujo por defecto de la cuenta. Solo ves flujos de tu cuenta.
Motivos de ganado GET
GET /api/v1/quote_won_reasons
{
"quote_won_reasons": [
{ "id": 3, "name": "Mejor precio", "description": null, "status": "active" }
]
}
Solo motivos activos. El id se manda como quote_won_reason_id al marcar ganada.
Motivos de perdido GET
GET /api/v1/quote_lost_reasons
{
"quote_lost_reasons": [
{ "id": 7, "name": "Precio", "description": null, "status": "active" }
]
}
El id se manda como quote_lost_reason_id al marcar perdida.
Objeto QuoteRequest
| Atributo | Tipo | Descripción |
|---|---|---|
| id | integer | ID interno |
| identification | string | Folio |
| status | string | Estatus del tablero |
| result | string | pending, won o lost |
| origin | string | Origen |
| destination | string | Destino |
| transport_mode | string | Modo de transporte |
| operation_kind | string | Tipo de operación |
| operation_mode | string | Modo de operación |
| service_scope | string | Alcance del servicio |
| incoterm | string | Incoterm |
| deal_amount | number | Monto del trato |
| closing_date | date | Cierre esperado |
| cargo_details | string | Detalle de la carga |
| contact | object | { id, alias, name } |
| owner | object | { id, name, email } |
| assignee | object | Pricing asignado |
| seller | object | Vendedor |
| opportunity | object | { id, identification, title } |
| list | object | { id, name } de la columna |
| created_at | datetime | Alta |
| updated_at | datetime | Último cambio |
El detalle agrega movement_details, total_pricing_amount, minutes_to_result, tags, shipment_items y los conteos de precios, cotizaciones, actividades, mensajes y tareas.
status: start, requested, quoted, vobo, finished, adjustments_requested
result: pending, won, lost
transport_mode: ocean, air, truck, rail, multimodal
operation_mode: land, aerial, maritime. Se mapea a transporte así: maritime → ocean, aerial → air, land → truck.
operation_kind: importation, exportation, domestic, crosstrade, transportation, consulting, export_trading_company, import_trading_company
service_scope: door_to_door, door_to_port_cy, door_to_port_cfs, port_cy_to_port_cy, port_cfs_to_port_cfs, port_cy_to_door, port_cfs_to_door, door_to_airport, airport_to_airport, airport_to_door, door_to_rail_ramp, rail_ramp_to_rail_ramp, rail_ramp_to_door, door_to_truck_terminal, truck_terminal_to_truck_terminal, truck_terminal_to_door, port_cy_to_airport, port_cfs_to_airport, airport_to_port_cy, airport_to_port_cfs, port_cy_to_rail_ramp, port_cfs_to_rail_ramp, rail_ramp_to_port_cy, rail_ramp_to_port_cfs, rail_ramp_to_airport, airport_to_rail_ramp
Listar solicitudes GET
GET /api/v1/quote_requests
curl "https://control.apunto.io/api/v1/quote_requests?status=requested&transport_mode=ocean" \
-H "Authorization: Bearer TU_TOKEN"
| Parámetro | Descripción |
|---|---|
| status | Estatus |
| result | pending, won o lost |
| transport_mode | ocean, air, truck, rail, multimodal |
| page | Página, default 1 |
| per_page | Default 25, máximo 100 |
Obtener una solicitud GET
GET /api/v1/quote_requests/:id
Crear una solicitud POST
POST /api/v1/quote_requests
Obligatorios: list_id, origin y destination. El contacto es opcional. Si mandas contact_code y no existe, responde 422.
curl -X POST "https://control.apunto.io/api/v1/quote_requests" \
-H "Authorization: Bearer TU_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"quote_request": {
"list_id": 10,
"contact_code": "ACME",
"opportunity_identification": "OP-00012",
"assignee_email": "pricing@apunto.com",
"seller_email": "ana@apunto.com",
"origin": "Manzanillo",
"destination": "Monterrey",
"operation_kind": "importation",
"operation_mode": "maritime",
"transport_mode": "ocean",
"service_scope": "port_cy_to_door",
"incoterm": "FOB"
}
}'
request.body = {
quote_request: {
list_id: 10,
contact_code: "ACME",
origin: "Manzanillo",
destination: "Monterrey",
operation_kind: "importation",
operation_mode: "maritime",
transport_mode: "ocean"
}
}.to_json
requests.post(
"https://control.apunto.io/api/v1/quote_requests",
headers={"Authorization": "Bearer TU_TOKEN"},
json={
"quote_request": {
"list_id": 10,
"contact_code": "ACME",
"origin": "Manzanillo",
"destination": "Monterrey",
"transport_mode": "ocean",
}
},
)
await fetch("https://control.apunto.io/api/v1/quote_requests", {
method: "POST",
headers: {
Authorization: "Bearer TU_TOKEN",
"Content-Type": "application/json"
},
body: JSON.stringify({
quote_request: {
list_id: 10,
contact_code: "ACME",
origin: "Manzanillo",
destination: "Monterrey",
transport_mode: "ocean"
}
})
});
| Campo | Requerido | Descripción |
|---|---|---|
| list_id | Sí | Columna del flujo |
| origin | Sí | Origen |
| destination | Sí | Destino |
| contact_id | No | ID del contacto |
| contact_code | No | Alias del contacto |
| opportunity_id | No | ID de la oportunidad |
| opportunity_identification | No | Folio de la oportunidad |
| assignee_id | No | ID de quien cotiza |
| assignee_email | No | Correo de quien cotiza |
| seller_id | No | ID del vendedor |
| seller_email | No | Correo del vendedor |
| origin_port_id | No | Puerto de origen |
| destination_port_id | No | Puerto de destino |
| incoterm | No | Incoterm |
| service_scope | No | Alcance |
| transport_mode | No | Modo de transporte |
| operation_kind | No | Tipo de operación |
| operation_mode | No | Modo de operación |
| cargo_details | No | Carga |
| movement_details | No | Movimiento |
| status | No | Estatus |
| volumetric_factor_code | No | Factor volumétrico |
| deal_amount | No | Monto. Si viene, deal_currency_id es obligatorio |
| deal_currency_id | Condicional | Moneda del monto |
| closing_date | No | Fecha de cierre |
| result | No | pending, won o lost |
| result_notes | No | Notas del resultado |
| tag_list | No | Etiquetas |
| shipment_items_attributes | No | Líneas de carga |
| document_routes_attributes | No | Rutas del documento |
Cada línea de shipment_items_attributes acepta cargo_type, package_type, pallet_type, container_type, quantity, length, width, height, weight, unit_length, unit_weight, overweight, container_number, unit_type_id. En una actualización, id y _destroy editan o quitan la línea.
Respuesta 201 con { "quote_request": { ... }, "message": "..." }.
Actualizar una solicitud PATCH
PATCH /api/v1/quote_requests/:id
Mismo cuerpo { "quote_request": { ... } }.
Crear la cotización POST
POST /api/v1/quote_requests/:id/create_quote
Arma un borrador de cotización con el contacto, el vendedor, la carga, el tipo y el modo de operación, y la moneda por defecto de la cuenta. Copia cada precio a una partida, copia las líneas de carga, guarda quote_id y pasa la solicitud a vobo.
No lleva cuerpo. Respuesta 201 con la cotización creada. Si no se puede armar, 422 con errors.
Precios de la solicitud
Listar GET
GET /api/v1/quote_requests/:quote_request_id/quote_request_pricings
Devuelve los precios publicados (draft: false), con la moneda incluida: { "pricings": [ ... ] }.
Crear POST
POST /api/v1/quote_requests/:quote_request_id/quote_request_pricings
Solo cuando la solicitud está en requested. Si no, 422.
{
"quote_request_pricing": {
"service_name": "Flete marítimo",
"service_description": "FCL 40",
"quantity": 1,
"unit_price": "1850.00",
"total_price": "1850.00",
"currency_id": 2,
"exchange_rate": "17.20"
}
}
currency_id es el ID de la moneda. Campos adicionales: unit_type, notes, is_suggestion, pricing_catalog_id, supplier_id y quote_request_pricing_line_items_attributes.
Respuesta 201 con { "pricing": { ... } }.
Sugerencias del tarifario GET
GET /api/v1/quote_requests/:quote_request_id/quote_request_pricings/suggestions
Requiere el flag pricing_catalogs. Sin el flag responde { "suggestions": [], "meta": { "message": "..." } }.
| Parámetro | Descripción |
|---|---|
| supplier_id | Limita las sugerencias a un proveedor |
| advanced | true para la búsqueda avanzada |
Marcar ganada POST
POST /api/v1/quote_requests/:id/mark_as_won
{
"won_at": "2026-10-05",
"result_notes": "El cliente confirmó el flete",
"quote_won_reason_id": 3,
"winner_quote_ids": [88]
}
won_at por defecto es hoy y no puede ser anterior a la última actividad. quote_won_reason_id sale de motivos de ganado. Si hay más de una cotización ligada, winner_quote_ids tiene que ser un subconjunto no vacío de esas cotizaciones.
Marcar perdida POST
POST /api/v1/quote_requests/:id/mark_as_lost
{
"lost_at": "2026-10-05",
"quote_lost_reason_id": 7,
"result_notes": "Se fue con otra tarifa",
"confirm_quote_loss_consequences": true
}
quote_lost_reason_id sale de motivos de perdido. Si al perder la solicitud también se pierden cotizaciones ligadas, confirm_quote_loss_consequences tiene que ser true o "1". Si no, 422.
Reabrir POST
POST /api/v1/quote_requests/:id/reopen
Quita el resultado de ganada o perdida. No lleva cuerpo.
Eliminar una solicitud DELETE
DELETE /api/v1/quote_requests/:id
Respuesta con { "message": "..." }.
Cotizaciones
La cotización es el documento que ve el cliente. Puede nacer directo, o desde una solicitud comercial con POST /quote_requests/:id/create_quote. Cuando está abierta se puede convertir en operación.
El alta deja la cotización en drafted y asigna como dueño al usuario del token. El contacto es obligatorio. Si no mandas moneda, usa la moneda por defecto de la cuenta. Si no mandas exchange_rate, queda 1.0.
Objeto Quote
| Atributo | Tipo | Descripción |
|---|---|---|
| id | integer | ID interno |
| identification | string | Folio |
| status | string | Estatus del documento |
| kind | string | Tipo de documento |
| date | date | Fecha |
| due_at | date | Vigencia |
| subtotal | number | Subtotal |
| total | number | Total |
| exchange_rate | number | Tipo de cambio |
| contact | object | { id, alias, name } |
| currency | object | { id, code, name }. code es el ISO |
| owner | object | { id, name, email } |
| seller | object | { id, name, email } |
| operation_kind | string | Tipo de operación |
| operation_mode | string | Modo de operación |
| service_scope | string | Alcance |
| incoterm | string | Incoterm |
| client_ref | string | Referencia del cliente |
| created_at | datetime | Alta |
| updated_at | datetime | Último cambio |
El detalle agrega description, tags, origin_address, destination_address, line_items_count y document_routes_count.
El camino de aprobación que expone la API es:
drafted → POST /quotes/:id/approve → approval_requested → POST /quotes/:id/open → opened
Una cotización ya guardada también puede estar en need_review, sent, viewed, partial, paid, voided, won, lose o invoiced. Esos valores se leen; approve y open no saltan a ellos.
operation_kind, operation_mode y service_scope usan las mismas claves que en solicitudes comerciales.
Listar cotizaciones GET
GET /api/v1/quotes
curl "https://control.apunto.io/api/v1/quotes?status=drafted" \
-H "Authorization: Bearer TU_TOKEN"
| Parámetro | Descripción |
|---|---|
| status | Filtra por estatus |
| page | Página, default 1 |
| per_page | Default 25, máximo 100 |
La respuesta es { "quotes": [ ... ], "pagination": { "page", "per_page", "total" } }.
Obtener una cotización GET
GET /api/v1/quotes/:id
Crear una cotización POST
POST /api/v1/quotes
curl -X POST "https://control.apunto.io/api/v1/quotes" \
-H "Authorization: Bearer TU_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"quote": {
"contact_code": "ACME",
"currency_code": "MXN",
"seller_email": "ana@apunto.com",
"operation_kind": "importation",
"operation_mode": "maritime",
"incoterm": "FOB",
"client_ref": "REF-001"
}
}'
request.body = {
quote: {
contact_code: "ACME",
currency_code: "MXN",
seller_email: "ana@apunto.com",
operation_kind: "importation",
operation_mode: "maritime",
incoterm: "FOB",
client_ref: "REF-001"
}
}.to_json
requests.post(
"https://control.apunto.io/api/v1/quotes",
headers={"Authorization": "Bearer TU_TOKEN"},
json={
"quote": {
"contact_code": "ACME",
"currency_code": "MXN",
"operation_kind": "importation",
"operation_mode": "maritime",
"incoterm": "FOB",
"client_ref": "REF-001",
}
},
)
await fetch("https://control.apunto.io/api/v1/quotes", {
method: "POST",
headers: {
Authorization: "Bearer TU_TOKEN",
"Content-Type": "application/json"
},
body: JSON.stringify({
quote: {
contact_code: "ACME",
currency_code: "MXN",
operation_kind: "importation",
operation_mode: "maritime",
incoterm: "FOB",
client_ref: "REF-001"
}
})
});
Un contacto que es solo prospecto no se puede convertir después en operación. Para ese paso el contacto tiene que ser cliente.
| Campo | Requerido | Descripción |
|---|---|---|
| contact_id | Sí, o contact_code |
ID del contacto |
| contact_code | Sí, o contact_id |
Alias del contacto |
| currency_id | No | ID de la moneda |
| currency_code | No | ISO, por ejemplo MXN o USD |
| date | No | Fecha |
| due_at | No | Vigencia |
| description | No | Descripción |
| operation_kind | No | Tipo de operación. Hace falta antes de crear la operación |
| operation_mode | No | Modo. Hace falta antes de crear la operación |
| exchange_rate | No | Tipo de cambio. Default 1.0 |
| hide_exchange_rate | No | Oculta el tipo de cambio |
| hide_expense_account | No | Oculta la cuenta de gasto |
| origin_address_id | No | Dirección de origen |
| destination_address_id | No | Dirección de destino |
| incoterm | No | Incoterm |
| service_scope | No | Alcance |
| seller_id | No | ID del vendedor |
| seller_email | No | Correo del vendedor |
| client_ref | No | Referencia del cliente |
| status | No | El alta lo fuerza a drafted |
| tag_list | No | Etiquetas |
| document_routes_attributes | No | Rutas: address_id, port_data_id, address_kind, address_type, location_name, position, description |
Respuesta 201 con { "quote": { ... }, "message": "..." }.
Actualizar una cotización PATCH
PATCH /api/v1/quotes/:id
Mismo cuerpo { "quote": { ... } }.
Pedir aprobación POST
POST /api/v1/quotes/:id/approve
Solo desde drafted. Pasa a approval_requested. Si el estatus no es drafted, responde 422.
Abrir POST
POST /api/v1/quotes/:id/open
Solo desde approval_requested. Pasa a opened.
Clonar POST
POST /api/v1/quotes/:id/clone
Copia la cotización a un borrador nuevo. Respuesta 201 con { "quote": { ... }, "message": "..." }.
Convertir en operación POST
POST /api/v1/quotes/:id/create_operation
Crea una operación nueva, o liga la cotización a una que ya existe. El contacto no puede ser solo prospecto. La cotización tiene que traer operation_kind y operation_mode.
Operación nueva (el default):
{
"operation_type": "new",
"visibility_scope": "everyone"
}
visibility_scope acepta everyone (default) o group. Con group manda work_group_id.
Operación existente:
{
"operation_type": "existing",
"existing_operation_id": 540
}
La operación tiene que ser de la misma cuenta, del mismo contacto y estar confirmed o active.
Para repartir partidas y carga puedes mandar, en el mismo cuerpo:
| Campo | Descripción |
|---|---|
| line_items_to_services | Partidas que se van a servicios ya existentes |
| new_services | Servicios nuevos a crear |
| line_items_to_new_services | Partidas que se van a esos servicios nuevos |
| line_items_links | Liga partidas entre sí |
| shipment_items_to_service_groups | Carga hacia grupos de servicio |
| shipment_items_to_services | Carga hacia servicios existentes |
| shipment_items_to_new_services | Carga hacia servicios nuevos |
Si la operación es nueva, la respuesta es 201. Si se liga a una existente, 200. El cuerpo trae operation con id, identification, kind, mode, status, client_ref y contact, más message. Un rechazo (prospecto, sin tipo o modo, operación ajena) es 422 con errors.
Eliminar una cotización DELETE
DELETE /api/v1/quotes/:id
Respuesta con { "message": "..." }.
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}`);
}
}
}