API Pública de Sourced v1
La API de Sourced permite que los sistemas ERP (como Calipso, SAP, JD Edwards) se integren directamente con la plataforma de compras de Sourced. Úsala para:
- Enviar requisiciones de compra desde tu ERP a Sourced
- Obtener órdenes de compra de vuelta en tu ERP una vez adjudicadas
- Verificar qué requisiciones ya fueron sincronizadas
- Monitorear el estado de tus requisiciones
La API sigue las convenciones REST, utiliza JSON para los cuerpos de solicitud y respuesta, y se autentica mediante API keys.
Autenticación
Todos los endpoints (excepto health check) requieren una API key enviada en el header X-API-Key.
Las API keys se crean en el panel de administración de Sourced en Configuración → API Keys. Cada key se muestra solo una vez al crearla. Guardala de forma segura.
curl -H "X-API-Key: sk_live_abc123..." \
https://api.gosourced.ai/api/v1/requisitionsURL Base
| Ambiente | URL Base |
|---|---|
| Producción | https://api.gosourced.ai |
| Desarrollo | https://api-dev.gosourced.ai |
Todos los endpoints tienen el prefijo /api/v1.
Límites de tasa
Todos los endpoints están limitados a 60 solicitudes por minuto por endpoint. Exceder este límite devuelve una respuesta 429 Too Many Requests.
Para operaciones masivas, usá el endpoint de importación por lotes (POST /requisitions) que acepta hasta 100 requisiciones por solicitud.
Errores
La API utiliza códigos de estado HTTP estándar. Los errores devuelven un cuerpo JSON con un campo detail.
{
"detail": "Invalid or revoked API key."
}| Estado | Significado |
|---|---|
| 200 | Éxito |
| 400 | Solicitud inválida - error de validación o cuerpo malformado |
| 401 | No autorizado - API key faltante o inválida |
| 404 | No encontrado - el recurso no existe o no es accesible |
| 422 | Entidad no procesable - el cuerpo de la solicitud no pasó la validación del esquema |
| 429 | Demasiadas solicitudes - límite de tasa excedido |
| 500 | Error interno del servidor - error inesperado de nuestro lado |
Ejemplos de errores
{
"detail": "Invalid or revoked API key."
}{
"detail": [
{
"loc": ["body", "requisitions", 0, "items", 0, "quantity"],
"msg": "Input should be greater than 0",
"type": "greater_than"
}
]
}{
"success": false,
"created_count": 2,
"updated_count": 0,
"skipped_count": 0,
"error_count": 1,
"errors": [
{
"index": 2,
"external_id": "REQ-2026-0099",
"error": "Failed to process requisition: duplicate external_line_id within items"
}
],
"requisition_ids": [1234, 1235]
}Nota: el endpoint POST /requisitions devuelve 200 incluso con fallos parciales. Siempre verificá el campo success y el array errors para detectar problemas por requisición.
Adjuntos
El archivo queda adjunto a la requisición, el comprador lo ve y viaja al proveedor cuando la requisición se lanza.
Las dos formas de mandar un archivo
| Por presign (file_key) | Por URL (url) | |
|---|---|---|
| Quién sube el archivo | Tu sistema, directo a Sourced | Sourced, desde tu URL |
| Tamaño máximo | 500 MB | 50 MB |
| Cuándo está disponible | Cuando responde el POST /requisitions | Después de la descarga, que ocurre luego de la respuesta; mientras tanto la requisición no se puede lanzar |
| Necesita una URL accesible desde internet | No | Sí |
Paso a paso con presign
1. Pedí la URL de subida
Mandá el nombre del archivo con purpose "attachment". Sin ese purpose el archivo no se adjunta a ninguna requisición.
curl -X POST https://api.gosourced.ai/api/v1/files/presign \
-H "X-API-Key: sk_live_abc123..." \
-H "Content-Type: application/json" \
-d '{
"filename": "plano-bomba.pdf",
"content_type": "application/pdf",
"purpose": "attachment"
}'{
"upload_url": "https://s3.amazonaws.com/your-bucket",
"fields": {
"key": "api-uploads/42/3f1c9a2e/plano-bomba.pdf",
"Content-Type": "application/pdf",
"policy": "eyJleHBpcmF0aW9uIjoi...",
"x-amz-signature": "a1b2c3..."
},
"key": "api-uploads/42/3f1c9a2e/plano-bomba.pdf",
"max_file_size": 524288000,
"expires_in": 600
}2. Subí el archivo a upload_url
Es un POST multipart/form-data: primero todos los campos de fields y al final el campo file. Si sale bien, S3 responde 204.
curl -X POST "https://s3.amazonaws.com/your-bucket" \
-F "key=api-uploads/42/3f1c9a2e/plano-bomba.pdf" \
-F "Content-Type=application/pdf" \
-F "policy=eyJleHBpcmF0aW9uIjoi..." \
-F "x-amz-signature=a1b2c3..." \
-F "file=@plano-bomba.pdf"
# HTTP 204 No Content3. Importá la requisición con file_key
Mandá el key de la respuesta como file_key. El adjunto de cabecera va en attachments y el de una línea en items[].attachments; esa línea tiene que traer external_line_id. Cada adjunto lleva url o file_key, nunca los dos. Repetí los pasos 1 y 2 por cada archivo.
curl -X POST https://api.gosourced.ai/api/v1/requisitions \
-H "X-API-Key: sk_live_abc123..." \
-H "Content-Type: application/json" \
-d '{
"requisitions": [
{
"external_id": "REQ-2026-0042",
"attachments": [
{
"file_key": "api-uploads/42/3f1c9a2e/plano-bomba.pdf",
"filename": "plano-bomba.pdf"
}
],
"items": [
{
"external_line_id": "L10",
"description": "Bomba centrifuga",
"quantity": 2,
"attachments": [
{
"file_key": "api-uploads/42/8b7d4c10/ficha-tecnica.pdf",
"filename": "ficha-tecnica.pdf"
}
]
}
]
}
]
}'{
"success": true,
"created_count": 1,
"updated_count": 0,
"skipped_count": 0,
"error_count": 0,
"errors": [],
"requisition_ids": [1234]
}4. Consultá el estado
Con file_key el archivo queda confirmado cuando responde el POST. GET /requisitions/{id} devuelve attachments con filename, scope, external_line_id, status y error de cada archivo.
curl -H "X-API-Key: sk_live_abc123..." \
https://api.gosourced.ai/api/v1/requisitions/1234{
"id": 1234,
"external_id": "REQ-2026-0042",
"status": "PENDING",
"items": [...],
"attachments": [
{ "filename": "plano-bomba.pdf", "scope": "header", "external_line_id": null, "status": "DOWNLOADED", "error": null },
{ "filename": "ficha-tecnica.pdf", "scope": "line", "external_line_id": "L10", "status": "DOWNLOADED", "error": null }
]
}Con una URL pública
Mandá url en lugar de file_key (hasta 50 MB). Sourced descarga el archivo después de responder, así que consultá GET /requisitions/{id} hasta que pase de PENDING a DOWNLOADED.
{
"requisitions": [
{
"external_id": "REQ-2026-0043",
"attachments": [
{ "url": "https://erp.example.com/files/plano-bomba.pdf", "filename": "plano-bomba.pdf" }
],
"items": [{ "description": "Bomba centrifuga", "quantity": 2 }]
}
]
}Qué significa cada estado y qué hacer
| PENDING | Se está descargando (solo pasa con url). Esperá: la requisición no se puede lanzar hasta que termine. Si pasan 15 minutos sin avance, pasa a FAILED con error timeout. |
| DOWNLOADED | Disponible: viaja con el pedido al lanzar. No tenés que hacer nada. |
| FAILED | Leé error. Si fue un problema del archivo o de la descarga, reenviá la requisición con ese adjunto. Si la línea cambió de material, mandá un archivo nuevo. No bloquea el lanzamiento. |
| SKIPPED_LAUNCHED | No quedaba dónde adjuntarlo porque la línea, o todas las de la requisición si es de cabecera, ya se lanzó. Los adjuntos viajan en el lanzamiento. No hay nada para hacer desde la API. |
Un adjunto mal armado queda FAILED con su error y el resto del lote sigue. Podés mandar hasta 20 adjuntos en la cabecera y hasta 20 en cada línea; los que pasen ese tope quedan FAILED.
Reenvíos
- Si attachments no viene en el reenvío, se conservan los que ya había.
- Si viene, reemplaza la lista de ese alcance (cabecera o línea); con [] los quita todos.
- Un archivo igual a uno ya cargado no se procesa de nuevo. Se compara por file_key, o por la URL y el nombre. De la URL solo se ignoran las firmas de S3, Google Cloud y Azure; cualquier otro parámetro cuenta, así que si tu sistema firma las URLs de otra forma el archivo se descarga de nuevo en cada reenvío.
- Si el material de una línea cambió, sus adjuntos no se conservan, porque eran del material anterior. Para confirmar un archivo con el material nuevo, mandá un archivo nuevo.
- Un archivo dirigido a una línea ya lanzada, o de cabecera cuando la requisición ya no tiene líneas abiertas porque alguna se lanzó, no se agrega (queda SKIPPED_LAUNCHED): los adjuntos viajan en el lanzamiento.
Al lanzar
- Una requisición con adjuntos todavía en descarga (PENDING) no se puede lanzar hasta que terminen.
- Un adjunto FAILED no bloquea el lanzamiento.
Health Check
/api/v1/healthVerificar que la API esté accesible. No requiere autenticación.
curl https://api.gosourced.ai/api/v1/health{
"status": "ok",
"api": "v1"
}Subir un archivo (Presigned)
/api/v1/files/presignAPI Key requeridaObtené una URL prefirmada para subir un archivo directo a S3. Los bytes no pasan por la API, así que los archivos pesados suben sin chocar los límites de tamaño.
Para adjuntar archivos a una requisición, mirá la guía de Adjuntos.
Cuerpo del request
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| filename | string | requerido | Nombre original del archivo (ej. 'plano-bomba.pdf'). Se eliminan los componentes de ruta. |
| content_type | string | opcional | Tipo MIME. Si se omite, se acepta cualquier tipo. |
| purpose | string | opcional | Usá "attachment" para un archivo que vas a referenciar como file_key en un adjunto de requisición. Sin purpose, el archivo no se adjunta a ninguna requisición. |
curl -X POST https://api.gosourced.ai/api/v1/files/presign \
-H "X-API-Key: sk_live_abc123..." \
-H "Content-Type: application/json" \
-d '{
"filename": "plano-bomba.pdf",
"content_type": "application/pdf",
"purpose": "attachment"
}'{
"upload_url": "https://s3.amazonaws.com/your-bucket",
"fields": {
"key": "api-uploads/42/3f1c9a2e/plano-bomba.pdf",
"Content-Type": "application/pdf",
"policy": "eyJleHBpcmF0aW9uIjoi...",
"x-amz-signature": "a1b2c3..."
},
"key": "api-uploads/42/3f1c9a2e/plano-bomba.pdf",
"max_file_size": 524288000,
"expires_in": 600
}Campos de la respuesta
- upload_url - la URL de S3 a la que se hace el POST del archivo.
- fields - campos del formulario que deben incluirse en el POST multipart, antes del campo file.
- key - la clave bajo la que se guardará el archivo.
- max_file_size - tamaño máximo permitido en bytes (lo aplica S3).
- expires_in - segundos hasta que vence la URL prefirmada.
# Use upload_url + every key in "fields", then the file (last).
curl -X POST "$UPLOAD_URL" \
-F "key=api-uploads/42/3f1c9a2e/plano-bomba.pdf" \
-F "Content-Type=application/pdf" \
-F "policy=eyJleHBpcmF0aW9uIjoi..." \
-F "x-amz-signature=a1b2c3..." \
-F "file=@plano-bomba.pdf"
# HTTP 204 No Content on successImportar requisiciones
/api/v1/requisitionsAPI Key requeridaImportar una o más requisiciones de compra desde tu ERP. Soporta importación por lotes de hasta 100 requisiciones por solicitud. Usa external_id para deduplicación.
Cuerpo de la solicitud
El cuerpo debe contener un array requisitions con 1 a 100 objetos de requisición.
Objeto Requisición
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| external_id | string | requerido | ID único en tu ERP (clave de deduplicación). Máx 250 caracteres. |
| items | array | requerido | Items de línea (1-200 items). Ver esquema de Item abajo. |
| requester_name | string | opcional | Nombre de la persona solicitante. Máx 200 caracteres. |
| requester_email | string | opcional | Email del solicitante. Máx 200 caracteres. |
| assigned_buyer | string | opcional | Comprador asignado en el ERP de origen (texto libre). Se muestra y filtra en el triage. Máx 200 caracteres. |
| description | string | opcional | Título o resumen de la requisición. Máx 500 caracteres. |
| comments | string | opcional | Instrucciones adicionales para compradores (HTML soportado). Máx 15.000 caracteres. |
| delivery_address | string | opcional | Dirección de entrega en texto libre. Máx 500 caracteres. |
| delivery_address_code | string | opcional | Código que coincide con una dirección configurada en Sourced. Máx 50 caracteres. |
| desired_delivery_lead_time_days | integer | opcional | Plazo de entrega deseado en días desde la confirmación de la OC. La solicitud de compra tiene un único plazo: este valor se usa solo si ninguna línea del paquete trae el suyo. |
| created_date | datetime | opcional | Fecha de creación de la requisición en tu sistema (ISO 8601). Se muestra como "Creación" en el triage. No es una fecha de entrega: la necesidad va por item en desired_delivery_date. |
| offer_deadline | datetime | opcional | Fecha límite para cotizaciones de proveedores (ISO 8601; sin zona horaria se toma como UTC). Al lanzar, precarga la fecha límite del modal; el comprador puede cambiarla. Si ya pasó, se ignora. |
| total_estimated_value | float | opcional | Valor total estimado. Se calcula automáticamente desde los items si se omite. |
| currency | string | opcional | Código de moneda (ej: "ARS", "USD", "PYG"). Por defecto, la moneda local de tu organización. Máx 10 caracteres. |
| department_code | string | opcional | Código de centro de costo. Se guarda como dato de la requisición; no se vincula a los departamentos de Sourced ni cambia el ruteo de aprobaciones. Máx 50 caracteres. |
| priority_level | string | opcional | Prioridad: "LOW", "MEDIUM", "HIGH" o "CRITICAL". En Sourced se ve como NORMAL (LOW y MEDIUM), HIGH o URGENT (CRITICAL). Si no lo mandás, la requisición queda NORMAL; en un re-envío sin este campo se conserva el valor anterior. |
| document_type | object | opcional | Tipo de documento de tu ERP: objeto { code, label? } (código máx 100 caracteres, etiqueta máx 200). Ej.: { "code": "NB", "label": "Solicitud estándar" }. En un re-envío: si no lo mandás se conserva; con null se borra. |
| internal_note | string | opcional | Nota interna de compras. Nunca se comparte con el proveedor. Si viene, tiene prioridad sobre comments. Máx 15.000 caracteres. En un re-envío: si no la mandás se conserva; con null se borra. |
| custom_fields | object | opcional | Campos libres de tu empresa, que se muestran en Sourced: objeto { clave: { label, value, type? } }. La clave usa minúsculas, números y _ (máx 50); hasta 30 campos; type es "text" (default), "number" o "date". No cambian ningún proceso. En un re-envío: si no los mandás se conservan; con null se borran. |
| attachments | array | opcional | Adjuntos de cabecera: viajan con toda la requisición (máx 20). Cada uno lleva exactamente una fuente: url o file_key. Para adjuntos de una línea usá items[].attachments. Ver esquema de Adjunto abajo. |
| raw_data | object | opcional | JSON arbitrario de tu ERP, almacenado para trazabilidad. |
Objeto Item
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| description | string | requerido | Visible para el proveedorNombre o descripción del item. Máx 1.000 caracteres. |
| quantity | float | requerido | Visible para el proveedorCantidad requerida (debe ser > 0). |
| external_line_id | string | opcional | ID de línea en tu ERP (ej: "REQ-001-L10"). Se devuelve en las OC para trazabilidad. Máx 100 caracteres. |
| unit_of_measure | string | opcional | Visible para el proveedorCódigo de UOM (ej: "KG", "EA", "LT", "M", "UN"). Se compara con el catálogo de tu org. Máx 50 caracteres. |
| target_price | float | opcional | Precio objetivo/presupuesto por unidad. |
| estimated_price | float | opcional | Obsoleto, se mantiene por compatibilidad: precio estimado de la línea, que Sourced trata como precio UNITARIO. Usá estimated_unit_price. |
| currency | string | opcional | Moneda para precios (ej: "ARS", "USD"). Máx 10 caracteres. |
| category | string | opcional | Categoría desde tu ERP. Máx 200 caracteres. |
| material_code | string | opcional | Visible para el proveedorCódigo de material/parte en tu ERP (ej: código de material SAP). Máx 100 caracteres. |
| specifications | object | opcional | Especificaciones técnicas. Ver esquema de Especificaciones abajo. |
| desired_delivery_date | datetime | opcional | Fecha de entrega deseada para este item (ISO 8601, ej: "2026-05-15T00:00:00Z"). |
| desired_delivery_lead_time_days | integer | opcional | Plazo de entrega deseado en días para esta línea. La solicitud de compra tiene un único plazo: si varias líneas del paquete lo traen, se aplica el menor; si ninguna lo trae, se usa el de cabecera. |
| detail | string | opcional | Visible para el proveedorDato técnico o nota de esta línea. Máx 20.000 caracteres: si es más largo se trunca, sin rechazar el lote. Se comparte con el proveedor: se incluye en el mail de solicitud de cotización bajo «Especificaciones», salvo que mandes specifications.technical_specs, que tiene prioridad. No lo uses para notas internas de compras. |
| technical_text | string | opcional | Visible para el proveedorTexto técnico de la línea. Es el mismo dato que detail con un nombre explícito: si mandás los dos, gana technical_text. Se comparte con el proveedor. Máx 20.000 caracteres: si es más largo se trunca, sin rechazar el lote. |
| estimated_unit_price | float | opcional | Precio unitario estimado de la línea. Si viene, tiene prioridad sobre estimated_price y target_price. |
| position | string | opcional | Posición o número de línea en tu ERP, para mostrar (ej. "0010"). Para identificar la línea se sigue usando external_line_id. Máx 50 caracteres. |
| internal_note | string | opcional | Nota interna de compras de esta línea. Nunca se comparte con el proveedor. Máx 2.000 caracteres. |
| delivery_address_code | string | opcional | Código del destino de esta línea (una dirección configurada en Sourced). Si no viene, se aplica el delivery_address_code de la requisición. Máx 50 caracteres. |
| assigned_buyer | string | opcional | Comprador asignado a esta línea. Si no viene, se aplica el assigned_buyer de la requisición. Máx 200 caracteres. |
| suggested_supplier | object | opcional | Proveedor sugerido por quien pidió: objeto { code?, name? } (al menos uno). Es solo informativo. |
| cost_center | object | opcional | Centro de costo de la línea: { code, label? }. Si no viene, se usa department_code de la requisición. |
| item_category | object | opcional | Clasificación del artículo en tu ERP (grupo de artículo): { code, label? }. Si viene, tiene prioridad sobre category. |
| purchasing_group | object | opcional | Grupo o área de compras que gestiona la línea: { code, label? }. Es solo un dato: no cambia el ruteo de aprobaciones. |
| released_at | datetime | opcional | Fecha de liberación o aprobación de la línea en tu ERP (ISO 8601). Si no la mandás, Sourced usa la primera vez que recibió la línea. |
| custom_fields | object | opcional | Campos libres de la línea, con el mismo formato que custom_fields de la requisición (hasta 30). |
| attachments | array | opcional | Adjuntos de esta línea (máx 20), mismo esquema que los de cabecera. Exige external_line_id en la línea: sin él, cada adjunto queda FAILED. |
| raw_data | object | opcional | JSON arbitrario para este item. |
Objeto Especificaciones
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| manufacturer_code | string | opcional | Visible para el proveedorNúmero de parte del fabricante (ej: "6ES7214-1AG40-0XB0"). Máx 100 caracteres. |
| manufacturer_name | string | opcional | Visible para el proveedorNombre del fabricante (ej: "Siemens"). Máx 200 caracteres. |
| manufacturer_description | string | opcional | Descripción del fabricante. Máx 500 caracteres. |
| buyer_code | string | opcional | Visible para el proveedorCódigo interno en tu sistema (ej: "MAT-001234"). Máx 100 caracteres. |
| buyer_code_description | string | opcional | Descripción del código interno. Máx 500 caracteres. |
| buyer_code_system | string | opcional | Nombre del sistema de origen (ej: "SAP", "Calipso"). Máx 50 caracteres. |
| technical_specs | string | opcional | Visible para el proveedorEspecificaciones técnicas en texto libre (ej: "220V, 50Hz, IP55"). Máx 20.000 caracteres: si es más largo se trunca, sin rechazar el lote. Se comparte con el proveedor en el mail de solicitud de cotización. Tiene prioridad sobre el detail del ítem. |
| requirements | string | opcional | Requisitos adicionales (ej: "Certificación ISO 9001 requerida"). Máx 2.000 caracteres. Uso interno: no llega al proveedor ni se incluye en el mail. |
Objeto Adjunto
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| url | string | opcional | URL públicamente accesible para descargar el archivo (hasta 50 MB). Máx 2.000 caracteres. Mandá url o file_key, nunca las dos ni ninguna. |
| file_key | string | opcional | Clave devuelta por POST /files/presign con purpose "attachment", después de subir el archivo (hasta 500 MB). Mandá url o file_key, nunca las dos ni ninguna. |
| filename | string | requerido | Nombre de archivo original (ej: "plano_motor.pdf"). Máx 255 caracteres. |
| description | string | opcional | Descripción del adjunto. Máx 500 caracteres. |
| file_type | string | opcional | Tipo MIME (se detecta automáticamente si no se proporciona). Máx 100 caracteres. |
Los adjuntos (cabecera y líneas), sus estados, los reenvíos y el lanzamiento están en la guía de Adjuntos.
Al lanzar
- offer_deadline precarga la fecha límite del modal de lanzamiento, y el comprador puede cambiarla.
- La solicitud de compra tiene un único plazo de entrega: se aplica el menor de desired_delivery_lead_time_days entre las líneas del paquete; si ninguna lo trae, el de cabecera.
- technical_specs, technical_text y detail aceptan hasta 20.000 caracteres; el texto más largo se trunca sin rechazar el lote. requirements es interno y no llega al proveedor.
Lógica de deduplicación
Cada requisición se identifica por su external_id (dentro del alcance de tu organización):
- Nueva: Si no se encuentra una requisición existente, se crea una nueva con estado
PENDING. - Actualizar: Si se encuentra una requisición existente con estado
PENDING, se actualiza in-place. Los items se reemplazan completamente. En la cabecera, los campos document_type, internal_note y custom_fields se conservan si no los mandás y se borran si mandás null; el resto mantiene su comportamiento actual. - Superseder: Si se encuentra una requisición existente con estado
SUPERSEDED, se reactiva (vuelve a PENDING) con los nuevos datos. - Lanzada: Si la requisición ya fue lanzada como Solicitud de Compra (
LAUNCHED), la importación se rechaza. La PR activa no puede ser sobreescrita. - Descartada: Si la requisición fue descartada previamente (
DISCARDED), se reactiva (vuelve a PENDING) con los nuevos datos. Útil cuando el ERP la cancela y luego la reenvía.
curl -X POST https://api.gosourced.ai/api/v1/requisitions \
-H "X-API-Key: sk_live_abc123..." \
-H "Content-Type: application/json" \
-d '{
"requisitions": [
{
"external_id": "REQ-2026-0042",
"items": [
{
"description": "Bearing SKF 6205",
"quantity": 10
}
]
}
]
}'Respuesta
ERPImportResult
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| success | boolean | requerido | true si error_count es 0. |
| created_count | integer | requerido | Cantidad de nuevas requisiciones creadas. |
| updated_count | integer | requerido | Cantidad de requisiciones PENDING existentes actualizadas. |
| skipped_count | integer | requerido | Cantidad de requisiciones omitidas (actualmente siempre 0). |
| error_count | integer | requerido | Cantidad de requisiciones que fallaron al importar. |
| errors | array | requerido | Array de {index, external_id, error} para cada requisición fallida. |
| requisition_ids | array | requerido | IDs internos de Sourced de las requisiciones creadas/actualizadas. |
{
"success": true,
"created_count": 1,
"updated_count": 0,
"skipped_count": 0,
"error_count": 0,
"errors": [],
"requisition_ids": [1234]
}Listar requisiciones
/api/v1/requisitionsAPI Key requeridaObtener una lista paginada de tus requisiciones importadas. Solo devuelve requisiciones creadas a través de la API.
Parámetros de consulta
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| page | integer | opcional | Número de página (por defecto: 1). |
| page_size | integer | opcional | Items por página, 1-100 (por defecto: 20). |
| status | string | opcional | Filtrar por estado: "PENDING", "LAUNCHED", "DISCARDED", "SUPERSEDED". |
curl -H "X-API-Key: sk_live_abc123..." \
"https://api.gosourced.ai/api/v1/requisitions?page=1&page_size=20&status=PENDING"{
"data": [
{
"id": 1234,
"external_id": "REQ-2026-0042",
"status": "PENDING",
"requester_name": "Juan Pérez",
"description": "Repuestos línea producción",
"items": [...],
"attachments": [
{ "filename": "plano.pdf", "scope": "header", "external_line_id": null, "status": "DOWNLOADED", "error": null }
],
"imported_at": "2026-03-07T14:30:00Z"
}
],
"total": 45,
"page": 1,
"page_size": 20,
"has_more": true
}La respuesta incluye attachments con filename, scope, external_line_id, status y error de cada archivo. Más detalle en la guía de Adjuntos.
Adjunto en la respuesta
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| filename | string | requerido | Nombre del archivo. |
| scope | string | requerido | "header" si es de cabecera, "line" si es de una línea. |
| external_line_id | string | opcional | external_line_id de la línea, o null si es de cabecera. |
| status | string | requerido | Estado del archivo: PENDING, DOWNLOADED, FAILED o SKIPPED_LAUNCHED. |
| error | string | opcional | Motivo, solo cuando el estado es FAILED; null en los demás casos. |
Detalle de requisición
/api/v1/requisitions/{requisition_id}API Key requeridaObtener una requisición individual por su ID interno de Sourced, incluyendo todos los items.
curl -H "X-API-Key: sk_live_abc123..." \
https://api.gosourced.ai/api/v1/requisitions/1234La respuesta incluye attachments con filename, scope, external_line_id, status y error de cada archivo. Más detalle en la guía de Adjuntos.
Devuelve el objeto completo de la requisición. Devuelve 404 si no se encuentra o no pertenece a tu organización.
Verificar requisiciones existentes
/api/v1/requisitions/check-existingAPI Key requeridaVerificar qué external_ids ya existen en Sourced antes de importar. Útil para evitar llamadas innecesarias a la API.
Cuerpo de la solicitud
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| external_ids | array | requerido | Lista de strings external_id a verificar (1-100 items). |
curl -X POST https://api.gosourced.ai/api/v1/requisitions/check-existing \
-H "X-API-Key: sk_live_abc123..." \
-H "Content-Type: application/json" \
-d '{
"external_ids": ["REQ-2026-0042", "REQ-2026-0043", "REQ-2026-0044"]
}'{
"existing": ["REQ-2026-0042"],
"not_found": ["REQ-2026-0043", "REQ-2026-0044"]
}Cancelar requisición
/api/v1/requisitions/cancelAPI Key requeridaCancelar una requisición por su external_id: PENDING (se descarta) o LAUNCHED (se cancela la Solicitud de Compra en Sourced).
Una requisición PENDING se descarta. Una LAUNCHED (ya convertida en Solicitud de Compra) también se puede cancelar desde acá: si ninguna línea tiene orden de compra, la Solicitud de Compra se cancela entera; si alguna línea ya tiene OC, ver las reglas de abajo. Sourced nunca cancela una orden de compra en cascada. Una requisición PENDING con alguna línea ya lanzada a una Solicitud de Compra sigue las mismas reglas que una LAUNCHED para esa solicitud, y sus líneas pendientes se descartan en la misma operación; un 409 no modifica nada (tampoco las líneas pendientes).
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| external_id | string | requerido | ID de la requisición en tu sistema externo (la misma que usaste al importar) |
| reason | string | opcional | Motivo de la cancelación (se almacena para auditoría) |
| cancel_remaining | boolean | opcional | Solo para requisiciones con líneas lanzadas a una Solicitud de Compra (LAUNCHED, o PENDING lanzada en parte) y alguna línea ya adjudicada: en vez de responder 409, cierra el saldo (cancela las líneas sin OC, pendientes incluidas, y deja las OC intactas). |
Requisición LAUNCHED (o PENDING con líneas ya lanzadas)
- Sin órdenes de compra: la Solicitud de Compra pasa a CANCELLED, las cotizaciones pendientes se rechazan y la requisición queda DISCARDED con el reason enviado. Responde cancelled. Es lo mismo que el botón "Cancelar" de la solicitud en Sourced.
- Con alguna línea adjudicada (sin cancel_remaining): responde 409 requisitionPartiallyAwarded con awarded_lines: cada línea comprada con su po_code y po_status. No se modifica nada.
- Con cancel_remaining=true: cierre de saldo: se cancelan solo las líneas sin OC, la Solicitud de Compra queda CLOSED y las OC no se tocan. En la requisición, las líneas compradas siguen LAUNCHED y el resto pasa a DISCARDED. Responde partially_cancelled con las OC que quedan vivas. Es lo mismo que "Cerrar saldo" en Sourced. Es idempotente: si ya no queda saldo (un reintento después de un cierre que ya se hizo, o todas las líneas ya tienen OC), vuelve a responder partially_cancelled con los mismos datos, sin modificar nada ni reenviar el webhook.
- Se cancela dentro de Sourced: si la Solicitud de Compra es compartida con otras requisiciones (o la requisición está repartida en varias), responde 409 requisitionSharedPrCancelInApp; si alguna línea se lanzó a una licitación o la Solicitud de Compra tiene una licitación vinculada que no está cancelada, 409 requisitionTenderCancelInApp. No se modifica nada.
- Para anular también las compras: primero cancelá cada OC con POST /purchase-orders/{po_id}/status (estado CANCELLED) y después llamá a este endpoint: sin OC activas, la Solicitud de Compra se cancela entera. Cada paso es explícito sobre lo que anula.
curl -X POST https://api.gosourced.ai/api/v1/requisitions/cancel \
-H "X-API-Key: sk_live_abc123..." \
-H "Content-Type: application/json" \
-d '{
"external_id": "REQ-2026-0042",
"reason": "Cancelado desde ERP"
}'// PENDING
{ "status": "discarded", "external_id": "REQ-2026-0042" }
// LAUNCHED sin OC: se cancela la Solicitud de Compra
{ "status": "cancelled", "external_id": "REQ-2026-0042", "pr_id": 8123, "pr_code": "PR-2026-0311" }
// LAUNCHED con OC y cancel_remaining=true: cierre de saldo
{ "status": "partially_cancelled", "external_id": "REQ-2026-0042", "pr_id": 8123,
"pr_code": "PR-2026-0311", "cancelled_lines": 2, "purchase_orders": ["OC-2026-0455"] }
// LAUNCHED con OC sin cancel_remaining
// 409 Conflict
{
"detail": {
"code": "requisitionPartiallyAwarded",
"message": "Requisition 'REQ-2026-0042' (PR PR-2026-0311) already has purchase orders for some lines. ...",
"params": { "external_id": "REQ-2026-0042", "pr_code": "PR-2026-0311" },
"awarded_lines": [
{ "external_line_id": "10", "description": "Rodamiento SKF 6205",
"po_code": "OC-2026-0455", "po_status": "SENT" }
]
}
}Respuestas posibles:
discarded- La requisición fue descartada exitosamente.cancelled- Requisición lanzada (entera o en parte) sin OC: la Solicitud de Compra fue cancelada y la requisición descartada.partially_cancelled- Requisición con OC y cancel_remaining=true: el saldo quedó cerrado. cancelled_lines cuenta las líneas de la Solicitud de Compra que quedaron canceladas (sin OC) y purchase_orders lista las OC que siguen vivas. Un reintento devuelve la misma respuesta.already_discarded- La requisición ya estaba descartada (idempotente).already_cancelled- La Solicitud de Compra ya estaba cancelada dentro de Sourced: la requisición se descarta igual (idempotente). Si la Solicitud tenía OC vivas, sus líneas compradas NO se descartan (siguen lanzadas) y purchase_orders lista esas OC.already_superseded- La requisición ya fue reemplazada por una versión más nueva.- Requisición LAUNCHED con alguna OC y sin cancel_remaining - devuelve
409 requisitionPartiallyAwardedcon awarded_lines. - La Solicitud de Compra se está procesando en este momento (por ejemplo, una adjudicación en curso) - devuelve
409 requisitionPrBusycon el header Retry-After. No se modificó nada: es reintentable, volvé a llamar con los mismos datos en unos segundos. - La requisición misma se está procesando en este momento (por ejemplo, se está lanzando desde el triage o actualizando por otra importación) - devuelve
409 requisitionBusycon el header Retry-After. No se modificó nada: es reintentable, volvé a llamar con los mismos datos en unos segundos. - Requisición no encontrada - devuelve
404.
Listar órdenes de compra
/api/v1/purchase-ordersAPI Key requeridaObtener órdenes de compra de tu organización. Soporta sincronización incremental a través del parámetro 'since'.
Parámetros de consulta
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| page | integer | opcional | Número de página (por defecto: 1). |
| page_size | integer | opcional | Items por página, 1-100 (por defecto: 20). |
| status | string | opcional | Filtrar por estado: "DRAFT", "CREATED", "SENT", "CONFIRMED", "REJECTED", "CANCELLED". |
| since | datetime | opcional | Timestamp ISO 8601. Devuelve solo OC creadas o actualizadas después de esta fecha. |
| requisition_external_id | string | opcional | Filtrar por el external_id de la requisición original en tu ERP. Devuelve las OC que se generaron a partir de esa requisición. |
curl -H "X-API-Key: sk_live_abc123..." \
"https://api.gosourced.ai/api/v1/purchase-orders?since=2026-03-01T00:00:00Z&status=CONFIRMED"{
"data": [
{
"id": 567,
"code": "PO-2026-0089",
"status": "CONFIRMED",
"award_type": "FULL",
"supplier_name": "Distribuidora Industrial SA",
"supplier_id": 42,
"supplier_external_id": "PROV-CAL-00123",
"supplier_tax_id": "30712345678",
"supplier_email": "ventas@distribuidora.com",
"total_price": 285000.00,
"currency": "ARS",
"delivery_address": "Av. Corrientes 1234, CABA",
"delivery_address_code": "001",
"observations": "Entregar en horario de mañana. Coordinar con depósito.",
"delivery_lead_time_days": 15,
"expected_delivery_date": "2026-03-20T16:00:00Z",
"payment_terms_code": "ADV100",
"payment_plan": {
"summary": "50% a 30 días fecha factura en echeq de terceros · 50% a 60 días fecha factura en echeq de terceros",
"exact_terms_match": false,
"installments": [
{"number": 1, "percentage": 50, "method": "ECHEQ", "cheque_origin": "THIRD_PARTY", "days": 30, "base": "INVOICE", "amount": 142500.0},
{"number": 2, "percentage": 50, "method": "ECHEQ", "cheque_origin": "THIRD_PARTY", "days": 60, "base": "INVOICE", "amount": 142500.0}
]
},
"awarded_at": "2026-03-05T16:00:00Z",
"awarded_by_name": "Juan Pérez",
"awarded_by_email": "juan.perez@empresa.com",
"created_at": "2026-03-05T16:00:00Z",
"updated_at": "2026-03-06T10:30:00Z",
"purchase_request_id": 1234,
"requisition_external_id": "REQ-2026-00142",
"exchange_rate_data": {
"date": "2026-03-05",
"usdToArs": 1450.0,
"arsToUsd": 0.00069
},
"total_nominal_savings": 15000.00,
"total_real_savings": 8500.00,
"savings_currency": "ARS",
"items": [
{
"description": "Bearing SKF 6205",
"quantity": 10,
"unit_price": 28500.00,
"total_price": 285000.00,
"unit_of_measure": "UN",
"currency": "ARS",
"delivery_lead_time_days": 15,
"expected_delivery_date": "2026-03-20T16:00:00Z",
"external_line_id": "REQ-2026-0042-L10",
"id": 9912,
"erp_po_line_position": "00010",
"erp_line_position": "0010",
"erp_requisition_ref": "REQ-2026-0042",
"cost_center": {"code": "CC-100", "label": "Mantenimiento"},
"purchasing_group": {"code": "G01", "label": "Repuestos"},
"item_category": null,
"custom_fields": {},
"buyer_code": "MAT-001234",
"buyer_code_system": "SAP",
"manufacturer_code": "6205-2RS",
"technical_specs": "220V, 50Hz, IP55"
}
]
}
],
"total": 12,
"page": 1,
"page_size": 20,
"has_more": false
}Detalle de orden de compra
/api/v1/purchase-orders/{po_id}API Key requeridaObtener una orden de compra individual por su ID interno de Sourced, incluyendo todos los items con trazabilidad hacia tu ERP via external_line_id.
curl -H "X-API-Key: sk_live_abc123..." \
https://api.gosourced.ai/api/v1/purchase-orders/567Este endpoint requiere el ID interno de Sourced (po_id). Si necesitás buscar órdenes de compra usando el ID de requisición de tu ERP, usá el endpoint GET /purchase-orders con el query parameter requisition_external_id:
También podés combinarlo con otros filtros: status para filtrar por estado de la OC, since para sincronización incremental, y page / page_size para paginación.
Objeto Orden de Compra
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| id | integer | requerido | ID interno de OC en Sourced. |
| code | string | requerido | Código de OC legible. |
| status | string | requerido | Estado de la OC: "DRAFT", "CREATED", "SENT", "CONFIRMED", "REJECTED", "CANCELLED". |
| award_type | string | opcional | Tipo de adjudicación: FULL (proveedor único) o PARTIAL (adjudicación dividida entre múltiples proveedores). |
| supplier_name | string | requerido | Nombre del proveedor. |
| supplier_id | integer | opcional | ID interno del proveedor en Sourced. |
| total_price | float | opcional | Valor total de la OC. |
| currency | string | opcional | Código de moneda. |
| delivery_address | string | opcional | Dirección de entrega. |
| delivery_address_code | string | opcional | Código de la dirección de entrega de la organización vinculada (ej. "001"). Usalo para mapear el destino contra tu ERP. Null para direcciones de texto libre sin registro vinculado. |
| observations | string | opcional | Comentarios u observaciones en texto libre que el comprador cargó en el paso de revisión pre-adjudicación. Usa las condiciones especiales cotizadas por el proveedor si el comprador no dejó nota. Null cuando no hay ninguna. |
| delivery_lead_time_days | integer | opcional | Plazo de entrega en días, según lo cotizado por el proveedor a nivel cabecera. Cuando el proveedor cotiza distintos plazos por línea, los items pueden tener valores diferentes - usá items[].delivery_lead_time_days para el valor por línea. |
| expected_delivery_date | datetime | opcional | Fecha de entrega calculada (awarded_at + delivery_lead_time_days). Null si falta cualquiera de los dos. Para fechas por línea usá items[].expected_delivery_date. |
| payment_terms_code | string | opcional | Código de condiciones de pago. |
| payment_plan | object | opcional | Plan de pagos: cuotas con medio (TRANSFER, ECHEQ, CHEQUE, CASH, OTHER o null), origen del cheque (OWN, THIRD_PARTY o null = a definir; solo cheque/echeq), porcentaje, días, base (ORDER, DELIVERY, INVOICE, INVOICE_EOM) y monto. Viene en todas las OC: el cargado por el comprador o uno armado desde payment_terms_code (method null). Es independiente de payment_terms_code. null si la OC no tiene plazo. |
| awarded_at | datetime | opcional | Cuándo se adjudicó la OC (ISO 8601). |
| awarded_by_name | string | opcional | Nombre del usuario que adjudicó la OC. |
| awarded_by_email | string | opcional | Email del usuario que adjudicó la OC. |
| created_at | datetime | opcional | Timestamp de creación (ISO 8601). |
| updated_at | datetime | opcional | Timestamp de última actualización (ISO 8601). |
| purchase_request_id | integer | opcional | ID de la solicitud de compra originante. |
| requisition_external_id | string | opcional | ID del documento origen en tu ERP (ej. número de requisición en Calipso/SAP/JDE). Se resuelve desde la ERPRequisition de origen cuando existe; si no, cae al external_id de la PR. Puede ser una lista separada por comas cuando una sola PR agrupa varias requisiciones origen. |
| exchange_rate_data | object | opcional | Tipos de cambio al momento del award. Formato: {"date": "2026-03-05", "usdToArs": 1450.0, "arsToUsd": 0.00069}. Null si no hay datos disponibles. |
| total_nominal_savings | float | opcional | Ahorro nominal total vs precios históricos (sin ajuste por inflación). |
| total_real_savings | float | opcional | Ahorro real total vs precios históricos (ajustado por inflación). |
| savings_currency | string | opcional | Moneda de los montos de ahorro. |
| items | array | requerido | Items de línea de la OC. Ver esquema de Item de OC abajo. |
Objeto payment_plan
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| summary | string | requerido | Resumen legible del plan, en el idioma de la organización (español si no tiene uno configurado). |
| exact_terms_match | boolean | requerido | Obsoleto, se mantiene por compatibilidad: true si las cuotas son exactamente las de payment_terms_code. Como el código ya no se deriva del plan, false solo indica que el plan tiene cuotas o días propios. |
| installments | array | requerido | Cuotas en orden, de 1 a 12. Los porcentajes suman 100. |
Objeto de cuota (payment_plan.installments[])
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| number | integer | requerido | Posición de la cuota, desde 1 (calculado). |
| percentage | float | requerido | Porcentaje del total de la OC. |
| method | string | opcional | Medio de pago: TRANSFER, ECHEQ, CHEQUE, CASH, OTHER o null (sin especificar). |
| cheque_origin | string | opcional | Solo con CHEQUE o ECHEQ: OWN (cheque propio), THIRD_PARTY (cheque de terceros) o null (a definir). Con otro medio siempre es null. |
| days | integer | requerido | Días contados desde base (entero, mayor o igual a 0). Con base ORDER siempre es 0. |
| base | string | requerido | Desde cuándo se cuentan los días: ORDER (anticipo, con la OC), DELIVERY (entrega), INVOICE (fecha de factura) o INVOICE_EOM (fecha de factura, fin de mes). |
| amount | float | requerido | total_price × percentage / 100, en la moneda de la OC (calculado). |
- Si el comprador no cargó un plan, se arma desde payment_terms_code: una cuota (o dos para ADV50) con method null. Si la OC no tiene código, payment_plan es null.
- payment_terms_code y payment_plan son independientes: el código es la condición acordada con el proveedor y el plan cómo se paga. Pueden no coincidir (p. ej. ADV100 con tres echeqs a 30/60/90 días) y ninguno se recalcula desde el otro.
- Reglas de validación: máximo 12 cuotas, porcentajes que suman 100, days entero mayor o igual a 0 (0 con ORDER), cheque_origin solo con CHEQUE/ECHEQ.
- Cada ERP traduce method y cheque_origin a sus propios códigos (condición de pago, medio de pago, tipo de cheque): Sourced no conoce esos códigos, el mapeo vive en tu integración.
Objeto Item de OC
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| description | string | requerido | Descripción del item. |
| quantity | float | opcional | Cantidad ordenada. |
| unit_price | float | opcional | Precio por unidad. |
| total_price | float | opcional | Precio total de la línea (cantidad x precio unitario). |
| unit_of_measure | string | opcional | Unidad de medida. |
| currency | string | opcional | Código de moneda. |
| delivery_lead_time_days | integer | opcional | Plazo de entrega en días para esta línea, según lo cotizado por el proveedor. Items distintos de una misma OC pueden tener plazos diferentes. |
| expected_delivery_date | datetime | opcional | Fecha de entrega calculada para esta línea (PO.awarded_at + delivery_lead_time_days del item). Null si falta cualquiera de los dos. |
| external_line_id | string | opcional | ID de línea original de tu ERP - usalo para vincular items de la OC con tus líneas de requisición. |
| delivery_address_code | string | opcional | Código de la dirección de entrega de esta línea. Si la línea no tiene destino propio, toma el delivery_address_code de la OC. |
| delivery_address | string | opcional | Dirección de entrega de esta línea, en texto. Si la línea no tiene destino propio, toma el delivery_address de la OC. |
| id | integer | opcional | ID de Sourced de esta línea de la OC. Es el que se manda en items[].id del PATCH /purchase-orders/{id}. |
| erp_po_line_position | string | opcional | Posición de esta línea en la OC que creó tu ERP (ej. EBELP en SAP), tal como la registraste con PATCH. No es erp_line_position: esa es la posición de la línea de la requisición. |
| erp_line_position | string | opcional | Posición de la línea de la requisición de origen. No es la posición de la línea en la OC de tu ERP: esa es erp_po_line_position. |
| erp_requisition_ref | string | opcional | Referencia de la requisición de origen en tu ERP. |
| cost_center | object | opcional | Centro de costo de la línea de requisición de origen, como { code, label }. Null si esa línea no tiene o si la OC no viene de una requisición (por ejemplo, una OC generada por una licitación). |
| purchasing_group | object | opcional | Grupo de compras de la línea de requisición de origen, como { code, label }. Null si no tiene o si la OC no viene de una requisición (por ejemplo, una licitación). |
| item_category | object | opcional | Categoría de ítem de la línea de requisición de origen, como { code, label }. Null si no tiene o si la OC no viene de una requisición (por ejemplo, una licitación). |
| custom_fields | object | opcional | Campos personalizados con los que vino la línea de requisición de origen. {} si no tiene o si la OC no viene de una requisición (por ejemplo, una licitación). |
| buyer_code | string | opcional | Código interno en tu sistema, tal como lo enviaste al importar la requisición (ej: "MAT-001234"). |
| buyer_code_system | string | opcional | Sistema de origen del código interno (ej: "SAP", "JDE", "Calipso"). |
| buyer_code_description | string | opcional | Descripción del código interno. |
| manufacturer_code | string | opcional | Número de parte del fabricante. |
| manufacturer_name | string | opcional | Nombre del fabricante (ej: "Siemens"). |
| manufacturer_description | string | opcional | Descripción del fabricante para la parte. |
| technical_specs | string | opcional | Especificaciones técnicas en texto libre (ej: "220V, 50Hz, IP55"). |
| requirements | string | opcional | Requisitos adicionales (ej: "Certificación ISO 9001 requerida"). |
- El plazo de entrega existe a dos niveles:
delivery_lead_time_daysa nivel cabecera (refleja el plazo general que cotizó el proveedor) yitems[].delivery_lead_time_daysa nivel línea (preciso por item). Cuando el proveedor cotiza distintos plazos por línea, conviene usar el valor por línea. expected_delivery_datees un campo calculado:awarded_at + delivery_lead_time_days. Lo computa Sourced; no es un dato que el proveedor envíe directamente.- Si
delivery_lead_time_daysviene null, también lo haráexpected_delivery_date. Esto suele significar que el proveedor no incluyó el plazo en su cotización.
Descargar legajo de la OC (expediente)
/api/v1/purchase-orders/{po_id}/legajoAPI Key requeridaObtiene un link temporal para descargar el legajo - un ZIP con la traza completa de la adjudicación: PDF/XLSX de comparativa, historial de emails, los adjuntos de todos los proveedores participantes, el registro de actividad y la bitácora interna (las notas marcadas para el legajo).
curl -H "X-API-Key: sk_live_abc123..." \
https://api.gosourced.ai/api/v1/purchase-orders/567/legajoObjeto de respuesta
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| download_url | string | requerido | URL presignada temporal para descargar el ZIP del legajo. |
| filename | string | requerido | Nombre sugerido para el archivo ZIP. |
| expires_in | integer | requerido | Segundos hasta que la URL de descarga expira. |
| size_bytes | integer | requerido | Tamaño del ZIP del legajo en bytes. |
- La respuesta es una URL presignada temporal (válida ~10 minutos). Descarga el ZIP directamente desde ahí - esa URL no requiere API key.
- El legajo corresponde a la solicitud (PR) detrás de la OC. En una adjudicación dividida (varias OC de una misma solicitud), las OC hermanas devuelven el mismo expediente.
Comparativa de cotizaciones de la OC
/api/v1/purchase-orders/{po_id}/quotationsAPI Key requeridaObtiene la comparativa completa de cotizaciones que originó esta adjudicación: todas las líneas cotizadas por cada proveedor para la solicitud (PR) detrás de la OC, adjudicadas y no adjudicadas - no solo las que terminaron en esta OC.
curl -H "X-API-Key: sk_live_abc123..." \
https://api.gosourced.ai/api/v1/purchase-orders/567/quotations{
"purchase_order_id": 567,
"purchase_order_code": "PO-4F2A91C3",
"requisition_external_id": "REQ-2026-0042",
"exchange_rate": {
"date": "2026-07-12",
"base": "USD",
"rates": { "ARS": 1450.5, "EUR": 0.92 }
},
"currencies": ["ARS", "USD"],
"items": [
{
"material_code": "MAT-000123",
"external_line_id": "10",
"description": "Guantes de nitrilo talle L",
"quantity": 500.0,
"quotes": [
{
"quotation_id": 8123,
"supplier_id": 42,
"supplier_name": "Proveedor A S.A.",
"supplier_tax_id": "30516242775",
"supplier_erp_code": "PROV-001",
"unit_price": 11.25,
"original_price": 12.5,
"discount": { "type": "percentage", "value": 10, "source": "manual" },
"currency": "ARS",
"quantity": 500,
"awarded": true,
"awarded_po_code": "PO-4F2A91C3",
"lead_time_days": 7,
"payment_term_code": "NET30",
"response_status": "PARTIAL",
"received_at": "2026-07-10T14:32:00+00:00"
},
{
"quotation_id": 8124,
"supplier_id": 57,
"supplier_name": "Proveedor B S.R.L.",
"supplier_tax_id": "30709876541",
"supplier_erp_code": null,
"unit_price": 0.0095,
"original_price": 0.0095,
"discount": null,
"currency": "USD",
"quantity": 500,
"awarded": false,
"awarded_po_code": null,
"lead_time_days": 15,
"payment_term_code": null,
"response_status": "INCOMPLETE",
"received_at": "2026-07-11T09:05:00+00:00"
}
]
}
]
}Objeto de respuesta
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| purchase_order_id | integer | requerido | ID de la OC consultada. |
| purchase_order_code | string | requerido | Código de la OC consultada (p. ej. PO-4F2A91C3). |
| requisition_external_id | string | opcional | ID externo de la requisición de origen (sistema ERP), si existe. |
| exchange_rate | object | opcional | Tipos de cambio guardados en la OC al momento de la adjudicación, para convertir las cotizaciones a una moneda común: { date, base: "USD", rates: { ARS: 1450.5, EUR: 0.92 } } (unidades de cada moneda por 1 USD). null si la OC no guardó una foto (licitación, catálogo u OC anterior a esta función). |
| currencies | array | requerido | Monedas distintas que aparecen en las cotizaciones, ordenadas (p. ej. ["ARS", "USD"]). |
| items | array | requerido | Líneas de la solicitud detrás de la OC, cada una con sus cotizaciones. |
Objeto ítem (items[])
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| material_code | string | opcional | Código de material del comprador, si la línea tiene uno. |
| external_line_id | string | opcional | ID de línea del sistema de origen (ERP), si existe. |
| description | string | requerido | Descripción del ítem solicitado. |
| quantity | float | opcional | Cantidad solicitada. |
| quotes | array | requerido | Cotizaciones recibidas para esta línea, una por proveedor que cotizó con precio. |
Objeto cotización (items[].quotes[])
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| quotation_id | integer | requerido | ID de la cotización. Un proveedor que cotizó más de una vez (rondas) aparece una vez por cotización. |
| supplier_id | integer | opcional | ID interno del proveedor en Sourced. Identidad estable entre líneas (el nombre puede repetirse). |
| supplier_name | string | opcional | Nombre del proveedor que cotizó. |
| supplier_tax_id | string | opcional | Identificador fiscal del proveedor (CUIT en Argentina, CNPJ en Brasil, RUT en Chile/Uruguay). |
| supplier_erp_code | string | opcional | Código ERP del proveedor en tu organización, si está configurado. |
| unit_price | float | opcional | Precio unitario FINAL, neto de descuento, en la moneda original del proveedor. Es el mismo número que ve el comprador en la comparativa de adjudicación y en el Excel del legajo, y el precio al que se adjudicó la OC. |
| original_price | float | opcional | Precio unitario antes del descuento. Igual a unit_price si no hubo descuento. |
| discount | object | opcional | Descuento aplicado: { type: "percentage" | "fixed", value, source }. source: supplier_quoted (lo ofreció el proveedor), negotiation, manual (lo cargó el comprador) o global. null si no hubo descuento. |
| currency | string | opcional | Moneda de la cotización (código ISO, p. ej. ARS, USD). Si el proveedor no indicó moneda en ninguna parte, se informa "USD". |
| quantity | float | opcional | Cantidad cotizada por el proveedor. |
| awarded | boolean | requerido | true si esta línea se adjudicó a este proveedor en alguna OC activa de la solicitud. |
| awarded_po_code | string | opcional | Código de la OC donde se adjudicó la línea (puede ser una OC hermana en adjudicación dividida). null si no fue adjudicada. |
| lead_time_days | integer | opcional | Plazo de entrega ofrecido, en días (entero). Un plazo no numérico se informa como null. |
| payment_term_code | string | opcional | Código de condición de pago ofrecida por el proveedor. |
| response_status | string | opcional | Clasificación del análisis AI de la respuesta del proveedor. Valores como INCOMPLETE, PARTIAL, NEEDS_HUMAN_REVIEW, EXPLICIT_REJECTION. |
| received_at | datetime | opcional | Fecha y hora de recepción de la cotización (ISO 8601). |
- La comparativa corresponde a la solicitud (PR) detrás de la OC: incluye las cotizaciones de todos los proveedores que participaron, aunque no hayan ganado. Una línea sin precio cotizado no genera entrada en quotes; una OC de catálogo (sin proceso de cotización) devuelve quotes vacíos.
awardedes a nivel solicitud: en una adjudicación dividida, una línea puede haberse adjudicado en una OC hermana distinta de la consultada -awarded_po_codesiempre indica el código de la OC ganadora de esa línea.- Los precios son los mismos que el comprador vio en la comparativa de adjudicación y los que figuran en el Excel del legajo:
unit_priceya tiene aplicado cualquier descuento negociado (original_priceydiscountmuestran el bruto y el descuento). Una cotización importada que el usuario todavía no confirmó no aparece. - Los precios se devuelven en la moneda cotizada por cada proveedor - no se aplica conversión.
exchange_ratetrae los tipos de cambio guardados en la OC al adjudicar (null si no hay) para que puedas convertir todo a una moneda común. - Todas las claves están siempre presentes; si un dato falta, el valor es null (el shape de la respuesta nunca cambia).
Actualizar estado de orden de compra
/api/v1/purchase-orders/{po_id}/statusAPI Key requeridaActualizar el estado de una orden de compra para reflejar su progreso en tu sistema externo (ERP).
Transiciones de estado permitidas:
| Desde | Hacia | Significado |
|---|---|---|
| DRAFT | CREATED | La OC fue creada en tu ERP. |
| CREATED | CONFIRMED | La OC fue aprobada completamente en tu ERP. |
| DRAFT | CONFIRMED | Atajo cuando no necesitás el paso intermedio. |
| DRAFT | REJECTED | La OC fue rechazada en tu ERP. La PR se reabre en Sourced. |
| CREATED | REJECTED | La OC fue rechazada en tu ERP después de haber sido creada. La PR se reabre en Sourced. |
| DRAFT / CREATED / SENT / CONFIRMED | CANCELLED | La compra se cayó. La OC se cancela y la PR también - no se reabre nada. Usá REJECTED si la necesidad sigue vigente y querés re-adjudicar. |
REJECTED y CANCELLED funcionan también cuando la OC fue una adjudicación parcial, siempre que sea la única OC activa de su PR. Si la PR tiene varias OCs activas (split a varios proveedores), el endpoint devuelve 409 y la reversión debe gestionarse dentro de Sourced.
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| status | string | requerido | Estado destino: CREATED (OC creada en tu ERP), CONFIRMED (OC aprobada en tu ERP), REJECTED (OC rechazada, reabre la PR) o CANCELLED (compra caída, cancela la OC y la PR). |
| external_id | string | opcional | Número o código de la OC en tu ERP (ej: OC-CAL-00045678). Se guarda para trazabilidad. |
| notes | string | opcional | Notas opcionales sobre el cambio de estado. |
curl -X POST https://api.gosourced.ai/api/v1/purchase-orders/567/status \
-H "X-API-Key: sk_live_abc123..." \
-H "Content-Type: application/json" \
-d '{
"status": "CREATED",
"external_id": "OC-CAL-00045678",
"notes": "Creada en Calipso"
}'{
"status": "created",
"po_id": 567,
"code": "PO-A1B2C3D4"
}Editar una OC
/api/v1/purchase-orders/{po_id}API Key requeridaActualiza los campos editables de una orden de compra. Solo se tocan los campos presentes en el body: número de OC en tu ERP, condición de pago o plan de pagos, plazo de entrega, observaciones y estado (misma máquina de transiciones que POST /status).
Cuerpo de la solicitud
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| external_id | string | opcional | Número o código de la OC en tu ERP. Sobrescribe el valor guardado. |
| payment_terms_code | string | opcional | Código de condición de pago del catálogo (p. ej. NET30). Insensible a mayúsculas; se valida contra los códigos activos - un código desconocido devuelve 400 con la lista permitida. Es independiente del plan de pagos: las cuotas propias se mantienen; si el plan es solo el plazo anterior con un medio de pago, el medio pasa al plazo nuevo. |
| payment_plan | object | opcional | Plan de pagos (objeto con installments: [...]; cada cuota con percentage, method, cheque_origin, days y base). Se valida (suma 100%, máximo 12 cuotas). Es independiente de payment_terms_code: no lo recalcula, y se pueden mandar los dos juntos. La OC tiene que tener un payment_terms_code (guardado o en el mismo PATCH). Un plan inválido, o una OC sin plazo, devuelve 422 invalidPaymentPlan con los motivos en params.errors (paymentPlan.noTermsCode si falta el plazo). |
| delivery_lead_time_days | integer | opcional | Plazo de entrega en días desde la confirmación de la OC. expected_delivery_date se deriva de este valor. |
| observations | string | opcional | Texto libre que se muestra en la OC. Una cadena vacía la limpia. |
| status | string | opcional | Estado destino: CREATED, CONFIRMED, REJECTED o CANCELLED - mismas transiciones que POST /status. Enviar el estado actual es un no-op. REJECTED/CANCELLED devuelven el contrato de esos flujos (pr_reopened / pr_code) más updated_fields. |
| notes | string | opcional | Notas sobre el cambio de estado. Solo válido junto con status. |
| items | array | opcional | Líneas de la OC cuya posición querés registrar (ver ejemplo). Cada fila lleva el id de la línea, o su external_line_id si es único dentro de esa OC (si vienen los dos, gana id), y la clave erp_po_line_position con la posición de esa línea en la OC de tu ERP. Es lo único editable de las líneas. |
curl -X PATCH https://api.gosourced.ai/api/v1/purchase-orders/567 \
-H "X-API-Key: sk_live_abc123..." \
-H "Content-Type: application/json" \
-d '{
"external_id": "OC-CAL-00045678",
"payment_terms_code": "NET30",
"status": "CONFIRMED"
}'{
"status": "updated",
"po_id": 567,
"code": "PO-A1B2C3D4",
"updated_fields": ["external_id", "payment_terms_code", "status"]
}curl -X PATCH https://api.gosourced.ai/api/v1/purchase-orders/567 \
-H "X-API-Key: sk_live_abc123..." \
-H "Content-Type: application/json" \
-d '{
"payment_plan": {
"installments": [
{"percentage": 30, "method": "TRANSFER", "cheque_origin": null, "days": 0, "base": "ORDER"},
{"percentage": 70, "method": "ECHEQ", "cheque_origin": "THIRD_PARTY", "days": 30, "base": "INVOICE"}
]
}
}'{
"status": "updated",
"po_id": 567,
"code": "PO-A1B2C3D4",
"updated_fields": ["payment_plan"]
}curl -X PATCH https://api.gosourced.ai/api/v1/purchase-orders/567 \
-H "X-API-Key: sk_live_abc123..." \
-H "Content-Type: application/json" \
-d '{
"items": [
{"id": 9912, "erp_po_line_position": "00010"},
{"external_line_id": "REQ-2026-0042-L20", "erp_po_line_position": "00020"},
{"id": 9914, "erp_po_line_position": null}
]
}'{
"status": "updated",
"po_id": 567,
"code": "PO-A1B2C3D4",
"updated_fields": ["items"]
}- Partial update real: solo se aplican los campos presentes en el body. Enviar null explícito es un error (400) - para dejar un campo como está, se omite.
- Toda la validación corre antes de aplicar nada: si la respuesta es 400 o 409, ningún campo del body se modificó.
- El cambio de estado usa la misma máquina de transiciones que
POST /purchase-orders/{po_id}/status(que sigue disponible). Una transición inválida devuelve 409. Editar una OC CANCELLED o REJECTED devuelve 409. - payment_terms_code es la condición acordada con el proveedor y payment_plan cómo se paga: son independientes y pueden no coincidir (p. ej. ADV100 con tres echeqs a 30/60/90 días). updated_fields lista payment_plan solo si el plan guardado cambió.
- Cambiar payment_terms_code no toca un plan con cuotas o días propios (si el plan era solo el plazo anterior con un medio, el medio pasa al plazo nuevo), y mandar un payment_plan no cambia payment_terms_code.
- payment_plan: null devuelve 400, como cualquier campo en null. Mientras la OC tenga una aprobación pendiente, el PATCH (también el de payment_plan) devuelve 409.
- Cambiar la condición de pago o el plazo de entrega no regenera el PDF que ya recibió el proveedor: el dato se actualiza en Sourced, pero el documento enviado no cambia.
- null o "" borra la posición de esa línea. Dos líneas pueden compartir la misma posición, por ejemplo si tu ERP las agrupa.
- Una línea que no pertenece a la OC, un external_line_id ambiguo, una fila sin id ni external_line_id, una fila sin erp_po_line_position, una posición de más de 50 caracteres o valores distintos para una misma línea devuelven 400 y no se aplica nada de lo enviado. Una lista items vacía se ignora: si el body no trae nada más, devuelve 400.
- Solo se edita erp_po_line_position: precios y cantidades no se pueden cambiar. La posición vuelve en cada línea de GET /purchase-orders. updated_fields lista items solo si cambió alguna posición: si no cambió ninguna y no mandaste otros campos, la OC no se modifica, updated_fields vuelve vacío y no sale el webhook.
- El contenido de la adjudicación (precios, ítems, moneda, proveedor) no es editable: para corregirlo, rechaza la OC (status REJECTED) y vuelve a adjudicar.
Listar proveedores
/api/v1/suppliersAPI Key requeridaListar los proveedores de tu organización. Usa ?has_erp_code=false para encontrar proveedores sin mapear al ERP. Usa ?detail=full para obtener contactos, categorías y cobertura.
Parámetros de consulta
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| page | integer | opcional | Número de página (default: 1). |
| page_size | integer | opcional | Resultados por página (1-200, default: 50). |
| search | string | opcional | Buscar por nombre, nombre personalizado, email, CUIT/CNPJ o código ERP. |
| erp_code | string | opcional | Filtrar por código ERP exacto. |
| has_erp_code | boolean | opcional | true = solo mapeados al ERP, false = solo sin mapear. |
| detail | string | opcional | Usar 'full' para incluir contactos, categorías y cobertura. |
# Light (default)
curl -H "X-API-Key: sk_live_abc123..." \
"https://api.gosourced.ai/api/v1/suppliers?has_erp_code=false"
# Full detail
curl -H "X-API-Key: sk_live_abc123..." \
"https://api.gosourced.ai/api/v1/suppliers?search=30712345678&detail=full"// Light response
{
"data": [
{
"id": 42,
"name": "Distribuidora Industrial SA",
"email": "ventas@distribuidora.com",
"tax_id": "30712345678",
"country_code": "AR",
"city": "Buenos Aires",
"is_active": true,
"is_preferred": true,
"erp_code": "PROV-CAL-00123",
"erp_type": "SAP_B1"
}
],
"total": 85,
"page": 1,
"page_size": 50,
"has_more": true
}
// Full response (?detail=full)
{
"data": [
{
"id": 42,
"name": "Distribuidora Industrial SA",
"email": "ventas@distribuidora.com",
"tax_id": "30712345678",
"country_code": "AR",
"city": "Buenos Aires",
"is_active": true,
"is_preferred": true,
"erp_code": "PROV-CAL-00123",
"erp_type": "SAP_B1",
"address": "Av. Corrientes 1234",
"state_code": "CABA",
"website": "https://distribuidora.com",
"description": "Distribuidor de insumos industriales",
"tax_regime": null,
"employee_count": "50+",
"years_in_business": "5+",
"performance_score": 4.2,
"reliability_score": 4.5,
"quality_score": 4.0,
"contacts": [
{
"name": "Juan Pérez",
"email": "juan@distribuidora.com",
"phone": "+54 11 5555-1234",
"role": "SALES",
"is_primary": true
}
],
"categories": [
{ "id": 15, "code": "IND-001", "name": "Insumos Industriales", "level": "LEVEL_1" }
],
"coverage": [
{ "country": "Argentina", "province": null, "is_nationwide": true }
]
}
],
"total": 1,
"page": 1,
"page_size": 50,
"has_more": false
}Crear proveedor
/api/v1/suppliersAPI Key requeridaCrear un proveedor para tu organización. Al menos uno de tax_id o erp_code es obligatorio. Se requiere al menos un contacto con rol 'sales'. Si ya existe un proveedor global con el mismo CUIT, se reutiliza y se vincula a tu organización.
Cuerpo de la solicitud
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| name | string | requerido | Nombre del proveedor para tu organización. |
| tax_id | string | opcional | CUIT, CNPJ, RUT, etc. Al menos uno de tax_id o erp_code es obligatorio. |
| erp_code | string | opcional | Código del proveedor en tu ERP. Al menos uno de tax_id o erp_code es obligatorio. |
| erp_type | string | opcional | Tipo de ERP (SAP_B1, JDE, ORACLE_CLOUD, etc.). |
| country_code | string | opcional | Código de país ISO 3166-1 (ej: AR, BR, US). |
| city | string | opcional | Ciudad del proveedor. |
| address | string | opcional | Dirección completa. |
| state_code | string | opcional | Estado/provincia (ej: CABA, SP). |
| website | string | opcional | Sitio web del proveedor. |
| contacts | array | requerido | Lista de contactos. Al menos uno con rol 'sales' es obligatorio. |
| contacts[].name | string | requerido | Nombre del contacto. |
| contacts[].email | string | requerido | Email del contacto. |
| contacts[].phone | string | opcional | Teléfono (opcional). |
| contacts[].role | string | requerido | Rol: SALES o LOGISTICS. |
| contacts[].is_primary | boolean | opcional | true si es el contacto principal (default: false). |
curl -X POST https://api.gosourced.ai/api/v1/suppliers \
-H "X-API-Key: sk_live_abc123..." \
-H "Content-Type: application/json" \
-d '{
"name": "Distribuidora Industrial SA",
"tax_id": "30712345678",
"erp_code": "PROV-CAL-00123",
"erp_type": "SAP_B1",
"country_code": "AR",
"city": "Buenos Aires",
"address": "Av. Corrientes 1234",
"contacts": [
{
"name": "Juan Pérez",
"email": "juan@distribuidora.com",
"phone": "+54 11 5555-1234",
"role": "SALES",
"is_primary": true
},
{
"name": "María García",
"email": "maria@distribuidora.com",
"role": "LOGISTICS"
}
]
}'// 201 Created
{
"id": 42,
"name": "Distribuidora Industrial SA",
"email": null,
"tax_id": "30712345678",
"country_code": "AR",
"city": "Buenos Aires",
"is_active": true,
"is_preferred": false,
"erp_code": "PROV-CAL-00123",
"erp_type": "SAP_B1",
"address": "Av. Corrientes 1234",
"state_code": null,
"website": null,
"description": null,
"tax_regime": null,
"employee_count": null,
"years_in_business": null,
"performance_score": null,
"reliability_score": null,
"quality_score": null,
"contacts": [
{
"name": "Juan Pérez",
"email": "juan@distribuidora.com",
"phone": "+54 11 5555-1234",
"role": "SALES",
"is_primary": true
},
{
"name": "María García",
"email": "maria@distribuidora.com",
"phone": null,
"role": "LOGISTICS",
"is_primary": false
}
],
"categories": [],
"coverage": []
}Crear o actualizar seguimientos de entrega
/api/v1/delivery-trackingsAPI Key requeridaUpsert por lotes de 1 a 200 líneas de OC para seguir su entrega. La clave natural es (po_code, line_position): reenviar la misma línea actualiza el seguimiento existente en vez de duplicarlo.
Límite de tasa: 30 solicitudes por minuto (los demás endpoints de este grupo usan el límite general de 60 por minuto).
Cuerpo de la solicitud
El cuerpo debe contener un array lines con 1 a 200 líneas. Cada línea lleva la OC, la posición, la fecha comprometida, el proveedor (código ERP o CUIT) y el detalle del ítem (descripción, cantidad pedida y cantidad pendiente): sin eso no hay nada que seguir ni qué mostrarle al proveedor. Un campo obligatorio ausente rechaza el batch completo con 422.
Objeto de línea (lines[])
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| po_code | string | requerido | Número de OC en el sistema del cliente. Máximo 100 caracteres. |
| line_position | integer | requerido | Posición de la línea dentro de la OC (10, 20, 30…). Entero ≥ 0. |
| original_delivery_date | date | requerido | Fecha de entrega comprometida (YYYY-MM-DD) |
| supplier_erp_code | string | Uno de los dos | Código del proveedor en el ERP del cliente. Identificador recomendado. Obligatorio si no se envía supplier_tax_id. Máximo 100 caracteres. |
| supplier_tax_id | string | Uno de los dos | CUIT/RUT del proveedor. Obligatorio si no se envía supplier_erp_code. Máximo 50 caracteres. |
| supplier_name | string | opcional | Solo texto para mostrar; nunca se usa para matchear al proveedor. Máximo 255 caracteres. |
| material_code | string | opcional | Código del material o ítem en el sistema del cliente. Máximo 100 caracteres. |
| description | string | requerido | Descripción del ítem. Es lo que el proveedor ve en el mail de seguimiento. Máximo 500 caracteres. |
| quantity_ordered | float | requerido | Cantidad pedida. ≥ 0. Es lo que el proveedor ve en el mail de seguimiento. |
| quantity_pending | float | requerido | Cantidad pendiente de entrega. ≥ 0. Es lo que el mail le reclama al proveedor: con entregas parciales, mandá lo que falta, no lo pedido. |
| unit_of_measure | string | opcional | Unidad de medida. Máximo 20 caracteres. |
| po_date | date | opcional | Fecha de la OC (YYYY-MM-DD). |
| buyer_email | string | opcional | CC de escalación y destinatario de los avisos de atraso. Si no se envía, la notificación in-app de atraso va al primer usuario con rol logistics de la organización (uno solo, no a todos). |
| is_urgent | boolean | opcional | Marca la línea como urgente. |
| business_unit_code | string | opcional | Código de la unidad de negocio de la OC en tu ERP (por ejemplo, la unidad de negocio de JDE). Habilita el filtro por unidad de negocio en la vista de seguimientos de Sourced. Si no lo enviás, se conserva el valor que ya tenía la línea. Se ignoran los espacios al principio y al final. |
| business_unit_name | string | opcional | Nombre de la unidad de negocio, para mostrar en Sourced. Si cambiás el código sin enviar nombre, el nombre anterior se borra. |
Hay que enviar al menos supplier_erp_code o supplier_tax_id. Para supplier_tax_id, un valor vacío o compuesto solo por separadores (por ejemplo "-" o " - ") cuenta como no enviado, y el match ignora guiones y espacios en ambos lados (30-71234567-8 matchea 30712345678). supplier_erp_code solo se recorta (trim): un "-" literal SÍ cuenta como enviado y se usa tal cual para buscar — si no matchea ningún proveedor, la línea queda en SUPPLIER_NOT_FOUND (no es un error).
Reglas del upsert
- Línea nueva: se crea en PENDING_SCHEDULE si el proveedor resolvió, o en SUPPLIER_NOT_FOUND si no. Nace activa (sin pausar).
- Línea existente, abierta, creada por esta misma API: se actualizan sus datos y cantidades. Un cambio de original_delivery_date o de las cantidades no reinicia el ciclo de seguimiento.
- Línea existente ya cerrada (DELIVERED o CANCELLED): se reactiva con un ciclo de seguimiento nuevo (vuelve a mandar mails de confirmación); el resultado es reactivated y cuenta dentro de updated_count.
- Corregir el proveedor de una línea existente: si llega con un supplier_erp_code o supplier_tax_id que resuelve a OTRO proveedor, el seguimiento se re-vincula a esa empresa y el ciclo arranca de nuevo (los mails previos eran con la empresa equivocada). Una línea que no resuelve ningún proveedor nunca desvincula el proveedor ya asignado.
- Sin cambios: el resultado es unchanged; no se escribe ningún evento ni se toca updated_at.
- Línea administrada por otra fuente (CSV o sincronización ERP): se rechaza con el error dlvTrackingManagedByOtherSource. Los seguimientos cargados por CSV o por la sincronización ERP no se pueden modificar por esta API.
- Cantidad pendiente 0: la línea ya se recibió entera. Sobre un seguimiento abierto se cierra como DELIVERED (evento delivered con reason quantity_pending_zero; la fecha real queda como el día del envío — si la conocés, usá mark_delivered con actual_delivery_date). Sobre uno ya cerrado es unchanged, no lo reactiva. Una línea NUEVA con pendiente 0 devuelve el error dlvNothingPending.
curl -X POST https://api.gosourced.ai/api/v1/delivery-trackings \
-H "X-API-Key: sk_live_abc123..." \
-H "Content-Type: application/json" \
-d '{
"lines": [
{
"po_code": "OC-2026-0455",
"line_position": 10,
"original_delivery_date": "2026-10-15",
"supplier_erp_code": "PROV-CAL-00123",
"material_code": "MAT-001234",
"description": "Rodamiento SKF 6205",
"quantity_ordered": 50,
"quantity_pending": 50,
"unit_of_measure": "UN",
"buyer_email": "compras@empresa.com",
"business_unit_code": "1010",
"business_unit_name": "Rental Buenos Aires"
},
{
"po_code": "OC-2026-0455",
"line_position": 20,
"original_delivery_date": "2026-10-15",
"supplier_tax_id": "30-71234567-8",
"description": "Filtro de aceite",
"quantity_ordered": 12,
"quantity_pending": 12,
"unit_of_measure": "UN"
}
]
}'Respuesta
Respuesta del upsert
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| created_count | integer | requerido | Cantidad de líneas creadas. |
| updated_count | integer | requerido | Cantidad de líneas actualizadas (incluye las reactivadas). |
| unchanged_count | integer | requerido | Cantidad de líneas sin cambios. |
| error_count | integer | requerido | Cantidad de líneas con error. |
| results | array | requerido | Resultado de cada línea del batch, en el mismo orden en que se enviaron. |
Objeto de resultado por línea (results[])
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| po_code | string | requerido | Número de OC, tal como se envió. |
| line_position | integer | requerido | Posición de línea, tal como se envió. |
| id | integer | opcional | Id interno del seguimiento. Ausente cuando la línea terminó en error. |
| result | string | requerido | created | updated | unchanged | reactivated | error. |
| status | string | opcional | Estado del seguimiento después del upsert. Ausente en las líneas con error. |
| code | string | opcional | Código del error (dlv*). Solo presente cuando result es error. |
| message | string | opcional | Detalle del error en inglés, pensado para logs. No traducir ni mostrar al usuario final: el code es la clave i18n. |
// 200 OK
{
"created_count": 1,
"updated_count": 0,
"unchanged_count": 0,
"error_count": 1,
"results": [
{
"po_code": "OC-2026-0455",
"line_position": 10,
"id": 8821,
"result": "created",
"status": "PENDING_SCHEDULE"
},
{
"po_code": "OC-2026-0455",
"line_position": 20,
"result": "error",
"code": "dlvSupplierReferenceAmbiguous",
"message": "supplier_tax_id '30-71234567-8' matches more than one supplier"
}
]
}El HTTP siempre es 200, incluso si hay líneas con error: revisá error_count y el code de cada línea en results para saber qué falló.
Listar seguimientos de entrega
/api/v1/delivery-trackingsAPI Key requeridaObtener los seguimientos de entrega de tu organización, de todas las fuentes (API, CSV y sincronización ERP). Soporta sincronización incremental con since.
Parámetros de consulta
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| page | integer | opcional | Número de página (por defecto: 1). |
| page_size | integer | opcional | Items por página, 1-200 (por defecto: 50). |
| status | string | opcional | Filtrar por estado (ver la tabla de estados más abajo). Incluye STAND_BY. |
| po_code | string | opcional | Filtrar por el po_code exacto. |
| supplier_erp_code | string | opcional | Filtrar por el código ERP del proveedor. |
| since | datetime | opcional | Timestamp ISO 8601. Devuelve solo seguimientos creados o modificados desde esa fecha, en orden ascendente por fecha de modificación. Sin zona horaria se interpreta como UTC: mandá siempre un offset explícito (o "Z") para evitar ambigüedad. |
| open_only | boolean | opcional | Excluye los seguimientos DELIVERED y CANCELLED. |
| business_unit_code | string | opcional | Solo las líneas de esa unidad de negocio (coincidencia exacta del código). |
since, el orden es por fecha de modificación ascendente (no por fecha de creación descendente, que es el orden por defecto): guardá el updated_at del último registro recibido y usalo como el próximo since. Para no perder filas escritas por una transacción larga (un CSV grande, un sync del ERP), el servidor corre el since 10 minutos hacia atrás: vas a volver a recibir registros de esos minutos, así que deduplicá por id (el upsert es idempotente de tu lado). Para un seguimiento que nunca se modificó, updated_at es igual a created_at (nunca null): el cursor siempre tiene un valor usable, sin que tengas que resolver vos el fallback.Estados
| Estado | Descripción |
|---|---|
| PENDING_SCHEDULE | Pendiente de programar. Cargada; todavía no se pidió confirmación al proveedor. |
| READY_TO_SEND | Lista para enviar. Lista para mandar el pedido de confirmación de entrega. |
| SENT_PENDING_RESPONSE | Enviada, esperando respuesta. Se pidió confirmación; el proveedor aún no respondió. |
| NO_RESPONSE | Sin respuesta. No respondió tras los recordatorios. |
| CONFIRMED_ON_TIME | Confirmada en fecha. El proveedor confirmó la fecha original. |
| CONFIRMED_DELAYED | Confirmada con demora. Confirmó, pero con fecha posterior a la original. |
| CONFIRMED_EARLY | Confirmada anticipada. Confirmó una fecha anterior a la original. |
| REQUIRES_REVIEW | Requiere revisión. Respondió algo ambiguo o cambió condiciones; hay que revisarlo. |
| DELIVERED | Entregada. Ya entregada. |
| CANCELLED | Cancelada. La OC fue cancelada. |
| SUPPLIER_NOT_FOUND | Proveedor no identificado. No se pudo matchear el proveedor (por código ERP o CUIT). |
| STAND_BY | Pausada manualmente y todavía abierta: no se le manda ningún mail. Es un estado virtual — un estado terminal (DELIVERED/CANCELLED) siempre le gana a STAND_BY, así que una línea pausada que se entrega o se cancela se informa por su estado real. |
curl -H "X-API-Key: sk_live_abc123..." \
"https://api.gosourced.ai/api/v1/delivery-trackings?since=2026-09-01T00:00:00Z&open_only=true"Objeto de seguimiento (delivery tracking)
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| id | integer | requerido | Id interno del seguimiento. |
| po_code | string | requerido | Número de OC en el sistema del cliente. |
| line_position | integer | requerido | Posición de la línea dentro de la OC. |
| source | string | requerido | Origen del seguimiento: api, csv o erp_sync. La API lee seguimientos de las tres fuentes. |
| supplier | object | opcional | Proveedor vinculado, o null si todavía no se pudo resolver (SUPPLIER_NOT_FOUND). |
| supplier.id | integer | opcional | Id interno del proveedor. |
| supplier.name | string | opcional | Razón social del proveedor. |
| supplier.tax_id | string | opcional | CUIT/RUT del proveedor. |
| supplier.erp_code | string | opcional | Código ERP del proveedor para tu organización. |
| supplier_name | string | opcional | Nombre del proveedor tal como se cargó (puede diferir de supplier.name si vino solo como texto libre). |
| material_code | string | opcional | Código del material o ítem. |
| description | string | opcional | Descripción del ítem. |
| quantity_ordered | float | opcional | Cantidad pedida. |
| quantity_pending | float | opcional | Cantidad pendiente de entrega. |
| unit_of_measure | string | opcional | Unidad de medida. |
| po_date | date | opcional | Fecha de la OC. |
| original_delivery_date | date | opcional | Fecha de entrega comprometida originalmente. |
| eta | date | opcional | Fecha estimada de entrega. La completa un comprador a mano desde la app (el modal de detalle); ningún proceso automático la calcula. En una línea creada por API queda null hasta que alguien la carga. |
| actual_delivery_date | date | opcional | Fecha real de entrega, cuando el estado es DELIVERED. |
| received_quantity | float | opcional | Cantidad recibida, cuando el estado es DELIVERED. |
| status | string | opcional | Estado público (ver la tabla de estados). |
| is_urgent | boolean | requerido | Si la línea está marcada como urgente. |
| paused | boolean | requerido | Si el envío de seguimiento está pausado (sending_paused). |
| pause_reason | string | opcional | Motivo de la pausa. null si no está pausada. |
| business_unit_code | string | opcional | Código de la unidad de negocio de la línea. null si nunca se envió. |
| business_unit_name | string | opcional | Nombre de la unidad de negocio. null si no se envió. |
| followup_count | integer | requerido | Cantidad de recordatorios enviados en el ciclo actual. |
| last_followup_sent_at | datetime | opcional | Fecha y hora del último recordatorio enviado. |
| next_followup_date | datetime | opcional | Fecha planificada para el próximo recordatorio. |
| supplier_response | object | requerido | Última respuesta del proveedor. |
| supplier_response.responded_at | datetime | opcional | Fecha y hora en que respondió el proveedor. |
| supplier_response.confirmed_delivery_date | date | opcional | Fecha de entrega que confirmó el proveedor. |
| supplier_response.delay_reason | string | opcional | Motivo de la demora, si el proveedor lo indicó. |
| supplier_response.notes | string | opcional | Notas de la última respuesta del proveedor. null si todavía no respondió. |
| supplier_response.quality | string | opcional | Calidad de la respuesta interpretada por la IA (por ejemplo, concrete). |
| created_at | datetime | opcional | Fecha de creación del seguimiento. |
| updated_at | datetime | opcional | Fecha de la última modificación. |
{
"data": [
{
"id": 8821,
"po_code": "OC-2026-0455",
"line_position": 10,
"source": "api",
"supplier": {
"id": 42,
"name": "Distribuidora Industrial SA",
"tax_id": "30712345678",
"erp_code": "PROV-CAL-00123"
},
"supplier_name": "Distribuidora Industrial SA",
"material_code": "MAT-001234",
"description": "Rodamiento SKF 6205",
"quantity_ordered": 50,
"quantity_pending": 20,
"unit_of_measure": "UN",
"po_date": null,
"original_delivery_date": "2026-10-15",
"eta": "2026-10-18",
"actual_delivery_date": null,
"received_quantity": null,
"status": "CONFIRMED_DELAYED",
"is_urgent": false,
"paused": false,
"pause_reason": null,
"business_unit_code": "1010",
"business_unit_name": "Rental Buenos Aires",
"followup_count": 2,
"last_followup_sent_at": "2026-09-10T13:05:00+00:00",
"next_followup_date": "2026-09-17T00:00:00+00:00",
"supplier_response": {
"responded_at": "2026-09-12T09:40:00+00:00",
"confirmed_delivery_date": "2026-10-18",
"delay_reason": "Demora del fabricante",
"notes": "Se despachó el lote parcial, el resto llega la semana próxima.",
"quality": "concrete"
},
"created_at": "2026-09-01T12:00:00+00:00",
"updated_at": "2026-09-12T09:40:00+00:00"
}
],
"total": 1,
"page": 1,
"page_size": 50,
"has_more": false
}Obtener un seguimiento de entrega
/api/v1/delivery-trackings/{id}API Key requeridaObtener el detalle de un seguimiento por su id interno.
Misma forma que cada elemento de GET /delivery-trackings.
curl -H "X-API-Key: sk_live_abc123..." \
https://api.gosourced.ai/api/v1/delivery-trackings/8821{
"id": 8821,
"po_code": "OC-2026-0455",
"line_position": 10,
"source": "api",
"supplier": {
"id": 42,
"name": "Distribuidora Industrial SA",
"tax_id": "30712345678",
"erp_code": "PROV-CAL-00123"
},
"supplier_name": "Distribuidora Industrial SA",
"material_code": "MAT-001234",
"description": "Rodamiento SKF 6205",
"quantity_ordered": 50,
"quantity_pending": 20,
"unit_of_measure": "UN",
"po_date": null,
"original_delivery_date": "2026-10-15",
"eta": "2026-10-18",
"actual_delivery_date": null,
"received_quantity": null,
"status": "CONFIRMED_DELAYED",
"is_urgent": false,
"paused": false,
"pause_reason": null,
"business_unit_code": "1010",
"business_unit_name": "Rental Buenos Aires",
"followup_count": 2,
"last_followup_sent_at": "2026-09-10T13:05:00+00:00",
"next_followup_date": "2026-09-17T00:00:00+00:00",
"supplier_response": {
"responded_at": "2026-09-12T09:40:00+00:00",
"confirmed_delivery_date": "2026-10-18",
"delay_reason": "Demora del fabricante",
"notes": "Se despachó el lote parcial, el resto llega la semana próxima.",
"quality": "concrete"
},
"created_at": "2026-09-01T12:00:00+00:00",
"updated_at": "2026-09-12T09:40:00+00:00"
}Un id que no existe, o que pertenece a otra organización, devuelve 404 dlvTrackingNotFound: nunca se confirma la existencia de un seguimiento ajeno.
Historial de eventos de un seguimiento
/api/v1/delivery-trackings/{id}/eventsAPI Key requeridaLínea de tiempo paginada de los eventos de un seguimiento, del más antiguo al más reciente.
Parámetros de consulta
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| page | integer | opcional | Número de página (por defecto: 1). |
| page_size | integer | opcional | Items por página, 1-200 (por defecto: 50). |
Objeto de evento
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| id | integer | requerido | Id interno del evento. |
| type | string | requerido | Tipo de evento (ver la tabla de tipos). |
| occurred_at | datetime | opcional | Fecha y hora en que ocurrió. |
| actor_type | string | requerido | Quién lo generó: system, supplier, user o api. |
| data | object | requerido | Detalle del evento; la forma depende de type (ver la tabla de tipos). |
Tipos de evento
| Tipo | data |
|---|---|
| created | { source } |
| reactivated | { source } (el path de la API también agrega from con el estado anterior) |
| cycle_reset | { reason? } — reinicio del ciclo de seguimiento sin reactivación (cambio de proveedor por API, reinicio manual desde la app): la respuesta del proveedor anterior a este evento deja de ser la vigente |
| line_updated | { <campo>: { from, to } } — una clave por cada campo modificado (excepción: buyer_email solo lleva to, sin from) |
| followup_sent | { followup_number, escalated } |
| response_received | { confirmed_delivery_date, delay_reason, notes, quality, summary, outcome } |
| status_changed | { from, to, reason? } |
| paused | { reason } |
| resumed | { reason? } |
| urgency_changed | { is_urgent } |
| delivered | { from, to, reason?, actual_delivery_date, received_quantity } (el cierre por snapshot agrega snapshot_id) |
| cancelled | { from, to, reason? } |
{
"data": [
{
"id": 55009,
"type": "created",
"occurred_at": "2026-09-01T12:00:00+00:00",
"actor_type": "api",
"data": { "source": "api" }
},
{
"id": 55012,
"type": "status_changed",
"occurred_at": "2026-09-12T09:40:00+00:00",
"actor_type": "supplier",
"data": {
"from": "SENT_PENDING_RESPONSE",
"to": "CONFIRMED_DELAYED",
"reason": "supplier_response"
}
}
],
"total": 2,
"page": 1,
"page_size": 50,
"has_more": false
}Acciones en bloque
/api/v1/delivery-trackings/actionsAPI Key requeridaAplicar una acción a hasta 200 líneas a la vez. Si se omite line_position en un target, la acción se aplica a todas las líneas abiertas de esa OC.
Cuerpo de la solicitud
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| action | string | requerido | mark_delivered, cancel, pause, resume, set_urgent o unset_urgent. |
| targets | array | requerido | Líneas a las que aplicar la acción. Entre 1 y 200. |
| targets[].po_code | string | requerido | Número de OC. Máximo 100 caracteres. |
| targets[].line_position | integer | opcional | Posición de línea. Si se omite, aplica a todas las líneas abiertas de la OC. |
| reason | string | opcional | Motivo. Obligatorio para pause; opcional para el resto. Máximo 500 caracteres. |
| actual_delivery_date | date | opcional | Fecha real de entrega. Solo se usa con mark_delivered; por defecto, hoy. |
| received_quantity | float | opcional | Cantidad recibida. Solo se usa con mark_delivered. ≥ 0. |
Acciones disponibles
| action | Efecto | Requiere |
|---|---|---|
| mark_delivered | Pasa a DELIVERED. actual_delivery_date usa la fecha enviada o, si no se envía, la fecha de hoy de la organización. received_quantity queda registrada si se envía. | — |
| cancel | Pasa a CANCELLED. | — |
| pause | Pausa el envío de recordatorios (sending_paused = true). | reason |
| resume | Reanuda el envío de recordatorios (sending_paused = false). | — |
| set_urgent | Marca la línea como urgente. | — |
| unset_urgent | Desmarca la línea como urgente. | — |
- Las acciones son idempotentes: repetir una acción ya aplicada no escribe un evento nuevo y devuelve uno de estos seis resultados: already_delivered, already_cancelled, already_paused, already_active (para resume), already_urgent o already_not_urgent.
- No se puede cruzar entre estados terminales: mark_delivered sobre una línea CANCELLED, o cancel sobre una línea DELIVERED, devuelve el error dlvActionNotAllowedInStatus. Para reabrir una línea cerrada hay que reenviarla por POST /delivery-trackings (el upsert sí reactiva).
- mark_delivered y cancel solo aplican a líneas creadas por esta API (source = "api"); sobre líneas de CSV o de la sincronización ERP devuelven dlvTrackingManagedByOtherSource. pause, resume, set_urgent y unset_urgent aplican a líneas de cualquier fuente, siempre que no estén en un estado terminal.
curl -X POST https://api.gosourced.ai/api/v1/delivery-trackings/actions \
-H "X-API-Key: sk_live_abc123..." \
-H "Content-Type: application/json" \
-d '{
"action": "mark_delivered",
"targets": [
{ "po_code": "OC-2026-0455", "line_position": 10 }
],
"received_quantity": 50
}'Respuesta de las acciones
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| applied_count | integer | requerido | Cantidad de seguimientos (no de targets del request) sobre los que se aplicó la acción. Los resultados already_* no suman acá. |
| error_count | integer | requerido | Cantidad de seguimientos (no de targets del request) con error. Los resultados already_* no suman acá. |
| results | array | requerido | Resultado de cada target. |
Objeto de resultado por target (results[])
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| po_code | string | requerido | Número de OC. |
| line_position | integer | opcional | Posición de línea afectada. Puede diferir del target si se omitió (una OC entera puede generar varios resultados). |
| id | integer | opcional | Id interno del seguimiento. Ausente cuando el target no matchea ningún seguimiento. |
| result | string | requerido | applied, error, o uno de los seis resultados idempotentes: already_delivered, already_cancelled, already_paused, already_active, already_urgent, already_not_urgent. |
| status | string | opcional | Estado del seguimiento después de la acción. |
| code | string | opcional | Código del error (dlv*). Solo presente cuando result es error. |
| message | string | opcional | Detalle del error en inglés, pensado para logs. |
// 200 OK
{
"applied_count": 1,
"error_count": 0,
"results": [
{
"po_code": "OC-2026-0455",
"line_position": 10,
"id": 8821,
"status": "DELIVERED",
"result": "applied"
}
]
}Pendiente completo (snapshot)
/api/v1/delivery-trackings/snapshotsAPI Key requeridaMandá en un solo envío TODAS las líneas que tenés pendientes de entrega, hasta 10.000. El cuerpo es el mismo que el de POST /delivery-trackings: un objeto con "lines", y cada línea con los mismos campos. La única diferencia es que acá va el pendiente completo junto, no partido en tandas de 200. Respondemos 202 con un id y lo procesamos en segundo plano; el resultado se consulta con GET /delivery-trackings/snapshots/{id}.
Límite de tasa: 6 snapshots aceptados por hora por API key (alcanza para mandar el pendiente completo una o dos veces por día). Un envío rechazado con 422 no consume cupo. Al pasarte, respondemos 429 dlvSnapshotRateLimited con el header Retry-After.
Cuerpo de la solicitud
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| lines | array | requerido | Todas tus líneas pendientes. Cada línea lleva exactamente los mismos campos que en POST /delivery-trackings (po_code, line_position, original_delivery_date, description, quantity_ordered, quantity_pending, supplier_erp_code o supplier_tax_id, y los opcionales). De 1 a 10.000. |
- Las líneas cargadas por API que estaban abiertas y no vienen en el snapshot se cierran como DELIVERED (evento delivered con reason absent_from_api_snapshot). Las líneas cargadas por CSV o por el ERP no se tocan. El alcance es toda la organización: incluye las líneas cargadas por API con cualquier API key y las enviadas por POST /delivery-trackings. Si tenés más de una integración, cada snapshot tiene que traer el pendiente de todas.
- Todo o nada: si una línea es inválida o una misma línea (po_code + line_position) viene repetida, se rechaza el snapshot completo con 422 y no se procesa nada.
- Solo cuenta el snapshot más reciente: si mandás uno nuevo antes de que el anterior empiece a procesarse, el anterior queda SUPERSEDED sin aplicarse. Si el anterior ya estaba en proceso, termina y el nuevo se aplica inmediatamente después.
- Protección: si el cierre fuera a cerrar más de 20 líneas y más de la mitad de las abiertas, no se cierra ninguna (closure_skipped = true); las altas y actualizaciones se aplican igual. Revisá que el envío esté completo y volvé a mandarlo.
- El cuerpo del envío no puede superar ~10 MB; con descripciones largas, 10.000 líneas pueden pasarse: en ese caso el gateway responde 413.
- POST /delivery-trackings (hasta 200 líneas) sigue disponible para cambios puntuales y nunca cierra líneas por ausencia.
Cómo pasar de POST /delivery-trackings a snapshots
- Juntá en un solo array todas las líneas que hoy mandás en varias tandas de 200. Cada línea queda igual que ahora.
- Mandá ese array en un solo POST a /delivery-trackings/snapshots y guardá el id de la respuesta (202).
- Consultá GET /delivery-trackings/snapshots/{id} cada 10 a 15 segundos hasta que status sea COMPLETED, FAILED o SUPERSEDED.
- Con COMPLETED, revisá errors (líneas que no se pudieron aplicar) y closure_skipped: si es true, no se cerró ninguna línea porque el envío parecía incompleto.
- Repetilo cada vez que corra tu proceso (una o dos veces por día). Las líneas que dejen de venir se cierran solas.
curl -X POST https://api.gosourced.ai/api/v1/delivery-trackings/snapshots \
-H "X-API-Key: sk_live_abc123..." \
-H "Content-Type: application/json" \
-d '{
"lines": [
{
"po_code": "OC-2026-0455", "line_position": 10,
"original_delivery_date": "2026-10-15",
"supplier_erp_code": "PROV-0042",
"description": "Rodamiento 6204-2RS",
"quantity_ordered": 50, "quantity_pending": 20
}
]
}'{
"id": 123,
"status": "PENDING",
"total_lines": 3120,
"received_at": "2026-09-28T13:05:00Z"
}Resultado de un snapshot
/api/v1/delivery-trackings/snapshots/{snapshot_id}API Key requeridaEstado y resultado de un snapshot. Consultalo cada algunos segundos hasta que status sea COMPLETED, FAILED o SUPERSEDED.
Objeto snapshot
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| status | string | requerido | PENDING, PROCESSING, COMPLETED, FAILED o SUPERSEDED. |
| processed_lines | integer | requerido | Líneas ya aplicadas: sirve para ver el avance. |
| created_count / updated_count / unchanged_count / error_count | integer | requerido | Mismos contadores que el upsert. |
| closed_count | integer | requerido | Líneas cerradas por no venir en el snapshot. |
| closure_skipped | boolean | requerido | true si el cierre no se hizo por la protección de cantidad. |
| closure_skipped_reason | string | opcional | Con closure_skipped = true, el motivo: too_many_absent (el cierre habría superado la protección de cantidad). Si no, null. |
| would_close_count | integer | requerido | Cuántas líneas se habrían cerrado. |
| superseded_by | integer | opcional | Con SUPERSEDED, el id del snapshot más nuevo que lo reemplazó. |
| error_code | string | opcional | Con FAILED, dlvSnapshotProcessingFailed. Un snapshot FAILED nunca cierra líneas: reenviarlo es seguro. |
| errors | array | requerido | Líneas que no se pudieron aplicar, con po_code, line_position, code y message. Esas líneas cuentan como presentes: no se cierran. |
curl https://api.gosourced.ai/api/v1/delivery-trackings/snapshots/123 \
-H "X-API-Key: sk_live_abc123..."{
"id": 123,
"status": "COMPLETED",
"received_at": "2026-09-28T13:05:00Z",
"started_at": "2026-09-28T13:05:01Z",
"completed_at": "2026-09-28T13:05:48Z",
"total_lines": 3120,
"processed_lines": 3120,
"created_count": 14,
"updated_count": 230,
"unchanged_count": 2860,
"error_count": 16,
"closed_count": 41,
"would_close_count": 41,
"closure_skipped": false,
"closure_skipped_reason": null,
"superseded_by": null,
"error_code": null,
"errors": [
{ "po_code": "OC-2026-0460", "line_position": 20, "code": "dlvSupplierReferenceAmbiguous", "message": "..." }
]
}Códigos de error — seguimiento de entregas
Códigos propios de los endpoints de seguimiento de entregas. Algunos son errores de la solicitud completa (columna HTTP 400/403/404/422); el resto viaja en el campo code de cada línea o target con error, dentro de una respuesta 200. Un campo obligatorio ausente o inválido (descripción, cantidades, proveedor sin código ERP ni CUIT, pendiente mayor que pedida) no tiene código propio: rechaza el batch completo con 422 y el detalle estándar de validación.
| Código | HTTP | Significado |
|---|---|---|
| dlvFeatureDisabled | 403 | El seguimiento de entregas no está habilitado para esta organización. |
| dlvTrackingNotFound | 404 | No se encontró el seguimiento (id inexistente o de otra organización). |
| dlvTrackingManagedByOtherSource | 200 | Esta línea la administra otra fuente (CSV o sincronización ERP). |
| dlvSupplierReferenceConflict | 200 | El código ERP y el CUIT corresponden a proveedores distintos. |
| dlvSupplierReferenceAmbiguous | 200 | El código ERP o el CUIT corresponde a más de un proveedor. |
| dlvDuplicateLineInBatch | 200 | La línea (mismo po_code y line_position) aparece más de una vez en el envío. |
| dlvNothingPending | 200 | La línea no tiene nada pendiente (quantity_pending = 0): no se crea un seguimiento para reclamar 0 unidades. |
| dlvConcurrentUpsert | 200 | La línea se modificó al mismo tiempo desde otro envío. Reintentá. |
| dlvLineWriteFailed | 200 | No se pudo guardar la línea. Revisá los datos e intentá de nuevo (no es un problema de concurrencia). |
| dlvInvalidStatusFilter | 400 | El valor de status no es un estado válido. |
| dlvInvalidSince | 400 | Fecha inválida en since. Usá formato ISO 8601. |
| dlvPauseReasonRequired | 400 | Para pause hay que indicar un reason. |
| dlvActionNotAllowedInStatus | 200 | La acción no aplica al estado actual del seguimiento. |
| dlvSnapshotDuplicateLines | 422 | El snapshot trae la misma línea (po_code + line_position) más de una vez. En params.keys van las repetidas (hasta 50). |
| dlvSnapshotNotFound | 404 | No se encontró el snapshot (id inexistente o de otra organización). |
| dlvSnapshotRateLimited | 429 | Ya se aceptaron 6 snapshots en la última hora con esta API key. En params.retry_after (y en el header Retry-After) van los segundos hasta que se libera un lugar. |
| dlvSnapshotProcessingFailed | 200 | El snapshot no se pudo procesar después de 3 intentos. Puede haber aplicado parte de las líneas, pero no cerró ninguna. Reenvialo. Este código no viaja en el code de una línea: llega en el error_code del snapshot (GET /delivery-trackings/snapshots/{id}, que responde 200). |
dlvTrackingNotFound es las dos cosas: 404 en los GET por id, y por target dentro de /actions.Webhooks
Recibí eventos en tiempo real cuando cambia una orden de compra, una requisición o un seguimiento de entrega, en vez de tener que consultarlos por polling.
Configuración
Los webhooks se configuran desde la aplicación (no por API key), con un usuario organization_admin, en Mi Organización → Desarrolladores → Webhooks.
Envelope
Todo evento llega con esta forma:
{
"id": "evt_5f2a1c9b8e7d4a3f1b2c",
"type": "delivery_tracking.status_changed",
"created_at": "2026-09-17T14:05:00+00:00",
"data": {
"tracking": "... mismo objeto que devuelve GET /delivery-trackings/{id} ...",
"event": "... mismo objeto que devuelve GET /delivery-trackings/{id}/events ..."
}
}Headers
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| X-Webhook-Signature | string | requerido | Firma HMAC-SHA256 del cuerpo crudo de la solicitud, con el prefijo sha256=. |
| X-Webhook-Event | string | requerido | El type del evento — el mismo valor que el campo type de nivel superior del envelope. No existe data.type: data es directamente el recurso (o el par tracking / event en los eventos de seguimiento). |
| X-Webhook-Delivery-Id | string | requerido | Id del evento, no de este intento de entrega en particular: es el mismo valor en cada reintento y en cada webhook suscrito que lo recibe (igual que id en el cuerpo) — por eso sirve para deduplicar. |
Verificar la firma
Calculá el HMAC-SHA256 sobre los bytes crudos del cuerpo de la solicitud (antes de parsear el JSON) usando el secret del webhook, y compará con X-Webhook-Signature con una comparación de tiempo constante.
import hashlib
import hmac
def verify_webhook(secret: str, raw_body: bytes, signature_header: str | None) -> bool:
if not signature_header or not signature_header.startswith("sha256="):
return False
expected = "sha256=" + hmac.new(
secret.encode(), raw_body, hashlib.sha256
).hexdigest()
# compare_digest on two strings of different length returns False,
# it never raises — safe even with a malformed header.
return hmac.compare_digest(expected, signature_header)
const crypto = require("crypto");
function verifyWebhook(secret, rawBody, signatureHeader) {
if (!signatureHeader || !signatureHeader.startsWith("sha256=")) {
return false;
}
const expected =
"sha256=" +
crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
const expectedBuf = Buffer.from(expected);
const receivedBuf = Buffer.from(signatureHeader);
// timingSafeEqual throws on a length mismatch — check first, it never
// needs to be constant-time (the lengths themselves aren't a secret).
if (expectedBuf.length !== receivedBuf.length) {
return false;
}
return crypto.timingSafeEqual(expectedBuf, receivedBuf);
}
Reintentos
Si la entrega falla (timeout, error de red o una respuesta que no sea 2xx), se reintenta hasta 3 veces más (4 intentos en total: el envío inicial más 3 reintentos). Cada intento fallido programa el siguiente con un backoff mínimo de 1, 5 y 15 minutos; ese reintento se dispara recién la próxima vez que corre el barrido de reintentos (reminder-checker), que se ejecuta cada hora en horario laboral argentino — el tiempo real hasta el próximo intento puede ser mayor a esos minutos.
- Intento 1: inmediato, al momento del evento.
- Intento 2: no antes de 1 minuto después.
- Intento 3: no antes de 5 minutos después.
- Intento 4: no antes de 15 minutos después. Si también falla, la entrega queda FAILED.
Eventos disponibles
| type | data | Cuándo se dispara |
|---|---|---|
| purchase_order.created | objeto de la OC | Se creó una nueva orden de compra. |
| purchase_order.updated | id, code, status, reason | Cambió el estado u otro dato de una orden de compra (por ejemplo, un rechazo). |
| requisition.discarded | external_id, requisition_id, status (discarded | cancelled | partially_cancelled | already_cancelled), pr_id, pr_code (solo si la requisición tenía líneas lanzadas a una Solicitud de Compra), discarded_by, discarded_at | Se descartó una requisición (por ejemplo, cancelada desde el ERP con POST /requisitions/cancel). |
| delivery_tracking.response_received | tracking, event | El proveedor respondió el pedido de confirmación de entrega. |
| delivery_tracking.status_changed | tracking, event | Cambió el estado del seguimiento (incluye la reactivación de una línea previamente cerrada, que vuelve a PENDING_SCHEDULE). |
| delivery_tracking.delivered | tracking, event | El seguimiento pasó a DELIVERED. |
| delivery_tracking.cancelled | tracking, event | El seguimiento pasó a CANCELLED. |
Referencia de esquemas
Referencia rápida de todos los esquemas de solicitud/respuesta usados en los endpoints.
Estados de requisición
| Estado | Descripción |
|---|---|
| PENDING | Importada, esperando revisión de un comprador en Sourced. |
| LAUNCHED | El comprador lanzó la requisición como Solicitud de Compra. |
| DISCARDED | La requisición fue descartada manualmente. |
| SUPERSEDED | Se importó una versión más nueva con el mismo external_id. |
Estados de adjuntos
| Estado | Descripción |
|---|---|
| PENDING | Se está descargando; la requisición no se puede lanzar hasta que termine. |
| DOWNLOADED | Disponible: viaja con el pedido al lanzar. |
| FAILED | No se pudo obtener el archivo, es de un material anterior o la requisición no tiene líneas abiertas donde adjuntarlo. No bloquea el lanzamiento; mandá un reenvío si lo necesitás. |
| SKIPPED_LAUNCHED | Estaba dirigido a una línea ya lanzada, así que no se agregó. |
Estados de orden de compra
| Estado | Descripción |
|---|---|
| DRAFT | OC creada en Sourced, pendiente de sincronización al ERP. |
| CREATED | OC creada en el ERP externo, pendiente de aprobación. |
| CONFIRMED | OC aprobada completamente en el ERP externo. |
| REJECTED | OC rechazada en el ERP externo. La PR asociada se reabre automáticamente. |
| CANCELLED | Compra caída. La OC se cancela y la PR asociada también - nada se reabre. |
Códigos de referencia
Catálogos de monedas y tipos de pago aceptados por la API. Usalos para mapear estos valores con los códigos equivalentes en tu ERP.
Monedas
Sourced no usa un enum cerrado de monedas: cualquier código ISO 4217 válido de 3 letras es aceptado. Las monedas más utilizadas por nuestros clientes son:
| Código | Moneda |
|---|---|
| ARS | Peso argentino |
| USD | Dólar estadounidense |
| EUR | Euro |
| BRL | Real brasileño |
Tipos de pago (payment_terms_code)
Catálogo global de condiciones de pago disponibles en Sourced. Usá el código (columna Code) en el campo payment_terms_code de la PO. El campo type indica la naturaleza del pago: IMMEDIATE (al recibir), ADVANCE (anticipado), NET_DAYS (a X días de la factura).
| Código | Nombre | Tipo | Días | Descripción |
|---|---|---|---|---|
| COD | Cash On Delivery | IMMEDIATE | 0 | Pago contra entrega |
| IMM | Immediate Payment | IMMEDIATE | 0 | Pago inmediato / Contado |
| ADV100 | Advance Payment | ADVANCE | 0 | Pago por adelantado 100% |
| ADV50 | Advance 50% | ADVANCE | 0 | Pago por adelantado del 50% |
| NET7 | Net 7 Days | NET_DAYS | 7 | Pago a 7 días de la fecha de factura |
| NET10 | Net 10 Days | NET_DAYS | 10 | Pago a 10 días de la fecha de factura |
| NET15 | Net 15 Days | NET_DAYS | 15 | Pago a 15 días de la fecha de factura |
| NET20 | Net 20 Days | NET_DAYS | 20 | Pago a 20 días de la fecha de factura |
| NET21 | Net 21 Days | NET_DAYS | 21 | Pago a 21 días de la fecha de factura |
| NET30 | Net 30 Days | NET_DAYS | 30 | Pago a 30 días de la fecha de factura |
| EOM30 | End of Month + 30 | NET_DAYS | 30 | Pago a fin de mes más 30 días |
| NET35 | Net 35 Days | NET_DAYS | 35 | Pago a 35 días de la fecha de factura |
| NET40 | Net 40 Days | NET_DAYS | 40 | Pago a 40 días de la fecha de factura |
| NET45 | Net 45 Days | NET_DAYS | 45 | Pago a 45 días de la fecha de factura |
| NET60 | Net 60 Days | NET_DAYS | 60 | Pago a 60 días de la fecha de factura |
| NET75 | Net 75 Days | NET_DAYS | 75 | Pago a 75 días de la fecha de factura |
| NET90 | Net 90 Days | NET_DAYS | 90 | Pago a 90 días de la fecha de factura |
Unidades de medida
Códigos de unidad de medida aceptados en items de requisiciones. Se envían en el campo unit_of_measure.
| Código | Unidad |
|---|---|
| EA | Unidad |
| PCS | Piezas |
| KG | Kilogramo |
| G | Gramo |
| LB | Libra |
| OZ | Onza |
| M | Metro |
| CM | Centímetro |
| MM | Milímetro |
| IN | Pulgada |
| FT | Pie |
| YD | Yarda |
| L | Litro |
| ML | Mililitro |
| GAL | Galón |
| QT | Cuarto de galón |
| PT | Pinta |
| FL_OZ | Onza líquida |
| M2 | Metro cuadrado |
| CM2 | Centímetro cuadrado |
| FT2 | Pie cuadrado |
| IN2 | Pulgada cuadrada |
| YD2 | Yarda cuadrada |
| M3 | Metro cúbico |
| CM3 | Centímetro cúbico |
| FT3 | Pie cúbico |
| IN3 | Pulgada cúbica |
| YD3 | Yarda cúbica |
| BOX | Caja |
| CASE | Caja/Estuche |
| PACK | Paquete |
| SET | Juego |
| KIT | Kit |
| BUNDLE | Manojo |
| ROLL | Rollo |
| SHEET | Hoja/Lámina |
| PALLET | Paleta |
| DRUM | Tambor/Bidón |
| BAG | Bolsa |
| BOTTLE | Botella |
| CEN | Centena |
Flujo de integración
Patrón de integración típico para una sincronización ERP programada (ej: cron job cada 15 minutos):
Recopilar nuevas requisiciones del ERP
Consultar tu ERP por requisiciones aprobadas que aún no fueron sincronizadas con Sourced.
Verificar existentes (opcional)
Llamar a POST /requisitions/check-existing con external_ids para filtrar requisiciones ya sincronizadas.
Importar requisiciones
Llamar a POST /requisitions con el lote de requisiciones nuevas/actualizadas. Guardar los requisition_ids devueltos.
Consultar órdenes de compra nuevas
Llamar a GET /purchase-orders?since={last_sync_timestamp}&status=DRAFT para obtener OC nuevas. Crear la OC en tu ERP y llamar a POST /purchase-orders/{id}/status con {"status": "CREATED"}.
Confirmar OC aprobadas
Cuando la OC se apruebe en tu ERP, llamar a POST /purchase-orders/{id}/status con {"status": "CONFIRMED"} para cerrar el ciclo.
# Step 1: Get pending requisitions from ERP
ERP_REQS=$(query_erp_pending_requisitions)
# Step 2: Check which ones already exist in Sourced
EXISTING=$(curl -s -X POST \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d "{"external_ids": $ERP_REQ_IDS}" \
https://api.gosourced.ai/api/v1/requisitions/check-existing)
# Step 3: Import only new requisitions
NEW_REQS=$(filter_not_found $ERP_REQS $EXISTING)
curl -s -X POST \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d "{"requisitions": $NEW_REQS}" \
https://api.gosourced.ai/api/v1/requisitions
# Step 4: Pull new POs since last sync
POS=$(curl -s \
-H "X-API-Key: $API_KEY" \
"https://api.gosourced.ai/api/v1/purchase-orders?since=$LAST_SYNC&status=DRAFT")
# Step 5: Create POs in ERP and update status
for PO in $POS; do
create_po_in_erp $PO
curl -s -X POST \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d "{"status": "CREATED", "external_id": "$ERP_PO_NUMBER"}" \
"https://api.gosourced.ai/api/v1/purchase-orders/$PO_ID/status"
done
# Step 6: When PO is approved in ERP, confirm it
curl -s -X POST \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"status": "CONFIRMED"}' \
"https://api.gosourced.ai/api/v1/purchase-orders/$PO_ID/status"Ejemplo completo
Una solicitud de importación realista con todos los campos disponibles:
curl -X POST https://api.gosourced.ai/api/v1/requisitions \
-H "X-API-Key: sk_live_abc123..." \
-H "Content-Type: application/json" \
-d '{
"requisitions": [
{
"external_id": "CAL-REQ-2026-0042",
"description": "Repuestos bomba centrífuga Planta Norte",
"requester_name": "Carlos Rodríguez",
"requester_email": "carlos.rodriguez@empresa.com",
"comments": "Urgente: bomba fuera de servicio desde 03/03. <b>Necesitamos entrega express.</b>",
"delivery_address": "Av. Industrial 4500, Parque Industrial Pilar, Buenos Aires",
"delivery_address_code": "PLANTA-NORTE",
"desired_delivery_lead_time_days": 7,
"created_date": "2026-03-04T09:15:00Z",
"offer_deadline": "2026-03-10T18:00:00Z",
"total_estimated_value": 1250000.00,
"currency": "ARS",
"department_code": "MANT-001",
"priority_level": "HIGH",
"items": [
{
"external_line_id": "CAL-REQ-2026-0042-L10",
"description": "Sello mecánico para bomba centrífuga KSB ETA 50-200",
"quantity": 2,
"unit_of_measure": "UN",
"target_price": 185000.00,
"estimated_price": 370000.00,
"currency": "ARS",
"category": "Repuestos Bombas",
"material_code": "MAT-BOM-0234",
"desired_delivery_lead_time_days": 10,
"detail": "Sello tipo cartucho, material: carburo de silicio / carburo de silicio",
"specifications": {
"manufacturer_code": "KSB-SEAL-50200",
"manufacturer_name": "KSB",
"manufacturer_description": "Mechanical seal for ETA 50-200 centrifugal pump",
"buyer_code": "MAT-BOM-0234",
"buyer_code_description": "Sello mecánico bomba KSB ETA 50-200",
"buyer_code_system": "Calipso",
"technical_specs": "Diámetro eje: 35mm, Material caras: SiC/SiC, Elastómeros: Viton",
"requirements": "Debe incluir certificado de calidad. Preferencia por repuesto original KSB."
}
},
{
"external_line_id": "CAL-REQ-2026-0042-L20",
"description": "Rodamiento SKF 6310-2RS",
"quantity": 4,
"unit_of_measure": "UN",
"target_price": 45000.00,
"currency": "ARS",
"category": "Rodamientos",
"material_code": "MAT-ROD-0089",
"specifications": {
"manufacturer_code": "6310-2RS1",
"manufacturer_name": "SKF",
"technical_specs": "50x110x27mm, sellado ambos lados, grasa estándar"
}
},
{
"external_line_id": "CAL-REQ-2026-0042-L30",
"description": "Aceite lubricante ISO VG 68",
"quantity": 20,
"unit_of_measure": "LT",
"target_price": 5500.00,
"currency": "ARS",
"category": "Lubricantes",
"specifications": {
"technical_specs": "Aceite mineral ISO VG 68, índice de viscosidad > 95"
}
}
],
"attachments": [
{
"url": "https://erp.empresa.com/files/plano-bomba-eta-50-200.pdf",
"filename": "plano_bomba_KSB_ETA_50-200.pdf",
"description": "Plano de despiece bomba KSB ETA 50-200",
"file_type": "application/pdf"
}
],
"raw_data": {
"calipso_doc_type": "SOL",
"calipso_doc_number": "0042",
"calipso_branch": "001",
"approved_by": "María González",
"cost_center": "CC-MANT-NORTE"
}
}
]
}'{
"success": true,
"created_count": 1,
"updated_count": 0,
"skipped_count": 0,
"error_count": 0,
"errors": [],
"requisition_ids": [1234]
}