Descripción general
Esta referencia cubre veintidós endpoints sobre cuatro objetos: contactos, su línea de tiempo de interacciones, currículums y los pipelines de contratación (listas) donde tus contactos aparecen como tarjetas. Los endpoints de lectura usan el alcance crm:read; toda escritura, incluidas las rutas de pipelines, usa crm:write. Usa /public/usage para consultar tu cuota restante sin gastarla.
/public/contacts/public/contacts/{id}/public/contacts/public/contacts/{id}/public/contacts/{id}/public/contacts/{id}/resume/public/contacts/{id}/interactions/public/contacts/{id}/interactions/public/interaction-types/public/listshasMore./public/listsstages o copia el esqueleto de etapas de otro pipeline. Acepta un encabezado Idempotency-Key./public/lists/{id}/public/lists/{id}/public/lists/{id}/public/lists/{id}/stages/public/lists/{id}/stages/{stageId}/public/lists/{id}/stages/orderstageIds con el orden completo deseado./public/lists/{id}/stages/{stageId}{ stagesRemaining }./public/lists/{id}/items/public/lists/{id}/items/{itemId}/public/lists/{id}/items/{itemId}/public/usageLa lista oculta correos y teléfonos
GET /public/contacts devuelve filas compactas para que un agente pueda navegar y luego abrir un contacto. Para proteger contra la extracción masiva, la proyección de la lista omite emails y phones (regresan como arreglos vacíos) y devuelve companyId, locationGeoNameId y updatedAt como null. Usa GET /public/contacts/{id} para el detalle completo de un contacto, incluidos sus canales. Un contacto que pertenece a otro workspace devuelve un 404 indistinguible, nunca un 403, así que la API no puede sondearse para saber si un contacto existe en otra parte.
Autenticación
Envía tu clave de API con el encabezado x-api-key o como un token Authorization: Bearer. Es la misma clave de CazVid Developer API que las demás familias de la API - una sola clave sirve para todos los productos, controlada por alcance. Las claves pertenecen a una organización; los propietarios y administradores las gestionan en la app en Perfil > Developer API. Las claves en texto plano se muestran una sola vez al crearlas y CazVid las almacena solo como hash. Trata las claves como secretos: no las pongas en código de frontend, apps móviles, capturas de pantalla, registros ni mensajes de soporte.
x-api-key: cazvid_jb_...
Authorization: Bearer cazvid_jb_...
Content-Type: application/json403 PLAN_REQUIRED; una clave sin el alcance de CRM recibe un 403 SCOPE_FORBIDDEN. Los propietarios y administradores gestionan las claves desde la sección Developer API en CazVid.Leer contactos
GET /public/contacts recibe un search opcional (de 1 a 200 caracteres) que coincide con nombre, empresa, puesto, habilidades, industria, educación, correo y teléfono, además de page (base 1) y pageSize (hasta 50, predeterminado 10). Pagina solo con hasMore - no hay conteo total. jobTitle es un arreglo: lleva todos los puestos del currículum del contacto, así que un contacto puede mostrar varios títulos. Las cadenas largas se truncan a 150 caracteres con un marcador ... [truncated].
/public/contactscurl "https://aio-backend-prod.cazvid.app/api/v3.0/crm/public/contacts?search=logistics%20manager&pageSize=10" \
-H "x-api-key: cazvid_jb_..."{
"items": [
{
"id": "6a57e240f55eb331f6e396c8",
"name": "Sidney G.",
"firstName": "Sidney",
"lastName": "G.",
"jobTitle": ["Logistics Manager", "Operations Coordinator"],
"companyName": "Acme Freight",
"companyId": null,
"emails": [],
"phones": [],
"linkedinUrl": "https://www.linkedin.com/in/example",
"locationName": "Bogota, Colombia",
"locationGeoNameId": null,
"tags": [{ "id": "6a1f0b2c9c7c630206ba1e77", "name": "priority" }],
"hasResume": true,
"resumeCount": 1,
"cazvidUserId": null,
"linkedCazvidUser": false,
"createdAt": "2026-07-01T12:30:00.000Z",
"updatedAt": null
}
],
"page": 1,
"pageSize": 10,
"hasMore": true
}GET /public/contacts/{id} devuelve el contacto completo. En el endpoint de lista, los campos marcados abajo como ocultos-en-lista están vacíos o en null por diseño.
/public/contacts/{id}| Campo | Tipo | Descripción |
|---|---|---|
| id | string | Id del contacto. Úsalo con los endpoints de contacto individual, interacciones, currículum, actualización y eliminación. |
| name | string | null | Nombre para mostrar. |
| firstName | string | null | Nombre de pila. |
| lastName | string | null | Apellido. |
| jobTitle | string[] | Arreglo de puestos del currículum del contacto; un contacto puede tener varios. |
| companyName | string | null | Nombre de la empresa o empleador. |
| companyId | string | null | Id de la empresa vinculada. Oculto-en-lista: siempre null en el endpoint de lista. |
| emails | { email, type }[] | Correos con etiquetas. Oculto-en-lista: vacío en el endpoint de lista; poblado en el de contacto individual. |
| phones | { phone, type }[] | Teléfonos con etiquetas. Oculto-en-lista: vacío en el endpoint de lista; poblado en el de contacto individual. |
| linkedinUrl | string | null | URL del perfil de LinkedIn, cuando se conoce. |
| locationName | string | null | Ubicación en formato legible. |
| locationGeoNameId | number | null | Id de GeoNames. Oculto-en-lista: siempre null en el endpoint de lista. |
| tags | { id, name }[] | Etiquetas del workspace en el contacto, cada una devuelta como un objeto { id, name }. |
| hasResume | boolean | Si el contacto tiene al menos un currículum adjunto. |
| resumeCount | number | Número de currículums adjuntos. |
| cazvidUserId | string | null | Id del usuario de la plataforma CazVid vinculado, cuando el contacto es un usuario de CazVid. |
| linkedCazvidUser | boolean | Si el contacto está vinculado a un usuario de la plataforma CazVid. |
| createdAt | string | null | Marca de tiempo de creación ISO 8601. |
| updatedAt | string | null | Marca de tiempo de última actualización ISO 8601. Oculto-en-lista: siempre null en el endpoint de lista. |
{
"id": "6a57e240f55eb331f6e396c8",
"name": "Sidney G.",
"firstName": "Sidney",
"lastName": "G.",
"jobTitle": ["Logistics Manager", "Operations Coordinator"],
"companyName": "Acme Freight",
"companyId": "6a1f0b2c9c7c630206ba1e44",
"emails": [{ "email": "sidney@example.com", "type": "work" }],
"phones": [{ "phone": "+57 300 000 0000", "type": "mobile" }],
"linkedinUrl": "https://www.linkedin.com/in/example",
"locationName": "Bogota, Colombia",
"locationGeoNameId": 3688689,
"tags": [{ "id": "6a1f0b2c9c7c630206ba1e77", "name": "priority" }],
"hasResume": true,
"resumeCount": 1,
"cazvidUserId": null,
"linkedCazvidUser": false,
"createdAt": "2026-07-01T12:30:00.000Z",
"updatedAt": "2026-07-03T09:15:00.000Z"
}Crear, actualizar y archivar contactos
Crea con POST /public/contacts (solo name es obligatorio), actualiza con PATCH /public/contacts/{id} (envía solo los campos que quieras cambiar) y archiva con DELETE /public/contacts/{id}. Los tres usan el alcance crm:write.
Crear es idempotente
Crear deduplica dentro del workspace por vínculo con usuario de CazVid, luego por correo y luego por teléfono, así que reenviar un contacto que lleve alguno de esos devuelve el existente (con deduplicated: true) en vez de crear un duplicado - no se necesita clave de idempotencia y reintentar tras un timeout es seguro. **Envía un correo o un teléfono si piensas reintentar**: solo name es obligatorio, pero un contacto creado solo con nombre no tiene clave de deduplicación, así que reintentarlo tras un timeout PUEDE crear un segundo contacto. deduplicated es orientativo (una heurística de marca de tiempo de creación que puede informar mal cuando el createdAt de un contacto existente cae en el mismo milisegundo que la solicitud, o en registros heredados con createdAt ausente o con desfase de reloj). Si un correo o teléfono coincide con más de un contacto existente, la API se niega con 409 CONTACT_CONFLICT en vez de adivinar cuál querías; reconcilia los duplicados y reintenta.
El cuerpo de creación acepta estos campos. La actualización acepta el mismo conjunto, todos opcionales.
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| name | string | Requerido | Nombre para mostrar. El único campo obligatorio al crear. |
| firstName | string | Opcional | Nombre de pila. |
| lastName | string | Opcional | Apellido. |
| jobTitle | string[] | Opcional | Uno o más puestos (hasta 10). Se guardan como el arreglo jobTitle del contacto. |
| companyName | string | Opcional | Nombre de la empresa o empleador. |
| linkedinUrl | string | Opcional | URL del perfil de LinkedIn. |
| locationName | string | Opcional | Ubicación en texto libre (ciudad, región o país). |
| locationGeoNameId | integer | Opcional | Id de GeoNames para una ubicación normalizada, cuando se conoce. |
| emails | { email, type? }[] | Opcional | Correos como objetos { email, type? } (type es work, personal u other; hasta 10 al crear). En la actualización, como mucho uno - reemplaza el correo principal. |
| phones | { phone, type? }[] | Opcional | Teléfonos como objetos { phone, type? } (type es mobile, desk, home u other; hasta 10 al crear). En la actualización, como mucho uno - reemplaza el teléfono principal. |
| tags | string[] | Opcional | Nombres de etiqueta del workspace (hasta 50). Un nombre de etiqueta desconocido crea esa etiqueta en el workspace. |
curl -X POST https://aio-backend-prod.cazvid.app/api/v3.0/crm/public/contacts \
-H "x-api-key: cazvid_jb_..." \
-H "Content-Type: application/json" \
-d '{
"name": "Sidney G.",
"companyName": "Acme Freight",
"jobTitle": ["Logistics Manager"],
"emails": [{ "email": "sidney@example.com", "type": "work" }],
"phones": [{ "phone": "+57 300 000 0000", "type": "mobile" }]
}'{
"id": "6a57e240f55eb331f6e396c8",
"deduplicated": false,
"name": "Sidney G.",
"firstName": "Sidney",
"lastName": "G.",
"jobTitle": ["Logistics Manager"],
"companyName": "Acme Freight",
"companyId": null,
"emails": [{ "email": "sidney@example.com", "type": "work" }],
"phones": [{ "phone": "+57 300 000 0000", "type": "mobile" }],
"linkedinUrl": null,
"locationName": null,
"locationGeoNameId": null,
"tags": [],
"hasResume": false,
"resumeCount": 0,
"cazvidUserId": null,
"linkedCazvidUser": false,
"createdAt": "2026-07-25T12:30:00.000Z",
"updatedAt": "2026-07-25T12:30:00.000Z"
}emails y phones se limitan a una sola entrada cada uno: un PATCH reemplaza el correo o teléfono principal del contacto en vez de añadir, así que envía como mucho uno de cada. deduplicated solo está presente en la respuesta de creación; se omite en la actualización.Currículums
POST /public/contacts/{id}/resume adjunta un currículum a un contacto como un archivo base64 en línea. fileName, mimeType y contentBase64 son todos obligatorios. Devuelve 202 Accepted y consume una llamada diaria de CRM más una de una cuota de subida de currículums separada por clave (predeterminado 50 por día UTC; ver el bloque resumeUpload en /public/usage).
/public/contacts/{id}/resumemimeTypedebe ser uno deapplication/pdf,application/mswordoapplication/vnd.openxmlformats-officedocument.wordprocessingml.document(PDF, DOC, DOCX).- El archivo decodificado debe pesar como mucho 10 MB, y su tipo real se verifica a partir de sus magic bytes contra
mimeType; una discrepancia devuelve400 MIME_MISMATCH, un archivo vacío400 EMPTY_FILE, y una entrada no decodificable400 INVALID_BASE64. - El análisis es asíncrono: el
202confirma que el currículum se encoló. Los campos analizados (nombre, correo, teléfono, ubicación, puesto, habilidades, LinkedIn) se escriben en el contacto después, llenando solo sus campos vacíos. ConsultaGET /public/contacts/{id}para ver actualizarsehasResumeyresumeCount.
curl -X POST https://aio-backend-prod.cazvid.app/api/v3.0/crm/public/contacts/6a57e240f55eb331f6e396c8/resume \
-H "x-api-key: cazvid_jb_..." \
-H "Content-Type: application/json" \
-d '{
"fileName": "sidney-resume.pdf",
"mimeType": "application/pdf",
"contentBase64": "JVBERi0xLjc...=="
}'Interacciones
Una interacción es una entrada de la línea de tiempo de un contacto - una llamada, correo, entrevista, oferta, nota, etc. Registra una con POST /public/contacts/{id}/interactions (type es obligatorio), lee la línea de tiempo de la más reciente a la más antigua con GET /public/contacts/{id}/interactions, y descubre los nombres de tipo válidos con GET /public/interaction-types. El registro nunca se deduplica: cada llamada crea una entrada nueva, así que no reintentes a ciegas una llamada que quizá ya tuvo éxito. Puedes pasar una date futura para registrar un seguimiento programado.
/public/contacts/{id}/interactions/public/contacts/{id}/interactions/public/interaction-typesEl catálogo de interacciones es fijo y global
type debe ser un nombre de un catálogo global fijo que es el mismo para cada workspace; un nombre desconocido se rechaza con 400 UNKNOWN_INTERACTION_TYPE, cuyo details lista todos los nombres válidos. Llama a GET /public/interaction-types para la lista autoritativa y actual en vez de codificarlos. Los nombres se agrupan en siete categorías:
communicationassessmentinterviewsoffernextstepsotherrejectedcurl -X POST https://aio-backend-prod.cazvid.app/api/v3.0/crm/public/contacts/6a57e240f55eb331f6e396c8/interactions \
-H "x-api-key: cazvid_jb_..." \
-H "Content-Type: application/json" \
-d '{
"type": "phonecall",
"notes": "Left a voicemail about the operations role.",
"date": "2026-07-04T16:00:00.000Z"
}'Listas y pipelines
Una lista es un pipeline de contratación: un tablero con nombre y etapas ordenadas que contiene una tarjeta por candidato. Es el mismo objeto que la app de CazVid llama lista, así que un pipeline que construyas aquí aparece en el tablero que usan tus reclutadores, y los movimientos que ellos hagan arrastrando tarjetas se ven a través de estas rutas. Doce endpoints cubren los tres niveles: el pipeline, sus etapas y sus tarjetas. Las lecturas usan crm:read; toda escritura usa crm:write.
/public/listshasMore./public/listsstages o copia el esqueleto de etapas de otro pipeline. Acepta un encabezado Idempotency-Key./public/lists/{id}/public/lists/{id}/public/lists/{id}/public/lists/{id}/stages/public/lists/{id}/stages/{stageId}/public/lists/{id}/stages/orderstageIds con el orden completo deseado./public/lists/{id}/stages/{stageId}{ stagesRemaining }./public/lists/{id}/items/public/lists/{id}/items/{itemId}/public/lists/{id}/items/{itemId}Las escrituras de pipelines son a nivel de workspace
Cualquier clave con alcance sobre un workspace puede gestionar cualquier pipeline dentro de él, sin importar qué usuario de la app lo creó: en estas rutas no hay propiedad por usuario. El límite del workspace sí es absoluto: un pipeline, etapa, tarjeta o contacto de otro workspace devuelve el mismo 404 NOT_FOUND que un id inexistente, así que la API no puede sondearse para saber qué existe en otra parte. La única regla más estrecha es copyFromListId al crear, que en la v1 solo resuelve pipelines creados por el usuario de tu propia clave; el pipeline de otro miembro devuelve 404 incluso dentro de tu workspace.
curl "https://aio-backend-prod.cazvid.app/api/v3.0/crm/public/lists?pageSize=10" \
-H "x-api-key: cazvid_jb_..."Crear un pipeline acepta un Idempotency-Key
POST /public/lists es la única escritura del CRM sin deduplicación natural - dos llamadas idénticas crean dos pipelines - así que acepta un encabezado opcional Idempotency-Key con cualquier cadena opaca que puedas reproducir al reintentar (un id de ejecución, una marca de semana). Una repetición con la misma clave Y el mismo cuerpo repite el primer resultado durante 24 horas, devolviendo el pipeline original con su id original y un encabezado de respuesta Idempotency-Replayed: true; una respuesta sin ese encabezado fue creada por esa solicitud. La misma clave con un cuerpo distinto devuelve 422 IDEMPOTENCY_KEY_REUSED y seguirá fallando, así que usa una clave nueva. Una repetición enviada mientras la primera creación sigue en curso devuelve 409 IDEMPOTENCY_IN_PROGRESS: no se duplicó nada, reintenta en un momento para obtener la repetición. Esa reserva en curso dura poco (unos 2 minutos, no 24 horas): si la solicitud original murió a mitad de la creación, la reserva expira sola y el siguiente reintento crea el pipeline. Una creación que FALLA nunca se recuerda, y la clave se recuerda por organización y por workspace resuelto.
curl -X POST https://aio-backend-prod.cazvid.app/api/v3.0/crm/public/lists \
-H "x-api-key: cazvid_jb_..." \
-H "Idempotency-Key: weekly-pipeline-2026-W32" \
-H "Content-Type: application/json" \
-d '{
"name": "Warehouse Hiring 2026",
"color": "#0EA5E9",
"stages": [
{ "name": "Sourced", "color": "#64748B" },
{ "name": "Phone Screen", "color": "#4F46E5" },
{ "name": "Offer", "color": "#16A34A" }
]
}'GET /public/lists devuelve todos los campos de abajo excepto updatedAt y stages. GET /public/lists/{id} añade esos dos y aplica la misma ventana page / pageSize a CADA etapa, devolviéndolos junto con hasMore en el nivel superior. pageSize llega hasta 50 y su valor predeterminado es 10; page es de base 1 y su máximo es 10000. hasMore es true cuando CUALQUIER etapa tiene más tarjetas, no solo la más grande.
| Campo | Tipo | Descripción |
|---|---|---|
| id | string | Id del pipeline. Úsalo en cada ruta /public/lists/{id}. |
| name | string | null | Nombre del pipeline tal como se muestra en el tablero. |
| color | string | null | Color del pipeline, tal como está guardado: un código hex o un token de color simple. |
| templateType | string | null | La marca de plantilla de etapas. TODO pipeline creado con esta API se marca como CANDIDATE_PIPELINE_EN, incluso uno creado con tus propias etapas o copiado de otro tablero, así que no indica cuáles son realmente las etapas; los creados automáticamente para una publicación de empleo llevan un valor JOBSEARCH_PIPELINE_. Es solo informativo y nunca cambia después de la creación. |
| associatedPosts | string[] | Ids de las publicaciones de empleo vinculadas a este pipeline. Vacío para un pipeline creado con esta API: los vínculos con publicaciones se hacen en la app de CazVid. |
| itemCount | number | Número de tarjetas activas en el pipeline, en todas sus etapas. |
| createdAt | string | null | Marca de tiempo de creación ISO 8601. |
| updatedAt | string | null | Marca de tiempo del último cambio ISO 8601. Solo en la lectura de un pipeline individual. |
| stages | Stage[] | Las etapas del tablero en orden de tablero (zorder ascendente), cada una con id, name, color, zorder, su itemCount total y una página de items. Solo en la lectura de un pipeline individual. |
{
"id": "6a3ff55406ae2ed06ddb5fd2",
"name": "Warehouse Hiring 2026",
"color": "#0EA5E9",
"templateType": "CANDIDATE_PIPELINE_EN",
"associatedPosts": [],
"itemCount": 1,
"createdAt": "2026-08-01T10:00:00.000Z",
"updatedAt": "2026-08-02T09:12:00.000Z",
"page": 1,
"pageSize": 10,
"hasMore": false,
"stages": [
{
"id": "6a3ff55406ae2ed06ddb5fe1",
"name": "Sourced",
"color": "#64748B",
"zorder": 1,
"itemCount": 1,
"items": [
{
"id": "6a3ff55406ae2ed06ddb5fd1",
"listId": "6a3ff55406ae2ed06ddb5fd2",
"stageId": "6a3ff55406ae2ed06ddb5fe1",
"contactId": "6a57e240f55eb331f6e396c8",
"authorId": null,
"rating": null,
"applicationStatus": null,
"disqualified": false,
"disqualifiedReason": null,
"source": "contact",
"createdAt": "2026-08-01T10:05:00.000Z",
"updatedAt": "2026-08-01T10:05:00.000Z"
}
]
}
]
}Una tarjeta lleva ids, nunca datos personales
Los doce campos de abajo son todo lo que expone una tarjeta: ids y estado del pipeline. Por diseño no hay nombre, correo, teléfono ni currículum en una tarjeta. Combina el contactId de la tarjeta con GET /public/contacts/{id} para esos datos: esa ruta es la única lectura que aplica la verificación de privacidad del propio candidato vinculado, y la lectura del tablero deliberadamente no repite nada de eso. authorId se establece cuando el postulante es un usuario de la plataforma CazVid, quien puede no tener ningún registro de contacto en el CRM, en cuyo caso contactId es null.
Cada tarjeta del arreglo items de una etapa lleva exactamente estos doce campos.
| Campo | Tipo | Descripción |
|---|---|---|
| id | string | Id de la tarjeta. Úsalo en las rutas /items/{itemId}. |
| listId | string | Id del pipeline al que pertenece esta tarjeta. |
| stageId | string | null | Id de la etapa en la que está la tarjeta actualmente. |
| contactId | string | null | Id del contacto del CRM detrás de esta tarjeta. Combínalo con GET /public/contacts/{id} para el nombre, correo y teléfono de la persona. |
| authorId | string | null | Id del usuario de la plataforma CazVid detrás de esta tarjeta, presente cuando el postulante es usuario de CazVid. Ese usuario puede no tener registro de contacto en el CRM, en cuyo caso contactId es null. |
| rating | "good_fit" | "maybe" | "not_a_fit" | null | Calificación del reclutador en la tarjeta, cuando se estableció en la app. No es escribible con esta API en la v1. |
| applicationStatus | "invited" | "in_progress" | "completed" | "disqualified" | null | Estado de la postulación de la tarjeta, cuando se estableció. No es escribible con esta API en la v1. |
| disqualified | boolean | Si la tarjeta fue descalificada. |
| disqualifiedReason | string | null | Motivo en texto libre registrado cuando se descalificó la tarjeta. |
| source | string | null | Cómo llegó la tarjeta al pipeline: contact para un alta por API o CRM, o el origen de la postulación cuando es un postulante a un empleo. |
| createdAt | string | null | Marca de tiempo ISO 8601 de cuando se añadió la tarjeta al pipeline. |
| updatedAt | string | null | Marca de tiempo ISO 8601 del último cambio de la tarjeta. |
curl -X POST https://aio-backend-prod.cazvid.app/api/v3.0/crm/public/lists/6a3ff55406ae2ed06ddb5fd2/items \
-H "x-api-key: cazvid_jb_..." \
-H "Content-Type: application/json" \
-d '{
"contactId": "6a57e240f55eb331f6e396c8",
"stageId": "6a3ff55406ae2ed06ddb5fe1"
}'zorderes de base 1 y contiguo. Cada cambio en las etapas renumera todo el tablero, así quezorder: 1es siempre la etapa donde caen los candidatos nuevos.- Un pipeline admite como máximo 30 etapas activas cuando envías tus propias
stageso añades una conPOST /public/lists/{id}/stages.copyFromListIddeliberadamente NO aplica ese límite, así que copiar un tablero más largo te deja un pipeline por encima del máximo, y uno que ya no se puede reordenar con esta API, porquestageIdstambién está limitado a 30. stagesycopyFromListIdson mutuamente excluyentes al crear (400 VALIDATION_ERRORsi envías ambos). Si no envías ninguno obtienes el pipeline de candidatos predeterminado. No hay selector de idioma: crea un tablero en español enviando tus propiasstageso copiando uno existente en español.- Las reglas de nombre son distintas para un pipeline y para sus etapas. El
namede un pipeline solo acepta palabras alfanuméricas separadas por un solo espacio, así que un acento, guion, apóstrofo, ampersand, punto o espacio doble devuelve400 VALIDATION_ERROR. Los nombres de etapa no tienen esa regla: aceptan caracteres acentuados y barras, que es como se nombran los tableros en español predefinidos. Así queContratación Almacénse rechaza como nombre de pipeline pero se acepta como nombre de etapa. - Añadir una tarjeta requiere un contacto existente.
POST /public/lists/{id}/itemsrecibe uncontactId, no los datos de la persona, así que crea primero el contacto. Añadir el mismo contacto dos veces nunca duplica la tarjeta, pero una repetición con unstageIddistinto la mueve: trata la ubicación en la etapa como una escritura intencional, no como una operación sin efecto. stageIddebe pertenecer al pipeline de la ruta. La etapa de otro pipeline devuelve404 NOT_FOUNDcondetails[0].fieldigual astageId; una tarjeta nunca puede moverse a un tablero ajeno. Añadir una tarjeta a un pipeline sin etapas devuelve409 LIST_HAS_NO_STAGES: el id es válido, así que añade una etapa en vez de buscar otro pipeline.- Las tarjetas nunca se pierden en silencio. Eliminar una etapa que contiene tarjetas exige
?moveToStageId, otra etapa activa del mismo pipeline; sin él la llamada devuelve400 MOVE_TO_STAGE_REQUIRED, y unmoveToStageIdque no sea una etapa activa de ese pipeline devuelve404 NOT_FOUNDnombrandomoveToStageId. La última etapa que le queda a un pipeline no puede eliminarse (400 LAST_STAGE_UNDELETABLE), y una etapa con una cantidad extrema de tarjetas devuelve400 TOO_MANY_ITEMS_TO_REASSIGN. PUT /public/lists/{id}/stages/orderrecibe el orden completo:stageIdsdebe contener exactamente los ids de las etapas activas actuales del pipeline, sin faltantes, sobrantes, ajenos ni repetidos.- Cada ruta de pipelines consume exactamente una llamada de la misma cuota diaria, el mismo límite por minuto y el mismo límite por IP que las rutas de contactos e interacciones. No hay un pool aparte ni un multiplicador por ruta.
- Eliminar un pipeline informa su alcance. La respuesta lleva
effectsconstagesDeletedeitemsDeleted, contados antes de ejecutar la cascada. La cascada borra de forma suave el pipeline, sus etapas y sus tarjetas, y quita el vínculo del pipeline en la publicación de empleo que apuntaba a él: como mucho una, y no se cuenta eneffects, así que vuelve a leer la publicación si necesitas confirmarlo. Los contactos en sí no se tocan.
{
"id": "6a3ff55406ae2ed06ddb5fd2",
"deleted": true,
"effects": {
"stagesDeleted": 3,
"itemsDeleted": 12
}
}Consultar la cuota restante
GET /public/usage devuelve tu cuota diaria de CRM y la cuota de subida de currículums separada por clave. Usa la misma autenticación que los demás endpoints y no cuenta para la cuota, así que puedes consultarlo antes de un lote para evitar respuestas 429. Las consultas están limitadas a 60 solicitudes por minuto por clave de API.
/public/usagecurl https://aio-backend-prod.cazvid.app/api/v3.0/crm/public/usage \
-H "x-api-key: cazvid_jb_..."| Campo | Tipo | Descripción |
|---|---|---|
| dailyQuota | number | Total de llamadas de CRM exitosas permitidas por organización por día UTC (500). |
| used | number | Llamadas de CRM que consumen cuota realizadas hoy (UTC) con las claves de esta organización. |
| remaining | number | Llamadas exitosas restantes antes de que se reinicie la cuota. |
| resetsAt | string (ISO 8601) | Marca de tiempo ISO 8601 de la próxima medianoche UTC, cuando se reinicia la cuota. |
| period | "utc_day" | Ventana de la cuota. Siempre utc_day en la v1. |
| resumeUpload | { dailyLimit, used, remaining } | La cuota de subida de currículums separada por clave para hoy: dailyLimit (50), used y remaining. |
{
"dailyQuota": 500,
"used": 39,
"remaining": 461,
"resetsAt": "2026-07-26T00:00:00.000Z",
"period": "utc_day",
"resumeUpload": {
"dailyLimit": 50,
"used": 0,
"remaining": 50
}
}Errores
Las respuestas de error usan valores errorCode estables para que las integraciones no tengan que analizar mensajes de texto. Cada error devuelve el path de la solicitud; las respuestas 429 añaden retryAfter, los segundos hasta que se reinicia la ventana del límite. Un 429 de la cuota diaria lleva details.period utc_day; un 429 del límite por minuto o del límite por IP lleva minute.
| Estado | Código | Significado |
|---|---|---|
| 400 | VALIDATION_ERROR | La solicitud no pasó la validación (por ejemplo un id mal formado, una página o un tamaño de página fuera de rango, un nombre de pipeline con acento o puntuación, o un campo del cuerpo que no está en la lista permitida). details indica cada campo con problema. |
| 400 | UNKNOWN_INTERACTION_TYPE | El type de una interacción registrada no está en el catálogo global. details lista todos los nombres válidos. |
| 400 | MIME_MISMATCH | El tipo real del archivo subido no coincide con el mimeType declarado. |
| 400 | EMPTY_FILE | El currículum subido se decodificó a un archivo vacío. |
| 400 | INVALID_BASE64 | No se pudo decodificar contentBase64. |
| 400 | MOVE_TO_STAGE_REQUIRED | Se envió una eliminación de una etapa que aún contiene tarjetas sin ?moveToStageId. La llamada se rechaza antes de escribir nada: las tarjetas nunca se eliminan en silencio. |
| 400 | LAST_STAGE_UNDELETABLE | La única etapa que le queda al pipeline no se puede eliminar. Un pipeline siempre conserva al menos una. |
| 400 | TOO_MANY_ITEMS_TO_REASSIGN | La etapa contiene más tarjetas de las que una sola solicitud puede reasignar. Mueve algunas a otra etapa primero. |
| 401 | API_KEY_MISSING | No se envió ninguna clave de API. |
| 401 | API_KEY_INVALID | La clave no existe o no se puede verificar. |
| 401 | API_KEY_EXPIRED | La clave superó su fecha de expiración. |
| 401 | API_KEY_REVOKED | La clave fue revocada. |
| 403 | PLAN_REQUIRED | La CRM API requiere un plan Platinum, Diamond o Enterprise. |
| 403 | SCOPE_FORBIDDEN | La clave carece del alcance de CRM requerido (crm:read para lecturas, crm:write para escrituras). |
| 403 | ORG_ACCESS_DENIED | La clave no puede acceder a esta organización. |
| 403 | WORKSPACE_ACCESS_DENIED | El workspaceId en la solicitud no pertenece a tu clave de API. |
| 404 | NOT_FOUND | No existe un contacto, pipeline, etapa o tarjeta con ese id en el workspace resuelto. Uno de otro workspace devuelve este mismo 404. En las rutas de pipelines, details[0].field indica qué id corregir (id, stageId, itemId, contactId o moveToStageId). |
| 409 | CONTACT_CONFLICT | El correo o teléfono coincide con más de un contacto existente, así que no se puede resolver automáticamente. Reconcilia los duplicados y reintenta. |
| 409 | LIST_HAS_NO_STAGES | Se añadió una tarjeta a un pipeline que no tiene ninguna etapa activa donde ubicarla. Añade una etapa y reintenta. |
| 409 | IDEMPOTENCY_IN_PROGRESS | Un POST /public/lists con este Idempotency-Key sigue en curso. No se duplicó nada; reintenta en un momento para obtener la repetición. |
| 413 | PAYLOAD_TOO_LARGE | El currículum decodificado supera el límite de 10 MB. |
| 422 | IDEMPOTENCY_KEY_REUSED | El Idempotency-Key ya se usó con un cuerpo de solicitud distinto. Usa una clave nueva; reintentar no ayudará. |
| 429 | RATE_LIMIT_EXCEEDED | Se alcanzó un límite: la cuota diaria (details.period utc_day) o el límite por minuto o por IP (details.period minute). |
| 500 | CRM_API_ERROR | Ocurrió un error inesperado del servidor, o una dependencia no estaba disponible. |
{
"statusCode": 429,
"errorCode": "RATE_LIMIT_EXCEEDED",
"message": "CRM API burst limit exceeded. Slow down and retry shortly.",
"details": {
"limit": 30,
"period": "minute"
},
"retryAfter": 43,
"path": "/api/v3.0/crm/public/contacts"
}Límites y comportamiento
- La cuota diaria de CRM es de 500 llamadas exitosas por organización por día UTC. Es una cuota separada de las demás familias de la API. Cada llamada con cuota (lecturas y escrituras) consume una;
GET /public/usagees gratis. - Un límite por organización de 30 llamadas por minuto, más un límite compartido de 60 por minuto por IP, protegen el backend. Al exceder cualquiera se devuelve
429condetails.periodenminute. - Las subidas de currículum tienen su propia cuota de 50 por clave de API por día UTC, además de la llamada diaria que cada subida también consume.
- El endpoint de lista oculta correos, teléfonos,
companyId,locationGeoNameIdyupdatedAt; consulta un contacto individual para esos.jobTitlees un arreglo, y las cadenas largas se truncan a 150 caracteres. - Un contacto en otro workspace devuelve un
404indistinguible. Rota las claves creando una nueva, actualizando tu integración y luego revocando la anterior. - Las rutas de pipelines comparten ese mismo pool: cada una consume una sola llamada diaria, y sus escrituras son a nivel de workspace (cualquier clave con alcance sobre el workspace puede gestionar cualquier pipeline dentro de él). Un pipeline, etapa o tarjeta de otro workspace devuelve el mismo
404indistinguible.
Usar desde un agente (MCP)
¿Prefieres controlar tu CRM desde un agente de IA en vez de HTTP directo? El servidor MCP de CazVid expone las nueve herramientas crm_* (buscar, obtener, crear, actualizar, eliminar contacto, registrar y listar interacciones, listar tipos de interacción y subir currículum) además de list_api_usage a Claude Code, Claude.ai, ChatGPT, Cursor y VS Code, con esta misma clave de API y cuota.