# Import a service's price table from CSV

```http
POST https://secure.yoprint.com/v1/api/store/{team_slug}/imports/csv
```

- API version: v1
- Operation ID: `v1_public_imports_csv_store`
- Section: Imports and exports › [CSV imports](https://developer.yoprint.com/reference/csv-imports.md)
- Web page: https://developer.yoprint.com/reference/csv-imports/v1-imports-csv-store/
- Authentication: `Authorization: Bearer YOUR_API_KEY`

Uploads a price table as a CSV file and saves it on a service in one request.

- `target_mode=existing_service`: permanently overwrites the service's whole price table. Any existing options or price breaks missing from the file are deleted. The service's name and locations do not change.
- `target_mode=create`: creates a new service with these prices.

Send `multipart/form-data`. The import runs while you wait, and it is all or nothing. If anything in the file is wrong, you get `422` with the first problem found (a row problem names its line), and nothing is saved: no price changes and no import job. Otherwise the response is the finished import job, always `completed` with `failed_item_count` 0. You can read it again later with `GET /v1/api/store/{team_slug}/imports/{hash_id}`. There is never a file of failed rows for these imports.

Only service pricing is accepted here (`entity_type=service_pricing`). Customer, vendor and product CSV files are imported in the YoPrint web app's Import Portal.

**The file.** Start from `PUT /v1/api/store/{team_slug}/csv/template/service_pricing`.

- A `.csv` file of at most 6,240 KB, with at most 600 data rows and at least one, in UTF-8.
- The first header must be exactly `qty/type`. Add at least one more column; each is an option of the service (for example `1 Color`). Header names can't be blank or repeated.
- Each row is a price break. `qty/type` is a number, the first row's must be 1, and no quantity can repeat.
- Every cell needs a value. A cell is one price (`12.50`) or two, `first|additional` (`12.50|9.00`). Prices are numbers with at most 3 decimal places, written with a digit before the point (`0.50`, not `.50`) and no spaces.

## Permission

To replace an existing service's prices: permission to update services. To create a service: permission to create and to update services.

## Business rules

- `entity_type` must be `service_pricing`. Any other value is a 422.
- `target_mode` is `existing_service` or `create`. With `existing_service`, send `service_id`. With `create`, send `name`, `allow_cust_loc`, `pricing_type` and at least one entry in `locations`.
- Every location id must be one of your team's product locations, each listed once, or the request fails with "One or more service locations are invalid."
- The file must have the `.csv` extension and be at most 6,240 KB. It must hold at least one and at most 600 data rows.
- First header exactly `qty/type`, then at least one option column. Option headers can't be blank or repeated.
- Each `qty/type` value is a number, the first data row's is 1, and quantities can't repeat.
- No empty cells (`0` is a value). Each cell is a price, or `first|additional`. Prices are numbers with at most 3 decimal places, between -2,147,483,648 and 2,147,483,648. Write a digit before the decimal point and no spaces: `.50` or ` 12` passes the number check but then fails with a server error.
- All or nothing: the whole file is checked before anything is saved, and any problem stops the import with nothing saved. A returned job is always `completed`, with one item and `failed_item_count` 0.
- One price import per service at a time, and one per new service name. A second upload waits up to 45 seconds for the first to finish; if the first is still running then, it fails with a server error. Retry after the first finishes.

## Side effects

- `existing_service`: permanently overwrites the service's whole price table with the file. Options and price breaks missing from the file are deleted. The service's name and locations do not change.
- `create`: creates a service at the end of your service list, links the locations, then saves the prices.
- If the service's pricing type is not `additional`, the second price in `first|additional` cells is ignored and the service keeps no additional-location prices.
- A successful import is recorded as an import job, with the uploaded file attached. A failed one leaves no job: it does not appear in the import jobs list.
- People with YoPrint open see the service and the import update live.

## Path parameters

- `team_slug` (string, required): Your team slug, shown as Team ID next to your API key in Profile. Example `your-team`.

## Request body

Content type `multipart/form-data`, required.

Which service gets the prices? Send one of these 2 bodies: **An existing service**, **A new service**.

### An existing service

Variant 1 of 2. On the web page: https://developer.yoprint.com/reference/csv-imports/v1-imports-csv-store/#variant-an-existing-service

Permanently overwrites the service's whole price table with the file; omitted options and breaks are deleted.

Fields:

- `entity_type` (string, required): Always `"service_pricing"`.
- `target_mode` (string, required): Always `"existing_service"`.
- `file` (string (binary), required): The CSV file: a name ending in `.csv`, at most 6,240 KB and 600 data rows.
- `service_id` (string, required): The service's id (a hash id), for example the `service_id` of a job's service.

Example request (An existing service):

```bash
curl -X POST 'https://secure.yoprint.com/v1/api/store/your-team/imports/csv' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Accept: application/json' \
  -H 'X-Yoprint-Decimal-Format: exact-v1' \
  -H 'Content-Type: application/json' \
  -d '{
    "entity_type": "service_pricing",
    "file": "qty/type,1 Color,2 Color\n1,10.00|11.00,12.00|13.00\n12,8.50|9.00,10.00|11.00\n",
    "service_id": "IIVE3hadAUOmIA",
    "target_mode": "existing_service"
  }'
```

### A new service

Variant 2 of 2. On the web page: https://developer.yoprint.com/reference/csv-imports/v1-imports-csv-store/#variant-a-new-service

Creates a service with this name, these locations and these prices.

Fields:

- `entity_type` (string, required): Always `"service_pricing"`.
- `target_mode` (string, required): Always `"create"`.
- `allow_cust_loc` (string, required): "Allow custom locations on jobs": `1` for yes, `0` for no. A form sends text, and `true` or `false` are refused. One of: `"1"`, `"0"`.
- `file` (string (binary), required): The CSV file: a name ending in `.csv`, at most 6,240 KB and 600 data rows.
- `locations` (array of objects, required): The product locations the service applies to, at least one, each once. Send each id as its own form field with a numbered index: `locations[0][id]`, `locations[1][id]`, and so on. A JSON text in a single `locations` field is refused (`locations` must be an array). At least 1 item.
  - Each item:
    - `id` (string, required): A product location's `id` (a hash id) from `GET /v1/api/store/{team_slug}/service_location`.
- `name` (string, required): The new service's name.
- `pricing_type` (string, required): `single`: per-item pricing. `additional`: per-item pricing with a second price for each additional location. `single_flat`: a flat rate per job. One of: `"single"`, `"additional"`, `"single_flat"`.

Example request (A new service):

```bash
curl -X POST 'https://secure.yoprint.com/v1/api/store/your-team/imports/csv' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Accept: application/json' \
  -H 'X-Yoprint-Decimal-Format: exact-v1' \
  -H 'Content-Type: application/json' \
  -d '{
    "allow_cust_loc": "1",
    "entity_type": "service_pricing",
    "file": "qty/type,1 Color,2 Color\n1,10.00|11.00,12.00|13.00\n12,8.50|9.00,10.00|11.00\n",
    "locations": [
      {
        "id": "454DGJ3i6EQpq0"
      },
      {
        "id": "XDz01LsIUDpguv"
      }
    ],
    "name": "Puff print",
    "pricing_type": "additional",
    "target_mode": "create"
  }'
```

## Response

`200` Successful response.

Fields:

- `data` (object, required)
  - `created_at` (string (date-time), required)
  - `display_status` (string, required): A status slug formatted for UI display: matches `status`, or is `imported_with_unresolved_rows` when some items succeeded and some failed.
  - `entity_type` (string, required): What was imported. For CSV imports: `service_pricing` (the CSV type supported via API), or older uploader types (`customer`, `vendor`, `simple_product`, `variant_product`, `custom_product`). For Printavo imports: Printavo record types (such as `customer`, `vendor`).
  - `failed_item_count` (integer, required): Items that failed. Always 0 on service pricing imports: they are all or nothing, and any error fails with 422 without creating an import job. Only Printavo imports and retired CSV uploader jobs can have failed items.
  - `id` (string, required): The import job's id (a hash id).
  - `source_type` (string, required): Where the data came from. One of: `"csv"`, `"printavo"`.
  - `status` (string, required): `completed`, `completed_with_errors` and `failed` are final. One of: `"pending"`, `"staged"`, `"ready_to_commit"`, `"committing"`, `"completed"`, `"completed_with_errors"`, `"failed"`.
  - `success_item_count` (integer, required): Items saved.
  - `total_item_count` (integer, required): Rows (items) in the job. A service pricing job has one item: the whole price table.
  - `already_imported_count` (integer, optional): Items skipped because they were imported before.
  - `auto_commit` (boolean, optional): True when the job saves its rows without a separate review step.
  - `completed_at` (string (date-time) | null, optional): When the job reached a final status.
  - `display_status_label` (string | null, optional): A human-readable status label: `"Imported with unresolved rows"` when `display_status` is `imported_with_unresolved_rows`, otherwise null.
  - `duplicate_count` (integer, optional): Items skipped as duplicates within the job.
  - `error_csv_url` (string | null, optional): Web app download URL for a failed-rows CSV. Null for service pricing and Printavo jobs, and for new imports. Only retired CSV uploader jobs (pre-September 2026) may have a link. Because this is a web app link that API keys cannot open, download the file via the API using `GET /v1/api/store/{team_slug}/imports/{hash_id}/artifacts/error-csv`. Check `failed_item_count` to see if items failed.
  - `legacy_status` (string | null, optional): An older status word kept for the web app, such as `import_success` or `validation_failed`. Prefer `status`.
  - `source_account_ref` (string | null, optional): The connected source account, for Printavo jobs.
  - `target_mode` (string | null, optional): Service pricing jobs: `existing_service` or `create`. Otherwise usually null.
  - `unresolved_item_count` (integer, optional): Failed items: the same number as `failed_item_count`.
  - `updated_at` (string (date-time) | null, optional)
- `status` (integer, required)
- `success` (boolean, required)

Example response:

```json
{
  "data": {
    "already_imported_count": 0,
    "auto_commit": true,
    "completed_at": "2026-11-03T09:30:02+00:00",
    "created_at": "2026-11-03T09:30:00+00:00",
    "display_status": "completed",
    "display_status_label": null,
    "duplicate_count": 0,
    "entity_type": "service_pricing",
    "error_csv_url": null,
    "failed_item_count": 0,
    "id": "L4wYdA6Rav79IZ",
    "legacy_status": "import_success",
    "source_account_ref": null,
    "source_type": "csv",
    "status": "completed",
    "success_item_count": 1,
    "target_mode": "existing_service",
    "total_item_count": 1,
    "unresolved_item_count": 0,
    "updated_at": "2026-11-03T09:30:02+00:00"
  },
  "status": 200,
  "success": true
}
```

## Errors

- `401` The API key is missing or wrong (`unauthenticated`).
- `403` You are not allowed to do this. `api_access_not_in_plan`: your team's plan doesn't include API access (Pro and above, with an active subscription or trial), so every request answers this until the team upgrades. When your role lacks the permission the operation needs, the body is only `{"message": "Insufficient Permission"}`. Other refusals use the error envelope, with `unauthorized` or `user_is_not_authorized`.
  - When: You lack the service permissions the target mode needs (a plain `{"message": "Insufficient Permission"}` body, not the usual envelope). Checked early, before `target_mode`, the service and the file's contents; without a valid `target_mode` it asks for both create and update. Fix: Ask a team admin for permission to create or update services.
- `404` Not found. `page_not_found`: the team slug is unknown, you are not a member of the team, or an id in the path is not one of this team's. `code: null`, with a message that starts "No query results for": there is no record with that id.
  - When: `service_id` matches no service of this team. Fix: Check the service's id.
- `422` The request was refused and changed nothing. `validation_failed` lists what to fix in `error.fields`; other codes name a business rule, explained in `error.message`. A few checks answer with only `{"message": "..."}`. An operation that calls another service (a payment provider, a carrier, an accounting app) can answer `connect_timeout` ("Please retry again") when that service does not answer in time: the change may or may not have happened there, so read the record back before you retry.
  - When: A field is missing or invalid, the file is not `.csv` or is over 6,240 KB, or a location id is not yours (`validation_failed`; `error.fields` names the field). Fix: Fix the field and send again.
  - When: The file breaks a rule: `error.code` `single_error`, the problem in `error.message`, and `error.detail`, a list holding the same text. A row problem starts with its line, as in "[LINE 3] Duplicate Quantity Break Detected: 12"; header, encoding and empty-file problems have no line. Fix: Fix the file and upload it again. Nothing was saved.
  - When: The file has more than 600 data rows: a plain `{"message": "This import expands to 601 source rows. The limit for service_pricing imports is 600 source rows per request."}` body, not the usual envelope. Fix: Split the table, or remove unused price breaks.
- `500` A business rule refused the request (see the operation's errors), or something failed inside YoPrint. Refusals use the error envelope with a code such as `action_not_allowed`; an unexpected failure answers `{"message": "Server Error"}`. Repeating a refusal gets the same answer; after an unexpected failure, read the record back before you retry a change.
  - When: A price or quantity is written like `.50` or with a leading space, or a second upload for the same service (or the same new service name) waited 45 seconds and the first was still running (`{"message": "Server Error"}`). Nothing was saved. Fix: Write numbers like `0.50` with no spaces; for the second case, retry after the first upload finishes.

## Notes

- Re-uploading the same file to the same service is safe: the result is the same table. With `create`, each successful upload makes another service, even with the same name.
- Line numbers in error messages count the header as line 1, so the first data row is line 2.
- Prices are saved as given, with up to 3 decimal places.
- The public API has no operation that lists services. A job's services carry their `service_id`; take it from there or from YoPrint. Product location ids come from `GET /v1/api/store/{team_slug}/service_location`.

## Related

- [Download the service pricing CSV template](https://developer.yoprint.com/reference/csv-imports/v1-csv-template-service-pricing.md): `PUT /v1/api/store/{team_slug}/csv/template/service_pricing`
- [List service locations](https://developer.yoprint.com/reference/service-locations/v1-service-location-index.md): `GET /v1/api/store/{team_slug}/service_location`
- [Get an import job](https://developer.yoprint.com/reference/csv-imports/v1-imports-show.md): `GET /v1/api/store/{team_slug}/imports/{hash_id}`
