diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a665d58ee3a..10076935a20 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,9 +2,9 @@ name: Build on: push: - branches: [ '*' ] + branches: ["*"] pull_request: - branches: [ '*' ] + branches: ["*"] jobs: test: @@ -15,28 +15,28 @@ jobs: ruby-version: [2.3, 2.4, 2.5, 2.6] steps: - - uses: actions/checkout@v2 - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: ${{ matrix.ruby-version }} - - - uses: actions/cache@v2 - with: - path: vendor/bundle - key: gems-${{ runner.os }}-${{ matrix.ruby-version }}-${{ hashFiles('**/Gemfile.lock') }} - restore-keys: | - gems-${{ runner.os }}-${{ matrix.ruby-version }}- - gems-${{ runner.os }}- - - # necessary to get ruby 2.3 to work nicely with bundler vendor/bundle cache - # can remove once ruby 2.3 is no longer supported - - run: gem update --system 3.3.27 - - - run: bundle config set deployment 'true' - - name: bundle install - run: | - bundle config path vendor/bundle - bundle install --jobs 4 --retry 3 - - - run: bundle exec middleman build + - uses: actions/checkout@v2 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby-version }} + + - uses: actions/cache@v3 + with: + path: vendor/bundle + key: gems-${{ runner.os }}-${{ matrix.ruby-version }}-${{ hashFiles('**/Gemfile.lock') }} + restore-keys: | + gems-${{ runner.os }}-${{ matrix.ruby-version }}- + gems-${{ runner.os }}- + + # necessary to get ruby 2.3 to work nicely with bundler vendor/bundle cache + # can remove once ruby 2.3 is no longer supported + - run: gem update --system 3.3.27 + + - run: bundle config set deployment 'true' + - name: bundle install + run: | + bundle config path vendor/bundle + bundle install --jobs 4 --retry 3 + + - run: bundle exec middleman build diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 341cd5f7fbc..451af2a9c20 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -2,7 +2,7 @@ name: Deploy on: push: - branches: [ 'main' ] + branches: ["main"] jobs: deploy: @@ -11,31 +11,31 @@ jobs: ruby-version: 2.5 steps: - - uses: actions/checkout@v2 - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: ${{ env.ruby-version }} + - uses: actions/checkout@v2 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ env.ruby-version }} - - uses: actions/cache@v2 - with: - path: vendor/bundle - key: gems-${{ runner.os }}-${{ matrix.ruby-version }}-${{ hashFiles('**/Gemfile.lock') }} - restore-keys: | - gems-${{ runner.os }}-${{ matrix.ruby-version }}- - gems-${{ runner.os }}- + - uses: actions/cache@v3 + with: + path: vendor/bundle + key: gems-${{ runner.os }}-${{ matrix.ruby-version }}-${{ hashFiles('**/Gemfile.lock') }} + restore-keys: | + gems-${{ runner.os }}-${{ matrix.ruby-version }}- + gems-${{ runner.os }}- - - run: bundle config set deployment 'true' - - name: bundle install - run: | - bundle config path vendor/bundle - bundle install --jobs 4 --retry 3 + - run: bundle config set deployment 'true' + - name: bundle install + run: | + bundle config path vendor/bundle + bundle install --jobs 4 --retry 3 - - run: bundle exec middleman build + - run: bundle exec middleman build - - name: Deploy - uses: peaceiris/actions-gh-pages@v3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./build - keep_files: true + - name: Deploy + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./build + keep_files: true diff --git a/.github/workflows/dev_deploy.yml b/.github/workflows/dev_deploy.yml index 5a655b8ced8..4d8943ad8cf 100644 --- a/.github/workflows/dev_deploy.yml +++ b/.github/workflows/dev_deploy.yml @@ -2,7 +2,7 @@ name: Dev Deploy on: push: - branches: [ 'dev' ] + branches: ["dev"] jobs: deploy: @@ -11,32 +11,32 @@ jobs: ruby-version: 2.5 steps: - - uses: actions/checkout@v2 - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: ${{ env.ruby-version }} + - uses: actions/checkout@v2 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ env.ruby-version }} - - uses: actions/cache@v2 - with: - path: vendor/bundle - key: gems-${{ runner.os }}-${{ matrix.ruby-version }}-${{ hashFiles('**/Gemfile.lock') }} - restore-keys: | - gems-${{ runner.os }}-${{ matrix.ruby-version }}- - gems-${{ runner.os }}- + - uses: actions/cache@v3 + with: + path: vendor/bundle + key: gems-${{ runner.os }}-${{ matrix.ruby-version }}-${{ hashFiles('**/Gemfile.lock') }} + restore-keys: | + gems-${{ runner.os }}-${{ matrix.ruby-version }}- + gems-${{ runner.os }}- - - run: bundle config set deployment 'true' - - name: bundle install - run: | - bundle config path vendor/bundle - bundle install --jobs 4 --retry 3 + - run: bundle config set deployment 'true' + - name: bundle install + run: | + bundle config path vendor/bundle + bundle install --jobs 4 --retry 3 - - run: bundle exec middleman build + - run: bundle exec middleman build - - name: Deploy - uses: peaceiris/actions-gh-pages@v3.7.0-8 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - destination_dir: dev - publish_dir: ./build - keep_files: true + - name: Deploy + uses: peaceiris/actions-gh-pages@v3.7.0-8 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + destination_dir: dev + publish_dir: ./build + keep_files: true diff --git a/README.md b/README.md index e76c2b31837..5e121103674 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,35 @@ +# DataScope API Documentation + +Public API reference for DataScope, published at **https://dscope.github.io/docs/**. + +All the content lives in one file: [`source/index.html.md`](source/index.html.md). The site is built with Slate, whose original README follows below. + +## Open your pull requests against `main`, not `master` + +This repository keeps two long-lived branches, and the defaults work against you: + +| Branch | Role | +| --- | --- | +| **`main`** | **The branch the site is built from.** `.github/workflows/deploy.yml` triggers on pushes here and publishes to `gh-pages` | +| `master` | GitHub's configured default branch, so a new pull request targets it automatically. Nothing merged here reaches the live site | + +Because `master` is the default, a pull request opened without changing the base lands on the branch that does not publish. That is exactly how the two drifted: documentation accumulated on both sides for months, and each ended up missing endpoints the other had. + +**Set the base branch to `main` when you open a pull request.** If you already opened one against `master`, you can retarget it from the PR page without losing the review. + +The two branches were reconciled in July 2026, porting to `main` everything that existed only on `master`. If you find documentation present on one branch and absent from the other, they have drifted again and need another pass. + +### Working on the docs + +```bash +bundle install +bundle exec middleman server # http://localhost:4567 +``` + +The build runs on Ruby 2.3 to 2.6 in CI. Newer Rubies will not install the pinned dependencies. + +--- +

Slate: API Documentation Generator
diff --git a/source/airbyte/datascope_source_manifest.yaml b/source/airbyte/datascope_source_manifest.yaml new file mode 100644 index 00000000000..757d23fd0c4 --- /dev/null +++ b/source/airbyte/datascope_source_manifest.yaml @@ -0,0 +1,735 @@ +# ============================================================================= +# DataScope - Airbyte Low-Code (Declarative) Source Connector +# ============================================================================= +# Created: 2026-07-23 +# Last updated: 2026-07-29 (base_url hardcodeado a produccion; campos +# faltantes en los schemas; subform_index +# normalizado a -1 via AddFields) +# +# Actualizar `Last updated` en cada cambio funcional del manifest (nuevos +# streams, cambios de PK/cursor, custom_fields, ajustes de extractor). +# Los cambios puramente cosméticos (comentarios, whitespace) no cuentan. +# La fecha permite a quien replica el manifest saber si tiene la versión +# actual comparándola con la última publicada por DataScope. +# ============================================================================= +# Apunta a producción (https://www.mydatascope.com). La URL es fija, no es un +# campo configurable. +# +# Cómo usarlo: +# 1. En Airbyte Cloud: Settings -> Sources -> "Build a connector". +# 2. Menú "..." -> "Import YAML" y selecciona este archivo. +# 3. En "Testing values" (o al configurar la source): ingresa `api_token` +# (el token de tu usuario de DataScope) y `start_date` en ISO 8601. +# 4. Publica el conector y crea la conexión con Sync mode = "Incremental | Dedup". +# +# Si Airbyte Cloud rechaza el import con un error genérico, probablemente sea +# la versión declarada en `version:`. Comparar con la versión que muestra el +# Connector Builder en la esquina del proyecto y ajustar (o comentar la línea +# para usar el default del deployment). +# ============================================================================= +# NO ACEPTAR EL "SCHEMA DETECTADO" QUE PROPONE EL CONNECTOR BUILDER +# ============================================================================= +# Al testear un stream, el Builder compara los schemas declarados acá con lo +# que infiere de la muestra de respuestas y muestra el aviso "Detected schema +# and declared schema are different", con dos botones: "Overwrite declared +# schema" y "Merge properties". +# +# No hay que apretar ninguno. No existe un botón de "descartar": ignorar el +# aviso ES la acción correcta. El warning queda como indicador en la pestaña +# Schema, pero no bloquea el test ni la sincronización, porque en runtime se +# usa el schema declarado en este archivo. +# +# El botón peligroso es "Overwrite declared schema". +# +# El motivo: Airbyte DESCARTA DEL REGISTRO las claves con valor null, y sobre +# ese registro ya recortado infiere el schema. Así que un campo que viene null +# en toda la muestra desaparece del schema detectado, y aceptarlo lo borra de +# la declaración. Varios campos son legítimamente null en cuentas con pocos +# datos: `assign_*` sin tareas asignadas, `form_state` sin estados, +# `subform_index` cuando ninguna respuesta está dentro de un grupo repetible. +# +# Ese mismo recorte de nulls es la razón por la que los streams `answers` y +# `answer_metadata_comments` llevan una transformación `AddFields` que +# normaliza `subform_index` a -1: como el campo forma parte de sus primary +# keys, y Airbyte marca las PK como `required`, un null lo hace desaparecer y +# el stream falla. El detalle completo está en el comentario de esa +# transformación, en el stream `answers`. +# +# Las diferencias que el Builder va a seguir mostrando son ruido de su propia +# normalización y se pueden ignorar: reescribe `$schema`, reordena +# `["null","string"]`, colapsa `integer` en `number` (no distingue int de float +# al inferir desde JSON), descarta `format: date-time`, y agrega un bloque +# `required` derivado de la PK y el cursor. +# ============================================================================= + +version: "5.10.2" + +type: DeclarativeSource + +check: + type: CheckStream + stream_names: + - form_answers + - answers + - answer_metadata_comments + +definitions: + # ---- Autenticación: Authorization: Bearer ---------------------- + authenticator: + type: BearerAuthenticator + api_token: "{{ config['api_token'] }}" + + # ---- Requester compartido -------------------------------------------------- + requester: + type: HttpRequester + # URL fija de producción. Antes esto era un campo configurable + # (`config['base_url']`), pero no había caso de uso real para apuntarlo a + # otra parte y sí un modo de falla: si el campo queda vacío al configurar + # la source, el test del stream falla con un error de URL inválida que no + # dice cuál es el campo faltante. + url_base: "https://www.mydatascope.com" + path: "/api/external/v5/answers" + http_method: GET + authenticator: + $ref: "#/definitions/authenticator" + request_parameters: + # Trae por fecha de modificación (nuevos + editados). + date_modified: "true" + # Ordena por form_answers.updated_at ASC. + # + # NO cambiar a DESC (via `sort_order: "desc"`) mientras `incremental_sync` + # esté activo. El DatetimeBasedCursor de Airbyte avanza el cursor state al + # MAX(updated_at) visto por sync; con DESC el primer record ya es el + # máximo, así que todo el resto queda "detrás del cursor" y se salta + # permanentemente en el siguiente sync. ASC es la única dirección + # compatible con incremental. + # + # Contrato de consistencia con ASC: `updated_at` solo avanza hacia + # adelante, así que una fila editada durante un sync en curso puede + # reaparecer en una página posterior con sus valores nuevos, o quedar + # para el sync siguiente si su nuevo `updated_at` supera la ventana. + # Airbyte deduplica por primary key, así que en el warehouse converge + # al último estado. No hay pérdida de datos. + order_date: "true" + version: "v5" + # Filtro opcional por form(s). Cuando el cliente deja el campo + # `form_id` vacío en la config de Airbyte, se manda como string vacía + # y el backend lo interpreta como "sin filtro" (split(',') sobre "" da + # []). Cuando el cliente pone "123" o "123,456,789", el endpoint hace + # `WHERE form_id IN (...)`. + form_id: "{{ config.get('form_id') or '' }}" + # NOTA: `limit` NO va acá: lo inyecta el paginator (page_size_option → limit=200). + # Duplicarlo causa "Request body collision, duplicate keys detected at key path: limit". + # + # ─── custom_fields activos (la lista de abajo, en orden) ────────────── + # STRUCTURAL: pilares del schema estable + PK del stream `answers`: + # answers_data_in_array → anida `answers[]` (schema estable) + # answers_extra_data → meta por answer (question_id, real_question_id, subform_index, metadata_id, question_type, name) + # answers_row_key → discriminador PK type-aware + # answers_activity_order → period index para activity_period_time + # answers_form_answer_updated_at → cursor incremental heredado del parent + # answers_activity_data → start/end/duration para activity_period_time (nombres fijos, schema-safe) + # answers_metadata_comments_array → fuente del stream `answer_metadata_comments` + # answers_latitude_longitude → geo por answer + # answers_form_identification → repite form_name y form_code dentro de cada answer del stream `answers` (evita el JOIN warehouse-side para consumers que quieren contexto de form al costado de cada fila) + # FORM-LEVEL: + # form_update_variations → updated, updated_date, updated_at_unix + # form_finished → boolean finished + # code_as_form_code → form_code (además del `code` estándar) + # form_answer_id_as_id → id == form_answer_id (PK simple del stream `form_answers`) + # TASKASSIGN CONTEXT: + # assign_base_data → assign_id, location_name, description, code + # assign_location_city → ciudad del assign + # + # ─── OPCIONALES (no van en la config baseline) ─────────────────────── + # No están activos por default porque en la mayoría de las + # configuraciones iniciales no aportan al modelo de datos que el cliente + # arma en su warehouse (agregarlos ensancha el schema sin beneficio + # inmediato). Se activan cuando el caso de uso lo pide, son campos + # reales, con consumidores reales: + # answers_selected_metadata → list_object metadata del alternative (name, description, code, attribute1/2 + location) + # task_description → descripción del TaskAssign asociado (requiere assign_base_data) + # task_gap, task_group_id, task_mandatory, + # task_late_response_allowed, task_mobile_user_id, task_start_time + # → atributos extra del TaskAssign + # assign_location_company_email, _company_code, _company_name, + # assign_location_country, _email, _latitude, _longitude, _region + # → metadata de location extendida + # + # ─── NO agregar (deliberadamente excluidos) ────────────────────────── + # answers_as_v3 → layer de retrocompat que reformatea a shape v3. + # Contradice el goal del rebuild (schema v5 puro). + # answers_comments → expone comments como columnas flat prefijadas + # (comment, comment_1, comment_type_1, …). + # Es el anti-pattern polimórfico que el + # stream `answer_metadata_comments` reemplaza. + # MUTUALLY EXCLUSIVE con + # `answers_metadata_comments_array`. + custom_fields: "answers_data_in_array,answers_extra_data,answers_form_answer_updated_at,answers_activity_order,answers_row_key,answers_activity_data,answers_metadata_comments_array,answers_latitude_longitude,answers_form_identification,form_update_variations,form_finished,code_as_form_code,form_answer_id_as_id,assign_base_data,assign_location_city" + + # ---- Paginación: keyset (seek) sobre (updated_at, form_answer_id) --------- + # Reemplaza el patrón OFFSET/LIMIT anterior. Motivos: + # 1. Correctness: si una fila se edita entre la página N y N+1 y se mueve + # dentro del sort ASC, un OFFSET N+1 podría re-emitirla (retrabajo, no + # pérdida). Keyset evita esa clase entera de escenario porque el + # "cursor" es una tupla concreta, no una posición. + # 2. Performance: OFFSET N escanea N filas antes de aplicar LIMIT, así + # que backfills largos crecen lineal en N. Keyset seek arranca en el + # punto exacto vía el índice compuesto, con costo constante por página. + # + # El backend acepta `since=|`. En la primera + # página el token no se envía (Airbyte no lo tiene aún) y el endpoint + # cae al path clásico `updated_at BETWEEN start AND end LIMIT`; a partir + # de la segunda página, el cursor extraído del último record navega + # `(updated_at, id) > (parsed_ts, parsed_id)`. `stop_condition` corta + # cuando la página devuelve menos de `page_size` filas. + # + # Dos paginators porque el campo del cursor difiere por stream: + # - `paginator_form_answers`: el record IS un form_answer → `updated_at` + # y `form_answer_id` viven en la raíz. + # - `paginator_child`: los records son answers o metadata_comments + # aplanados; el par vive como `form_answer_updated_at` y + # `form_answer_id`. + paginator_form_answers: + type: DefaultPaginator + page_size_option: + type: RequestOption + field_name: "limit" + inject_into: "request_parameter" + page_token_option: + type: RequestOption + field_name: "since" + inject_into: "request_parameter" + pagination_strategy: + type: CursorPagination + page_size: 200 + # `last_record` (singular) es el único record accesor que expone esta + # versión del low-code CDK. Usar `last_records[-1]` genera + # "Jinja macro has undeclared variables: {'last_records'}". El + # `stop_condition` usa `last_page_size` (numérico) por el mismo motivo: + # `last_records | length` referencia la variable plural que no existe. + cursor_value: "{{ last_record['updated_at'] }}|{{ last_record['form_answer_id'] }}" + stop_condition: "{{ last_page_size < 200 }}" + + paginator_child: + type: DefaultPaginator + page_size_option: + type: RequestOption + field_name: "limit" + inject_into: "request_parameter" + page_token_option: + type: RequestOption + field_name: "since" + inject_into: "request_parameter" + pagination_strategy: + type: CursorPagination + page_size: 200 + cursor_value: "{{ last_record['form_answer_updated_at'] }}|{{ last_record['form_answer_id'] }}" + stop_condition: "{{ last_page_size < 200 }}" + + # ---- Selector para el stream `answers`: extrae cada form_answer del array raíz + record_selector: + type: RecordSelector + extractor: + type: DpathExtractor + field_path: [] + + # ---- Selector para el stream `answers` (hijo): aplana el array `answers` + # anidado dentro de cada form_answer. + # + # Path shape sigue las mismas tres constraints empíricas del stream + # `answer_metadata_comments` (ver el bloque de ese stream más abajo): + # - `**` en position 0: obligatorio para atravesar el list-root response + # - `*` literal en otro nivel: triggerea el modo wildcard en Airbyte + # (equality-check `"*" in path`), evita que caiga a `dpath.get()` + # - Trailing `*`: itera el array `answers` a items individuales + record_selector_answers: + type: RecordSelector + extractor: + type: DpathExtractor + field_path: ["**", "answers", "*"] + + # ---- Retriever para `answers` (headers de form_answer) -------------------- + retriever: + type: SimpleRetriever + requester: + $ref: "#/definitions/requester" + record_selector: + $ref: "#/definitions/record_selector" + paginator: + $ref: "#/definitions/paginator_form_answers" + + # ---- Retriever para `answers` (question-value pairs aplanados) ------------ + retriever_answers: + type: SimpleRetriever + requester: + $ref: "#/definitions/requester" + record_selector: + $ref: "#/definitions/record_selector_answers" + paginator: + $ref: "#/definitions/paginator_child" + + # ---- Selector para el stream `answer_metadata_comments`: extrae los + # comentarios (text + image de multiphoto) de las preguntas tipo + # `select_option_metadata_comments`. Cada answer_item de ese tipo lleva un + # array anidado `metadata_comments[]` cuando el backend tiene activo el + # custom_field `answers_metadata_comments_array`. + # + # Path shape derivada empíricamente. Las tres constraints son obligatorias: + # - `**` en position 0: OBLIGATORIO cuando el body root es un array. + # `*` en position 0 no itera el list root (silent 0-record failure). + # - `*` literal en algún otro nivel: obligatorio para que Airbyte detecte + # wildcards y use `dpath.values()` en vez de `dpath.get()` (equality + # check `"*" in path_list`, y `**` solo no cuenta). + # - Trailing `*`: obligatorio cuando el leaf es un array container. + # Sin él dpath devuelve list-of-arrays y Airbyte tropieza intentando + # `dict.update([{N-key dict}, ...])`. + record_selector_metadata_comments: + type: RecordSelector + extractor: + type: DpathExtractor + field_path: ["**", "answers", "*", "metadata_comments", "*"] + + # ---- Retriever para `answer_metadata_comments` ---------------------------- + retriever_metadata_comments: + type: SimpleRetriever + requester: + $ref: "#/definitions/requester" + record_selector: + $ref: "#/definitions/record_selector_metadata_comments" + paginator: + $ref: "#/definitions/paginator_child" + + # ---- Incremental por updated_at (para el stream `answers`) --------------- + incremental_cursor: + type: DatetimeBasedCursor + cursor_field: "updated_at" + # Formato principal ISO 8601 UTC compacto. Los fallbacks de abajo aceptan + # variaciones con/sin milisegundos y con/sin offset explícito. + datetime_format: "%Y-%m-%dT%H:%M:%SZ" + cursor_datetime_formats: + - "%Y-%m-%dT%H:%M:%SZ" + - "%Y-%m-%dT%H:%M:%S%z" + - "%Y-%m-%dT%H:%M:%S.%fZ" + - "%Y-%m-%dT%H:%M:%S.%f%z" + start_datetime: + type: MinMaxDatetime + datetime: "{{ config['start_date'] }}" + datetime_format: "%Y-%m-%dT%H:%M:%SZ" + # El endpoint acota la ventana a 90 días; usamos pasos de 30 días. + step: "P30D" + cursor_granularity: "P1D" + # Re-consulta 1 día para no perder registros; modo Dedup en destino evita duplicados. + lookback_window: "P1D" + start_time_option: + type: RequestOption + field_name: "start" + inject_into: "request_parameter" + end_time_option: + type: RequestOption + field_name: "end" + inject_into: "request_parameter" + + # ---- Incremental por form_answer_updated_at (para el stream `answers`) + # Idéntico al cursor del padre salvo por `cursor_field`: los answer records + # individuales no tienen `updated_at` propio, pero sí `form_answer_updated_at` + # (inyectado por el backend cuando `answers_form_answer_updated_at` está en + # custom_fields). + incremental_cursor_answers: + type: DatetimeBasedCursor + cursor_field: "form_answer_updated_at" + datetime_format: "%Y-%m-%dT%H:%M:%SZ" + cursor_datetime_formats: + - "%Y-%m-%dT%H:%M:%SZ" + - "%Y-%m-%dT%H:%M:%S%z" + - "%Y-%m-%dT%H:%M:%S.%fZ" + - "%Y-%m-%dT%H:%M:%S.%f%z" + start_datetime: + type: MinMaxDatetime + datetime: "{{ config['start_date'] }}" + datetime_format: "%Y-%m-%dT%H:%M:%SZ" + step: "P30D" + cursor_granularity: "P1D" + lookback_window: "P1D" + start_time_option: + type: RequestOption + field_name: "start" + inject_into: "request_parameter" + end_time_option: + type: RequestOption + field_name: "end" + inject_into: "request_parameter" + +streams: + # --------------------------------------------------------------------------- + # Stream 1: `form_answers`: headers de cada envío del formulario. + # Cada fila es un form_answer completo. El array `answers[]` queda anidado + # para quienes prefieran usar UNNEST en BigQuery directamente, sin joinear + # contra el stream `answers` aplanado. + # --------------------------------------------------------------------------- + - type: DeclarativeStream + name: form_answers + primary_key: "form_answer_id" + retriever: + $ref: "#/definitions/retriever" + incremental_sync: + $ref: "#/definitions/incremental_cursor" + schema_loader: + type: InlineSchemaLoader + schema: + $schema: "http://json-schema.org/draft-07/schema#" + type: object + additionalProperties: true + properties: + id: + type: ["null", "integer"] + form_answer_id: + type: ["null", "integer"] + form_id: + type: ["null", "integer"] + form_name: + type: ["null", "string"] + form_state: + type: ["null", "string"] + code: + type: ["null", "string"] + form_code: + type: ["null", "string"] + user_name: + type: ["null", "string"] + user_identifier: + type: ["null", "string"] + finished: + type: ["null", "boolean"] + deleted: + type: ["null", "boolean"] + latitude: + type: ["null", "string", "number"] + longitude: + type: ["null", "string", "number"] + created_at: + type: ["null", "string"] + format: date-time + updated_at: + type: ["null", "string"] + format: date-time + # Variantes compactas de las fechas, en formato YYYYMMDDHHMMSS y + # YYYYMMDD. `created`/`created_date` vienen siempre; `updated`, + # `updated_date` y `updated_at_unix` los agrega + # `form_update_variations`. + created: + type: ["null", "string"] + created_date: + type: ["null", "string"] + updated: + type: ["null", "string"] + updated_date: + type: ["null", "string"] + updated_at_unix: + type: ["null", "integer"] + # ", " en un solo string. Solo presente cuando + # la respuesta trae coordenadas. + latlong: + type: ["null", "string"] + assign_id: + type: ["null", "string", "integer"] + assign_internal_id: + type: ["null", "integer"] + assign_location_name: + type: ["null", "string"] + assign_location_code: + type: ["null", "string"] + assign_location_city: + type: ["null", "string"] + answers: + type: ["null", "array"] + items: + type: object + additionalProperties: true + + # --------------------------------------------------------------------------- + # Stream 2: `answers`: cada question-value pair como fila individual + # Aplana el array `answers[]` de cada form_answer. `form_answer_id` es FK al + # stream `form_answers`. En BigQuery queda como una tabla tabulada y joinable + # (una fila por respuesta). + # + # KNOWN LIMITATION: orphan rows on deletion (activity periods, multi-select + # unchecked, subform row removed, etc.). El API filtra `expired=true` y no + # emite tombstone, así que la fila removida se queda en el warehouse. El + # `form_answer_updated_at` de la fila queda "congelado" en la versión previa + # a la edición → downstream filtra por generation con: + # + # SELECT * + # FROM `dataset.answers` a + # WHERE a.form_answer_updated_at = ( + # SELECT MAX(form_answer_updated_at) + # FROM `dataset.answers` + # WHERE form_answer_id = a.form_answer_id + # AND real_question_id = a.real_question_id + # AND COALESCE(subform_index, -1) = COALESCE(a.subform_index, -1) + # ) + # + # Partición por `(form_answer_id, real_question_id, subform_index)`, no + # solo por `form_answer_id`, para no filtrar filas de OTRAS preguntas del + # mismo form_answer que legítimamente tienen timestamps distintos. + # --------------------------------------------------------------------------- + - type: DeclarativeStream + name: answers + # PK compuesta type-safe: cubre single-value edits, multi-value adds, + # subforms, activity_period_time, y sub-preguntas de repeatable groups. + # - `real_question_id`: identidad estable del template question. + # Necesario para distinguir sub-preguntas de un mismo repeatable + # group: `question_id` apunta al grupo padre, `real_question_id` + # apunta a la sub-question específica. + # - `subform_index`: nº de repetición dentro de un repeatable group. + # - `answer_row_key`: discriminador polimórfico según tipo, calculado + # por el backend y poblado cuando `answers_row_key` está en + # custom_fields. + # * Single-value (text/number/date/photo/signature/`select_metadata`): + # `null`. La identidad no depende de `metadata_id`, así el edit + # de una selección single-choice sigue el mismo tuple PK. + # * Multi-value metadata (checklist/checkbox/etc.): `"m"`. + # * `select_activity_period_time`: `"m-p"`. + # * `attachment`/`multi_photo`: `"f"` (parent label + # stripped de `question_name`, dejando solo el índice). + primary_key: + - form_answer_id + - real_question_id + - subform_index + - answer_row_key + retriever: + $ref: "#/definitions/retriever_answers" + # Normaliza `subform_index` null a -1. OBLIGATORIO, no es cosmético. + # + # Airbyte descarta del registro las claves con valor null antes de inferir + # el schema y antes de validar los campos requeridos. Como marca como + # `required` a todo campo de la primary key, un `subform_index` null hace + # fallar el stream con: + # + # Path [] does not have field `subform_index` in the schema and hence + # can't be marked as required. + # + # El endpoint devuelve `"subform_index": null` correctamente (verificable + # en la pestaña Response del Connector Builder); es Airbyte el que lo + # filtra. Y el campo no se puede sacar de la PK: es lo único que + # distingue dos respuestas de filas distintas de un mismo grupo repetible. + # + # -1 significa "la pregunta no está dentro de un grupo repetible". No + # colisiona con la fila 0 real: una pregunta dada está siempre dentro de + # un grupo o siempre fuera, nunca las dos cosas, así que para un mismo + # `real_question_id` el -1 y el 0 no coexisten. + # + # Se aplica igual en el stream `answer_metadata_comments` para que el join + # entre ambos por (form_answer_id, real_question_id, subform_index, + # answer_row_key) siga siendo válido en el warehouse. + transformations: + - type: AddFields + fields: + - type: AddedFieldDefinition + path: ["subform_index"] + value: "{{ -1 if record.get('subform_index') is none else record['subform_index'] }}" + value_type: integer + incremental_sync: + $ref: "#/definitions/incremental_cursor_answers" + schema_loader: + type: InlineSchemaLoader + schema: + $schema: "http://json-schema.org/draft-07/schema#" + type: object + additionalProperties: true + properties: + form_answer_id: + type: ["null", "integer"] + form_id: + type: ["null", "integer"] + form_code: + type: ["null", "string"] + # Emitted alongside form_code inside each nested answer when + # `answers_form_identification` is active. Saves a warehouse-side + # JOIN to `form_answers` for consumers that want form context + # co-located with each answer row (dashboards, per-form + # analytics tables). + form_name: + type: ["null", "string"] + form_state: + type: ["null", "string"] + latitude: + type: ["null", "number"] + longitude: + type: ["null", "number"] + question_id: + type: ["null", "integer"] + # Stable id de la sub-pregunta en el template del form. Difiere de + # `question_id` en sub-preguntas de un `Group of Repeatable Fields` + # (donde `question_id` apunta al grupo padre y `real_question_id` a + # cada sub-pregunta individual). Load-bearing en la PK. + real_question_id: + type: ["null", "integer"] + question_name: + type: ["null", "string"] + question_type: + type: ["null", "string"] + question_value: + type: ["null", "string"] + name: + type: ["null", "string"] + metadata_id: + type: ["null", "integer"] + metadata_type: + type: ["null", "string"] + subform_index: + type: ["null", "integer"] + # Server-side period index para select_activity_period_time. Poblado + # por el backend cuando `answers_activity_order` está en custom_fields. + # Null para todos los otros question types. + activity_order: + type: ["null", "integer"] + # Discriminador polimórfico calculado por el backend (ver PK docs + # arriba). Null para single-value types, deliberadamente, así + # `select_metadata` edits mantienen PK estable aunque `metadata_id` + # cambie. + answer_row_key: + type: ["null", "string"] + # Sub-fields de select_activity_period_time. Nombres fijos, + # schema-estable. Poblado por el backend cuando `answers_activity_data` + # está en custom_fields. Null para todos los otros question types. + start_time: + type: ["null", "string"] + end_time: + type: ["null", "string"] + duration: + type: ["null", "string"] + day_start: + type: ["null", "string"] + full_duration: + type: ["null", "string"] + # Cursor field para incremental, inyectado por el backend cuando + # `answers_form_answer_updated_at` está activo en custom_fields. + # Hereda el `updated_at` del form_answer padre. + form_answer_updated_at: + type: ["null", "string"] + format: date-time + # Array anidado presente solo en answers de tipo + # `select_option_metadata_comments`. Es la fuente del stream + # `answer_metadata_comments`, que lo extrae como filas propias. Se + # declara acá también porque el campo igual viaja en este stream, y + # si no está declarado Airbyte lo reporta como diferencia de schema. + metadata_comments: + type: ["null", "array"] + items: + type: ["null", "object"] + additionalProperties: true + + # --------------------------------------------------------------------------- + # Stream 3: `answer_metadata_comments`: comentarios (texto + fotos) de las + # respuestas de tipo `select_option_metadata_comments`. Es la tercera capa + # del modelo: form_answer → answer → metadata_comment. + # + # Cada fila = un metadata comment (texto O una foto de multiphoto). En el + # warehouse se joinea al stream `answers` con: + # ON (form_answer_id, real_question_id, subform_index, answer_row_key) + # + # Se agrega separado del stream `answers` para evitar columnas polimórficas + # (comment, comment_1, comment_type_1, ...) que romperían la estabilidad + # del schema del stream padre. + # --------------------------------------------------------------------------- + - type: DeclarativeStream + name: answer_metadata_comments + # PK compuesta: FK al parent answer + discriminadores propios (data_type + # + data_index) para distinguir múltiples comentarios en la misma answer. + primary_key: + - form_answer_id + - real_question_id + - subform_index + - answer_row_key + - data_type + - data_index + retriever: + $ref: "#/definitions/retriever_metadata_comments" + # Misma normalización que en el stream `answers`, y por el mismo motivo + # (ver el comentario extenso allá). Tiene que estar en los dos streams + # para que el join por (form_answer_id, real_question_id, subform_index, + # answer_row_key) compare -1 contra -1 y no -1 contra null. + transformations: + - type: AddFields + fields: + - type: AddedFieldDefinition + path: ["subform_index"] + value: "{{ -1 if record.get('subform_index') is none else record['subform_index'] }}" + value_type: integer + incremental_sync: + $ref: "#/definitions/incremental_cursor_answers" + schema_loader: + type: InlineSchemaLoader + schema: + $schema: "http://json-schema.org/draft-07/schema#" + type: object + additionalProperties: true + properties: + # FK subset: matchea la PK del stream `answers`. El cliente joinea + # por estos cuatro campos para reconstituir la relación padre/hijo. + form_answer_id: + type: ["null", "integer"] + real_question_id: + type: ["null", "integer"] + subform_index: + type: ["null", "integer"] + answer_row_key: + type: ["null", "string"] + # Contexto informativo del parent (no PK, solo conveniencia). + metadata_id: + type: ["null", "integer"] + # Identidad propia del comment. + data_type: + type: ["null", "string"] + data_index: + type: ["null", "integer"] + value: + type: ["null", "string"] + # Cursor field: hereda del form_answer padre. Mismo mecanismo que + # `form_answer_updated_at` en el stream `answers`. + form_answer_updated_at: + type: ["null", "string"] + format: date-time + +# ============================================================================= +# Spec: parámetros que pide Airbyte al configurar la fuente +# ============================================================================= +spec: + type: Spec + connection_specification: + $schema: "http://json-schema.org/draft-07/schema#" + type: object + required: + - api_token + - start_date + additionalProperties: true + properties: + api_token: + type: string + title: API Token + description: >- + Token de la API de DataScope (User.token). Se envía como + "Authorization: Bearer ". + airbyte_secret: true + order: 0 + start_date: + type: string + title: Start date + description: Fecha desde la cual sincronizar (por updated_at). Formato ISO 8601 UTC. + format: date-time + examples: + - "2024-01-01T00:00:00Z" + order: 1 + form_id: + type: string + title: Form ID(s) + description: >- + Opcional. Filtra la sincronización a uno o más formularios + específicos. Ingresa un ID (ej. "123") o una lista separada por + comas (ej. "123,456,789"). Dejar vacío para sincronizar todos los + formularios de la cuenta. + examples: + - "123" + - "123,456,789" + order: 2 diff --git a/source/index.html.md b/source/index.html.md index e4f3ecd7b07..ac6c715f495 100644 --- a/source/index.html.md +++ b/source/index.html.md @@ -20,10 +20,9 @@ Welcome to the DataScope API! You can use our API to access DataScope API endpoi We have language bindings in Shell and Ruby! You can view code examples in the dark area to the right, and you can switch the programming language of the examples with the tabs in the top right. -Last Update Date: 5-Nov-2025 -Last Updates: -- Added Ticket's Endpoints (5-Nov-2025) -- Updated old https://mydatascope.com/webhooks to https://app.mydatascope.com/integrations (5-Nov-2025) +See the [Changelog](#changelog) for what changed recently. + + # Authentication @@ -228,6 +227,222 @@ answers | Array | array with all the question of the specific form answer. assign_location_description | String | Description of the location assigned assign_location_code" | String | Code of the location assigned +## Get All Answers V5 (Beta) + +```ruby +require 'rest-client' +require 'json' + +url = 'https://www.mydatascope.com/api/external/v5/answers' +response = RestClient.get url, { +:Authorization => 'b1cd93mfls9fdmfkadn23', + :params => { + :date_modified => true, + :order_date => true, + :custom_fields => 'answers_data_in_array,answers_extra_data' + } +} +JSON.parse(response) +``` + +```shell +curl "https://www.mydatascope.com/api/external/v5/answers?date_modified=true&order_date=true&custom_fields=answers_data_in_array,answers_extra_data" + -H "Authorization: b1cd93mfls9fdmfkadn23" +``` + +> With `answers_data_in_array` the questions are nested in an `answers` array instead of becoming top-level keys, so the response shape stays the same across forms: + +```json +[ + { + "form_answer_id":257189, + "form_id":432, + "form_name":"Example Form", + "form_code":"2342", + "form_state":"Accepted", + "user_name":"Example User", + "user_identifier":"user@email.com", + "created_at":"2026-07-16T16:52:05.000Z", + "updated_at":"2026-07-18T09:14:22.000Z", + "latitude":-33.398803, + "longitude":-70.559834, + "answers":[ + { + "question_value":"Example value", + "question_name":"Example question", + "question_type":"text", + "question_id":1180, + "real_question_id":1180, + "subform_index":null, + "metadata_type":null, + "metadata_id":null + }, + { + "question_value":"Alternative 1", + "question_name":"Example checklist", + "question_type":"select_check_metadata", + "question_id":1181, + "real_question_id":1181, + "subform_index":null, + "metadata_type":"list_objects", + "metadata_id":90412 + } + ] + } +] +``` + +Answers V5 is a single endpoint that can reproduce the response shape of every previous version (v1, v2, v3, v4) and adds opt-in fields on top. Instead of a new endpoint per feature, you pick the version you want as a baseline through `version` and switch extra data on through `custom_fields`. + +The most requested use case is the `answers_data_in_array` shape shown above. In v1 to v4 each question becomes its own top-level key, so every form produces a different set of columns and a relational destination (BigQuery, Snowflake, Postgres) cannot model it cleanly. With `answers_data_in_array` the questions move into a nested `answers` array and the response schema stops changing per form. + +

+ + + +### HTTP Request + +`GET https://www.mydatascope.com/api/external/v5/answers` + +### Query Parameters + +Parameter | Type | Default | Description +--------- | ---- | ------- | ----------- +version | String | v5 | Reproduce the response shape of a previous version. One of `v1`, `v2`, `v3`, `v4`, `v5`. +custom_fields | String | blank | Comma separated list of extra data to include. See the table below. +form_id | Integer or String | blank | One form ID, or several comma separated. This ID is in the URL when you edit a form, eg. https://app.mydatascope.com/task_forms/XXXX/edit +user_id | Integer | blank | If set, only answers from that user +location_id | Integer | blank | If set, only answers from that location +days_limit | Integer | 7 | Days between start and end when you do not send them (max range 90 days) +start | String | last `days_limit` days | Start of the date range, ISO 8601 (eg. `2026-07-01T00:00:00Z`) +end | String | date of the last answer | End of the date range, ISO 8601. Max range 90 days +date_modified | Boolean | false | Filter by modification date (`updated_at`) instead of creation date. Use it to pull edits, not only new submissions +order_date | Boolean | false | Sort by `updated_at` instead of the default question order. Required whenever you page through modified answers +sort_order | String | asc | Direction for `order_date`. `asc` or `desc`. Keep `asc` for any incremental integration +limit | Integer | 200 | Records per page. Default and max are 200 +page | Integer | 1 | Page number. With a limit of 200, page 2 returns the next 200 records +offset | Integer | 0 | Shifts the start of the pagination +since | String | blank | Keyset pagination cursor, built from the `updated_at` and `form_answer_id` of the last record you read. More efficient than `page` or `offset` on large exports. See "Pagination with since" below + + + +### Pagination with since + +`page` and `offset` work as in previous versions. For large or recurring exports prefer `since`, which pages from the last record you already read instead of counting rows from the beginning: + +1. Request the first page normally, with `date_modified=true` and `order_date=true`. +2. Take `updated_at` and `form_answer_id` from the last record of the response. +3. Send them back as `since=|` to get the next page. +4. Repeat until a page returns fewer records than `limit`. + +A malformed cursor returns `400` with `{"error": "invalid_since_cursor"}`. When `since` is present the response is always sorted ascending, regardless of `sort_order`. + +### Custom Fields + +`custom_fields` unlocks extra data. Send the names comma separated, for example `custom_fields=answers_data_in_array,answers_extra_data,answers_selected_metadata`. Unknown names are ignored silently. + +Custom Field | Description +------------ | ----------- +answers_data_in_array | Nest the questions in an `answers` array instead of top-level keys. This is what keeps the schema stable across forms (already in v1) +answers_extra_data | Per answer metadata: `question_id`, `real_question_id`, `subform_index`, `question_type`, `metadata_type`, `metadata_id`, `name` +answers_latitude_longitude | Latitude and longitude of each answer (already in v2 and v4) +answers_selected_metadata | Name, description, code and attributes of the list object or location selected in an answer +answers_activity_data | `start_time`, `end_time`, `duration`, `day_start` and `full_duration` for Activity List answers +answers_activity_order | Period index for Activity List answers. Distinguishes several periods that selected the same alternative +answers_row_key | Stable per row discriminator. Combined with `form_answer_id`, `real_question_id` and `subform_index` it gives each answer row a primary key that survives edits +answers_form_answer_updated_at | `updated_at` of the parent form answer repeated inside each answer. Useful as an incremental cursor when you consume the answers as their own table +answers_metadata_comments_array | Checklist comments as a nested `metadata_comments` array on each answer, instead of flat prefixed columns +answers_form_identification | `form_name` and `form_code` repeated inside each answer, so you do not need to join back to the form answer +answers_comments | Checklist comments as flat prefixed columns (`comment`, `comment_1`, ...). Already in versions above 2. Do not combine with `answers_metadata_comments_array` +answers_as_v3 | Reproduce the special v3 answer format +code_as_form_code | Include the answer code as `form_code` (already in versions above 2) +form_answer_id_as_id | Include the form answer ID as `id` (already in v3) +form_finished | Include whether the form synchronized completely (already in v3) +form_update_variations | Include `updated_date` and `updated_at_unix` +assign_base_data | Basic data of the assigned Task: internal ID, custom ID, name, description, code. Required by the `task_*` and `assign_location_*` fields below +task_description | Description of the assigned Task +task_mandatory | Whether the assigned Task is mandatory +task_late_response_allowed | Whether the assigned Task accepts answers after the deadline +task_group_id | Group ID of the assigned Task +task_mobile_user_id | User ID of the assigned Task +task_start_time | Start datetime of the assigned Task +task_gap | Hour window to complete the assigned Task +assign_location_city | City of the location of the assigned Task (already in v3) +assign_location_region | Region of the location of the assigned Task +assign_location_country | Country of the location of the assigned Task +assign_location_email | Email of the location of the assigned Task +assign_location_latitude | Latitude of the location of the assigned Task +assign_location_longitude | Longitude of the location of the assigned Task +assign_location_company_email | Company email of the location. Only for Locations +assign_location_company_name | Company name of the location. Only for Locations +assign_location_company_code | Company code of the location. Only for Locations + +### Airbyte Cloud connector + +If your destination is a data warehouse, you do not need to write the pagination and incremental logic yourself. DataScope publishes a ready to use low-code connector manifest for Airbyte: + +[Download the manifest (YAML)](https://raw.githubusercontent.com/DScope/docs/main/source/airbyte/datascope_source_manifest.yaml) + +It defines three related streams that you can join in your warehouse: + +Stream | One row per | Primary key +------ | ----------- | ----------- +form_answers | Form answer (one submission) | `form_answer_id` +answers | Answer (one question and its value) | `form_answer_id`, `real_question_id`, `subform_index`, `answer_row_key` +answer_metadata_comments | Checklist comment (text or photo) | the four above plus `data_type` and `data_index` + + + +To install it: in Airbyte Cloud go to Settings, Sources, "Build a connector", then use the "..." menu and "Import YAML". Configure your API token and a start date, optionally restrict it to specific forms with `form_id`, publish the connector and create the connection with Sync mode "Incremental | Dedup". + + + + + +### Customizing the manifest + +The manifest is a starting point, not a black box. It is plain YAML and you can edit it in the Connector Builder before publishing. Some parts are cosmetic, but others are load-bearing: the primary keys, the cursor fields, the extractor paths and a handful of `custom_fields` are what keep the incremental sync and the deduplication correct. Changing those without knowing what they do tends to produce silent problems rather than errors, usually duplicated rows, missing rows, or a stream that returns nothing at all. + +**Safe to change** + +What | Notes +---- | ----- +Stream names | `form_answers`, `answers` and `answer_metadata_comments` become table names in your destination. Rename them to match your own conventions +`page_size` | 200 is the endpoint maximum, so you can only lower it. Lower values mean more requests for the same data +Optional `custom_fields` | Add any field from the Custom Fields table above. When you add one, add it to that stream's schema too, so the destination types the column instead of guessing +`form_id` | A configuration field, meant to be set per source + +**Change with care** + +What | What breaks +---- | ----------- +`order_date`, or the sort direction | Incremental sync depends on ascending `updated_at`. Descending puts the newest record on the first page, the cursor jumps to it, and everything behind it is never synced again +`primary_key` on any stream | These tuples are what make deduplication correct across edits. Shortening one collapses rows that are actually distinct; adding a mutable field creates a new row on every edit +`cursor_field`, or the `incremental_sync` blocks | The child streams inherit the parent form answer's `updated_at`. Pointing them somewhere else stalls the sync state +Extractor `field_path` | The wildcard shape is exact. A wrong path returns zero records with no error, which looks like an empty account +The structural `custom_fields` | `answers_data_in_array`, `answers_extra_data`, `answers_row_key`, `answers_form_answer_updated_at` and `answers_metadata_comments_array` feed the primary keys and the cursors. Removing one breaks whatever depended on it +Adding `limit` to `request_parameters` | The paginator already injects it, and duplicating it fails the sync with a request collision + + + + + ## Change Answer ```ruby @@ -347,7 +562,7 @@ company_code | String | Code of the company company_name | Date | Name of the Company ## Create a Location @@ -358,6 +573,7 @@ require 'json' url = 'https://www.mydatascope.com/api/external/locations' response = RestClient.post url, { + location: { name: "Test Location", description: "This is a test location created by API", code: "LOC_TEST01", @@ -370,6 +586,7 @@ response = RestClient.post url, { longitude: 151.285829, phone: "+18888888", email: "location@test.com" + } }.to_json, { :Authorization => 'b1cd93mfls9fdmfkadn23', :params => {} @@ -382,18 +599,20 @@ curl "https://www.mydatascope.com/api/external/locations" -H "Authorization: b1cd93mfls9fdmfkadn23" -X POST -d '{ - "name": "Test Location", - "description": "This is a test location created by API", - "code": "LOC_TEST01", - "company_code": "DSCODE_1", - "company_name": "Datascope Home", - "address": "P. Sherman, 42 Wallaby Way", - "city": "Sydney", - "country": "Australia", - "latitude": -33.673992, - "longitude": 151.285829, - "phone": "+18888888", - "email": "location@test.com" + "location": { + "name": "Test Location", + "description": "This is a test location created by API", + "code": "LOC_TEST01", + "company_code": "DSCODE_1", + "company_name": "Datascope Home", + "address": "P. Sherman, 42 Wallaby Way", + "city": "Sydney", + "country": "Australia", + "latitude": -33.673992, + "longitude": 151.285829, + "phone": "+18888888", + "email": "location@test.com" + } }' ``` @@ -443,18 +662,18 @@ email | String | Email of the Company ### Return Codes: -``` -201: Successfull -403: Forbidden -422: Wrong parameters, check documentation -``` +Code | Description +---- | ----------- +201 | Successfull +403 | Forbidden +422 | Wrong parameters, check documentation -## Create a Location +## Update a Location ```ruby require 'rest-client' @@ -462,18 +681,20 @@ require 'json' url = 'https://www.mydatascope.com/api/external/locations/123456' response = RestClient.post url, { - name: "Test Location", - description: "This is a test location created by API", - code: "LOC_TEST01", - company_code: "DSCODE_1", - company_name: "Datascope Home", - address: "P. Sherman, 42 Wallaby Way", - city: "Sydney", - country: "Australia", - latitude: -33.673992, - longitude: 151.285829, - phone: "+18888888", - email: "location@test.com" + location: { + name: "Test Location", + description: "This is a test location created by API", + code: "LOC_TEST01", + company_code: "DSCODE_1", + company_name: "Datascope Home", + address: "P. Sherman, 42 Wallaby Way", + city: "Sydney", + country: "Australia", + latitude: -33.673992, + longitude: 151.285829, + phone: "+18888888", + email: "location@test.com" + } }.to_json, { :Authorization => 'b1cd93mfls9fdmfkadn23', :params => {} @@ -486,18 +707,20 @@ curl "https://www.mydatascope.com/api/external/locations/123456" -H "Authorization: b1cd93mfls9fdmfkadn23" -X POST -d '{ - "name": "Test Location", - "description": "This is a test location created by API", - "code": "LOC_TEST01", - "company_code": "DSCODE_1", - "company_name": "Datascope Home", - "address": "P. Sherman, 42 Wallaby Way", - "city": "Sydney", - "country": "Australia", - "latitude": -33.673992, - "longitude": 151.285829, - "phone": "+18888888", - "email": "location@test.com" + "location": { + "name": "Test Location", + "description": "This is a test location created by API", + "code": "LOC_TEST01", + "company_code": "DSCODE_1", + "company_name": "Datascope Home", + "address": "P. Sherman, 42 Wallaby Way", + "city": "Sydney", + "country": "Australia", + "latitude": -33.673992, + "longitude": 151.285829, + "phone": "+18888888", + "email": "location@test.com" + } }' ``` @@ -522,7 +745,7 @@ curl "https://www.mydatascope.com/api/external/locations/123456" ``` -This endpoint create a location +This endpoint updates a location ### HTTP Request @@ -547,15 +770,14 @@ email | String | Email of the Company ### Return Codes: -``` -200: Successfull -403: Forbidden -404: Not found -422: Wrong parameters, check documentation -``` +Code | Description +---- | ----------- +201 | Successfull +403 | Forbidden +422 | Wrong parameters, check documentation @@ -699,169 +921,866 @@ attribute2 | String | Custom attribute of the element of the list created_at | Datetime | Date when the list element was created updated_at | Datetime | Date when the list element was updated -# Task Assigns -## Create Task Assign +## Create a List Element ```ruby require 'rest-client' require 'json' -url = 'https://www.mydatascope.com/api/external/assign_task' +url = 'https://www.mydatascope.com/api/external/metadata_object?metadata_type=LIST_TEST2' response = RestClient.post url, { -:Authorization => 'b1cd93mfls9fdmfkadn23', + list_object: { + name: "Test List Object 2", + description: "This is a test Object created by API", + code: "LIST_OBJECT_TEST2", + attribute1: "ATTR1", + attribute2: "ATTR2" + } +}.to_json, { + :Authorization => 'b1cd93mfls9fdmfkadn23', :params => {} } JSON.parse(response) ``` ```shell -curl "https://www.mydatascope.com/api/external/assign_task" +curl "https://www.mydatascope.com/api/external/metadata_object?metadata_type=LIST_TEST2" -H "Authorization: b1cd93mfls9fdmfkadn23" + -X POST + -d '{ + "list_object": { + "name": "Test List Object 2", + "description": "This is a test Object created by API", + "code": "LIST_OBJECT_TEST2", + "attribute1": "ATTR1", + "attribute2": "ATTR2" + } + }' ``` -> The above command returns JSON structured like this, you can check the description of each parameter below: +> When successfull the above command returns JSON structured like this, you can check the description of each parameter below: ```json -[ - { - "form_id":432432, - "user_id": "user1@email.com", - "date":"2021-05-10 15:00", - "l_code":"l25", - "task_instruction":"", - "gap":"5" - } -] +{ + "id": 123456, + "name": "Test List Object 2", + "description": "This is a test Object created by API", + "code": "LIST_OBJECT_TEST2", + "created_at": "2024-09-05T06:31:59.000-03:00", + "updated_at": "2024-09-05T06:31:59.000-03:00", + "metadata_type": "LIST_TEST2" +} ``` +This endpoint creates a list element ### HTTP Request -`POST https://www.mydatascope.com/api/external/assign_task` +`POST https://www.mydatascope.com/api/external/metadata_object` -### Query Parameters +### Query params: +Parameter | Type | Description +--------- | ------- | ----------- +metadata_type | blank | Internal code to identify the list (products, and more*) + +*For locations objects use locations API + +### Input Parameter Parameter | Type | Description --------- | ------- | ----------- -form_id | Integer | Internal identifier of the Form. ID in the URL https://www.mydatascope.com/task_forms/[ID]/edit -user_id | String | User Email -date | Datetime | Date of the assigned Task (YYY-mm-dd HH:MM) -l_code | String | Code of the Location -location_name | String | Name of the location. Only needed if need to create or update -location_address | String | Address of the Location -l_phone | String | Phone of the Location -l_email | String | Email of the Location -c_name | String | Company Name of the location -c_code | String | Company tax id of the location -latitude | String | Latitude of the location -longitude | String | Longitude of the location -task_instruction | String | Instruction of the task -gap | Integer | Hours to perform task -code | String | Code to identify the task +name | String | Name of the element of the list +description | String | Description of the element of the list +code | String | Internal code of the element of the list +attribute1 | String | Custom attribute of the element of the list +attribute2 | String | Custom attribute of the element of the list + +### Return Codes: + +Code | Description +---- | ----------- +201 | Successfull +403 | Forbidden +422 | Wrong parameters, check documentation -# Notifications -## List Last notifications +## Update a List Element ```ruby require 'rest-client' require 'json' -url = 'https://www.mydatascope.com/api/external/notifications' -response = RestClient.get url, { -:Authorization => 'b1cd93mfls9fdmfkadn23', - :params => { start: '10/12/2019', end: '30/12/2019'} +url = 'https://www.mydatascope.com/api/external/metadata_object/123456' +response = RestClient.post url, { + list_object: { + name: "Test List Object 2", + description: "This is a test Object created by API", + code: "LIST_OBJECT_TEST2", + attribute1: "ATTR1", + attribute2: "ATTR2" + } +}.to_json, { + :Authorization => 'b1cd93mfls9fdmfkadn23', + :params => {} } JSON.parse(response) ``` ```shell -curl "https://www.mydatascope.com/api/external/notifications" +curl "https://www.mydatascope.com/api/external/metadata_object/123456" -H "Authorization: b1cd93mfls9fdmfkadn23" + -X POST + -d '{ + "list_object": { + "name": "Test List Object 2", + "description": "This is a test Object created by API", + "code": "LIST_OBJECT_TEST2", + "attribute1": "ATTR1", + "attribute2": "ATTR2" + } + }' ``` -> The above command returns JSON structured like this, you can check the description of each parameter below: +> When successfull the above command returns JSON structured like this, you can check the description of each parameter below: ```json -[ - { - "id":2345, - "type":"PDF", - "url":"https://www.mydatascope.com/pdf_url_example", - "form_name":"Form Name", - "form_code":"25", - "user":"user@email.com", - "created_at":"2019-12-03T17:48:47.000-02:00" - }] - +{ + "id": 123456, + "name": "Test List Object 2", + "description": "This is a test Object created by API", + "code": "LIST_OBJECT_TEST2", + "created_at": "2024-09-05T06:31:59.000-03:00", + "updated_at": "2024-09-05T06:31:59.000-03:00", + "metadata_type": "LIST_TEST2" +} ``` -This endpoint retrieves all list items of a specific list +This endpoint updates a list object ### HTTP Request -`GET https://www.mydatascope.com/api/external/notifications` +`POST https://www.mydatascope.com/api/external/metadata_object/{id}` -### Query Parameters +### Input Parameter -Parameter | Default | Description +Parameter | Type | Description --------- | ------- | ----------- -start | last 7 days | set the start date range -end | today | set the end date range +name | String | Name of the element of the list +description | String | Description of the element of the list +code | String | Internal code of the element of the list +attribute1 | String | Custom attribute of the element of the list +attribute2 | String | Custom attribute of the element of the list -### Output Parameter +### Return Codes: -Parameter | Type | Description ---------- | ------- | ----------- -id | String | Identifier of the notification -type | String | Type of notification: PDF or Excel -url | String | URL of the notified file -form_name | String | Name of the form -form_code | String | Code of the form -user | String | Name of the user -created_at | Datetime | Date when the list element was created -updated_at | Datetime | Date when the list element was updated +Code | Description +---- | ----------- +201 | Successfull +403 | Forbidden +422 | Wrong parameters, check documentation -# Files + -## List Last generated files + +## Create a empty List ```ruby require 'rest-client' require 'json' -url = 'https://www.mydatascope.com/api/external/files' -response = RestClient.get url, { -:Authorization => 'b1cd93mfls9fdmfkadn23', - :params => { start: '10/12/2019', end: '30/12/2019'} +url = 'https://www.mydatascope.com/api/external/metadata_types' +response = RestClient.post url, { + list: { + name: "Test List", + description: "This is a test location created by API", + code: "LIST_TEST", + list_type: "standard" + } +}.to_json, { + :Authorization => 'b1cd93mfls9fdmfkadn23', + :params => {} } JSON.parse(response) ``` ```shell -curl "https://www.mydatascope.com/api/external/files" +curl "https://www.mydatascope.com/api/external/metadata_types" -H "Authorization: b1cd93mfls9fdmfkadn23" + -X POST + -d '{ + "list": { + "name": "Test List", + "description": "This is a test location created by API", + "code": "LIST_TEST", + "list_type": "standard" + } + }' ``` -> The above command returns JSON structured like this, you can check the description of each parameter below: +> When successfull the above command returns JSON structured like this, you can check the description of each parameter below: ```json -[ - { - "id":2345, - "type":"PDF", - "url":"https://www.mydatascope.com/pdf_url_example", - "form_name":"Form Name", - "form_code":"25", - "user":"user@email.com" - }] +{ + "id": 123456, + "name": "Test List", + "description": "This is a test location created by API", + "code": "LIST_TEST", + "list_type": "standard" +} + +``` + +This endpoint creates a empty list + +### HTTP Request + +`POST https://www.mydatascope.com/api/external/metadata_types` + +### Input Parameter + +Parameter | Type | Description +--------- | ------- | ----------- +name | String | Name of the element of the list +description | String | Description of the element of the list +code | String | Internal code of the element of the list +list_type | String | Valid values: ("standard", "percent", "price") + +### Return Codes: + +Code | Description +---- | ----------- +201 | Successfull +403 | Forbidden +422 | Wrong parameters, check documentation + + + + +## Update a List + +```ruby +require 'rest-client' +require 'json' + +url = 'https://www.mydatascope.com/api/external/metadata_types/123456' +response = RestClient.post url, { + list: { + name: "Test List", + description: "This is a test location created by API", + code: "LIST_TEST", + list_type: "standard" + } +}.to_json, { + :Authorization => 'b1cd93mfls9fdmfkadn23', + :params => {} +} +JSON.parse(response) +``` + +```shell +curl "https://www.mydatascope.com/api/external/metadata_types/123456" + -H "Authorization: b1cd93mfls9fdmfkadn23" + -X POST + -d '{ + "list": { + "name": "Test List", + "description": "This is a test location created by API", + "code": "LIST_TEST", + "list_type": "standard" + } + }' +``` + +> When successfull the above command returns JSON structured like this, you can check the description of each parameter below: + +```json +{ + "id": 123456, + "name": "Test List", + "description": "This is a test location created by API", + "code": "LIST_TEST", + "list_type": "standard" +} + +``` + +This endpoint updates a list + +### HTTP Request + +`POST https://www.mydatascope.com/api/external/metadata_types/{id}` + +### Input Parameter + +Parameter | Type | Description +--------- | ------- | ----------- +name | String | Name of the element of the list +description | String | Description of the element of the list +code | String | Internal code of the element of the list +list_type | String | Valid values: ("standard", "percent", "price") + +### Return Codes: + +Code | Description +---- | ----------- +201 | Successfull +403 | Forbidden +422 | Wrong parameters, check documentation + + + + +## Bulk Update List Elements + +This endpoint allows bulk updating of metadata list objects, with soft deletion of objects not in the incoming list. If an object with an existing code is provided, it will be updated. If a new code is used, the object will be created. + +> **Warning**: This operation will delete all existing objects for the specified metadata_type and replace them with the new objects provided. This endpoint is currently in an **experimental stage**. Changes may be made to functionality or structure as we continue testing and refining its implementation. + +```ruby +require 'rest-client' +require 'json' + +url = 'https://www.mydatascope.com/api/external/metadata_objects/bulk_update' +response = RestClient.post url, { + metadata_type: "your_list_code", + name: "Safety Equipment List", + list_objects: [ + { + code: "PPE001", + name: "Helmet", + description: "Safety helmet in good condition", + attribute1: "Mandatory", + attribute2: "Daily check" + }, + { + code: "PPE002", + name: "Boots", + description: "Steel-toed safety boots", + attribute1: "Mandatory", + attribute2: "Check for wear" + }, + { + code: "PPE003", + name: "Glasses", + description: "Safety glasses with side shields", + attribute1: "Mandatory", + attribute2: "Clean daily" + } + ] +}.to_json, { + :Authorization => '', + :params => {} +} +JSON.parse(response) +``` + +```shell +curl "https://www.mydatascope.com/api/external/metadata_objects/bulk_update" + -H "Authorization: " + -X POST + -d '{ + "metadata_type": "your_list_code", + "name": "Safety Equipment List", + "list_objects": [ + { + "code": "PPE001", + "name": "Helmet", + "description": "Safety helmet in good condition", + "attribute1": "Mandatory", + "attribute2": "Daily check" + }, + { + "code": "PPE002", + "name": "Boots", + "description": "Steel-toed safety boots", + "attribute1": "Mandatory", + "attribute2": "Check for wear" + }, + { + "code": "PPE003", + "name": "Glasses", + "description": "Safety glasses with side shields", + "attribute1": "Mandatory", + "attribute2": "Clean daily" + } + ] + }' +``` + +> When successful, the above command returns JSON structured like this: + +```json +{ + "id": 1, + "name": "Safety Equipment List", + "description": "List for safety equipment", + "code": "your_list_code", + "list_type": "standard", + "length": 3 +} +``` + +### HTTP Request + +`POST https://www.mydatascope.com/api/external/metadata_objects/bulk_update` + +### Input Parameters + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| metadata_type | String | Internal code to identify the list (must not be "locations") | +| name | String | Name of the list to be created or updated | +| list_objects | Array | Array of objects to be created or updated | + +### List Object Structure + +Each object in the `list_objects` array should have the following structure: + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| code | String | Internal code of the list element | +| name | String | Name of the list element | +| description | String | Description of the list element | +| attribute1 | String | Custom attribute of the list element | +| attribute2 | String | Custom attribute of the list element | + +### Return Codes + +| Code | Description | +| ---- | ----------- | +| 200 | Successful | +| 400 | Bad Request if metadata_type is `locations` | +| 403 | Forbidden | +| 422 | Wrong parameters, check documentation | +| 500 | Internal Server Error | + +### Response + +The response includes: + +| Field | Type | Description | +| ----- | ---- | ----------- | +| id | Integer | ID of the updated list | +| name | String | Name of the updated list | +| description | String | Description of the updated list | +| code | String | Code for the updated list | +| list_type | String | Type of the list | +| length | Integer | Number of active list objects | + + + +# Task Assigns + +## Create Task Assign + +```ruby +require 'rest-client' +require 'json' + +url = 'https://www.mydatascope.com/api/external/assign_task' +response = RestClient.post url, { +:Authorization => 'b1cd93mfls9fdmfkadn23', + :params => {} +} +JSON.parse(response) +``` + +```shell +curl "https://www.mydatascope.com/api/external/assign_task" + -H "Authorization: b1cd93mfls9fdmfkadn23" +``` + +> The above command returns JSON structured like this, you can check the description of each parameter below: + +```json +[ + { + "form_id":432432, + "user_id": "user1@email.com", + "date":"2021-05-10 15:00", + "l_code":"l25", + "task_instruction":"", + "gap":"5" + } +] + +``` + + +### HTTP Request + +`POST https://www.mydatascope.com/api/external/assign_task` + +### Query Parameters + +Parameter | Type | Description +--------- | ------- | ----------- +form_id | Integer | Internal identifier of the Form. ID in the URL https://www.mydatascope.com/task_forms/[ID]/edit +user_id | String | User Email +date | Datetime | Date of the assigned Task (YYY-mm-dd HH:MM) +l_code | String | Code of the Location +location_name | String | Name of the location. Only needed if need to create or update +location_address | String | Address of the Location +l_phone | String | Phone of the Location +l_email | String | Email of the Location +c_name | String | Company Name of the location +c_code | String | Company tax id of the location +latitude | String | Latitude of the location +longitude | String | Longitude of the location +task_instruction | String | Instruction of the task +gap | Integer | Hours to perform task +code | String | Code to identify the task + + + +## Get Task Assign by ID + +```shell +curl "https://www.mydatascope.com/api/external/task_assigns/4821" \ + -H "Authorization: b1cd93mfls9fdmfkadn23" +``` + +```ruby +require 'rest-client' +require 'json' + +url = 'https://www.mydatascope.com/api/external/task_assigns/4821' +response = RestClient.get url, { + :Authorization => 'b1cd93mfls9fdmfkadn23' +} +JSON.parse(response) +``` + +> The above command returns JSON structured like this, you can check the description of each parameter below: + +```json +{ + "id": 4821, + "assign_id": "TA-2024-001", + "response_code": "FA-0099", + "priority": 1, + "start_time": "2025-03-10 09:00:00", + "form_name": "Safety Inspection", + "user_email": "inspector@company.com", + "description": "Monthly fire extinguisher check", + "location_name": "Main Warehouse", + "location_code": "WH-001", + "location_type": "Location", + "location_address": "123 Main Ave", + "location_email": "warehouse@company.com", + "location_phone": "+1-555-0100", + "gap": 2, + "checklist": "Check extinguisher,Verify seal,Sign log", + "location_latitude": -33.4489, + "location_longitude": -70.6693, + "completed": "Yes", + "on_time": "Yes", + "delay_time": "0d/00h/00m", + "completed_datetime": "2025-03-10 10:45:00", + "late_response_allowed": false, + "mandatory": "for_everyone", + "confirmation_status": "completed", + "status": "completed", + "time_to_perform_minutes": 105.5, + "response_start": "2025-03-10 09:05:00", + "response_end": "2025-03-10 10:45:00", + "created_at": "2025-03-01 08:00:00", + "created_by": "Admin User" +} +``` + +This endpoint retrieves the full detail of a single task assignment by its internal ID. Returns the same fields as the list endpoint. Returns 404 if the assignment does not belong to the authenticated account. + +### HTTP Request + +`GET https://www.mydatascope.com/api/external/task_assigns/:id` + +### Path Parameters + +Parameter | Type | Description +--------- | ------- | ----------- +id | Integer | Internal database ID of the task assignment + +### Response Fields + +Field | Type | Description +--------- | ------- | ----------- +id | Integer | Internal database ID — unique across all periods +assign_id | String | User-defined task code — may repeat across periods +response_code | String | Code of the submitted form answer, if completed +priority | Integer | Task priority level (set when creating the task) +start_time | String | Scheduled start time (account timezone) +form_name | String | Name of the associated form +user_email | String | Email of the assigned inspector +description | String | Task description or instructions +location_name | String | Name of the location +location_code | String | Code of the location +location_type | String | `Location` or `NestableLocation` +location_address | String | Address of the location +location_email | String | Email of the location +location_phone | String | Phone of the location +gap | Integer | Estimated hours to perform the task +checklist | String | Comma-separated checklist items +location_latitude | Float | Latitude of the location +location_longitude | Float | Longitude of the location +completed | String | `Yes` if a form answer exists, `No` otherwise +on_time | String | `Yes` if completed before deadline, `No` if late, `null` if not completed +delay_time | String | Delay formatted as `Xd/HHh/MMm`. `null` if not completed +completed_datetime | String | When the last answer was submitted (account timezone) +late_response_allowed | Boolean | Whether late submissions are permitted +mandatory | String | `for_nobody`, `for_one`, or `for_everyone` +confirmation_status | String | Acceptance state: `required`, `accepted`, `rejected`, `completed`, or `null` +status | String | Task state: `completed`, `incomplete`, `assigned`, `accepted`, or `rejected` +time_to_perform_minutes | Float | Minutes between first and last answer submission. `null` if not completed +response_start | String | When the first answer was submitted (account timezone) +response_end | String | When the last answer was submitted (account timezone) +created_at | String | When the task assignment was created (account timezone) +created_by | String | Full name of the user who created the task assignment + + + +## Get Task Assigns by Period + +```shell +curl "https://www.mydatascope.com/api/external/task_assigns?start=2025-03-01&end=2025-03-31" \ + -H "Authorization: b1cd93mfls9fdmfkadn23" +``` + +```ruby +require 'rest-client' +require 'json' + +url = 'https://www.mydatascope.com/api/external/task_assigns' +response = RestClient.get url, { + :Authorization => 'b1cd93mfls9fdmfkadn23', + :params => { start: '2025-03-01', end: '2025-03-31' } +} +JSON.parse(response) +``` + +> The above command returns JSON structured like this, you can check the description of each parameter below: + +```json +{ + "task_assigns": [ + { + "id": 4821, + "assign_id": "TA-2024-001", + "response_code": "FA-0099", + "priority": 1, + "start_time": "2025-03-10 09:00:00", + "form_name": "Safety Inspection", + "user_email": "inspector@company.com", + "description": "Monthly fire extinguisher check", + "location_name": "Main Warehouse", + "location_code": "WH-001", + "location_type": "Location", + "location_address": "123 Main Ave", + "location_email": "warehouse@company.com", + "location_phone": "+1-555-0100", + "gap": 2, + "checklist": "Check extinguisher,Verify seal,Sign log", + "location_latitude": -33.4489, + "location_longitude": -70.6693, + "completed": "Yes", + "on_time": "Yes", + "delay_time": "0d/00h/00m", + "completed_datetime": "2025-03-10 10:45:00", + "late_response_allowed": false, + "mandatory": "for_everyone", + "confirmation_status": "completed", + "status": "completed", + "time_to_perform_minutes": 105.5, + "response_start": "2025-03-10 09:05:00", + "response_end": "2025-03-10 10:45:00", + "created_at": "2025-03-01 08:00:00", + "created_by": "Admin User" + } + ], + "total": 1, + "limit": 100, + "offset": 0 +} +``` + +This endpoint retrieves a paginated list of task assignments for the authenticated account. The response fields match the platform's Excel export plus the internal `id`, enabling automated integrations without manual downloads. The `total` field reflects the number of task assignments matching the applied filters — without filters, it returns the count of all historical tasks in the account. + +### HTTP Request + +`GET https://www.mydatascope.com/api/external/task_assigns` + +### Input Parameters + +Parameter | Type | Description +--------- | ------- | ----------- +start | String | Optional. Start date in `YYYY-MM-DD` format. Filters by `start_time >= date` +end | String | Optional. End date in `YYYY-MM-DD` format. Filters by `start_time <= date` +status | String | Optional. Filter by status: `completed`, `incomplete`, `assigned`, `accepted`, `rejected` +location_id | Integer | Optional. Filter by location ID +user_email | String | Optional. Filter by the assigned inspector's email +limit | Integer | Optional. Max number of results to return. Default: 100, max: 300 +offset | Integer | Optional. Number of results to skip (for pagination). Default: 0 + +### Response Fields + +Field | Type | Description +--------- | ------- | ----------- +id | Integer | Internal database ID — unique across all periods +assign_id | String | User-defined task code — may repeat across periods +response_code | String | Code of the submitted form answer, if completed +priority | Integer | Task priority level (set when creating the task) +start_time | String | Scheduled start time (account timezone) +form_name | String | Name of the associated form +user_email | String | Email of the assigned inspector +description | String | Task description or instructions +location_name | String | Name of the location +location_code | String | Code of the location +location_type | String | `Location` or `NestableLocation` +location_address | String | Address of the location +location_email | String | Email of the location +location_phone | String | Phone of the location +gap | Integer | Estimated hours to perform the task +checklist | String | Comma-separated checklist items +location_latitude | Float | Latitude of the location +location_longitude | Float | Longitude of the location +completed | String | `Yes` if a form answer exists, `No` otherwise +on_time | String | `Yes` if completed before deadline, `No` if late, `null` if not completed +delay_time | String | Delay formatted as `Xd/HHh/MMm`. `null` if not completed +completed_datetime | String | When the last answer was submitted (account timezone) +late_response_allowed | Boolean | Whether late submissions are permitted +mandatory | String | `for_nobody`, `for_one`, or `for_everyone` +confirmation_status | String | Acceptance state: `required`, `accepted`, `rejected`, `completed`, or `null` +status | String | Task state: `completed`, `incomplete`, `assigned`, `accepted`, or `rejected` +time_to_perform_minutes | Float | Minutes between first and last answer submission. `null` if not completed +response_start | String | When the first answer was submitted (account timezone) +response_end | String | When the last answer was submitted (account timezone) +created_at | String | When the task assignment was created (account timezone) +created_by | String | Full name of the user who created the task assignment + + + + + +# Notifications + +## List Last notifications + +```ruby +require 'rest-client' +require 'json' + +url = 'https://www.mydatascope.com/api/external/notifications' +response = RestClient.get url, { +:Authorization => 'b1cd93mfls9fdmfkadn23', + :params => { start: '10/12/2019', end: '30/12/2019'} +} +JSON.parse(response) +``` + +```shell +curl "https://www.mydatascope.com/api/external/notifications" + -H "Authorization: b1cd93mfls9fdmfkadn23" +``` + +> The above command returns JSON structured like this, you can check the description of each parameter below: + +```json +[ + { + "id":2345, + "type":"PDF", + "url":"https://www.mydatascope.com/pdf_url_example", + "form_name":"Form Name", + "form_code":"25", + "user":"user@email.com", + "created_at":"2019-12-03T17:48:47.000-02:00" + }] + + +``` + +This endpoint retrieves all list items of a specific list + +### HTTP Request + +`GET https://www.mydatascope.com/api/external/notifications` + +### Query Parameters + +Parameter | Default | Description +--------- | ------- | ----------- +start | last 7 days | set the start date range +end | today | set the end date range + +### Output Parameter + +Parameter | Type | Description +--------- | ------- | ----------- +id | String | Identifier of the notification +type | String | Type of notification: PDF or Excel +url | String | URL of the notified file +form_name | String | Name of the form +form_code | String | Code of the form +user | String | Name of the user +created_at | Datetime | Date when the list element was created +updated_at | Datetime | Date when the list element was updated + +# Files + +## List Last generated files + +```ruby +require 'rest-client' +require 'json' + +url = 'https://www.mydatascope.com/api/external/files' +response = RestClient.get url, { +:Authorization => 'b1cd93mfls9fdmfkadn23', + :params => { start: '10/12/2019', end: '30/12/2019'} +} +JSON.parse(response) +``` + +```shell +curl "https://www.mydatascope.com/api/external/files" + -H "Authorization: b1cd93mfls9fdmfkadn23" +``` + +> The above command returns JSON structured like this, you can check the description of each parameter below: + +```json +[ + { + "id":2345, + "type":"PDF", + "url":"https://www.mydatascope.com/pdf_url_example", + "form_name":"Form Name", + "form_code":"25", + "user":"user@email.com" + }] ``` @@ -891,7 +1810,7 @@ form_code | String | Code of the form user | String | Name of the user # Webhooks @@ -904,7 +1823,7 @@ DataScope Webhook notifications are sent in an HTTP POST request, and their cont ## Configuration -To configure the webhook you need to go to the Integrations section and then Webhooks and click on [New Webhook](https://mydatascope.com/webhooks/new). +To configure the webhook you need to go to the Integrations section and then Webhooks and click on [New Webhook](https://app.mydatascope.com/integrations). ![New Webhook](https://data-scope.s3-us-west-2.amazonaws.com/images/other/Captura+de+pantalla+2020-09-08+a+la(s)+13.23.28.png "New Webhook") @@ -930,11 +1849,257 @@ To configure the webhook you need to go to the Integrations section and then Web ``` +## Webhooks or the Answers API + +Both move data out of DataScope, and they answer different questions. + +A **webhook** fits when something has to happen the moment a form arrives: notify a system, start a workflow, post to a channel. DataScope pushes, your endpoint reacts. It covers new submissions, and edits too when you enable **Send modifications** on the webhook. + +The **[Answers V5 endpoint](#get-all-answers-v5-beta)** fits when you need a queryable copy of your data, a table in BigQuery, Snowflake or Postgres that stays current. You pull on a schedule rather than receive pushes, and you get three things in exchange: + +What the API adds | How +----------------- | --- +Access to everything, not only what comes next | A webhook starts sending at the moment you configure it, per the tip above, and cannot replay what your endpoint missed while it was down. The API takes a `start_date`, so a first sync backfills history and any later sync can re-read a window with `date_modified=true` +A schema you can model | `answers_data_in_array` returns the answers inside a nested array rather than as top-level keys, so the response shape stays the same regardless of the form or how many questions it has. That is what makes a relational destination practical +Control over the payload | `custom_fields` selects exactly which fields you receive, so you can start minimal and add only what your model needs. `version` reproduces the response shape of any earlier API version if you are migrating from one + +If the destination is a data warehouse, you do not have to write the receiver, the retry handling or the deduplication yourself: the [Airbyte Cloud connector](#airbyte-cloud-connector) covers all of it. + +Using both is normal. A webhook for the immediate reaction, the connector for the warehouse copy. + # Tickets (FKA Issues) + +## Get Tickets by Period + +```shell +curl "https://www.mydatascope.com/api/external/findings/list?start=01-01-2026&end=31-01-2026" + -H "Authorization: b1cd93mfls9fdmfkadn23" +``` + +```ruby +require 'rest-client' +require 'json' + +url = 'https://www.mydatascope.com/api/external/findings/list' +response = RestClient.get url, { +:Authorization => 'b1cd93mfls9fdmfkadn23', + :params => { start: '01-01-2026', end: '31-01-2026' } +} +JSON.parse(response) +``` + +> The above command returns JSON structured like this, you can check the description of each parameter below: + +```json +[ + { + "id": "Krdz3aFoWZ4ZVgpuAart", + "code": 1, + "name": "ejemplo", + "description": "Ejemplo", + "type": "Safety", + "status": "closed", + "priority": "high", + "creation_date": "30/01/2026 19:53", + "expiration_date": "23/01/2026 19:53", + "closure_date": "05/02/2026 15:23", + "closure_message": "Addressed", + "location_id": 42, + "location_name": "Main Office", + "location_code": "LOC-001", + "asset_name": null, + "asset_identifier": null, + "creator_id": 7, + "creator_email": "juan.perez@example.com", + "creator_name": "Juan Perez", + "assignees_concatenated": "user_id:7;;email:juan.perez@example.com;;name:Juan Perez&&user_id:12;;email:maria.lopez@example.com;;name:Maria Lopez", + "invitees_concatenated": "user_id:18;;email:carlos.silva@example.com;;name:Carlos Silva", + "assignees": { + "0": { "id": 7, "full_name": "Juan Perez", "email": "juan.perez@example.com" }, + "1": { "id": 12, "full_name": "Maria Lopez", "email": "maria.lopez@example.com" } + }, + "invitees": { + "0": { "id": 18, "full_name": "Carlos Silva", "email": "carlos.silva@example.com" } + }, + "last_updated_by": "Juan Perez", + "form_answer_id": 101, + "form_answer_code": "FA-12345", + "task_form_title": "Daily Inspection", + "task_form_question": "What issues were found?" + } +] +``` + +This endpoint retrieves a list of Tickets filtered by creation date period. + +### HTTP Request + +`GET https://www.mydatascope.com/api/external/findings/list` + +### Input Parameters + +Parameter | Type | Description +--------- | ------- | ----------- +start | String | Optional. Start date in `dd-mm-yyyy` format. Defaults to 7 days ago +end | String | Optional. End date in `dd-mm-yyyy` format. Defaults to today +status | String | Optional. Filter by status: `open`, `in_progress`, `paused`, `closed` +task_form_id | Integer | Optional. Filter by the ID of the associated form. Only returns tickets linked to that form +limit | Integer | Optional. Max number of results to return. Default: 200, max: 200 +offset | Integer | Optional. Number of results to skip (for pagination). Default: 0, max: 2000 + +The maximum allowed date range is **90 days**. Requests with a wider range will return `422 Unprocessable Entity`. + +### Pagination + +Results are paginated using `limit` and `offset`. `limit` controls how many results are returned, and `offset` controls how many to skip from the beginning. + +For example, if there are 400 tickets in the period: + +- First page: `limit=200&offset=0` → returns tickets 1–200 +- Second page: `limit=200&offset=200` → returns tickets 201–400 + +### Response Fields + +Field | Type | Description +--------- | ------- | ----------- +id | String | Firestore document ID +code | Integer | Sequential ticket number within the account +name | String | Ticket name +description | String | Ticket description +type | String | Resolved ticket type name (null if no type assigned) +status | String | Current status: `open`, `in_progress`, `paused`, `closed` +priority | String | Priority level: `low`, `medium`, `high`, `critical` +creation_date | String | Date and time the ticket was created, formatted according to account preferences (e.g. `30/01/2026 19:53`) +expiration_date | String | Date and time the ticket expires, formatted according to account preferences (null if none) +closure_date | String | Date and time the ticket was closed, formatted according to account preferences (null if not closed) +closure_message | String | Message provided when closing the ticket (null if not closed) +location_id | Integer | Database ID of the associated location (null if none) +location_name | String | Name of the associated location (null if none) +location_code | String | Code of the associated location (null if none) +asset_name | String | Name of the linked asset (null if none) +asset_identifier | String | Code/identifier of the linked asset (null if none) +creator_id | Integer | Database ID of the user who created the ticket +creator_email | String | Email of the user who created the ticket +creator_name | String | Full name of the user who created the ticket +assignees_concatenated | String | Assigned users as a `&&`-separated string, each entry formatted as `user_id:{id};;email:{email};;name:{name}` (empty string if none) +invitees_concatenated | String | Invited users as a `&&`-separated string, same format as `assignees_concatenated` (empty string if none) +assignees | Object | Assigned users as an indexed object: `{ "0": { "id": Integer, "full_name": String, "email": String }, ... }` (empty object if none) +invitees | Object | Invited users as an indexed object, same shape as `assignees` (empty object if none) +last_updated_by | String | Full name of the user who last updated the ticket (null if not available) +form_answer_id | Integer | Database ID of the linked form answer (null if none) +form_answer_code | String | Code of the linked form answer (null if none) +task_form_title | String | Title of the linked form (null if no form answer linked) +task_form_question | String | Question from the linked form answer (null if no form answer linked) + +### Return Codes + +``` +200: OK +401: Unauthorized +422: Unprocessable Entity (invalid date range or range exceeds 90 days) +``` + + + + +## Get Ticket + +```shell +curl "https://www.mydatascope.com/api/external/findings/get/Krdz3aFoWZ4ZVgpuAart" + -H "Authorization: b1cd93mfls9fdmfkadn23" +``` + +```ruby +require 'rest-client' +require 'json' + +url = 'https://www.mydatascope.com/api/external/findings/get/Krdz3aFoWZ4ZVgpuAart' +response = RestClient.get url, { +:Authorization => 'b1cd93mfls9fdmfkadn23', +} +JSON.parse(response) +``` + +> The above command returns JSON structured like this, you can check the description of each parameter below: + +```json +{ + "id": "Krdz3aFoWZ4ZVgpuAart", + "code": 1, + "name": "ejemplo", + "description": "Ejemplo", + "type": null, + "status": "closed", + "priority": "high", + "creation_date": "2026-01-30T19:53:52.528+00:00", + "expiration_date": "2026-01-23T19:53:00.000+00:00", + "closure_date": "2026-02-05T15:23:12.360+00:00", + "closure_message": "Addressed", + "location_name": "Main Office", + "creator_name": "Juan Perez", + "assignees": "Juan Perez, Maria Lopez", + "invitees": "Carlos Silva", + "last_updated_by": "Juan Perez", + "form_answer_id": 12345, + "task_form_title": "Daily Inspection", + "task_form_question": "What issues were found?" +} +``` + +This endpoint retrieves a single Ticket by its Firestore document ID. + +### HTTP Request + +`GET https://www.mydatascope.com/api/external/findings/get/:id` + +### URL Parameter + +Parameter | Type | Description +--------- | ------- | ----------- +id | String | Required. The Firestore document ID of the ticket. This can be obtained from the ticket URL in the DataScope web app (`?selected=`) + +### Response Fields + +Field | Type | Description +--------- | ------- | ----------- +id | String | Firestore document ID +code | Integer | Sequential ticket number within the account +name | String | Ticket name +description | String | Ticket description +type | String | Ticket Type ID (null if no type assigned) +status | String | Current status: `open`, `in_progress`, `paused`, `closed` +priority | String | Priority level: `low`, `medium`, `high`, `critical` +creation_date | Datetime | Date and time the ticket was created (ISO 8601) +expiration_date | Datetime | Date and time the ticket expires (ISO 8601) +closure_date | Datetime | Date and time the ticket was closed (null if not closed) +closure_message | String | Message provided when closing the ticket (null if not closed) +location_name | String | Name of the associated location (null if none) +creator_name | String | Full name of the user who created the ticket +assignees | String | Comma-separated list of assigned users' full names +invitees | String | Comma-separated list of invited users' full names (empty string if none) +last_updated_by | String | Full name of the user who last updated the ticket (null if not available) +form_answer_id | Integer | ID of the linked form answer (null if none) +task_form_title | String | Title of the linked form (null if no form answer linked) +task_form_question | String | Question from the linked form answer (null if no form answer linked) + +### Return Codes + +``` +200: OK +404: Not Found +403: Forbidden +``` + + + ## Get last 5 tickets ```shell curl "https://www.mydatascope.com/api/external/last_findings" @@ -962,7 +2127,7 @@ JSON.parse(response) ``` ## Get Ticket Types @@ -1012,7 +2177,7 @@ search | String | Optional. Name to search ``` ## Create Ticket @@ -1048,6 +2213,27 @@ creation_date | String or Datetime | Optional. Date or String with a custom crea ``` +# Changelog + +**04-Aug-2026** + +- Documented three Ticket endpoints that were missing from this reference: last tickets, ticket types and ticket creation +- Updated the developer portal links to https://app.mydatascope.com/integrations +- Moved this changelog into its own section + +**30-Jul-2026** + +- Added [Answers V5 (Beta)](#get-all-answers-v5-beta), its `custom_fields`, and the [Airbyte Cloud connector manifest](#airbyte-cloud-connector) + +**Apr-2026** + +- Added the Ticket endpoints for fetching by period and by id +- Added the Task Assign endpoints for fetching by period and by id +- Added CRUD for Lists and List Elements, including bulk update + +**Nov-2025** + +- Added the first Ticket endpoints