> ## Documentation Index
> Fetch the complete documentation index at: https://docs.closient.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Start or resume a catalog import job

> Queue the staged rows for the server-side merge and return 202 with the job in `queued`. Poll `GET .../jobs/{job_id}` for progress — `processed_rows` advances every `batch_size` rows, `chunks_done` and `result` every `chunk_rows` — and for the outcome.

The merge runs as a sequential chain of chunk tasks of `chunk_rows` rows each, so no single worker task approaches its time limit. **Calling this on a `failed` job resumes it where it stopped**: every finished chunk keeps its rows and its share of the census, and so does every finished batch of the chunk that was in flight, so an interrupted run costs at most `batch_size` rows of repeated work rather than a chunk or a feed.

Returns 409 when the job is `queued`, `running` or `completed` — the first two already have a chain and a second would race it on one cursor, and the third already has its census — or when no rows are staged, because an empty run would report zeros that read like a clean import.



## OpenAPI

````yaml /openapi/openapi-products.json post /products/api/v1/import/retailer-catalog/jobs/{job_id}/start
openapi: 3.1.0
info:
  title: Products API
  version: 1.0.0
  description: >
    Look up, claim, and browse GTINs in the Closient product repository.


    ## Authentication


    All endpoints require an API key passed via the `X-API-Key` HTTP header,
    unless otherwise noted.


    ```

    X-API-Key: csb_<body>_<checksum>

    ```


    Generate API keys in **Settings > API Keys** in your dashboard, or via the
    Account API.

    Session-based (cookie) authentication is also accepted for browser-based
    access.


    ## Rate Limits


    | Tier        | Requests / minute | Requests / day |

    |-------------|-------------------|----------------|

    | Default     | 300               | 10,000         |

    | Custom      | Contact us        | Contact us     |


    Rate-limit headers are included on every response so callers can
    self-throttle without

    hitting our 429s ("informed governor"):


    - `RateLimit-Policy` — every active window, e.g. `300;w=60, 10000;w=86400`

    - `RateLimit-Limit` — quota for the **most-restrictive** currently-active
    window

    - `RateLimit-Remaining` — requests left in that window

    - `RateLimit-Reset` — seconds until that window resets (relative; clock-skew
    safe)


    Legacy `X-RateLimit-*` aliases are also emitted for back-compat.
    `X-RateLimit-Reset`

    keeps the absolute Unix-timestamp shape to avoid breaking existing
    consumers.


    When rate-limited, you receive `429 Too Many Requests` with a
    `retry_after_seconds` field

    in the error envelope and a `Retry-After` header.


    ## Pagination


    List endpoints return paginated results in this envelope:


    ```json

    {
      "data": [...],
      "pagination": {
        "page": 1,
        "page_size": 25,
        "total_count": 342,
        "total_pages": 14,
        "has_next": true,
        "has_previous": false
      }
    }

    ```


    Use `?page=2&page_size=50` query parameters. Maximum page size is 100.


    ## Error Responses


    All errors conform to [RFC 9457 Problem
    Details](https://www.rfc-editor.org/rfc/rfc9457)

    with `Content-Type: application/problem+json`:


    ```json

    {
      "type": "https://closient.com/docs/errors/not_found",
      "title": "Not Found",
      "status": 404,
      "detail": "The requested resource was not found.",
      "error_code": "not_found",
      "retryable": false,
      "timestamp": "2026-03-31T12:00:00+00:00"
    }

    ```


    Common error codes: `unauthorized` (401), `forbidden` (403), `not_found`
    (404),

    `validation_error` (422), `rate_limited` (429), `internal_error` (500).
  termsOfService: https://www.closient.com/terms/
servers:
  - url: https://www.closient.com
security: []
tags:
  - name: Products
    description: Look up, claim, and browse products and trade items.
  - name: Product Group
    description: Manage GDSN packaging hierarchy relationships.
  - name: Import
    description: >-
      Bulk import products, batch/lots (GS1 AI 10) and serial numbers (GS1 AI
      21) from CSV, TSV or XLSX files, with a downloadable template per entity.
  - name: QR
    description: Generate QR codes encoding GS1 Digital Link URLs.
  - name: Codes
    description: Generate GS1 DataMatrix and 1D barcodes (EAN/UPC/ITF-14/Code128).
  - name: Digital Link
    description: >-
      Parse GS1 Digital Link URIs into structured AIs (GTIN, lot, expiry,
      serial).
  - name: Labels
    description: >-
      Bulk label-export jobs: ZIPs of QR / DataMatrix / 1D symbols and multi-up
      sheet PDFs, async via Celery with status polling.
  - name: Lots
    description: >-
      Lot generator v2: format templates with persistent counters,
      reserve/commit/discard runs, soft/hard collision handling, and an async
      Celery path with SSE progress.
  - name: Retailer Catalog Import
    description: >-
      Server-side retailer catalog import jobs: stage normalised rows, then run
      the GTIN-keyed cross-retailer merge with a dry-run mode, per-field
      provenance and a quarantine report for every row refused.
  - name: HRI Presets
    description: >-
      Saved, named per-organisation Human Readable Interpretation (HRI)
      configurations — placement, layout, notation and typography. Referenceable
      by name from the QR generation endpoint, with an org default applied when
      no options or preset are supplied.
  - name: Product Links
    description: >-
      Generic, GS1-link-type-keyed brand CTA links (e.g. reviews, promotions,
      loyalty programs, homepage) attached to a product or a brand and rendered
      on the hosted page.
externalDocs:
  description: Closient Documentation
  url: https://docs.closient.com
paths:
  /products/api/v1/import/retailer-catalog/jobs/{job_id}/start:
    post:
      tags:
        - Retailer Catalog Import
      summary: Start or resume a catalog import job
      description: >-
        Queue the staged rows for the server-side merge and return 202 with the
        job in `queued`. Poll `GET .../jobs/{job_id}` for progress —
        `processed_rows` advances every `batch_size` rows, `chunks_done` and
        `result` every `chunk_rows` — and for the outcome.


        The merge runs as a sequential chain of chunk tasks of `chunk_rows` rows
        each, so no single worker task approaches its time limit. **Calling this
        on a `failed` job resumes it where it stopped**: every finished chunk
        keeps its rows and its share of the census, and so does every finished
        batch of the chunk that was in flight, so an interrupted run costs at
        most `batch_size` rows of repeated work rather than a chunk or a feed.


        Returns 409 when the job is `queued`, `running` or `completed` — the
        first two already have a chain and a second would race it on one cursor,
        and the third already has its census — or when no rows are staged,
        because an empty run would report zeros that read like a clean import.
      operationId: apps_products_api_retailer_catalog_start_catalog_job
      parameters:
        - in: path
          name: job_id
          schema:
            description: '`job_id` returned when the job was opened.'
            format: shortuuid
            maxLength: 22
            minLength: 22
            pattern: ^[23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{22}$
            title: Job Id
            type: string
          required: true
          description: '`job_id` returned when the job was opened.'
      responses:
        '202':
          description: Accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CatalogJobOut'
        '400':
          description: Bad Request
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ErrorOut'
        '401':
          description: Unauthorized
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ErrorOut'
        '403':
          description: Forbidden
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ErrorOut'
        '404':
          description: Not Found
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ErrorOut'
        '405':
          description: Method Not Allowed
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ErrorOut'
        '409':
          description: Conflict
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ErrorOut'
        '422':
          description: Unprocessable Content
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ErrorOut'
        '429':
          description: Too Many Requests
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ErrorOut'
      security:
        - APIKeyHeaderAuth: []
        - OAuthTokenAuth: []
        - CookieGatedSessionAuth: []
components:
  schemas:
    CatalogJobOut:
      description: A job's current state. The same shape from open, append, start and poll.
      examples:
        - chunk_rows: 10000
          chunks_done: 25
          chunks_total: 25
          completed_at: '2026-09-11T07:41:00Z'
          dry_run: true
          job_id: b2c3d4e5-f678-9012-abcd-ef2345678901
          poll_url: >-
            /products/api/v1/import/retailer-catalog/jobs/b2c3d4e5-f678-9012-abcd-ef2345678901
          processed_rows: 248566
          reclaim_unattributed: true
          result:
            dual_listed: 14300
            listings_created: 247566
            products_created: 233241
            products_enriched: 14300
            quarantine:
              - detail: net_content is 117 characters, limit 100
                raw_gtin: '3378872412345'
                reason: field_too_long
                row_number: 18342
                source_reference: '2598765'
            quarantine_reasons:
              field_too_long: 1
            quarantined: 1
            rows_read: 248566
          source: ulta_catalog
          source_file: ulta_catalog_2025-12.psv
          staged_rows: 248566
          started_at: '2026-09-11T07:10:00Z'
          status: completed
      properties:
        job_id:
          description: Identifier of the job. Use it on the row, start and poll calls.
          format: shortuuid
          maxLength: 22
          minLength: 22
          pattern: ^[23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{22}$
          title: Job Id
          type: string
        status:
          description: >-
            `staging` — accepting rows; the run has not started. `queued` —
            start accepted, waiting for a worker. `running` — the merge is
            executing; `chunks_done` and `result` advance as it goes.
            `completed` — finished; `result` carries the whole feed's census.
            `failed` — the run gave up; `error` says why, `result` carries the
            census of the work that did finish, and `POST .../start` resumes
            from where it stopped.
          title: Status
          type: string
        source:
          description: Catalog source this run merges under.
          title: Source
          type: string
        source_file:
          default: ''
          description: Feed drop name recorded as listing provenance.
          title: Source File
          type: string
        dry_run:
          description: Whether this run rolls back each batch.
          title: Dry Run
          type: boolean
        reclaim_unattributed:
          description: Whether this run may claim unattributed fields.
          title: Reclaim Unattributed
          type: boolean
        staged_rows:
          description: Rows accepted into the job so far.
          minimum: 0
          title: Staged Rows
          type: integer
        chunk_rows:
          description: >-
            Staged rows merged by one chunk task. The run is a sequential chain
            of these, so no single task approaches the worker's time limit. Set
            at open time from `dry_run` unless you named one: 10,000 for a dry
            run, 2,000 for a commit, because a commit does an order of magnitude
            more work per row.
          minimum: 1
          title: Chunk Rows
          type: integer
        chunks_total:
          description: >-
            Chunks this job's staged rows divide into. Zero until rows are
            staged.
          minimum: 0
          title: Chunks Total
          type: integer
        chunks_done:
          description: >-
            Chunks fully merged and recorded. A resumed run continues from here,
            so these rows are never merged twice.
          minimum: 0
          title: Chunks Done
          type: integer
        processed_rows:
          description: >-
            Staged rows already merged. A floor, not an estimate: rows counted
            here are recorded and survive a restart. It advances **within** a
            chunk as well as between chunks — a chunk checkpoints every
            `batch_size` rows — so it keeps moving during the minutes a commit
            chunk takes, and is the field to watch to tell a slow run from a
            stopped one.
          minimum: 0
          title: Processed Rows
          type: integer
        poll_url:
          description: Path to poll for this job's status and result.
          title: Poll Url
          type: string
        started_at:
          anyOf:
            - format: date-time
              type: string
            - type: 'null'
          description: When the run began. Null before it starts.
          title: Started At
        completed_at:
          anyOf:
            - format: date-time
              type: string
            - type: 'null'
          description: When the run reached a terminal state.
          title: Completed At
        error:
          anyOf:
            - type: string
            - type: 'null'
          description: Why the run failed. Null unless `status` is `failed`.
          title: Error
        result:
          additionalProperties: true
          description: >-
            The engine's census. Empty until the first chunk finishes, then the
            running total for the chunks merged so far, and the whole feed's
            figures once `status` is `completed` — so read `status`, not this
            field, to decide whether a run is done. Carries `rows_read`,
            `products_created` / `products_enriched` / `products_unchanged`,
            `listings_created` / `listings_updated`, `dual_listed` (products
            another retailer's feed had already created — the cross-retailer
            overlap), `duplicate_gtins_in_feed`, `quarantined` with
            `quarantine_reasons`, brand and ingredient counts, `field_decisions`
            (per field, how each value was decided), `reclaimable_fields`, and
            `quarantine` — the refused rows themselves, capped at 500 entries
            with `quarantined` remaining the true total. `errors` is capped the
            same way, with `errors_total` as its true count. `census` is the
            same figures as human-readable lines.
          title: Result
          type: object
      required:
        - job_id
        - status
        - source
        - dry_run
        - reclaim_unattributed
        - staged_rows
        - chunk_rows
        - chunks_total
        - chunks_done
        - processed_rows
        - poll_url
      title: CatalogJobOut
      type: object
    ErrorOut:
      description: |-
        RFC 9457 Problem Details response.

        All API errors are returned in this format with Content-Type:
        application/problem+json.
      examples:
        - detail: The requested resource was not found.
          error_code: not_found
          retryable: false
          status: 404
          timestamp: '2026-03-31T12:00:00+00:00'
          title: Not Found
          type: https://closient.com/docs/errors/not_found
        - detail: Validation error.
          details:
            - loc:
                - body
                - name
              msg: Field required
              type: missing
          error_code: validation_error
          retryable: false
          status: 422
          timestamp: '2026-03-31T12:00:00+00:00'
          title: Validation Error
          type: https://closient.com/docs/errors/validation_error
        - detail: Rate limit exceeded. Please try again later.
          error_code: rate_limited
          retry_after: 31
          retryable: true
          status: 429
          timestamp: '2026-03-31T12:00:00+00:00'
          title: Rate Limited
          type: https://closient.com/docs/errors/rate_limited
      properties:
        type:
          description: URI reference identifying the error type.
          title: Type
          type: string
        title:
          description: Short human-readable summary of the error.
          title: Title
          type: string
        status:
          description: HTTP status code.
          title: Status
          type: integer
        detail:
          description: Human-readable explanation of this specific occurrence.
          title: Detail
          type: string
        error_code:
          description: Machine-readable error code (e.g. not_found, unauthorized).
          title: Error Code
          type: string
        retryable:
          default: false
          description: Whether retrying the same request can succeed.
          title: Retryable
          type: boolean
        timestamp:
          description: ISO 8601 timestamp of when the error occurred.
          title: Timestamp
          type: string
        retry_after:
          anyOf:
            - type: integer
            - type: 'null'
          description: Seconds to wait before retrying (when applicable).
          title: Retry After
        owner_action_required:
          anyOf:
            - type: boolean
            - type: 'null'
          description: Whether the error requires account owner intervention.
          title: Owner Action Required
        details:
          description: Additional context (validation errors, etc.).
          title: Details
      required:
        - type
        - title
        - status
        - detail
        - error_code
        - timestamp
      title: ErrorOut
      type: object
  securitySchemes:
    APIKeyHeaderAuth:
      type: apiKey
      in: header
      name: X-API-Key
    OAuthTokenAuth:
      type: http
      scheme: bearer
    CookieGatedSessionAuth:
      type: apiKey
      in: cookie
      name: sessionid

````