v1

Introducción

La Digital Hub API permite a sistemas externos enviar embarques hacia Digital Hub (push) y consultar embarques existentes (pull), con soporte de webhooks salientes para notificar eventos en tiempo real.

Base URL
https://api.fulter.net
Autenticación
OAuth2 Client Credentials
Formato de error
RFC 7807 Problem Details
Token expira en
3600 segundos
Scopes y enterprise scope: Cada credencial tiene scopes (shipments:read / shipments:write) y un listado de empresas habilitadas.
Todos los endpoints filtran resultados por esas restricciones, el server nunca devuelve datos fuera del alcance configurado.
POST

Obtener token de acceso

/oauth/token

Implementa OAuth2 Client Credentials Grant (RFC 6749). El JWT devuelto debe incluirse en todas las requests como Authorization: Bearer <token>.
Expira a los 3600 segundos

Body (application/json)
CampoDescripción
grant_typerequeridoDebe ser client_credentials
client_idrequeridoID de cliente provisto por Digital Hub
client_secretrequeridoSecret provisto por Digital Hub
Ejemplo
curl -X POST https://api.fulter.net/oauth/token \
  -H "Content-Type: application/json" \
  -d '{"grant_type":"client_credentials","client_id":"mi-client-id","client_secret":"mi-secret"}'
Respuesta 200
json
{ "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "token_type": "Bearer", "expires_in": 3600 }
GET

Listar embarques

/shipments

Lista paginada de embarques accesibles para el cliente. Los resultados se filtran automáticamente por las empresas habilitadas en las credenciales.
Scope: shipments:read

Query params
ParámetroDescripción
pageopcionalNúmero de página. Default: 1
pageSizeopcionalResultados por página. Default: 20. Máximo: 100
enterpriseCodeopcionalFiltrar por empresa (debe estar en el enterprise scope del cliente)
statusopcionalinCoordination · inProgress · arrived · notAvailable
awbopcionalFiltrar por número de AWB
clientReferenceopcionalFiltrar por referencia del cliente
Ejemplo
curl "https://api.fulter.net/shipments?status=inProgress&page=1&pageSize=20" \
  -H "Authorization: Bearer $TOKEN"
Respuesta 200
json
{
  "data": [{ "id": "a1b2c3d4...", "awb": "020-98765432", "bl": null, "enterpriseCode": 999,
    "origin": "MIA", "destiny": "EZE", "land": "aerial", "status": "inProgress",
    "containers": [], "events": [], "createdAt": "2026-08-01T10:00:00.000Z", "updatedAt": "2026-08-10T14:00:00.000Z" }],
  "pagination": { "page": 1, "pageSize": 20, "totalItems": 1999, "totalPages": 8 }
}
GET

Obtener embarque por ID

/shipments/id/:id

Detalle completo de un embarque (contenedores y eventos). Devuelve 404 si no existe o si el enterpriseCode está fuera del scope del cliente.
Scope: shipments:read

Ejemplo
curl "https://api.fulter.net/shipments/id/a1b2c3d4-e5f6-7890-abcd-ef1234567890" \
  -H "Authorization: Bearer $TOKEN"
Respuesta 200
json
{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "awb": null, "bl": "MSCUXX1234567",
  "hawb": null, "clientReference": null,
  "enterpriseCode": 999, "origin": "SHA", "destiny": "BUE",
  "land": "maritime", "operationType": "importMaritime",
  "shipper": "ACME Corp", "status": "inCoordination",
  "ETD": "2026-09-01", "ETA": "2026-10-15",
  "containers": [{ "containerNumber": "MSCU1234567", "volume": 33.2, "quantity": 1, "weight": 12500, "type": "20DV" }],
  "events": [{ "eventDate": "2026-08-01T14:30:00.000Z", "statusCode": "DEPARTED", "eventPlace": "Shanghai", "flightNumber": null }],
  "createdAt": "2026-07-28T10:00:00.000Z", "updatedAt": "2026-08-01T14:30:00.000Z"
}
Para marítimos, bl contiene el Bill of Lading y awb es null
Para aéreos/terrestres es al revés.
GET

Buscar por AWB

/shipments/awb/:awb

Devuelve el embarque aéreo o terrestre con ese AWB/guía. Si el número corresponde a un embarque marítimo, devuelve 404 para marítimos usá /shipments/bl/:bl.
Scope: shipments:read

Ejemplo
curl "https://api.fulter.net/shipments/awb/020-98765432" \
  -H "Authorization: Bearer $TOKEN"
Respuesta 200
json
{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "awb": "020-98765432", "bl": null,
  "hawb": null, "clientReference": "REF-2026-001",
  "enterpriseCode": 999, "origin": "MIA", "destiny": "EZE",
  "land": "aerial", "operationType": "importAerial",
  "shipper": "ACME Corp", "trackingNumber": null,
  "ETD": "2026-09-01", "ETA": "2026-09-02", "status": "inProgress",
  "containers": [],
  "events": [
    { "eventDate": "2026-09-02T07:15:00.000Z", "statusCode": "ARRIVED", "eventPlace": "Ezeiza", "flightNumber": "AA931" }
  ],
  "createdAt": "2026-08-20T12:00:00.000Z", "updatedAt": "2026-09-02T07:15:00.000Z"
}
GET

Buscar por Bill of Lading

/shipments/bl/:bl

Devuelve el embarque marítimo con ese Bill of Lading. Si existe pero no es marítimo, devuelve 404.
Scope: shipments:read

Ejemplo
curl "https://api.fulter.net/shipments/bl/MSCUXX1234567" \
  -H "Authorization: Bearer $TOKEN"
Respuesta 200
json
{
  "id": "c3d4e5f6-a7b8-9012-cdef-012345678901",
  "awb": null, "bl": "MSCUXX1234567",
  "hawb": null, "clientReference": null,
  "enterpriseCode": 999, "origin": "SHA", "destiny": "BUE",
  "land": "maritime", "operationType": "importMaritime",
  "shipper": "ACME Corp", "trackingNumber": null,
  "ETD": "2026-09-01", "ETA": "2026-10-15", "status": "inCoordination",
  "containers": [
    { "containerNumber": "MSCU1234567", "volume": 33.2, "quantity": 1, "weight": 12500, "type": "20DV" }
  ],
  "events": [],
  "createdAt": "2026-07-28T10:00:00.000Z", "updatedAt": "2026-07-28T10:00:00.000Z"
}
GET

Buscar por referencia del cliente

/shipments/referenceNumber/:referenceNumber

Devuelve un arreglo de embarques que coincidan con la referencia dada. Puede retornar más de uno si la misma referencia fue usada en distintos embarques.
Scope: shipments:read

Ejemplo
curl "https://api.fulter.net/shipments/referenceNumber/REF-2026-001" \
  -H "Authorization: Bearer $TOKEN"
Respuesta 200
json — array
[
  { "id": "a1b2...", "awb": "020-98765432", "bl": null, "clientReference": "REF-2026-001", /* ... */ },
  { "id": "c3d4...", "awb": null, "bl": "MSCUXX9876543", "clientReference": "REF-2026-001", /* ... */ }
]
GET

Buscar por número de contenedor

/shipments/containers/:containerNumber

Devuelve el embarque marítimo que contiene ese número de contenedor. Devuelve 404 si no existe o si el contenedor no pertenece a un embarque marítimo.
Scope: shipments:read

Ejemplo
curl "https://api.fulter.net/shipments/containers/MSCU1234567" \
  -H "Authorization: Bearer $TOKEN"
Respuesta 200

Devuelve el embarque marítimo que contiene ese contenedor.

json
{
  "id": "c3d4e5f6-a7b8-9012-cdef-012345678901",
  "awb": null, "bl": "MSCUXX1234567",
  "hawb": null, "clientReference": null,
  "enterpriseCode": 999, "origin": "SHA", "destiny": "BUE",
  "land": "maritime", "operationType": "importMaritime",
  "shipper": "ACME Corp", "trackingNumber": null,
  "ETD": "2026-09-01", "ETA": "2026-10-15", "status": "inCoordination",
  "containers": [
    { "containerNumber": "MSCU1234567", "volume": 33.2, "quantity": 1, "weight": 12500, "type": "20DV" }
  ],
  "events": [],
  "createdAt": "2026-07-28T10:00:00.000Z", "updatedAt": "2026-07-28T10:00:00.000Z"
}
GET

Eventos de un embarque

/shipments/:id/events

Devuelve el historial de eventos de trazabilidad del embarque (salidas, llegadas, escalas, etc.).
Scope: shipments:read

La ruta es /shipments/:id/events — el ID va directo en el path, sin el prefijo /id/ que usan los otros endpoints.
Ejemplo
curl "https://api.fulter.net/shipments/a1b2c3d4-e5f6-7890-abcd-ef1234567890/events" \
  -H "Authorization: Bearer $TOKEN"
Respuesta 200
json — array
[
  { "eventDate": "2026-08-01T14:30:00.000Z", "statusCode": "DEPARTED", "eventPlace": "Shanghai", "flightNumber": null },
  { "eventDate": "2026-08-20T08:00:00.000Z", "statusCode": "ARRIVED", "eventPlace": "Buenos Aires", "flightNumber": null }
]
POST

Crear embarque (push)

/shipments

Registra un nuevo embarque. El enterpriseCode debe estar dentro del enterprise scope de las credenciales.
Scope requerido: shipments:write

Body (application/json)
CampoDescripción
awbsi es areo o terrestreAir Waybill o guía terrestre. Requerido cuando land es aerial o land.
blsi es marítimoBill of Lading. Requerido cuando land es maritime.
hawbopcionalHouse Air Waybill
clientReferenceopcionalReferencia interna del cliente. Máx. 250 caracteres.
enterpriseCoderequeridoCódigo de empresa (entero). Debe estar en el enterprise scope del cliente.
originrequeridoOrigen (código de aeropuerto, puerto o texto libre)
destinyrequeridoDestino
landrequeridomaritime · aerial · land
operationTyperequeridoimportAerial · exportAerial · importMaritime · exportMaritime · importLand · exportLand
shipperrequeridoNombre del shipper
trackingNumberopcionalNúmero de tracking (entero)
ETDrequeridoFecha estimada de salida. Formato: YYYY-MM-DD
ETArequeridoFecha estimada de llegada. Formato: YYYY-MM-DD
statusopcionalEstado inicial. Default: inCoordination. Valores: inCoordination · inProgress · arrived · notAvailable
containersopcionalArreglo de contenedores. Puede estar vacío o ausente, incluso para embarques marítimos.
Para los embarques terrestres el campo land debe ser land
Estructura de cada contenedor
CampoDescripción
containerNumberrequeridoNúmero de contenedor (ej. MSCU1234567)
volumerequeridoVolumen en m³ (número)
quantityrequeridoCantidad de bultos (entero)
weightrequeridoPeso en kg (número)
typeopcionalTipo de contenedor (ej. 20DV, 40HC)
Ejemplo — embarque aéreo
curl -X POST https://api.fulter.net/shipments \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "awb": "020-98765432",
    "enterpriseCode": 999,
    "origin": "MIA", "destiny": "EZE",
    "land": "aerial", "operationType": "importAerial",
    "shipper": "ACME Corp",
    "ETD": "2026-09-01", "ETA": "2026-09-02"
  }'
Ejemplo — embarque marítimo con contenedores
json — body
{
  "bl": "MSCUXX1234567",
  "enterpriseCode": 999, "origin": "SHA", "destiny": "BUE",
  "land": "maritime", "operationType": "importMaritime",
  "shipper": "ACME Corp", "ETD": "2026-09-01", "ETA": "2026-10-15",
  "containers": [
    { "containerNumber": "MSCU1234567", "volume": 33.2, "quantity": 1, "weight": 12500, "type": "20DV" }
  ]
}
Los contenedores son opcionales en todos los tipos de embarque, incluyendo marítimos. Podés cargar el embarque sin contenedores y agregarlos después vía PATCH.
Idempotencia: Si enviás el mismo embarque dos veces (mismo enterpriseCode + mismo AWB o BL), el servidor responde 409 Conflict.
Ver sección de idempotencia.
Respuesta 201
json
{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "awb": "020-98765432", "bl": null,
  "hawb": null, "clientReference": null,
  "enterpriseCode": 999, "origin": "MIA", "destiny": "EZE",
  "land": "aerial", "operationType": "importAerial",
  "shipper": "ACME Corp", "trackingNumber": null,
  "ETD": "2026-09-01", "ETA": "2026-09-02", "status": "inCoordination",
  "containers": [], "events": [],
  "createdAt": "2026-08-14T17:00:00.000Z", "updatedAt": "2026-08-14T17:00:00.000Z"
}
PATCH

Actualizar embarque

/shipments/id/:id

Actualización parcial de un embarque. Solo se modifican los campos presentes en el body. Al actualizar, se dispara el evento shipment.updated a los webhooks suscriptos de los demás clientes.
Scope requerido: shipments:write

Body (application/json) — todos los campos son opcionales
CampoDescripción
awbNuevo AWB (para embarques aéreos/terrestres)
blNuevo BL (para embarques marítimos)
hawbHouse AWB
clientReferenceReferencia del cliente. Máx. 250 caracteres.
originNuevo origen
destinyNuevo destino
operationTypeTipo de operación (ver valores en POST)
shipperShipper
trackingNumberNúmero de tracking (entero)
ETDFecha estimada de salida (YYYY-MM-DD)
ETAFecha estimada de llegada (YYYY-MM-DD)
containersReemplaza todos los contenedores. Si se incluye, debe tener al menos 1 contenedor.
Caso 1 — Corregir ETA

El barco tuvo demora y el ETA cambió. Solo se envía el campo que se quiere modificar.

curl -X PATCH \
  https://api.fulter.net/shipments/id/c3d4e5f6-a7b8-9012-cdef-012345678901 \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"ETA": "2026-11-10"}'
Caso 2 — Agregar contenedores

El embarque se cargó sin contenedores. Cuando la información estuvo disponible se los carga con PATCH. El campo containers reemplaza la lista completa: si ya había contenedores hay que incluirlos también en el array para no perderlos.

json — body
{
  "containers": [
    { "containerNumber": "MSCU1234567", "volume": 33.2, "quantity": 480, "weight": 12500, "type": "20DV" },
    { "containerNumber": "TCKU9876543", "volume": 67.5, "quantity": 210, "weight": 18200, "type": "40HC" }
  ]
}
Respuesta 200

Devuelve el embarque con todos los campos actualizados.

json
{
  "id": "c3d4e5f6-a7b8-9012-cdef-012345678901",
  "awb": null, "bl": "MSCUXX1234567",
  "hawb": null, "clientReference": null,
  "enterpriseCode": 999, "origin": "SHA", "destiny": "BUE",
  "land": "maritime", "operationType": "importMaritime",
  "shipper": "ACME Corp", "trackingNumber": null,
  "ETD": "2026-09-01", "ETA": "2026-11-10", "status": "inCoordination",
  "containers": [
    { "containerNumber": "MSCU1234567", "volume": 33.2, "quantity": 480, "weight": 12500, "type": "20DV" },
    { "containerNumber": "TCKU9876543", "volume": 67.5, "quantity": 210, "weight": 18200, "type": "40HC" }
  ],
  "events": [],
  "createdAt": "2026-07-28T10:00:00.000Z", "updatedAt": "2026-08-14T16:42:00.000Z"
}
GET

Listar suscripciones de webhook

/webhook-subscriptions

Devuelve todas las suscripciones activas del cliente autenticado.
Scope: shipments:read

Ejemplo
curl https://api.fulter.net/webhook-subscriptions \
  -H "Authorization: Bearer $TOKEN"
Respuesta 200
json
[
  {
    "id": "b9c8d7e6-f5a4-3210-fedc-ba9876543210",
    "url": "https://mi-sistema.com/webhooks/dh",
    "events": ["shipment.created", "shipment.updated"],
    "status": "active",
    "createdAt": "2026-08-01T09:00:00.000Z"
  }
]
POST

Crear suscripción de webhook

/webhook-subscriptions

Registra una URL para recibir notificaciones de eventos en tiempo real. Digital Hub hace un POST a esa URL cada vez que ocurre uno de los eventos suscriptos.
Scope requerido: shipments:write

¿Qué empresas cubre la suscripción? La suscripción cubre automáticamente todas las empresas habilitadas en las credenciales del cliente. Si tu cliente tiene acceso a los enterprise codes 999, 1225 y 87 (tres razones sociales distintas), una sola suscripción recibe eventos de las tres.
No necesitás crear una suscripción por empresa. Si querés recibir eventos de un subconjunto específico, contactá a Digital Hub para ajustar el enterprise scope de tus credenciales.
Sin auto-notificación: Si vos mismo originaste el evento — por ejemplo, hiciste POST /shipments — no recibirás ese webhook.
El sistema excluye al cliente originador automáticamente para evitar loops. Sí recibirás eventos generados por otros sistemas o clientes que operen sobre las mismas empresas.
Body (application/json)
CampoDescripción
urlrequeridoURL HTTPS a la que Digital Hub enviará los eventos. Debe ser accesible públicamente.
eventsrequeridoEventos a suscribir: "shipment.created" y/o "shipment.updated"
secretrequeridoSecret HMAC que Digital Hub usará para firmar cada entrega (X-DH-Signature). Guardalo de forma segura — no se devuelve en consultas posteriores.
Ejemplo
curl -X POST https://api.fulter.net/webhook-subscriptions \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://mi-sistema.com/webhooks/dh",
    "events": ["shipment.created", "shipment.updated"],
    "secret": "mi-secret-hmac-muy-seguro"
  }'
Respuesta 201
json
{
  "id": "b9c8d7e6-f5a4-3210-fedc-ba9876543210",
  "url": "https://mi-sistema.com/webhooks/dh",
  "events": ["shipment.created", "shipment.updated"],
  "status": "active",
  "createdAt": "2026-08-13T12:00:00.000Z"
}
DELETE

Eliminar suscripción de webhook

/webhook-subscriptions/:id

Elimina una suscripción. Devuelve 204 No Content si se eliminó correctamente, o 404 si no existe o pertenece a otro cliente.
Scope requerido: shipments:write

Ejemplo
curl -X DELETE \
  https://api.fulter.net/webhook-subscriptions/b9c8d7e6-f5a4-3210-fedc-ba9876543210 \
  -H "Authorization: Bearer $TOKEN"

Formato de entrega

Digital Hub hace un POST a la URL registrada con Content-Type: application/json. El body tiene esta estructura:

json — evento shipment.created
{
  "id": "evt-uuid-v4",                         // ID único del evento
  "type": "shipment.created",                 // o "shipment.updated"
  "createdAt": "2026-08-13T12:30:00.000Z",    // ISO 8601 UTC
  "data": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "awb": "020-98765432", "bl": null,
    "enterpriseCode": 999,
    "land": "aerial", "status": "inCoordination",
    // ... mismo schema que GET /shipments/id/:id
  }
}
Reintentos: Si la URL responde con un código diferente a 2xx, Digital Hub reintenta hasta 5 veces con backoff exponencial (1s → 2s → 4s → 8s → 16s). Después del 5° intento fallido, el evento queda registrado como fallido.
Tiempo de respuesta: Tu endpoint debe responder antes de 10 segundos. Si solo necesitás confirmar recepción, respondé 200 inmediatamente y procesá en background.

Verificación HMAC

Cada entrega incluye el header X-DH-Signature con un HMAC-SHA256 del body completo, firmado con el secret que configuraste al crear la suscripción. Verificar esta firma es la única forma de confirmar que el request viene de Digital Hub.

Cómo verificar
import { createHmac, timingSafeEqual } from 'crypto';

function verifySignature(rawBody: string, signature: string, secret: string): boolean {
  const expected = createHmac('sha256', secret).update(rawBody).digest('hex');
  return timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}

// Express — leer el body raw antes de parsear JSON:
app.post('/webhooks/dh', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['x-dh-signature'] as string;
  if (!verifySignature(req.body.toString(), sig, process.env.WEBHOOK_SECRET!))
    return res.status(401).send('Invalid signature');
  const event = JSON.parse(req.body.toString());
  res.sendStatus(200);
});
Usá siempre una comparación timing-safe para evitar ataques de timing. Una comparación con === no es suficiente.

Errores (RFC 7807)

Todos los errores usan Problem Details for HTTP APIs (RFC 7807) con Content-Type: application/problem+json.

Estructura
json
{
  "type":   "https://docs.digitalhub.com/errors/validation-error",
  "title":  "Error de validación",
  "status": 400,
  "detail": "El campo ETD es obligatorio\nEl campo ETA es obligatorio"
}
Códigos
StatusslugCuándo ocurre
400validation-errorBody inválido o campos incorrectos. El detail describe cada error, uno por línea.
401unauthorizedToken ausente, expirado o inválido.
403forbiddenEl token no tiene el scope requerido para este endpoint.
404not-foundRecurso inexistente o fuera del enterprise scope del cliente.
409conflictEl embarque ya existe (ver idempotencia).
500internal-errorError inesperado. El detail es genérico por seguridad.

Idempotencia

Para evitar embarques duplicados, POST /shipments aplica idempotencia automática basada en la combinación de enterpriseCode + identificador principal del embarque (awb para aéreos/terrestres, bl para marítimos).

Clave de idempotencia
formato interno
// Embarque aéreo con awb="020-98765432" en la empresa 999:
key = "999::020-98765432"

// Embarque marítimo con bl="MSCUXX1234567" en la empresa 999:
key = "999::MSCUXX1234567"

Si enviás el mismo embarque dos veces (misma clave), el servidor responde 409 Conflict:

json — 409
{
  "type":   "https://docs.digitalhub.com/errors/conflict",
  "title":  "Conflicto",
  "status": 409,
  "detail": "Este embarque ya se encuentra en nuestro sistema"
}
La clave es por empresa + número de embarque, no por contenedor. Podés actualizar los contenedores de un embarque ya cargado usando PATCH /shipments/id/:id sin generar un 409.