API Reference · v1
cURL Ruby Python JavaScript

Introducción

Bienvenido a la documentación de referencia de la API de Apunto (v1).

Aquí encontrarás cómo conectar tu cuenta de freight forwarding con sistemas externos: autenticación, recursos (operaciones, servicios, centro de costos, contactos, facturas, documentos) y manejo de errores.

Empieza por Información general para entender el contexto del producto, el modelo de datos y los pasos para tu primera integración. Después continúa con Autenticación y el recurso que necesites en el menú lateral.

Información general

La API de Apunto es el canal programático hacia tu cuenta en Apunto: la plataforma donde tu equipo de freight forwarding gestiona operaciones, servicios, contactos, documentos y la economía de cada embarque (centro de costos, facturas y gastos).

Con la API puedes leer y actualizar la misma información que ves en la aplicación web, conectar sistemas externos y automatizar flujos sin depender de captura manual.

Qué es Apunto (contexto del sistema)

Apunto está pensado para forwarders y operadores logísticos que coordinan importaciones, exportaciones y movimientos domésticos. En la plataforma web el trabajo se organiza así:

  1. 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.
  2. Operación — El expediente del embarque o proyecto logístico (cliente, moneda, agente operativo, estado, márgenes).
  3. Servicio — Cada tramo o actividad dentro de la operación (marítimo, aéreo, terrestre, aduanas, etc.).
  4. 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.
  5. Contactos y direcciones — Catálogo de clientes, proveedores, carriers y ubicaciones (puertos, plantas, domicilios fiscales).
  6. 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.
  7. Colaboración — Comentarios (messages), tareas (to_dos) y carpetas de archivos en operaciones, servicios y contactos.

La API refleja ese mismo modelo: casi siempre navegarás operación → servicios → centro de costos, y usarás contactos y direcciones como catálogos de referencia.

Cuenta (tenant)
├── Operaciones
│   ├── Servicios
│   │   └── Centros de costo (líneas ingreso/gasto)
│   ├── Comentarios, tareas, carpetas de archivos
│   └── Agregado de centros de costo (solo lectura)
├── Contactos (+ direcciones anidadas, mensajes, tareas)
├── Direcciones (nivel cuenta)
├── Facturas y bills
└── Adjuntos (p. ej. extensión Chrome)

Qué puedes construir con la API

Algunos ejemplos habituales de integración:

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

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)

  1. Acceso — Usuario activo en tu cuenta Apunto con permisos para los módulos que integrarás.
  2. Token — En la web: Configuración → Tokens de API → crea un token y guárdalo de forma segura (solo se muestra al crearlo).
  3. Primera petición — Prueba GET /api/v1/operations con Authorization: Bearer TU_TOKEN.
  4. Explora recursos — Lee una operación (show), luego sus servicios y centros de costo anidados o vía endpoints dedicados.
  5. Escrituras — Empieza con contactos o comentarios de bajo riesgo antes de tocar facturación o centro de costos.
  6. 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:

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:

  1. Crear un token de API en la configuración de tu cuenta
  2. Incluir el token en el header Authorization de cada petición

Crear un Token de API

Los tokens de API se pueden crear a través de la interfaz web:

  1. Navega a ConfiguraciónTokens de API
  2. Haz clic en Nuevo Token de API
  3. Dale un nombre descriptivo a tu token
  4. 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
email string Correo electrónico del usuario
password string Contraseña del usuario
otp_attempt string No Código OTP (si 2FA está activado)

Respuesta

Retorna un objeto que contiene el token de API.

Operaciones

Las operaciones representan procesos completos de freight forwarding (importación, exportación, transporte doméstico, etc.).

Objeto Operation

Atributos Principales

Atributo Tipo Descripción
id integer Identificador único
identification string Identificador legible (ej: "IMP-001-2024")
kind string Tipo: importation, exportation, domestic, crosstrade, transportation, consulting, export_trading_company, import_trading_company
mode string Modo de transporte: land, aerial, maritime
status string Estado: confirmed, active, finished, closed, canceled
client_ref string Referencia del cliente
contact object Cliente de la operación
currency object Moneda de la operación
operational_agent object Agente operativo asignado
profit_amount decimal Monto de ganancia
profit_percentage decimal Porcentaje de ganancia
services_count integer Número de servicios asociados
comments_count integer Número de comentarios
tasks_count integer Número de tareas
folders_count integer Número de carpetas de documentos
goods_description string Descripción de la mercancía
incoterm string INCOTERM aplicable
service_scope string Alcance del servicio (enum; ej: door_to_door, port_cy_to_port_cy, airport_to_airport)
economic_month date Mes económico (primer día del mes; acepta fecha parseable)
regime string Régimen aduanero (texto libre)
income_amount decimal Ingresos
expense_amount decimal Gastos
quote_external_id string ID externo de cotización
nomenclature string Nomenclatura / fracción arancelaria
tags array Etiquetas (tag_list al crear/actualizar)
services array Servicios completos (solo en show); cada servicio incluye cost_centers
created_at datetime Fecha de creación
updated_at datetime Fecha de última actualización

Listar Operaciones GET

Definición

GET /api/v1/operations

Ejemplo de llamada

curl "https://control.apunto.io/api/v1/operations" \
  -H "Authorization: Bearer TU_TOKEN" \
  -H "Content-Type: application/json"
require 'uri'
require 'net/http'

uri = URI('https://control.apunto.io/api/v1/operations')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true

request = Net::HTTP::Get.new(uri)
request['Authorization'] = 'Bearer TU_TOKEN'
request['Content-Type'] = 'application/json'

response = http.request(request)
puts response.body
import requests

url = "https://control.apunto.io/api/v1/operations"
headers = {
    "Authorization": "Bearer TU_TOKEN",
    "Content-Type": "application/json"
}

response = requests.get(url, headers=headers)
print(response.json())
fetch('https://control.apunto.io/api/v1/operations', {
  method: 'GET',
  headers: {
    'Authorization': 'Bearer TU_TOKEN',
    'Content-Type': 'application/json'
  }
})
.then(response => response.json())
.then(data => console.log(data));

Respuesta JSON

{
  "operations": [
    {
      "id": 123,
      "identification": "IMP-001-2024",
      "kind": "importation",
      "mode": "maritime",
      "status": "active",
      "client_ref": "REF-001",
      "contact": {
        "alias": "ACME",
        "name": "ACME SA DE CV"
      },
      "currency": {
        "code": "MXN",
        "name": "Peso Mexicano"
      },
      "operational_agent": {
        "email": "agente@apunto.com",
        "name": "Juan Pérez"
      },
      "profit_amount": 5000.00,
      "profit_percentage": 15.5,
      "services_count": 3,
      "comments_count": 8,
      "tasks_count": 5,
      "folders_count": 2,
      "created_at": "2024-01-15T10:30:00Z",
      "updated_at": "2024-01-15T10:30:00Z"
    }
  ],
  "pagination": {
    "page": 1,
    "per_page": 25,
    "total": 150
  }
}

Retorna una lista paginada de operaciones de la cuenta.

Parámetros Query

Parámetro Descripción
page Número de página (default: 1)
per_page Registros por página (default: 25, max: 100)

Buscar Operaciones GET

Definición

GET /api/v1/operations/search

Búsqueda ligera (máximo 20 resultados) por identification, client_ref, goods_description o number. Usada por integraciones (p. ej. extensión de Chrome) para elegir destino al subir adjuntos.

Parámetros Query

Parámetro Descripción
q Texto de búsqueda (opcional; sin q devuelve las 20 operaciones más recientes por updated_at)

Respuesta JSON

{
  "operations": [
    {
      "id": 123,
      "identification": "IMP-001-2024",
      "number": 1,
      "client_ref": "REF-001",
      "kind": "importation",
      "mode": "maritime",
      "status": "active",
      "goods_description": "Maquinaria",
      "contact_name": "ACME",
      "updated_at": "2024-01-15T10:30:00Z",
      "services": [
        { "id": 789, "identification": "SRV-001", "mode": "maritime", "status": "active" }
      ]
    }
  ]
}

Obtener una Operación GET

Definición

GET /api/v1/operations/:id

Ejemplo de llamada

curl "https://control.apunto.io/api/v1/operations/123" \
  -H "Authorization: Bearer TU_TOKEN"

Respuesta JSON

{
  "operation": {
    "id": 123,
    "identification": "IMP-001-2024",
    "kind": "importation",
    "mode": "maritime",
    "status": "active",
    "client_ref": "REF-001",
    "contact": {
      "alias": "ACME",
      "name": "ACME SA DE CV"
    },
    "currency": {
      "code": "MXN",
      "name": "Peso Mexicano"
    },
    "goods_description": "Maquinaria industrial",
    "incoterm": "FOB",
    "service_scope": "door_to_door",
    "quote_external_id": "QT-12345",
    "nomenclature": "8479.89.99",
    "profit_amount": 5000.00,
    "profit_percentage": 15.5,
    "services_count": 3,
    "comments_count": 8,
    "tasks_count": 5,
    "folders_count": 2,
    "tags": ["urgente", "cliente-vip"],
    "to_dos": [
      {
        "id": 55,
        "title": "Revisar documentación",
        "completed": false,
        "required": true,
        "start_at": null,
        "end_at": "2024-01-20T18:00:00Z"
      }
    ],
    "folders": [
      {
        "id": 10,
        "name": "BL",
        "parent_id": null,
        "files_count": 1,
        "attachments": [
          {
            "id": 501,
            "filename": "bl.pdf",
            "byte_size": 245000,
            "content_type": "application/pdf",
            "url": "https://control.apunto.io/rails/active_storage/blobs/redirect/..."
          }
        ],
        "children": []
      }
    ],
    "services": [
      {
        "id": 789,
        "identification": "SRV-001-2024",
        "mode": "maritime",
        "status": "active",
        "shipment_type": "fcl",
        "shipment_kind": "international",
        "supplier": {
          "alias": "MAERSK",
          "name": "Maersk Line"
        },
        "eta_date": "2024-02-15",
        "etd_date": "2024-01-20",
        "bl": "BL123456",
        "booking": "BOOK789",
        "cost_centers": [
          {
            "id": 901,
            "concept": "Flete marítimo",
            "quantity": 1,
            "income_amount": 15000,
            "expense_amount": 9000,
            "profit_amount": 6000,
            "profit_percentage": 40.0,
            "currency": { "code": "MXN", "name": "Peso Mexicano" },
            "supplier": { "id": 111, "alias": "MAERSK", "name": "Maersk Line" },
            "linked": {
              "quote_line_item_id": null,
              "invoice_line_item_id": null,
              "bill_line_item_id": null
            }
          }
        ]
      }
    ],
    "created_at": "2024-01-15T10:30:00Z",
    "updated_at": "2024-01-15T10:30:00Z"
  }
}

Retorna los detalles completos de una operación específica.

Crear Operación POST

Definición

POST /api/v1/operations

Ejemplo de llamada

curl -X POST "https://control.apunto.io/api/v1/operations" \
  -H "Authorization: Bearer TU_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "operation": {
      "contact_code": "ACME",
      "currency_code": "MXN",
      "operational_agent_email": "agente@apunto.com",
      "kind": "importation",
      "mode": "maritime",
      "client_ref": "REF-001",
      "goods_description": "Maquinaria industrial",
      "incoterm": "FOB"
    }
  }'
require 'uri'
require 'net/http'
require 'json'

uri = URI('https://control.apunto.io/api/v1/operations')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true

request = Net::HTTP::Post.new(uri)
request['Authorization'] = 'Bearer TU_TOKEN'
request['Content-Type'] = 'application/json'
request.body = {
  operation: {
    contact_code: 'ACME',
    currency_code: 'MXN',
    operational_agent_email: 'agente@apunto.com',
    kind: 'importation',
    mode: 'maritime'
  }
}.to_json

response = http.request(request)
puts response.body
import requests
import json

url = "https://control.apunto.io/api/v1/operations"
headers = {
    "Authorization": "Bearer TU_TOKEN",
    "Content-Type": "application/json"
}
data = {
    "operation": {
        "contact_code": "ACME",
        "currency_code": "MXN",
        "operational_agent_email": "agente@apunto.com",
        "kind": "importation",
        "mode": "maritime"
    }
}

response = requests.post(url, headers=headers, data=json.dumps(data))
print(response.json())
fetch('https://control.apunto.io/api/v1/operations', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer TU_TOKEN',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    operation: {
      contact_code: 'ACME',
      currency_code: 'MXN',
      operational_agent_email: 'agente@apunto.com',
      kind: 'importation',
      mode: 'maritime'
    }
  })
})
.then(response => response.json())
.then(data => console.log(data));

Respuesta JSON (201 Created)

{
  "operation": {
    "id": 124,
    "identification": "IMP-002-2024",
    "kind": "importation",
    "mode": "maritime",
    "status": "confirmed",
    "client_ref": "REF-001",
    "goods_description": "Maquinaria industrial",
    "incoterm": "FOB",
    "created_at": "2024-01-16T09:15:00Z"
  },
  "message": "Operación creada exitosamente"
}

Crea una nueva operación.

Parámetros

Parámetro Tipo Requerido Descripción
contact_code string 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 Tipo de operación
mode string 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 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 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 (finishedclosed). 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 finishedcanceled). 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

  1. 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.
  2. 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_idcost_center_id.

  1. 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 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 Archivo (máx. 25 MB)
folderable_type Operation o Service
folderable_id 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

  1. GET /api/v1/operation_cancellation_reasons (o servicios) al iniciar sesión o cachear por cuenta.
  2. Mostrar al usuario la lista de name (como el modal web).
  3. Al cancelar, enviar operation_cancellation_reason_name con el texto elegido o el id obtenido 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
email 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 Nombre del contacto
alias string Alias corto (sin espacios)
kind array Tipos de contacto
identification string No RFC o identificación fiscal
legal_name string No Razón social
email 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 Nombre de la ubicación
address_type string Tipo de dirección
street string Calle
neighborhood string Colonia
city string Ciudad
state string Estado
postal_code string Código postal
country string 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 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 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

  1. Verifica códigos de estado - No te bases solo en el body
  2. Implementa reintentos - Para errores 5xx y rate limits (429)
  3. Usa backoff exponencial - Al reintentar peticiones fallidas
  4. Registra errores - Guarda respuestas de error para debugging
  5. Valida antes de enviar - Reduce errores 422 validando en cliente
  6. Maneja expiración de tokens - Prepárate para refrescar tokens
  7. 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}`);
    }
  }
}