# Create a bulk export job

`POST /v1/exports`

Queues an asynchronous bulk export for the caller's organization. The
response returns immediately with `status: PENDING` and a `status_url`
pointing at [Retrieve an export](/api/exports/getExport); poll there
until `status` reaches `COMPLETED` to obtain the short-lived presigned
download URL.

Org scope is enforced by the access token; the optional `filters`
block narrows within the org. Each caller is limited to one in-flight
`PENDING`/`PROCESSING` export at a time — additional submissions
return `409 EXPORT_CONCURRENCY_LIMIT`.

## CSV format

Exports are CSV in v1 (JSONL and Parquet deferred to v2). The header
row is fixed per `subject`; carousel rows in `subject=images` exports
emit empty values for hero-only columns so partners can rely on a
stable column set.

### `subject=pdps` column set

| Column | Type | Notes |
| --- | --- | --- |
| `pdp_id` | uuid | PDP identifier; stable across refreshes |
| `portfolio_id` | uuid | Owning portfolio |
| `retailer_region` | string | e.g. `amazon_us`, `walmart_us` |
| `item_id` | string | ASIN or GTIN, depending on retailer |
| `name` | string \| null | PDP display name |
| `brand` | string \| null | |
| `category_id` | uuid \| null | Vizit category identifier |
| `category` | string \| null | Category display name |
| `listing_score` | int \| null | 0–100, resolved per org |
| `raw_listing_score` | float \| null | Pre-blend, pre-penalty |
| `listing_score_blended` | int \| null | 0–100, pre-penalty |
| `score_penalty` | bool \| null | True if any penalty applied |
| `image_count_penalty` | float \| null | |
| `image_mix_penalty` | float \| null | |
| `image_order_penalty` | float \| null | |
| `high_scoring_asset_count` | int \| null | |
| `is_ideal_content_mix` | bool \| null | |
| `images_matching_ideal_mix` | int \| null | |
| `score_status` | string | `CREATED`, `PROCESSING`, `SCORED`, or `CATEGORY_NOT_FOUND` |
| `created_at` | date-time | |
| `updated_at` | date-time | |
| `last_refreshed` | date-time \| null | Last successful scrape + score |

### `subject=images` column set

Hero-score and mobile-readiness columns are populated only for hero
rows; carousel rows emit empty values in those columns.

| Column | Type | Notes |
| --- | --- | --- |
| `image_id` | uuid | |
| `pdp_id` | uuid | Parent PDP |
| `portfolio_id` | uuid | Denormalized from PDP for row-standalone exports |
| `retailer_region` | string | |
| `item_id` | string | ASIN or GTIN of the parent PDP |
| `image_type` | string | `hero` or `carousel` |
| `image_url` | string | |
| `position` | int | 1-based position within the PDP (matches `CarouselImageResponse.position`) |
| `source` | string \| null | e.g. `scraped` |
| `vizit_score` | int \| null | 0–100, resolved per org |
| `raw_score` | float \| null | |
| `hero_score` | int \| null | 0–100; null for carousel rows. The `hero_score_breakdown` sub-scores (heroism, category_edge, technical_integrity, local_support) are intentionally omitted from v1 — the hero formula is in flight |
| `mobile_ready` | bool \| null | All five clarity scores ≥ 80. Null for carousel |
| `mobile_brand_clarity` | float \| null | Null for carousel |
| `mobile_product_type_clarity` | float \| null | Null for carousel |
| `mobile_variant_clarity` | float \| null | Null for carousel |
| `mobile_count_clarity` | float \| null | Null for carousel |
| `mobile_size_clarity` | float \| null | Null for carousel |
| `mobile_scored_model` | string \| null | Null for carousel |
| `classification` | string \| null | e.g. `Lifestyle`, `Packshot` |
| `classification_confidence` | float \| null | 0–1 |
| `classification_high_confidence` | bool \| null | |
| `created_at` | date-time | |
| `updated_at` | date-time | |
| `scored_at` | date-time \| null | Last successful score |

## Request Body

### application/json

- **object** — Body for creating an asynchronous bulk export job.

The request schedules a background task that paginates the requested
data, produces a CSV, and updates job status. Poll
[Retrieve an export](/api/exports/getExport) until `status` reaches
`COMPLETED` to obtain the download URL.

  - `subject` (enum: "pdps" | "images", required) — Which dataset to export. `pdps` returns PDP-level scores; `images`
returns image-level scores. CSV column layout per subject is
documented in the `createExport` endpoint description.

  - `filters` (object) — Optional filters narrowing the export. Org scope is always enforced
by the access token; these filters refine within the caller's org.
Unknown filter keys are rejected with `INVALID_FILTER`.

    - **object** — Optional filters narrowing the export. Org scope is always enforced
by the access token; these filters refine within the caller's org.
Unknown filter keys are rejected with `INVALID_FILTER`.

      - `portfolio_id` (string) — Restrict to a single portfolio in the caller's organization.
      - `retailer_region` (string) — Retailer-region identifier (e.g. `amazon_us`, `walmart_us`). Must
be a value present in the configured `retailer_regions`; invalid
values are rejected with `INVALID_RETAILER_REGION`.

      - `last_refreshed_after` (string) — Only include rows whose underlying PDP was refreshed after this
timestamp (RFC 3339). Drives the "give me what changed since my
last sync" use case. Invalid timestamps return `INVALID_TIMESTAMP`.


## Responses

### 202 — Export job accepted for processing.

**application/json**

- **object** — Acknowledgment returned synchronously by `POST /v1/exports`. The export
itself runs asynchronously; clients poll `status_url` until `status`
reaches `COMPLETED`.

  - `export_id` (string, required) — Unique identifier for the export job.
  - `subject` (enum: "pdps" | "images", required) — Which dataset was requested.
  - `status` (enum: "PENDING", required) — Always `"PENDING"` on the accepted response — poll the status URL for transitions.
  - `status_url` (string, required) — Relative path to poll for status and (eventually) the download URL.
  - `requested_at` (string, required) — Server timestamp when the job was accepted.

### 400 — Validation error. Check `error_code` to distinguish:
  - INVALID_SUBJECT — `subject` is not `pdps` or `images`
  - INVALID_FILTER — request contains an unknown filter key
  - INVALID_TIMESTAMP — `last_refreshed_after` is not a valid RFC 3339 timestamp
  - INVALID_RETAILER_REGION — `retailer_region` is not in the configured set


**application/json**

- **object** — Standardized error format for all `/v1/*` endpoints. `error_code` is a
stable SCREAMING_SNAKE_CASE identifier clients can switch on.

  - `detail` (string) — Human-readable error message
  - `status_code` (integer) — HTTP status code
  - `error_code` (string) — Stable machine-readable error code
  - `request_id` (any) — Request identifier for correlation
  - `extra` (any) — Endpoint-specific additional context. For example,
IMAGE_DOWNLOAD_FAILED populates extra.failed_urls with per-URL
failure reasons so clients can triage bulk submissions.

  - `timestamp` (string)

### 401 — Missing or invalid Bearer token

### 404 — `portfolio_id` does not belong to the caller's organization.
`error_code: PORTFOLIO_NOT_FOUND`. (We return 404 — not 403 — so
cross-org existence is not leaked.)


**application/json**

- **object** — Standardized error format for all `/v1/*` endpoints. `error_code` is a
stable SCREAMING_SNAKE_CASE identifier clients can switch on.

  - `detail` (string) — Human-readable error message
  - `status_code` (integer) — HTTP status code
  - `error_code` (string) — Stable machine-readable error code
  - `request_id` (any) — Request identifier for correlation
  - `extra` (any) — Endpoint-specific additional context. For example,
IMAGE_DOWNLOAD_FAILED populates extra.failed_urls with per-URL
failure reasons so clients can triage bulk submissions.

  - `timestamp` (string)

### 409 — Caller already has an export in `PENDING` or `PROCESSING`. Poll
the existing job before submitting a new one.
`error_code: EXPORT_CONCURRENCY_LIMIT`.


**application/json**

- **object** — Standardized error format for all `/v1/*` endpoints. `error_code` is a
stable SCREAMING_SNAKE_CASE identifier clients can switch on.

  - `detail` (string) — Human-readable error message
  - `status_code` (integer) — HTTP status code
  - `error_code` (string) — Stable machine-readable error code
  - `request_id` (any) — Request identifier for correlation
  - `extra` (any) — Endpoint-specific additional context. For example,
IMAGE_DOWNLOAD_FAILED populates extra.failed_urls with per-URL
failure reasons so clients can triage bulk submissions.

  - `timestamp` (string)

### 429 — Rate limit exceeded for this organization
