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

# Ready — Data Import

> Bulk-import business profile and catalog data via ezForge JSON or CSV

The data import pipeline lets you seed or replace a business's catalog in two steps:

1. **Upload & preview** — POST a file to receive a `preview_id` and a summary of what will be imported.
2. **Confirm** — POST to the confirm endpoint to write everything to the database in a single transaction.

Previews expire after **1 hour**. The confirm endpoint is single-use; the preview row is deleted on success.

***

## Upload & preview

```
POST /api/v1/ready/businesses/{id}/import
```

Parses the uploaded file, validates it against the selected format's rules, stores a preview row, and returns a summary. No data is written to the business profile or catalog at this stage.

***

### Authentication

| Mode    | Mechanism     | Scope                 |
| ------- | ------------- | --------------------- |
| Session | Cookie or JWT | Must own the business |

API-key callers are not supported on this endpoint. If the authenticated user does not own the business, the endpoint returns `404` (same as if the business did not exist).

***

### Path parameters

| Parameter | Type   | Description                         |
| --------- | ------ | ----------------------------------- |
| `id`      | string | ULID of the business (max 26 chars) |

***

### Request body

`Content-Type: multipart/form-data`

| Field  | Type | Required | Description                                                                                                                                                                                                                           |
| ------ | ---- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `file` | file | ✓        | The import file. Max size: **10 MB**. Accepted MIME types: `application/json` (ezForge JSON v1.0) and `text/csv` (vertical CSV). The file extension (`.json` or `.csv`) is used for format detection when the MIME type is ambiguous. |

***

### Response `200`

```json theme={null}
{
  "data": {
    "preview_id": "01J9PABCDE0000000000000001",
    "source_format": "ezforge_json",
    "record_count": 12,
    "field_mappings": [
      { "source_field": "catalog_items[].name", "target_field": "catalog_items.name", "sample_value": "Spaghetti Carbonara" },
      { "source_field": "catalog_items[].price", "target_field": "catalog_items.price" }
    ],
    "warnings": [
      {
        "type": "value_coerced",
        "message": "catalog_items[0].item_type \"dish\" is invalid — defaulted to \"product\""
      }
    ],
    "expires_at": "2026-07-16T15:00:00.000Z"
  }
}
```

| Field            | Type                        | Description                                                       |
| ---------------- | --------------------------- | ----------------------------------------------------------------- |
| `preview_id`     | string                      | ULID identifying this preview. Pass it to the confirm endpoint.   |
| `source_format`  | `"ezforge_json"` \| `"csv"` | Detected input format                                             |
| `record_count`   | integer                     | Number of catalog items parsed from the file                      |
| `field_mappings` | array                       | Per-field mapping report                                          |
| `warnings`       | array                       | Non-fatal issues. See [Warning types](#warning-types).            |
| `expires_at`     | string (ISO 8601)           | UTC timestamp after which this preview can no longer be confirmed |

***

### Error codes

| HTTP  | Code                 | Description                                                                                                             |
| ----- | -------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `401` | `unauthorized`       | Request is not authenticated                                                                                            |
| `404` | `not_found`          | Business not found, or authenticated user does not own this business                                                    |
| `413` | `file_too_large`     | File exceeds the 10 MB limit                                                                                            |
| `422` | `unsupported_format` | File extension is not `.json` or `.csv`                                                                                 |
| `422` | `validation_error`   | File fails validation: invalid JSON, missing required fields, CSV missing `name` column, or CSV with no importable rows |
| `429` | —                    | Rate limit exceeded. Uses the `free` tier limit keyed to business ID + caller IP.                                       |

***

## Confirm import

```
POST /api/v1/ready/businesses/{id}/import/{preview_id}/confirm
```

Writes the previewed data to the database in a **single atomic transaction**:

* Updates `businesses.name`, `description`, `website`, and `industry` (ezForge JSON imports only, when the file includes a `business.name`).
* Upserts the business profile (phone, city, state, zip, custom fields) — ezForge JSON imports only.
* Resolves catalog categories: existing categories are matched by name; missing ones are created in the order they appear in the import, with `sortOrder` appended after any existing categories.
* Inserts catalog items with `sortOrder` appended after existing items. `currency` is always `USD`; `isAvailable` defaults to `true`.
* Appends a row to the import audit log.
* Deletes the preview row (single-use; a second call returns `404`).

***

### Authentication

| Mode    | Mechanism     | Scope                 |
| ------- | ------------- | --------------------- |
| Session | Cookie or JWT | Must own the business |

***

### Path parameters

| Parameter    | Type   | Description                      |
| ------------ | ------ | -------------------------------- |
| `id`         | string | ULID of the business             |
| `preview_id` | string | ULID returned by the upload step |

***

### Request body

No request body required.

***

### Response `200`

```json theme={null}
{
  "data": {
    "created_items": 12,
    "warnings_count": 1
  }
}
```

| Field            | Type    | Description                                                |
| ---------------- | ------- | ---------------------------------------------------------- |
| `created_items`  | integer | Number of catalog items inserted                           |
| `warnings_count` | integer | Number of non-fatal warnings carried over from the preview |

***

### Error codes

| HTTP  | Code              | Description                                                                                                                 |
| ----- | ----------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `401` | `unauthorized`    | Request is not authenticated                                                                                                |
| `404` | `not_found`       | Business not found, authenticated user does not own this business, preview does not exist, or preview was already confirmed |
| `422` | `preview_expired` | The preview's 1-hour TTL has elapsed. Upload the file again to get a fresh preview.                                         |
| `429` | —                 | Rate limit exceeded. Uses the `free` tier limit keyed to business ID + caller IP.                                           |

***

## Preview TTL contract

Previews expire exactly **1 hour** after the upload request completes. The `expires_at` field in the upload response is the authoritative expiry timestamp.

* Clients should call confirm promptly. If the TTL elapses, upload the file again — no data was written during the preview phase.
* A background cleanup cron sweeps expired preview rows hourly. Expired previews that are not confirmed are silently discarded.

***

## Supported formats

### ezForge JSON v1.0

The native import format. Supports business profile updates in addition to catalog items.

**Schema**

```json theme={null}
{
  "version": "1.0",
  "business": {
    "name": "Rosario's Kitchen",
    "description": "Family-owned Italian restaurant serving the Midwest since 1987.",
    "vertical": "restaurant",
    "website": "https://rosarioskitchen.com",
    "phone": "+1-313-555-0198",
    "location": {
      "city": "Detroit",
      "state": "MI",
      "zip": "48210"
    },
    "custom_fields": {
      "seating_capacity": "80",
      "accepts_reservations": "true"
    }
  },
  "catalog_items": [
    {
      "name": "Spaghetti Carbonara",
      "description": "Classic Roman pasta with guanciale, egg, and Pecorino Romano.",
      "item_type": "menu_item",
      "price": 18.95,
      "price_type": "fixed",
      "category": "Pasta"
    }
  ]
}
```

**`business` object**

| Field            | Type   | Required | Constraints                |
| ---------------- | ------ | -------- | -------------------------- |
| `name`           | string | ✓        | Must be non-empty          |
| `description`    | string | ✓        | Must be non-empty          |
| `vertical`       | string | —        | Free-text industry label   |
| `website`        | string | —        | Must start with `https://` |
| `phone`          | string | —        |                            |
| `location.city`  | string | —        |                            |
| `location.state` | string | —        |                            |
| `location.zip`   | string | —        |                            |
| `custom_fields`  | object | —        | Max 20 key/value pairs     |

**`catalog_items` array**

| Field         | Type   | Required | Constraints                                                                               |
| ------------- | ------ | -------- | ----------------------------------------------------------------------------------------- |
| `name`        | string | ✓        | Must be non-empty                                                                         |
| `description` | string | —        |                                                                                           |
| `item_type`   | string | —        | `menu_item`, `service`, `product`, `consultation`. Default: `product`                     |
| `price`       | number | —        | **Dollars** (e.g. `8.99`). Stored internally as integer cents (`899`). Do not pass cents. |
| `price_type`  | string | —        | `fixed`, `starting_at`, `hourly`, `free_estimate`. Default: `fixed`                       |
| `category`    | string | —        | Category name. Created automatically if it does not exist.                                |

**Limits**

| Constraint        | Value |
| ----------------- | ----- |
| Max file size     | 10 MB |
| Max catalog items | 1,000 |
| Max custom fields | 20    |

**Fatal validation errors** (upload returns `422 validation_error`)

* File is not valid JSON
* Root is not an object
* `version` is not `"1.0"`
* `business` key is missing or not an object
* `business.name` is empty or missing
* `business.description` is empty or missing

***

### Vertical CSV

A simpler tabular format for catalog-only imports. Business profile fields are **not** updated for CSV imports. Use ezForge JSON v1.0 if you need to update the business name, description, or contact details.

The CSV parser accepts any subset of the columns below; unknown columns are silently ignored. Missing optional columns use the default values shown in the table.

| Column        | Required | Default   | Notes                                                                      |
| ------------- | -------- | --------- | -------------------------------------------------------------------------- |
| `name`        | ✓        | —         | Item name                                                                  |
| `description` | —        | `""`      |                                                                            |
| `price`       | —        | `null`    | **Dollars** (e.g. `8.99`). Same dollar-to-cents conversion as JSON format. |
| `price_type`  | —        | `fixed`   | `fixed`, `starting_at`, `hourly`, `free_estimate`                          |
| `item_type`   | —        | `product` | `menu_item`, `service`, `product`, `consultation`                          |
| `category`    | —        | `""`      | Category name                                                              |

Rows with an empty `name` column are skipped. If all rows are skipped after parsing, the upload returns `422 validation_error`.

***

## CSV templates

Four pre-built templates are available to download directly from the web app. Each template includes sample rows demonstrating valid column values.

### Generic catalog

**Download:** `/hub/templates/generic_catalog.csv`

Columns: `name`, `description`, `price`, `price_type`, `item_type`, `category`

Covers all four `item_type` and `price_type` values. Use this as a starting point if no vertical-specific template fits your business.

### Restaurant menu

**Download:** `/hub/templates/restaurant_menu.csv`

Columns: `name`, `description`, `price`, `category`, `item_type`

All sample rows use `item_type: menu_item`. Sample categories: Appetizers, Salads, Pizzas, Pasta, Desserts.

Note: this template omits the `price_type` column — the parser defaults to `fixed` for all rows. Add the column manually if you need `starting_at`, `hourly`, or `free_estimate` pricing.

### Retail inventory

**Download:** `/hub/templates/retail_inventory.csv`

Columns: `name`, `description`, `price`, `category`, `item_type`

All sample rows use `item_type: product`. Sample categories: Electronics, Apparel, Kitchenware, Accessories, Fitness.

Note: this template omits the `price_type` column — the parser defaults to `fixed`.

### Services catalog

**Download:** `/hub/templates/services_catalog.csv`

Columns: `name`, `description`, `price`, `price_type`, `category`, `item_type`

Mix of `service` and `consultation` item types. Demonstrates all four `price_type` values. Use for professional services, home services, or consulting businesses.

***

## Warning types

Non-fatal issues are returned in the `warnings` array of the upload response. They do not block the import.

Each warning object has the following fields:

| Field     | Type    | Description                                                                                      |
| --------- | ------- | ------------------------------------------------------------------------------------------------ |
| `type`    | string  | Warning category. See types below.                                                               |
| `message` | string  | Human-readable description of the issue                                                          |
| `context` | string? | Optional machine-readable value that caused the warning (e.g. the raw field value or item index) |

| Type              | Description                                                                                                                                                              |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `field_skipped`   | A known field value was present but failed validation and was skipped (e.g., a non-HTTPS website URL, or a non-numeric price)                                            |
| `value_coerced`   | A field value was not in the allowed enum; the parser substituted the default                                                                                            |
| `limit_truncated` | A list exceeded the allowed limit (e.g. more than 1,000 catalog items or more than 20 custom fields). Items beyond the limit were dropped.                               |
| `format_error`    | An item or row could not be parsed due to a structural problem (e.g., a catalog item that is not an object, or a row with an empty `name`). The item or row was skipped. |
