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, 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í:

  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. Ciclo comercial — Prospecto, oportunidad, solicitud comercial y cotización. La cotización abierta se convierte en operación.
  7. 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.
  8. 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:

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ón → Tokens 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 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

  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_id → cost_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 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

  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 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
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 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

  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}`);
    }
  }
}