Ir al contenido

Guía de la API de identificadores

Un identificador conecta una marca física con una Ficha: un identificador DUST, código QR, código de barras, símbolo Data Matrix, chip NFC o un código de texto impreso. Una vez vinculado, un escaneo sobre el terreno se resuelve en el registro digital. El espacio de nombres de la API es /api/v1/tags: una denominación heredada que se conserva en las rutas y los esquemas; esta documentación utiliza identificador en el texto.

Esquemas completos de solicitudes y respuestas: Referencia de la API. Todos los resultados que puede producir cada operación, incluidos los que llegan como errores HTTP, se recogen en una única tabla en Errores y resultados de escaneo.

OperaciónMétodo y rutaSemántica
ExtraerPOST /api/v1/tags/extractAnalizar una captura DUST para obtener una huella canónica, sin vincularla
VincularPOST /api/v1/tags/bindAsociar un identificador con una Ficha
IdentificarPOST /api/v1/tags/identifyBuscar: ¿qué Ficha coincide con este escaneo?
VerificarPOST /api/v1/tags/verifyComparar un escaneo con los identificadores de una Ficha específica
DesvincularPOST /api/v1/tags/unbindSeparar un identificador de su Ficha
Establecer textoPOST /api/v1/tags/textCambiar el nombre o la descripción de un identificador vinculado
ActualizarPOST /api/v1/tags/updateCiclo de vida: privacidad, archivar/restaurar (el valor y el tipo son inmutables)

Identificar frente a verificar: identificar responde a «¿qué es esto?»: busca entre las Fichas visibles (con el ámbito definido por searchTeamIds) y devuelve la coincidencia, si la hay. Verificar responde a «¿es este el artículo que afirma ser?»: se indican un threadId y los identificadores candidatos vinculados a él, y la API confirma o rechaza la coincidencia. Utiliza la verificación para tomar decisiones de autenticación y la identificación para búsquedas.

Los endpoints de escaneo aceptan multipart/form-data, y la forma de data depende del tipo de identificador:

tagTypedataProcedencia
DUSTUna imagen: una parte de archivo binario o una URL de datos en base64 (data:image/jpeg;base64,…)Una captura óptica DUST procedente de un escáner
QR, BAR_CODE, DATA_MATRIX, NFCEl contenido de la cadena decodificada (o el ID hexadecimal NFC)Cualquier escáner de símbolos
TEXTEl código impreso legible por una persona, tal y como esta lo leeEntrada mediante teclado o inscripción de una Etiqueta

Una captura DUST es una fotografía del identificador, no un valor decodificado: el servidor extrae la huella. Las capturas proceden de hardware de escaneo DUST: consulta Integración con DUST Go para realizar capturas desde dispositivos móviles y React Scanner para utilizar un componente web listo para incorporar que gestiona todos los modos.

En los cuerpos multipart, los campos estructurados (options, tags, searchTeamIds) se envían como cadenas JSON.

TEXT es el código legible impreso en un artículo o una Etiqueta, por ejemplo, un número de serie como AB00017. No hay ningún símbolo que decodificar, por lo que el valor se introduce manualmente (o procede del registro de inscripción de una Etiqueta) y se almacena exactamente como se introdujo. Dado que quien lo lee es una persona, TEXT es el único tipo para el que la plataforma busca coincidencias sin distinguir entre mayúsculas y minúsculas: al identificar o verificar con ab00017, se encuentra un AB00017 vinculado. Todos los demás tipos se comparan byte por byte.

Al igual que los valores QR, de código de barras, Data Matrix y NFC, un código de texto puede copiarse y no posee unicidad propia: el mismo código puede aparecer legítimamente en varias Fichas o en todas las Etiquetas de una Bobina. Los valores repetidos no se rechazan; la Bobina simplemente los notifica mediante una advertencia.

La extracción analiza una captura para obtener una huella canónica y devuelve su calidad; resulta útil para comprobar una captura antes de inscribirla o para preparar una vinculación:

Ventana de terminal
curl -fsS "$APID_URL/api/v1/tags/extract" \
-H "Authorization: Bearer $DUST_TOKEN" \
-H "Dust-Ctx-Org-Id: $DUST_ORG_ID" \
-F "data=@scan.jpeg" \
-F 'options={"enrollmentSessionId":"3d5e…"}'

La respuesta es { id, qualityScore, annotatedImage?, forensics?, scan? }: id es un ID de huella que posteriormente puedes vincular sin volver a cargar la imagen (como se muestra más adelante). options también contiene metadatos de captura (dispositivo, óptica y geolocalización) que la plataforma almacena con el escaneo.

Todas las operaciones que envían una imagen —extracción, vinculación, identificación, verificación y análisis de alteraciones— devuelven un objeto scan que indica qué se almacenó:

{ "scanId": "…", "fingerprintId": "…", "dustId": "…" }
  • scanId siempre está presente una vez almacenada la imagen. Es la identidad estable de la captura y el valor que debes conservar si registras las operaciones de escaneo en tu sistema.
  • fingerprintId está presente cuando la extracción se realizó correctamente (null en caso contrario).
  • dustId está presente cuando la operación te devolvió un DUST: el identificador que creó una vinculación, confirmó una verificación o resolvió una identificación. Es null en caso de discrepancia, ausencia de coincidencias, extracción o análisis de alteraciones (en este último caso, el identificador es uno que proporcionaste, no uno que la imagen resolviera), así como en una identificación que devolvió varios candidatos (cada candidato contiene su propio identificador).

Una discrepancia de verificación y una identificación sin coincidencias conservan su estado y código de error actuales, e incluyen el mismo comprobante en detail.scan: el escaneo se almacenó aunque el resultado fuera negativo. Lo mismo ocurre con una captura rechazada por su calidad —por tener muy pocos puntos clave utilizables o ninguno—, independientemente del código de error que la operación comunique (/tags/extract responde con SCAN_EXTRACTION_FAILURE; la identificación expone SCAN_LOW_KEYPOINTS / SCAN_NO_KEYPOINTS; la verificación responde con su IDENTIFIER_VERIFY_FAILED habitual): la imagen se conserva y su comprobante contiene fingerprintId: null, porque no pudo extraerse nada útil. Solo una imagen que la plataforma no pudo decodificar en absoluto no almacena nada ni tiene comprobante.

POST /api/v1/tags/bind acepta tres formas, diferenciadas por tagType y la carga útil:

Ventana de terminal
curl -fsS "$APID_URL/api/v1/tags/bind" \
-H "Authorization: Bearer $DUST_TOKEN" \
-H "Dust-Ctx-Org-Id: $DUST_ORG_ID" \
-F "threadId=$THREAD_ID" \
-F "tagType=DUST" \
-F "tagDescription=Inbound receiving scan" \
-F "data=@scan.jpeg" \
-F 'options={"enrollmentSessionId":"3d5e…"}'

options.enrollmentSessionId es opcional en una vinculación DUST. Proporciona un UUID generado por el cliente —el mismo durante toda una ejecución— cuando varias capturas deban agruparse, por ejemplo, si una estación de inscripción procesa un lote o realiza una captura de un artículo desde varios ángulos; la plataforma agrupará esos escaneos en esa sesión. Omítelo por completo para una vinculación puntual. Las vinculaciones de imágenes DUST también pueden devolver la captura anotada mediante options.returnAnnotatedImage: true.

Si el identificador escaneado pertenece a una Etiqueta propiedad del Equipo de la Ficha (consulta Etiquetas), la vinculación no crea un identificador independiente. Vincula toda la Etiqueta: todos los identificadores miembros activos se asocian a la Ficha en una sola operación, y la respuesta contiene label (la Etiqueta, su Bobina y su posición), además de boundTags (todos los miembros vinculados), junto con el tag habitual, que es el miembro escaneado. Envía activateLabel: true para hacer también identificables los identificadores DUST de la Etiqueta como parte de la vinculación; esto es opcional. Una Etiqueta que ya esté vinculada a otra Ficha devuelve IDENTIFIER_ALREADY_BOUND con detail.compositeTagId.

Identificar una Ficha a partir de un escaneo

Sección titulada «Identificar una Ficha a partir de un escaneo»
Ventana de terminal
curl -fsS "$APID_URL/api/v1/tags/identify" \
-H "Authorization: Bearer $DUST_TOKEN" \
-H "Dust-Ctx-Org-Id: $DUST_ORG_ID" \
-F "tagType=DUST" \
-F "data=@scan.jpeg" \
-F 'searchTeamIds=["'"$TEAM_ID"'"]'

La identificación también acepta cargas útiles de valores (tagType de QR/BAR_CODE/DATA_MATRIX/NFC con el data decodificado, o TEXT con el código impreso, cuya coincidencia no distingue entre mayúsculas y minúsculas) o un ID de identificador por sí solo (tagType: "ANY" con tagId).

Una coincidencia devuelve 200 con el identificador coincidente y su Ficha. Una ausencia de coincidencia es un estado de error, no un 200 con un resultado vacío: una ausencia de coincidencia definitiva es 404 IDENTIFIER_NOT_FOUND, mientras que una búsqueda que no pudo completarse es 503 SCAN_SEARCH_INCOMPLETE, una situación distinta que no debe mostrarse a un operador como «no encontrado». Ambos incluyen el comprobante de escaneo en detail.scan.

La tabla canónica de los ocho resultados —Ficha identificada, varios candidatos, Etiqueta sin vincular, ausencia de coincidencias, búsqueda incompleta, coincidencia ambigua, captura rechazada e identificador no vinculado—, con el estado, el código y la respuesta correcta del cliente para cada uno, se encuentra en Errores y resultados de escaneo → Resultados canónicos de identificación. Bifurca según code y consulta detail.outcome (no_match, search_incomplete, ambiguous, quality_reject) cuando necesites la distinción más precisa.

Todo identificador devuelto que pertenezca a una Etiqueta contiene tag.label (su Etiqueta, Bobina y posición). Cuando el escaneo coincide con un miembro de una Etiqueta sin vincular del inventario del Equipo activo, el resultado es { type: "label", label: { label, tags } }: aún no hay ninguna Ficha, pero sí están disponibles la Etiqueta y sus identificadores miembros para que un cliente pueda ofrecer vincularla (consulta Etiquetas).

searchTeamIds es un array JSON de UUID de Equipos (una cadena JSON en los cuerpos multipart). Si se omite, la identificación busca exactamente en un Equipo: el indicado por Dust-Ctx-Team-Id, que a su vez utiliza de forma predeterminada el Equipo raíz de la organización.

Los ID no son arbitrarios. Los Equipos de la misma organización a los que perteneces siempre se incluyen en el ámbito; solo se puede acceder al Equipo de una organización asociada mediante una Conexión activa que permita que sus datos fluyan hasta ti. Cualquier otro Equipo se elimina silenciosamente del ámbito en lugar de hacer que falle la solicitud, por lo que un ámbito aparentemente amplio puede dar lugar a una búsqueda limitada. Descubre los ID válidos en lugar de codificarlos de forma fija:

  • GET /api/v1/teams: los Equipos de tu organización a los que pertenece tu credencial ({ teams: [{ teamId, orgId, name, … }], total }).
  • GET /api/v1/teams/connected: los Equipos asociados en los que puedes buscar, como registros de Conexión que indican los dos Equipos vinculados.

Verificar un escaneo con respecto a una Ficha

Sección titulada «Verificar un escaneo con respecto a una Ficha»

La verificación es la operación básica de autenticación: dado un escaneo reciente, un threadId y los tags candidatos ya vinculados a esa Ficha, se realiza correctamente si coincide algún candidato.

tags es obligatorio y es un array de objetos —cada uno con { "tagId": "…", "tagType": "…" }—, no un array de cadenas de ID. En un cuerpo multipart se envía como una cadena JSON:

Ventana de terminal
curl -fsS "$APID_URL/api/v1/tags/verify" \
-H "Authorization: Bearer $DUST_TOKEN" \
-H "Dust-Ctx-Org-Id: $DUST_ORG_ID" \
-F "threadId=$THREAD_ID" \
-F "tagType=DUST" \
-F "data=@scan.jpeg" \
-F 'tags=[{"tagId":"'"$TAG_ID"'","tagType":"DUST"}]'
const form = new FormData();
form.set("threadId", threadId);
form.set("tagType", "DUST");
form.set("data", scanBlob);
form.set("tags", JSON.stringify([{ tagId, tagType: "DUST" }]));

Los valores tagId son los identificadores ya vinculados a la Ficha que estás comprobando. Obtenlos de la Ficha: GET /api/v1/threads/{thread_id} los devuelve en thread.tags, cada uno con su tagId y tagType. Por tanto, una verificación habitual obtiene la Ficha, filtra sus identificadores según el tipo que acabas de capturar y los envía como lista de candidatos:

const record = await getThread(threadId); // GET /api/v1/threads/{thread_id}
const candidates = (record.thread.tags ?? [])
.filter((tag) => tag.tagType === "DUST")
.map((tag) => ({ tagId: tag.tagId, tagType: tag.tagType }));

Enviar un identificador que no esté vinculado a esa Ficha hace que falle la verificación en lugar de buscar una coincidencia distinta.

El número de candidatos cambia la forma de la respuesta, lo que constituye el error de integración más frecuente:

Longitud de tagsCoincidenciaSin coincidencia
Exactamente uno200 con { tag, scan? }IDENTIFIER_VERIFY_FAILED (HTTP 500), comprobante en detail.scan
Dos o más200 con { success: true, verifiedTag, attemptedCount, failedCount, scan? }200 con { success: false, attemptedCount, failedCount, error, scan? }

Por tanto, una verificación con varios candidatos que falle es una llamada HTTP correcta que contiene success: false. Nunca interpretes response.ok como prueba de autenticidad: lee success siempre que envíes más de un candidato. Consulta Errores y resultados de escaneo → Resultados de verificación.

Estos son endpoints JSON normales; todos requieren tanto el tagId como el threadId de la Ficha a la que está vinculado el identificador:

  • POST /api/v1/tags/text: establece name y/o description.
  • POST /api/v1/tags/update: establece name, description, isPrivate y archivedAt (una marca de tiempo ISO archiva el identificador; null lo restaura). El valor y el tipo del identificador son inmutables; en su lugar, vuelve a vincularlo.
  • POST /api/v1/tags/unbind: separa el identificador de la Ficha.

En /api/v1/tamper, un Análisis de alteraciones compara un nuevo escaneo de un identificador DUST con la referencia capturada al vincularlo y registra las mediciones obtenidas. La API solo devuelve mediciones y evidencias: no existe ningún número de resumen, intervalo, umbral ni campo de resultado generado por la plataforma en toda la superficie, y alignmentOutcome indica únicamente si los dos escaneos pudieron compararse (cuando no es posible, las mediciones no son comparables, lo que no constituye ninguna afirmación sobre el identificador).

OperaciónMétodo y ruta
Ejecutar un AnálisisPOST /api/v1/tamper/analyses: codificado como formulario con threadId, tagId y exactamente uno de data o queryFingerprintId
Registrar una ObservaciónPOST /api/v1/tamper/observations: { analysisId, result }
Enumerar los Análisis de una FichaGET /api/v1/tamper/analyses?threadId=… (opcionalmente tagId, limit)
Obtener un AnálisisGET /api/v1/tamper/analyses/{analysis_id}
Obtener un mapa de bits de resultadosGET /api/v1/tamper/analyses/{analysis_id}/artifacts/{name}

La ejecución de un Análisis acepta un cuerpo multipart/form-data o application/x-www-form-urlencoded con threadId, tagId y exactamente uno de los siguientes:

  • data: el propio escaneo DUST, como archivo o imagen codificada en base64. El servicio lo extrae por ti.
  • queryFingerprintId: un ID de huella que ya hayas obtenido mediante POST /api/v1/tags/extract (arriba), si realizaste la extracción por separado.

Enviar ambos, o ninguno, provoca el rechazo de la solicitud. En ambos casos, una captura DUST normal es una entrada válida: no existe una ruta de captura separada para el análisis de alteraciones. Si no puede leerse el escaneo enviado, la solicitud falla y no se registra ningún Análisis.

Una Observación de alteraciones es la única conclusión que almacena la plataforma y la registra una persona: result puede ser consistent, expected, inconsistent o unknown, no tiene valor predeterminado y es obligatorio. expected registra el desgaste normal correspondiente al caso de uso y al sustrato del identificador. Las Observaciones son inmutables y se atribuyen a su autor; una nueva nunca reemplaza a otra anterior, y las lecturas devuelven la serie completa (observations, primero la más reciente), no un único resultado actual. No deduzcas ningún resultado a partir de las métricas ni reduzcas la serie a un único valor en tu propia interfaz.

Un Análisis contiene metrics (un objeto transferido sin modificaciones con las fracciones de cobertura y los recuentos de marcadores del algoritmo), markerPoints opcionales y artifactNames. Cada conjunto de coordenadas de marcadores utiliza el espacio de píxeles de su propio escaneo; para componerlos en un único marco, aplica metrics.transformation_matrix a los puntos de la consulta. Los mapas de bits de resultados son contenido protegido: obtenlos mediante el endpoint de artefactos, que vuelve a autorizar cada solicitud y devuelve bytes no almacenables en caché.

Una Etiqueta (nombre en el protocolo: composite tag, espacio de nombres /api/v1/composite-tags) es una etiqueta física que contiene uno o más identificadores de cualquier tipo; no es obligatorio que incluya DUST. Las Etiquetas ocupan una posición en una Bobina (collection.kind = "reel", identificada por su UUID; su name es el número de bobina impreso o cualquier título y nunca es único). Las Bobinas pueden archivarse en una Colección de etiquetas (kind = "reel_collection"), una carpeta que nunca se envía. El campo expectedIdentifiers de una Bobina indica cuántos Identificadores de cada tipo contiene una Etiqueta completa de esa Bobina, con el formato [{ "tagType", "count" }] (de forma predeterminada, un TEXT, un DUST y un QR; un count de 0 en la entrada significa que no se espera ese tipo). Es una indicación para las estaciones de inscripción, no una restricción, y el indicador complete de una Etiqueta significa que contiene al menos esa cantidad de Identificadores activos de cada tipo previsto.

OperaciónMétodo y ruta
Enumerar o crear Colecciones de etiquetasGET, POST /api/v1/composite-tags/collections; PATCH …/collections/{collection_id}
Enumerar BobinasGET /api/v1/composite-tags/reels?collectionId=…&unfiled=…&transferred=any|only|hide&q=…
Crear una BobinaPOST /api/v1/composite-tags/reels: { name, description?, collectionId?, expectedIdentifiers? }
Obtener o actualizar una BobinaGET, PATCH /api/v1/composite-tags/reels/{reel_collection_id} (cambiar el nombre o la composición prevista; collectionId para moverla y null para dejarla sin colección)
Crear una EtiquetaPOST /api/v1/composite-tags/reels/{reel_collection_id}/labels (multipart)
Añadir o quitar un identificador miembroPOST /api/v1/composite-tags/{composite_tag_id}/identifiers (multipart); DELETE …/identifiers/{tag_id}
Enumerar u obtener EtiquetasGET /api/v1/composite-tags?reelCollectionId=…&bound=any|only|unbound&transferred=…&q=…; GET …/{composite_tag_id}
Resolver una Etiqueta por el valor de un miembroPOST /api/v1/composite-tags/resolve: { tagType, value, reelCollectionId? } (TEXT busca coincidencias sin distinguir entre mayúsculas y minúsculas); la respuesta contiene detail (primera coincidencia) y candidates[] (todas las coincidencias, ordenadas por posición cuando se proporciona una Bobina)
Mover o cortar EtiquetasPOST /api/v1/composite-tags/move; comprobación previa con POST /api/v1/composite-tags/move/preview
Vincular o desvincular una EtiquetaPOST /api/v1/composite-tags/{composite_tag_id}/bind: { threadId, options?: { indexing: "default" } }; POST …/unbind
Vincular en bloque un Intervalo de la bobinaPOST /api/v1/composite-tags/reels/{reel_collection_id}/bulk-bind: { fromPosition, toPosition, threadIds, activate?, dryRun? }
Archivar o restaurar una EtiquetaPOST …/{composite_tag_id}/archive, POST …/unarchive
ActivarPOST …/{composite_tag_id}/activate; POST /api/v1/composite-tags/reels/{reel_collection_id}/activate (en segundo plano)
Activar identificadores DUST independientesPOST /api/v1/tags/activate: { tagIds[] } (hasta 200); un resultado por identificador, solo hacia delante
Ventana de terminal
curl -fsS "$APID_URL/api/v1/composite-tags/reels" \
-H "Authorization: Bearer $DUST_TOKEN" \
-H "Dust-Ctx-Org-Id: $DUST_ORG_ID" \
-H "Dust-Ctx-Team-Id: $DUST_TEAM_ID" \
-H "Content-Type: application/json" \
--data '{ "name": "0030", "expectedIdentifiers": [{ "tagType": "TEXT", "count": 1 }, { "tagType": "DUST", "count": 1 }, { "tagType": "QR", "count": 1 }] }'

La respuesta es { reel }, un resumen de la Bobina con recuentos a cero. Conserva reel.collectionId, ya que identifica la Bobina. Crear una Bobina siempre crea una nueva: no se reutiliza ninguna por su nombre.

Cada Etiqueta requiere una solicitud multipart. Proporciona como máximo una imagen DUST en data; los demás miembros se envían mediante identifiers, un array JSON de entradas { tagType, value }, o mediante { tagType: "DUST", fingerprintId } para otro DUST ya extraído con POST /api/v1/tags/extract. humanReadable y qrValue son formas abreviadas de añadir, respectivamente, un miembro TEXT y uno QR. position utiliza de forma predeterminada la siguiente posición libre de la Bobina.

Ventana de terminal
curl -fsS "$APID_URL/api/v1/composite-tags/reels/$REEL_COLLECTION_ID/labels" \
-H "Authorization: Bearer $DUST_TOKEN" \
-H "Dust-Ctx-Org-Id: $DUST_ORG_ID" \
-H "Dust-Ctx-Team-Id: $DUST_TEAM_ID" \
-F "position=1" \
-F "data=@scan.jpeg" \
-F 'identifiers=[{"tagType":"TEXT","value":"AB00001"},{"tagType":"QR","value":"https://v.example/ab00001"}]' \
-F 'options={"indexing":"none"}'

Debe generarse al menos un identificador. Una respuesta correcta contiene outcome: "created"; un reintento con la misma marca DUST en la misma posición se concilia como outcome: "already_enrolled". Un DUST que ya esté en otra Etiqueta o ya esté vinculado devuelve 409 COMPOSITE_TAG_CONFLICT. Los valores TEXT o QR repetidos nunca se rechazan: la Bobina los incluye en warnings, porque algunas bobinas repiten legítimamente un valor.

options.indexing selecciona el modo de indexación DUST para la imagen de data: default (identificable), salvo que solicites none (solo verificación). Las estaciones de inscripción desatendidas suelen inscribir en modo de solo verificación y activar posteriormente; la activación indexa cada DUST y ejecuta la comprobación de duplicados de la plataforma, por lo que una Etiqueta cuyo DUST duplique uno ya indexado se notifica y se omite.

El flujo equivalente con el cliente tipado es:

const created = await client.compositeTags.createReel({ name: "0030" });
const form = new FormData();
form.set("position", "1");
form.set("data", scanBlob);
form.set("identifiers", JSON.stringify([{ tagType: "TEXT", value: "AB00001" }]));
await client.compositeTags.createLabel(created.reel.collectionId, form);
const state = await client.compositeTags.getReel(created.reel.collectionId);
await client.compositeTags.activateReel(created.reel.collectionId);

POST /api/v1/composite-tags/move recibe un source, un target y un expectedCount opcional.

Hay tres formas posibles para el origen:

  • { compositeTagIds }: Etiquetas seleccionadas manualmente, que se mueven en el orden indicado.
  • { reelCollectionId, fromPosition, toPosition? }: un intervalo de posiciones tipado (un corte); toPosition utiliza de forma predeterminada la última posición de la Bobina.
  • { fromCompositeTagId, toCompositeTagId }: un corte delimitado por escaneos: la primera y la última Etiqueta del tramo, en cualquier orden. El servidor lee sus posiciones bajo bloqueo; ambas deben ser Etiquetas activas de la misma Bobina (de lo contrario, detail.reason contiene endpoints_on_different_reels, endpoint_archived o endpoint_not_on_reel). Resuelve cada Etiqueta a partir de un valor escaneado mediante /resolve (limitado a la Bobina para que un código impreso repetido aparezca como varios candidates que la aplicación debe desambiguar) o, en el caso de un DUST activado, mediante POST /api/v1/tags/identify.

El target es { reelCollectionId } o { newReel: { name, description?, collectionId?, expectedIdentifiers? } } (una Bobina nueva hereda la composición de la Bobina de origen si no se proporciona ninguna).

Todas las Etiquetas activas del tramo se mueven; las posiciones que contengan una Etiqueta archivada o enviada, o que no contengan ninguna, son huecos que permanecen en la Bobina de origen. Las posiciones se conservan cuando todas están libres en el destino; de lo contrario, el lote completo se añade después de la última posición del destino, siguiendo el orden del origen. La respuesta enumera moved[] y, para un origen basado en un intervalo o delimitado por escaneos, cut: { sourceReel, fromPosition, toPosition, count, boundCount, boundPositions, gaps[] }.

expectedCount convierte el recuento en el contrato: cuando se proporciona, el movimiento se rechaza con 400 INVALID_REQUEST y detail.reason: "count_mismatch" (expected, actual, fromPosition, toPosition), salvo que fueran a moverse exactamente esa cantidad de Etiquetas activas.

POST /api/v1/composite-tags/move/preview acepta el mismo source, un target opcional y expectedCount, no modifica nada y devuelve span, count, boundCount, las Etiquetas first y last del lote, predictedOutcome (kept_positions / appended / null), countMatches y suggestedLast: la Etiqueta situada más adelante en la Bobina que permitiría satisfacer expectedCount cuando el tramo sea demasiado corto. Es una sugerencia para que el operador la escanee; el servidor nunca la aplica. La vista previa requiere el nivel de miembro; el movimiento requiere que se sea administrador del Equipo o de la Organización.

Vinculación en bloque de un Intervalo de la bobina

Sección titulada «Vinculación en bloque de un Intervalo de la bobina»

POST /api/v1/composite-tags/reels/{reel_collection_id}/bulk-bind vincula las Etiquetas de las posiciones fromPosition..toPosition (ambas incluidas) con los threadIds, en orden: la k-ésima posición con la k-ésima Ficha. La operación es estricta y se realiza por completo o no se realiza: el intervalo debe abarcar exactamente threadIds.length posiciones (toPosition es obligatorio, no se deduce, para que la aplicación indique el intervalo que verificó en la bobina), se permiten como máximo 500 pares por llamada y cada posición debe contener una Etiqueta activa sin vincular. El servidor nunca omite una posición, porque hacerlo desplazaría silenciosamente todos los emparejamientos posteriores.

Envía dryRun: true para realizar una comprobación previa sin vincular. La forma de la respuesta es la misma en ambos casos:

  • outcome: "bound" después de una vinculación real o "preflight" para una ejecución de prueba.
  • rows[]: una entrada por emparejamiento con index, position, compositeTagId (null para una posición vacía), labelName, textValue (el miembro TEXT de la Etiqueta, es decir, el código impreso), threadId, threadName y threadDescription.
  • blockers[] y warnings[]: { kind, index, position, compositeTagId?, threadId?, tagType?, existing? }.
  • activation: "queued", "not_requested", "already_active" o "no_dust".
  • reel: el resumen de la Bobina con los recuentos actualizados.

Tipos de bloqueadores: position_empty, label_archived, label_transferred, label_bound, label_no_identifiers, identifier_bound_elsewhere, identifier_in_other_team_label, thread_not_owned, thread_unavailable, thread_in_transfer, thread_not_editable y thread_repeated. Tipos de advertencias: label_incomplete (una Etiqueta con menos Identificadores de los previstos por la Bobina) y thread_has_label (la Ficha ya contiene una Etiqueta; existing[] las identifica). Las advertencias nunca impiden una vinculación.

Una confirmación con algún bloqueador falla con 409 COMPOSITE_TAG_CONFLICT; detail contiene los mismos rows, blockers y warnings que una ejecución de prueba, por lo que el cliente solo tiene que analizar una forma. Un intervalo cuya longitud sea distinta de threadIds.length produce 400 INVALID_REQUEST.

Autoridad: pertenencia al Equipo de la Bobina y permiso de edición sobre cada Ficha. Una Ficha que la persona que realiza la llamada no pueda editar genera un bloqueador thread_not_editable para esa fila, en lugar de provocar el rechazo inmediato de toda la solicitud, y cada Ficha debe ser propiedad del Equipo de la Bobina; una Ficha simplemente compartida con el Equipo genera thread_not_owned.

Con activate: true, la vinculación se confirma primero y, a continuación, un único trabajo en segundo plano activa exactamente las marcas DUST de las Etiquetas vinculadas; una Etiqueta cuya activación falle permanece vinculada y en modo de solo verificación. Consulta periódicamente counts.identifiableCount de la Bobina para conocer el progreso.

Ventana de terminal
# Preflight
curl -fsS "$APID_URL/api/v1/composite-tags/reels/$REEL_COLLECTION_ID/bulk-bind" \
-H "Authorization: Bearer $DUST_TOKEN" \
-H "Dust-Ctx-Org-Id: $DUST_ORG_ID" \
-H "Dust-Ctx-Team-Id: $DUST_TEAM_ID" \
-H "Content-Type: application/json" \
--data '{ "fromPosition": 1, "toPosition": 3, "threadIds": ["'$THREAD_1'", "'$THREAD_2'", "'$THREAD_3'"], "dryRun": true }'
# Commit, activating the bound Labels afterwards
curl -fsS "$APID_URL/api/v1/composite-tags/reels/$REEL_COLLECTION_ID/bulk-bind" \
-H "Authorization: Bearer $DUST_TOKEN" \
-H "Dust-Ctx-Org-Id: $DUST_ORG_ID" \
-H "Dust-Ctx-Team-Id: $DUST_TEAM_ID" \
-H "Content-Type: application/json" \
--data '{ "fromPosition": 1, "toPosition": 3, "threadIds": ["'$THREAD_1'", "'$THREAD_2'", "'$THREAD_3'"], "activate": true }'

Con el cliente tipado:

const preview = await client.compositeTags.bulkBind(reelCollectionId, {
fromPosition: 1,
toPosition: threadIds.length,
threadIds,
dryRun: true,
});
if (preview.blockers.length === 0) {
await client.compositeTags.bulkBind(reelCollectionId, {
fromPosition: 1,
toPosition: threadIds.length,
threadIds,
activate: true,
});
}

La vinculación deja exactamente los eventos que dejarían N vinculaciones individuales: un evento bind por identificador miembro, cada uno con la Ficha, el identificador y la Etiqueta como destinos, y todos con el mismo ID de operación.

Una Etiqueta se vincula como un todo: POST …/{composite_tag_id}/bind asocia la Etiqueta y todos sus identificadores miembros activos a la Ficha; options.indexing: "default" también la activa. Lo mismo ocurre al llamar al endpoint normal POST /api/v1/tags/bind con cualquier identificador miembro (consulta Vincular una Etiqueta), que es lo que hacen los escáneres. La vinculación requiere permiso de edición sobre la Ficha y que el Equipo sea propietario de la Etiqueta, y la Ficha debe pertenecer al mismo Equipo: si se escanea un identificador miembro para asociarlo a una Ficha que otro Equipo ha compartido contigo, la operación se rechaza (409 COMPOSITE_TAG_CONFLICT, reason: "label_owned_by_other_team") en lugar de vincularlo como una copia independiente. POST …/unbind desvincula la Etiqueta y todos sus miembros.

La propiedad del Equipo y de la Organización procede únicamente de las cabeceras de contexto, y el creador se obtiene del token de portador verificado. Para leer y activar Etiquetas es necesario pertenecer al Equipo. La creación de Bobinas, la inscripción, el movimiento y el archivado aceptan a un administrador del Equipo o de la Organización que haya iniciado sesión, o a una Cuenta de servicio con ámbito de Organización que pertenezca al Equipo seleccionado; una Cuenta de servicio no puede aprovechar la autoridad de administrador de la Organización.

En los Envíos, una Bobina es un elemento del manifiesto ({ kind: "reel", collectionId }) y se envía completa, pero solo cuando ninguna de sus Etiquetas está vinculada. Una Etiqueta vinculada se envía con su Ficha y nunca incorpora su Bobina al Envío. Las Bobinas y Etiquetas enviadas siguen siendo legibles para el remitente con transferredAt establecido; fíltralas mediante transferred=only o transferred=hide.

Consulta Etiquetas y Bobinas para conocer el flujo de trabajo de DICE.