API Reference¶
Base URL in development (from the host, via the gateway's published port):
http://localhost:19080/api/v1All endpoints (except the public ones) requireAuthorization: Bearer {token}.The "port NNNN" noted in each section below is the internal container port inside the
orpycamcp-netDocker network — never accessed directly from the host, always through theapi-gateway(published port 19080). See the full internal↔published mapping in Architecture.
Conventions¶
| Convention | Value |
|---|---|
| Authentication | Authorization: Bearer {JWT} |
| Pagination | ?page=1&size=20 — response includes X-Total-Count |
| Dates | ISO 8601 UTC — 2024-01-15T10:30:00Z |
| IDs | UUID v4 |
| Multi-tenancy | Header X-Tenant-Slug injected by the gateway after validating the JWT |
Standard error format¶
{
"error": "document_not_found",
"detail": "Radicado 2024-ICETEX-E-000001 no encontrado",
"status": 404
}
auth-service — port 8001¶
POST /api/v1/auth/token — public¶
Authentication with credentials. Returns a pair of JWT tokens.
// Request
{ "username": "operador", "password": "orpycamcp_dev", "tenant_slug": "demo" }
// Response 200
{
"access_token": "eyJ...",
"refresh_token": "eyJ...",
"token_type": "Bearer",
"expires_in": 300
}
POST /api/v1/auth/refresh — public¶
Renews the access token with a valid refresh token.
POST /api/v1/auth/logout¶
Invalidates the refresh token in Keycloak.
GET /api/v1/auth/validate¶
Validates a Bearer token. Used internally by the api-gateway.
// Response 200
{
"valid": true,
"user_id": "uuid",
"username": "operador",
"tenant_slug": "demo",
"roles": ["operator"]
}
GET /api/v1/auth/me¶
Information about the authenticated user extracted from the token.
// Response 200
{
"user_id": "uuid",
"username": "operador",
"email": "operador@demo.orpycamcp.local",
"tenant_slug": "demo",
"roles": ["operator"]
}
URD and dependencia context (E08)¶
GET /api/v1/auth/users/{user_id}/urd·POST·DELETE /{urd_id}— manages the User-Role-Dependencia assignments (one per (user, dependencia); a single primary one). RequiresUSUA_PERM_ADMIN.GET /api/v1/auth/context— caller context:{user_id, active_depe_id, available_depts[]}.POST /api/v1/auth/context/switch{ "depe_id": 120 }— sets the active dependencia (organizational unit) for the user themselves. Does not reissue the token (ADR-013): it persists the context and audits it.400 no_urd_for_dependencyif the user has no active URD there.
Security classification (E08, RF-SEG-08, Law 1712/2014)¶
GET /api/v1/auth/security-levels— catalog of levels (1 Public, 2 Reserved, 3 Classified).PUT /api/v1/auth/groups/{group_id}/clearance{ "max_level": 2 }— sets the maximum accessible level for a group (requiresUSUA_PERM_ADMIN).GET /api/v1/auth/clearance— caller clearance:{user_id, max_level, accessible_levels[]}(MAX across their groups; ROOT = 3).GET /api/v1/auth/clearance/check?level=N—{level, max_level, allowed}(least privilege:allowed = max_level ≥ level).
GET /api/v1/audit¶
Queries the tenant's immutable audit log (ADR-008), most recent first. Requires USUA_PERM_ADMIN. Filters: ?action=&object_type=&object_ref=&actor=&page=1&size=20. Header X-Total-Count.
[ { "id": 42, "ts": "2024-01-15T10:30:00Z", "service": "document-service", "actor": "uuid",
"canal": "api", "action": "document.radicado_created", "object_type": "radicado",
"object_ref": "2024-ICETEX-E-000001", "payload": { "...": "..." }, "hash": "sha256..." } ]
GET /api/v1/audit/verify¶
Recomputes the tenant's hash chain and reports its integrity (ADR-008). Requires USUA_PERM_ADMIN.
ok=false with broken_id = id of the first row whose chain does not match.
tenant-service — port 8002¶
POST /api/v1/tenants¶
Creates an institution and provisions its PostgreSQL schema tenant_{slug}.
// Request
{ "slug": "icetex", "name": "Instituto Colombiano de Crédito Educativo", "code": "ICETEX" }
// Response 201
{
"id": "uuid",
"slug": "icetex",
"name": "Instituto Colombiano de Crédito Educativo",
"code": "ICETEX",
"status": "active",
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:00Z"
}
slug: lowercase letters, numbers and hyphens only (^[a-z0-9-]+$) — immutablecode: alphanumeric uppercase (^[A-Z0-9_]+$) — appears in radicado (official tracking record) numbers — immutable- 409 if slug or code already exist
GET /api/v1/tenants¶
Lists institutions. Query: ?status=active&page=1&size=20. Header X-Total-Count.
GET /api/v1/tenants/{id}¶
Get by UUID.
GET /api/v1/tenants/by-slug/{slug}¶
Get by slug. Used by other services to validate the JWT's tenant.
PATCH /api/v1/tenants/{id}¶
Update name or status. slug and code are immutable.
Tenant administration (E14)¶
- Dependencias (organizational chart):
GET/POST/PATCH /api/v1/dependencias,GET /api/v1/dependencias/by-codigo/{codigo}(hierarchical). - Catálogos (lookups):
GET/POST/PATCH/DELETE /api/v1/catalogos/{catalogo}—tipos-identificacion,tipos-remitente,medios-recepcion,tipos-anexo,causales,formas-envio,soportes,mensajes-rapidos. - Parameters:
GET /api/v1/config/params,GET /api/v1/config/params/{key}. - Non-business days / holidays:
GET /api/v1/config/holidays(?year=),POST /api/v1/config/holidays(create),DELETE /api/v1/config/holidays/{id},POST /api/v1/config/holidays/seed({year}→ seeds the automatically computed Colombian holidays). - Business days:
GET /api/v1/business-days/calculate?from_date=&days=— computes the due date applying weekends + holidays + the tenant's non-working days (basis for RF-RAD-04).
document-service — port 8003¶
Requires the
X-Tenant-SlugandX-User-Idheaders (injected by the gateway).
POST /api/v1/documents¶
Registers a new radicado (official tracking record). Assigns an atomic radicado number.
// Request
{
"doc_type": "E",
"subject": "Solicitud de certificado de notas",
"doc_class": "Derechos de Petición",
"dest_dept_code": 120,
"sender_name": "María García",
"sender_entity": "Ciudadana",
"pages": 2,
"response_days": 15,
"metadata": { "area": "juridica", "prioridad": "alta" },
"anexos": [
{ "file_id": "uuid", "filename": "solicitud.pdf", "file_size": 45231, "mime_type": "application/pdf",
"tipo_anexo": "soporte", "es_principal": true, "folios": 3 }
]
}
// Response 201
{
"id": "uuid",
"tracking_number": "2024-ICETEX-E-000001",
"doc_type": "E",
"year": 2024,
"sequence": 1,
"subject": "Solicitud de certificado de notas",
"status": "registered",
"registered_at": "2024-01-15T10:30:00Z",
"due_date": "2024-02-05",
"dest_dept": "Registro y Control",
"dest_dept_code": 120,
"anexos": [{ "id": "uuid", "file_id": "uuid", "filename": "solicitud.pdf", ... }]
}
doc_type:E(Entrada / inbound) |S(Salida / outbound) |I(Interno / internal)tracking_number: immutable once assignedresponse_days(optional): response deadline in business days (RF-RAD-04). If provided, document-service computesdue_dateby queryingtenant-service(GET /api/v1/business-days/calculate, which applies Colombian holidays and the tenant's non-working days). If omitted,due_dateisnull.due_date: due date (ISOYYYY-MM-DD) ornull.origin_dept_code/dest_dept_code(optional): code (depe_codi) of the dependencia (organizational unit) in the organizational chart. If provided, document-service validates them againsttenant-service(GET /api/v1/dependencias/by-codigo/{codigo}) and denormalizes the name of the dependencia intoorigin_dept/dest_dept(legal snapshot). Nonexistent code or inactive dependencia →400 invalid_dependencia.- If
tenant-serviceis unavailable when resolving the deadline or the dependencia, creation responds502 tenant_service_unavailableand the document is not registered. metadata(optional): variable metadata of the document type (ADR-007). If thedoc_classhas an active template, the values are validated against its JSON Schema before registration; if they do not comply →422 metadata_validation_failedand the document is not registered. Without a template, they are stored as-is. The column is indexed with GIN.
POST /api/v1/metadata/templates¶
Defines the metadata template (JSON Schema) of a document type. A single active template per tipo_documental.
// Request
{ "tipo_documental": "Factura", "version": 1, "activo": true,
"json_schema": { "type": "object", "required": ["valor"],
"properties": { "valor": { "type": "number" } } } }
422 invalid_json_schema if json_schema is not a valid JSON Schema; 409 conflict if that version, or an active one for the type, already exists.
GET /api/v1/metadata/templates¶
Lists templates; optional filter ?tipo_documental=Factura.
/api/v1/metadata/elements (metadata element catalog, RF-MET-03)¶
CRUD for reusable metadata field definitions (POST/GET/GET {id}/PATCH {id}/DELETE {id}). Attributes: clave, etiqueta, tipo_dato (text/number/date/boolean/select), longitud, ocurrencia_min/max, modificable, valor_default, opciones, orden, searchable, mapeo_dublin_core (RF-MET-09 hook). 409 if the key already exists.
GET /api/v1/documents¶
List radicados. Filters: ?doc_type=E&status=registered&page=1&size=20. Filter by metadata (RF-MET-02): ?meta.<campo>=valor (one or several) uses JSONB containment over the GIN index, e.g. ?meta.area=juridica.
GET / PATCH /api/v1/documents/{id}/disposition¶
Document disposition metadata (RF-MET-08): programa, retention_until, accion (conserve/delete/transfer/select), confirmado, marcado_eliminacion. PATCH merges the provided fields. 404 if the radicado does not exist.
GET /api/v1/documents/{id}¶
Get by UUID. Includes the list of anexos (attachments).
GET /api/v1/documents/by-tracking/{tracking_number}¶
Get by radicado number (e.g.: 2024-ICETEX-E-000001).
PATCH /api/v1/documents/{id}¶
Update subject, dest_dept, observations, status, pages.
tracking_number, doc_type, year, sequence are immutable.
storage-service — port 8005¶
Requires the
X-Tenant-Slugheader. Files stored in the MinIO bucketorpycamcp-{slug}-documents.
POST /api/v1/storage/upload¶
Uploads a file (multipart/form-data, field file).
- Computes SHA-256; if it already exists in the tenant, returns the existing record with
200(deduplication) - Creates the tenant's bucket if it does not exist
- Returns
201on a new upload - Validates the format against the list of allowed MIME types (
ALLOWED_MIME_TYPES, configurable;*= all) →415 unsupported_media_typeif not allowed (RF-DIG-04)
// Response 201
{
"id": "uuid",
"filename": "solicitud.pdf",
"mime_type": "application/pdf",
"file_size": 45231,
"sha256": "e3b0c44298fc...",
"uploaded_at": "2024-01-15T10:30:00Z"
}
GET /api/v1/storage/files/{file_id}¶
File metadata (without content).
GET /api/v1/storage/files/{file_id}/download¶
Returns a MinIO pre-signed URL valid for 1 hour.
{ "url": "http://minio:9000/orpycamcp-icetex-documents/uuid/solicitud.pdf?X-Amz-...", "expires_in": 3600 }
GET /api/v1/storage/files/{file_id}/verify¶
Re-verifies integrity (RF-DIG-02): downloads the object from MinIO, recomputes the SHA-256 and compares it with the recorded one.
-409 integrity_check_failed if the object in MinIO does not match the recorded fingerprint (integrity alert).
POST /api/v1/storage/files/{file_id}/replace¶
Replaces an anexo (attachment) by creating a new immutable version (RF-DIG-01; multipart/form-data, field file). The previous version is kept.
- 201 with the new version (version incremented); 200 if the content is identical (no version created); 409 not_current_version if {file_id} is not the current version; 415 if the format is not allowed.
GET /api/v1/storage/files/{file_id}/versions¶
Returns the version chain of the anexo (ordered by version). Each element includes version and is_current.
DELETE /api/v1/storage/files/{file_id}¶
Deletes from MinIO and from the database. Responds 204.
Digital preservation (E10, OAIS/AGN 001/2024)¶
GET /api/v1/preservacion/plan(current) ·PUT /api/v1/preservacion/plan(new version) ·GET /api/v1/preservacion/plan/versions— Versioned Digital Preservation Plan:formatos_destino,num_copias,periodicidad_fixity_dias,politica_migracion,contingencia.POST /api/v1/preservacion/eventos{object_ref, tipo, resultado, hash_before?, hash_after?, detalle?}·GET /api/v1/preservacion/eventos?object_ref=— immutable PREMIS log (INGESTA/FIJACION/MIGRACION/VALIDACION_PDFA/WORM/REPLICA).POST /api/v1/preservacion/aip{expediente_id, documentos:[{document_id, nombre, valor_huella?}], indice_sha256?}— packages the AIP (BagIt + PREMIS) and returnsbagit,premis_xml,aip_sha256.GET /api/v1/preservacion/aip/{expediente_id}retrieves the current AIP.
workflow-service — port 8006¶
Requires the
X-Tenant-SlugandX-User-Idheaders.Automatic distribution (RF-FLU-04): in addition to the endpoints, workflow-service consumes the
orpycamcp.document.eventsstream. When a document is registered (document.radicado.created) it evaluates the active rules and, if any matches, auto-assigns the radicado to its dependencia (step + append-only event, author = system). It is idempotent (it does not redistribute if the radicado already has steps).
POST /api/v1/workflows/assign¶
Assigns a radicado to a dependencia (first step or reassignment).
// Request
{
"radicado_id": "uuid",
"tracking_number": "2024-ICETEX-E-000001",
"to_dept": "Dirección Jurídica",
"assigned_to": "uuid-usuario",
"action": "assign",
"notes": "Para concepto jurídico"
}
// Response 201 — FlowStepResponse
POST /api/v1/workflows/transfer¶
Transfers the radicado to another dependencia. Requires from_dept.
GET /api/v1/workflows/{radicado_id}/history¶
Complete history of steps ordered by step_number ASC.
{
"radicado_id": "uuid",
"tracking_number": "2024-ICETEX-E-000001",
"steps": [
{ "step_number": 1, "from_dept": null, "to_dept": "Ventanilla", "action": "assign", "status": "completed", ... },
{ "step_number": 2, "from_dept": "Ventanilla", "to_dept": "Dirección Jurídica", "action": "transfer", "status": "pending", ... }
]
}
GET /api/v1/workflows/inbox¶
Typed inbox of active steps (RF-FLU-03). ?box=entrada|salida|internos (maps to doc_type E/S/I), &dept=&assigned_to=&page=&size=. Prioritizes by age; header X-Total-Count. 400 invalid_box if the box is not valid. Each step includes due_at and semaforo (green/yellow/red/overdue, RF-FLU-07).
POST /api/v1/workflows/overdue/scan¶
Overdue sweep (RF-FLU-07): detects active overdue steps not yet alerted, emits workflow.step.overdue to the bus (→ notifications) and marks them to avoid duplication. Idempotent per step; intended for periodic invocation by a scheduler. Responds { "alerted": N }.
Sequential approvals (vistos buenos) (legacy parity)¶
POST /api/v1/workflows/{radicado_id}/vistos-buenos{revisores: [uuid,...]}— creates the ordered review chain.POST /api/v1/workflows/{radicado_id}/vistos-buenos/decidir{aprobar, comentario?}— the reviewer whose turn it is (lowest pending order) approves/rejects;403if it is not their turn; a rejection stops the chain.GET /api/v1/workflows/{radicado_id}/vistos-buenos— chain + global status (en_revision|aprobado|rechazado).
POST /api/v1/workflows/{radicado_id}/devolver (return to sender)¶
Re-routes a misassigned radicado: { "to_dept": "...", "causal": "DEV-DEP", "comentario"? }. Closes the active step as returned and creates a new one towards to_dept, with the reason code (tenant causales catalog) in the history. 404 if there is no active step.
Procedure transactions (RF-FLU-01)¶
GET /api/v1/workflows/transaction-types— catalog (informar, NRR, agendar, no_agendar, change_folder, marcar_leido, validate_trd_send, solicitar_firma, vobo, cerrar_exp, anular…), with their atomic permission and state effect.POST /api/v1/workflows/{radicado_id}/transactions{ "tipo_tx": "anular", "comentario": "...", "detalles": {} }— executes the transaction: applies the effect to the active step (e.g.marcar_leido→in_progress,anular→cancelled) and records the append-only event.404 unknown_transaction/no_flow.
POST /api/v1/workflows/{radicado_id}/rollback¶
Reverts the last assignment (RF-FLU-08): cancels the current step —only if it is still pending— and reactivates the previous one, atomically, with a tipo_tx=rollback event. 409 no_active_step (no active step) or cannot_rollback_initial (it is the initial registration).
POST /api/v1/workflows/reassign/cascade¶
Reassigns, in a single transaction, all the active steps of a user or dependencia to a new responsible party, leaving no orphan radicados (RF-FLU-08).
// Request (provide from_assigned_to or from_dept)
{ "from_dept": "Dirección Jurídica", "to_assigned_to": "uuid" }
// Response 200
{ "reassigned": 12 }
GET /api/v1/workflows/{radicado_id}/events¶
Append-only history of the radicado's transactions (RF-FLU-02, unalterable). Unlike /history (a projection of steps), it records every transaction and its table rejects UPDATE/DELETE. Access control (E05 §10 / RF-SEG-08): this is the operational route map (Channel B) — visible to any user who can read the radicado, scoped by their clearance (no read-up); requires X-User-Id, and a radicado above the clearance returns an empty history. No admin permission required (that is the audit_log forensic audit).
[ { "id": "uuid", "tipo_tx": "assign", "from_dept": null, "to_dept": "Ventanilla",
"actor": "uuid", "comentario": "...", "detalles": { "step_number": 1 }, "ts": "2024-01-15T10:30:00Z" } ]
GET /api/v1/workflows/{radicado_id}/current¶
Current active step of the radicado. 404 if it is closed or unassigned.
GET /api/v1/workflows/pending¶
Pending radicados. Filters: ?dept=Dirección+Jurídica&assigned_to={uuid}&page=1&size=20.
PATCH /api/v1/workflows/{step_id}/complete¶
Marks a step as completed.
archive-service — port 8004¶
Requires the
X-Tenant-SlugandX-User-Idheaders.
Expedientes¶
POST /api/v1/expedientes¶
Creates an expediente (case file). Generates an automatic code EXP-{YEAR}-{SEQ:04d}.
// Request
{ "name": "Pensión María García - 2024", "description": "...", "trd_serie_id": "uuid",
"metadata": { "tipo_contrato": "prestación" } }
// Response 201
{ "id": "uuid", "code": "EXP-2024-0001", "name": "...", "status": "open", "metadata": { ... }, ... }
metadata(optional, ADR-007): if the TRD (document retention schedule) series (trd_serie_id) has an active template, the values are validated against its JSON Schema →422 metadata_validation_failedif they do not comply. Column indexed with GIN.
POST /api/v1/expediente-metadata/templates¶
Defines the metadata template (JSON Schema) of a TRD series. A single active one per series. 422 invalid_json_schema / 409 conflict. GET lists (filter ?trd_serie_id=).
Documentary transfers (E12)¶
POST /api/v1/transferencias{tipo: primaria|secundaria, expediente_id, ubicacion_origen_id?, ubicacion_destino_id?}— creates (precondition: expedienteclosed;409 expediente_not_closedotherwise). Cyclepreparada→enviada→recibida|rechazada.POST /{id}/enviar,POST /{id}/recibir(on receipt, the expediente moves totransferredand is frozen),POST /{id}/rechazar.GET /api/v1/transferencias?estado=&tipo=,GET /{id},GET /{id}/fuid(deliverable: FUID (single documentary inventory format) from E17 filtered by the expediente).
Physical archive (E17, ADR-017)¶
POST/GET /api/v1/ubicaciones,GET /api/v1/ubicaciones/{id}— recursive topology (site→…→drawer);codigounique per tenant; derivedruta. Filters?parent_id=&activo=.POST/GET /api/v1/unidades,GET /api/v1/unidades/{id}— conservation units (box|folder|…); whenubicacion_idis provided, the topographic signature is derived (DEP01-E05-C0124, unique). Filters?ubicacion_id=&tipo=.POST /api/v1/unidades/{id}/expedientes— link an expediente to a unit (folio_inicio/fin).GET /api/v1/unidades/{id}/expedientes— what the unit contains.GET /api/v1/expedientes/{id}/unidades— where the expediente is (signature + range).- Loans (RF-ARF-07):
POST /api/v1/unidades/{id}/prestamos(409 if already lent out),POST /api/v1/prestamos/{id}/devolver,GET /api/v1/prestamos?estado=prestado|devuelto|vencido,GET /api/v1/unidades/{id}/prestamos.vencidois derived fromfecha_devolucion_esperada. - History (RF-ARF-08):
GET /api/v1/unidades/{id}/movimientos— append-only movements (RETIRADO/DEVUELTO/…). - FUID (RF-ARF-10):
GET /api/v1/fuid— documentary inventory (expediente↔unit↔signature↔series) in JSON with consecutive ordering and AGN 042/2002 fields; filters?ubicacion_id=&trd_serie_id=.GET /api/v1/fuid.xmlexports as XML.
TRD/CCD (E04, ADR-015)¶
POST/GET/PATCH /api/v1/trd— documentary series and subseries. Fields:code,name,parent_id(subseries),archivo_gestion_years/archivo_central_years(two-phase retention),disposition(AGN:CT/E/S/M),version,valid_from.GET /api/v1/trd/{serie_id}/retention?closed_at=YYYY-MM-DD— retention calendar:fin_archivo_gestion,fin_archivo_centraland final disposition (disposition_code/disposition_label) derived from the series and the closing date.
POST /api/v1/expedientes/{id}/radicados/batch (bulk inclusion)¶
Links several radicados at once {radicados:[{radicado_id, tracking_number, content_hash?}]} (up to 500; idempotent, skips those already linked); regenerates the index only once. Returns {vinculados, omitidos, total}. 409 if the expediente is not open.
Expediente lifecycle (E02)¶
POST /api/v1/expedientes/{id}/close— closes the expediente (open→closed), materializes the TRD disposition indispositionand generates+signs the final version of the electronic index via signature-service (best-effort; leavesexpediente_indice.estado='firmado').POST /api/v1/expedientes/{id}/transfer— transfers (closed→transferred).409if the transition is invalid.GET /api/v1/expedientes/{id}/eventos— append-only route map of the expediente (E02 §9, RF-EXP-09): chronological list of{id, accion, actor, comentario, detalles, ts}withaccion∈abierto|radicado_vinculado|radicado_excluido|cerrado|transferido|.... Access control (Channel B / RF-SEG-08): visible to anyone who can read the expediente, scoped by their clearance (no read-up); no admin required. Distinct fromaudit_log(forensic audit) and/unidades/{id}/movimientos(physical movements, E17).GET /api/v1/expedientes/{id}/foliado— foliation: radicados ordered with a sequentialfolio({expediente_id, code, total_folios, items[]}). (The normative electronic index is E15, below.)
Electronic index (E15, AGN Agreement 001/2024)¶
GET /api/v1/expedientes/{id}/indice(?version=N) — current index or by version:{version, estado, num_documentos, xml_sha256, algoritmo, items[]}.GET /api/v1/expedientes/{id}/indice.xml(?version=N) — XML body (application/xml, namespaceurn:orpycamcp:indice:v1).GET /api/v1/expedientes/{id}/indice/versions— version history (append-only) withX-Total-Count.GET /api/v1/expedientes/{id}/indice/verify(?version=N) — verifies integrity: compares the index'svalor_huellaagainst the current hash of each document ({valido, documentos[], firma{presente,valida}, fecha_verificacion}).POST /api/v1/expedientes/{id}/indice/rebuild— regenerates (new version if the set changed;{unchanged:true}if identical). Version 0 is created automatically when the expediente is created.- When linking a radicado (
POST /api/v1/expedientes/{id}/radicados) acontent_hash(SHA-256 of the document) can be supplied, which the index uses asvalor_huella. Linking/excluding regenerates the index automatically; on a non-openexpediente it returns409.
GET /api/v1/expedientes/search (FTS search, E09)¶
Full-text search of expedientes (ADR-014). q optional (tsvector over code/name/description + ts_rank ranking); filters ?status=&date_from=&date_to=&meta.<campo>=. Header X-Total-Count. Without text = filtered query ordered by opened_at.
/api/v1/tipos-documentales (3rd TRD level, RF-MET-07)¶
CRUD for the catalog of documentary types (POST/GET/GET {id}/PATCH {id}/DELETE {id}). Fields: code, nombre, trd_serie_id (series/subseries), obligatorio, metadata_template (associated template). Filters ?trd_serie_id=&activo=. 409 if the (serie, code) already exists.
GET /api/v1/expedientes¶
List with filters ?status=open&page=1&size=20.
GET /api/v1/expedientes/{id}¶
Get by UUID. Includes the list of linked radicados.
PATCH /api/v1/expedientes/{id}¶
Update name, description, trd_serie_id, status.
Valid transitions: open → closed → transferred. It cannot be reopened.
POST /api/v1/expedientes/{id}/radicados¶
Link a radicado to the expediente.
DELETE /api/v1/expedientes/{id}/radicados/{radicado_id}¶
Unlink a radicado. 204.
TRD¶
POST /api/v1/trd¶
Create a documentary series.
{ "code": "100", "name": "Contratos", "retention_years": 5, "total_retention": 10, "disposition": "conserve" }
GET /api/v1/trd¶
List series. Filter: ?is_active=true&page=1&size=50.
GET /api/v1/trd/{id}¶
Get series by UUID.
PATCH /api/v1/trd/{id}¶
Update series (except code).
notification-service — port 8007¶
POST /api/v1/notifications/send¶
Manual sending of an email notification. Responds 202 (accepted for async sending). The history is persisted in the database of the indicated tenant_slug.
{ "recipient_email": "juridica@icetex.gov.co", "subject": "Nuevo radicado", "body": "...", "tenant_slug": "icetex" }
GET /api/v1/notifications/history¶
Notification history of the tenant (persisted in the database per institution). Requires X-Tenant-Slug. Query: ?limit=20.
GET /api/v1/notifications/{id}¶
Get notification by UUID. Requires X-Tenant-Slug. 404 if it does not exist.
Electronic signature — signature-service, port 8008 (E06, ADR-016)¶
Pluggable provider (nativa by default: hash+identity; PKI XAdES/PAdES/TSA as a swap).
- POST /api/v1/signature/sign-xml {payload_xml, objeto_tipo, objeto_id?, formato?, firmante_id?} — signs the payload and persists the signature; returns {firma_id, payload_sha256, provider, ...}.
- POST /api/v1/signature/verify {firma_id, payload_xml} — recomputes and returns {valida} (detects tampering). 404 if the signature does not exist.
MCP layer (E18, ADR-019) — mcp-server, port 8009¶
Separate service (not behind the gateway): exposes the API as tools to agents/LLMs, while itself being a client of the gateway.
- GET /api/v1/mcp/tools — catalog of tools visible to the user (filters by X-User-Permissions; X-User-Roles with ROOT sees all).
- POST /api/v1/mcp/tools/{name}/invoke {...args} — invokes the tool: propagates Authorization + X-Tenant-Slug to the gateway. Write operations require X-Confirm-Write: true (412 if missing) and the corresponding permission (403 if missing). Tools: radicar_documento, consultar_radicado, buscar_radicados, consultar_expediente, indice_expediente, listar_trd, bandeja_tramite, consultar_fuid.
Knowledge layer (E21, ADR-006) — knowledge-service, port 8011¶
Advisory/derived and opt-in layer (not a source of truth). Embeddings with a pluggable provider (local stub by default).
- POST /api/v1/knowledge/ingest {source_type, source_ref, texto, metadata?, acl?} — computes the embedding and persists the fragment (pgvector) with its ACL.
- POST /api/v1/knowledge/search {query, top_k?, acl?} — semantic retrieval (cosine kNN) with ACL pre-filtering (only fragments whose acl contains the filter). Returns [{id, source_type, source_ref, texto, metadata, score}].
Outbound webhooks (E11, ADR-018)¶
POST /api/v1/webhooks{url, event_types: [...], secret?}— subscribes an external endpoint (emptyevent_types= all events). Thesecretis not returned.GET /api/v1/webhooks— lists the subscriptions (X-Total-Count).DELETE /api/v1/webhooks/{id}— deletes a subscription.- When a domain event occurs, OrpycaMCP
POSTs the canonical envelope to the matching subscriptions, signed in theX-OrpycaMCP-Signature: sha256=<hmac>header (best-effort with retries). The receiver must be idempotent.
Redis Streams Events¶
The workflow-service publishes to orpycamcp.workflow.events when the state of a flow changes:
{
"event_type": "flow_step_created",
"radicado_id": "uuid",
"tracking_number": "2024-ICETEX-E-000001",
"to_dept": "Dirección Jurídica",
"action": "assign",
"tenant_slug": "icetex",
"timestamp": "2024-01-15T10:30:00Z"
}
The notification-service consumes this stream (consumer group notification-service) and sends an email to the destination department.
Batch API — document-service¶
Allows creating multiple radicados or expedientes in a single call. Processing is asynchronous.
Routing contract (drift fix, 2026-08).
document-serviceandarchive-serviceshare the same root prefix/api/v1/batch, but each one owns a disjoint job type. Job status is therefore nested under the job type (/batch/documents/{job_id}/status,/batch/expedientes/{job_id}/status) — both services previously exposed the identical shapeGET /api/v1/batch/{job_id}/status, which made it impossible for the api-gateway to decide which upstream to forward to by prefix (thejob_idalone carries no service information). ThePOSTalready discriminated by subpath (/documentsvs/expedientes); theGETnow follows the same criterion.POST /api/v1/batch/documentsand its statusGETrequirePERM_RADI(same as the single-documentPOST /api/v1/documents/) — previously neither route required any permission.
POST /api/v1/batch/documents¶
Creates a job to register 1-1000 radicados. Returns 202 Accepted immediately. Requires PERM_RADI.
// Request
{
"documents": [
{ "doc_type": "E", "subject": "Solicitud 1", "sender_name": "Juan Pérez" },
{ "doc_type": "E", "subject": "Solicitud 2", "sender_name": "Ana Gómez" }
]
}
// Response 202
{ "job_id": "uuid", "job_type": "documents", "status": "pending", "total_items": 2, "created_at": "..." }
GET /api/v1/batch/documents/{job_id}/status¶
Monitors the progress of the job. Requires PERM_RADI.
// Response 200
{
"job_id": "uuid", "job_type": "documents",
"status": "completed", // pending | processing | completed | failed
"total_items": 2, "processed_items": 2, "failed_items": 0,
"items": [
{ "item_index": 0, "status": "success", "result_id": "uuid-radicado" },
{ "item_index": 1, "status": "failed", "error_message": "..." }
]
}
Batch API — archive-service¶
POST /api/v1/batch/expedientes¶
Creates 1-100 expedientes with linked radicados. Returns 202 Accepted.
// Request
{
"expedientes": [
{
"name": "Pensión García 2024",
"trd_serie_id": "uuid",
"radicado_ids": ["uuid1", "uuid2"],
"tracking_numbers": ["2024-ICETEX-E-000001", "2024-ICETEX-E-000002"]
}
]
}
// Response 202 — same format as batch documents
GET /api/v1/batch/expedientes/{job_id}/status¶
Monitors the progress of an expediente batch job. Same response shape as the document
batch status. Requires USUA_PERM_EXPEDIENTE.
Full-Text Search & Reports — document-service¶
Output templates and drafts (legacy parity)¶
GET/POST/DELETE /api/v1/plantillas— reusable templates (nombre,cuerpo,tipo_documental).POST /api/v1/borradores{doc_type, subject, dest_dept?, cuerpo?, plantilla_id?}— creates a draft (ifplantilla_idis given and nocuerpo, it inherits the body from the template).GET /api/v1/borradores(?estado=),GET /{id},PATCH /{id}(edit only if not yet registered),POST /{id}/aprobar(draft→approved),POST /{id}/radicar(→ generates the real radicado and marks the draftradicado).
Quick response (legacy parity)¶
POST /api/v1/documents/{id}/respuesta{subject?, dest_dept?, observations?, response_days?}— creates a Salida (outbound) radicado in response to this one (inherits theRE: …subject and dependencia if omitted), linked to the antecedent (responde_a). Returns the new radicado.GET /api/v1/documents/{id}/respuestas— outbound documents that respond to this radicado.
Two-step radicado annulment (legacy parity)¶
The law prohibits deleting radicados: they are annulled with supervisory approval.
- POST /api/v1/documents/{id}/anulacion {causal, motivo?} — requests (step 1). 409 if it is already annulled or there is a pending request.
- POST /api/v1/documents/{id}/anulacion/aprobar {observacion?} — approves (step 2): the radicado moves to anulado (the number is kept).
- POST /api/v1/documents/{id}/anulacion/rechazar {observacion?} — rejects. GET /api/v1/documents/{id}/anulacion — last request.
Electronic signature (E06, ADR-016)¶
POST /api/v1/documents/{id}/signatures{ "content_hash": "<sha256 hex>", "anexo_id"?, "reason"? }— signs the radicado/anexo: records identity (gateway claims),content_hashand timestamp.422if the hash is not SHA-256.GET /api/v1/documents/{id}/signatures— lists the signatures (X-Total-Count).GET /api/v1/documents/{id}/signatures/verify?content_hash=<hex>—{content_hash, valid, signatures[]}:valid=trueif any signature of the radicado matches that hash (if the content changed, it stops verifying).
Postal dispatches (E20)¶
POST /api/v1/documents/{id}/envios{operador, destinatario?, direccion?}— dispatches the radicado via a postal operator (4-72/servientrega/otro); generates the tracking guide andregistradostatus.GET /api/v1/documents/{id}/envios·GET /api/v1/envios/{id}— dispatches of the radicado / detail with event history.POST /api/v1/envios/{id}/estado{estado, descripcion?}— updates the delivery status (operator callback/polling). State machineregistrado→en_transito→entregado|devuelto|fallido;409if the transition is invalid.
POST /api/v1/ingest/email/poll (email ingestion, E19)¶
Internal operation (requires JWT). Fetches the unread emails from the configured IMAP mailbox, creates one Entrada (inbound) radicado per email (metadata.source=email, sender and attachments in metadata) and marks them as read. Tenant in X-Tenant-Slug. Response { "ingested": n, "tracking_numbers": [...] }. 503 if IMAP is not configured (IMAP_HOST empty).
POST /api/v1/public/{tenant}/pqrs (citizen registration, Law 1755/2015)¶
No authentication. A citizen files a PQRS (citizen request: petition/complaint/claim): { "tipo": "peticion|queja|reclamo|sugerencia|denuncia", "nombre", "identificacion"?, "email"?, "asunto", "descripcion" }. Creates an Entrada (inbound) radicado (metadata.canal=pqrs_ciudadano) and returns {radicado_id, tracking_number, verification_code} for later tracking with the verification endpoint.
GET /api/v1/public/{tenant}/verify/{code} (public lookup, E13)¶
No authentication. Verifies the traceability of a radicado by its verification code (an unguessable token, distinct from the radicado number). The tenant travels in the path. Returns only NON-sensitive information: tracking_number, doc_type, status, registered_at, dest_dept. 404 if the code does not exist; 400 if the tenant slug is invalid.
GET /api/v1/reports/radicados¶
Statistical summary of the tenant's radicados (E09): total and counts by_doc_type, by_status, by_month (YYYY-MM), by_dependencia. Optional filters ?date_from=&date_to=. Native SQL aggregations (GROUP BY/date_trunc).
GET /api/v1/reports/radicados.csv¶
Exports the summary in CSV (seccion,clave,valor with type/status/month/dependencia sections) — for periodic reports to the AGN/oversight bodies. Same filters ?date_from=&date_to=.
{ "total": 8, "by_doc_type": {"E": 5, "S": 3}, "by_status": {"registered": 6, "archived": 2},
"by_month": [{"month": "2024-01", "count": 4}, {"month": "2024-02", "count": 4}] }
GET /api/v1/search¶
Advanced radicado search (E09, ADR-014: PostgreSQL FTS). Full text with ranking (tsvector/ts_rank) + fuzzy (pg_trgm), combinable with structured filters.
Searches in: tracking_number (weight A), subject (B), sender_name / sender_entity (C), dest_dept / observations (D).
Query params (all optional): q (text, min 2 chars — optional: without text it is a filtered query ordered by date), doc_type, status, date_from/date_to (range over registered_at), dest_dept_code, meta.<campo>=valor (metadata filter, JSONB/GIN containment), page, size. Header X-Total-Count.
// Response 200
{
"query": "tutela derechos",
"total": 3,
"page": 1,
"size": 20,
"hits": [
{
"id": "uuid",
"tracking_number": "2024-ICETEX-E-000042",
"doc_type": "E",
"subject": "Acción de tutela derechos fundamentales",
"sender_name": "María García",
"status": "registered",
"registered_at": "2024-06-01T10:30:00Z",
"rank": 0.756
}
]
}
Workflow Rules Engine — workflow-service¶
Configures automatic routing rules without code. When a radicado arrives, the active rules are evaluated in priority order.
Requires X-Tenant-Slug (and X-User-Id to record the author).
POST /api/v1/workflow/rules¶
Create a rule. Two condition forms (compatible):
- Operator engine (
conditions+match_mode): list of{field, op, value}combined byall(AND) orany(OR). Fields:doc_type, doc_class, dest_dept, origin_dept, subject, sender_name, sender_entity, pages. Operators:eq, ne, contains, not_contains, in, gt, lt, gte, lte, regex. - Legacy (fixed columns
doc_type/doc_class/dest_dept/subject_contains, AND): used ifconditionsis empty.
// Request (operator engine)
{
"name": "Tutelas voluminosas → Jurídica",
"priority": 10,
"match_mode": "all",
"conditions": [
{ "field": "doc_type", "op": "in", "value": "E,S" },
{ "field": "subject", "op": "contains", "value": "tutela" },
{ "field": "pages", "op": "gt", "value": 10 }
],
"assign_to_dept": "Dirección Jurídica",
"assign_notes": "Responder en máximo 10 días hábiles"
}
// Response 201 — WorkflowRuleResponse with id, conditions, match_mode, timestamps
GET /api/v1/workflow/rules¶
List rules. Query: ?active_only=true&page=1&size=50.
GET /api/v1/workflow/rules/{id}¶
Get rule by UUID.
PATCH /api/v1/workflow/rules/{id}¶
Partial update (e.g.: change priority, enable/disable).
DELETE /api/v1/workflow/rules/{id}¶
Delete rule. Responds 204.
POST /api/v1/workflow/rules/evaluate¶
Evaluate rules against a radicado (returns the first one that matches).
// Request
{
"radicado_id": "uuid",
"tracking_number": "2024-ICETEX-E-000042",
"doc_type": "E",
"subject": "Acción de tutela por mora en servicio"
}
// Response 200 — if there is a match
{ "matched": true, "rule_id": "uuid", "rule_name": "Tutelas → Dirección Jurídica", "assign_to_dept": "Dirección Jurídica", "assign_notes": "..." }
// Response 200 — no match
{ "matched": false }
POST /api/v1/workflow/rules/evaluate/batch¶
Evaluates several radicados (1-500) against the same set of rules in a single call.