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

# Merchant import

> List import platforms, preview a catalog migration, and start or read a job with a Bearer API key.

All routes require:

```txt theme={null}
Authorization: Bearer mk_live_<your_key>
```

The caller must be able to access that store. A key with store scope `SELECTED` must include that store. Access mode `FULL` skips the scope list and still requires dashboard permission `manageProducts`. Access mode `RESTRICTED` must include the scope on the route and that same permission. `import:write` also satisfies `import:read`.

```http theme={null}
GET  /api/merchant/v1/stores/{storeId}/import/platforms
POST /api/merchant/v1/stores/{storeId}/import/preview
POST /api/merchant/v1/stores/{storeId}/import
GET  /api/merchant/v1/stores/{storeId}/import/jobs/{jobId}
```

`apiKey` in the preview and start bodies is the **source platform** credential, 8–2048 characters. It is not a Join Markt key. The path `storeId` selects the destination store. A `storeId` property in the JSON body is discarded.

Jobs are stored in memory on the server process that accepted `POST /import`. A restart, or a request handled by another process, returns `404`.

## Platforms

`GET /import/platforms` returns `{ "platforms": [ ... ] }`. The list does not depend on store data. Auth still requires the store id.

**id** `string`

`sellauth`, `sellhub`, `shoppex`, `komerza`, `sellapp`, or `shoppy`.

**name** `string`

Display name.

**docsUrl** `string`

Vendor API docs URL.

**resources** `string[]`

Resource ids this platform can supply, in this order when present: `products`, `orders`, `reviews`, `categories`, `groups`, `coupons`.

**requiresShopId** `boolean`

`true` when the source API expects a shop identifier besides the API key.

**shopIdLabel** `string`

Label for that identifier.

**apiKeyPlaceholder** `string`

Hint for the source credential.

| `id` | `name` | `requiresShopId` | `shopIdLabel` | `resources` | `apiKeyPlaceholder` | `docsUrl` |
| - | - | - | - | - | - | - |
| `sellauth` | SellAuth | `true` | Shop ID | products, orders, reviews, categories, groups, coupons | SellAuth API key (Bearer) | `https://docs.sellauth.com/` |
| `sellhub` | SellHub | `false` | Shop ID | products, orders, reviews, categories, groups, coupons | SellHub API key (storeId\_secret) | `https://docs.sellhub.cx/api` |
| `shoppex` | Shoppex | `false` | Shop ID | products, orders, reviews, categories, groups, coupons | Shoppex API key (shx\_…) | `https://docs.shoppex.io/` |
| `komerza` | Komerza | `true` | Store ID | products, orders, reviews, categories, coupons | Komerza API key (Bearer) | `https://docs.komerza.com/` |
| `sellapp` | SellApp | `true` | Store slug | products, orders, reviews, categories, groups, coupons | SellApp API key (Bearer) | `https://sell.app/docs/api` |
| `shoppy` | Shoppy | `false` | Shop ID | products, orders, reviews, coupons | Shoppy API key | `https://dev.shoppy.gg/` |

```json theme={null}
{
  "platforms": [
    {
      "id": "sellauth",
      "name": "SellAuth",
      "docsUrl": "https://docs.sellauth.com/",
      "resources": ["products", "orders", "reviews", "categories", "groups", "coupons"],
      "requiresShopId": true,
      "shopIdLabel": "Shop ID",
      "apiKeyPlaceholder": "SellAuth API key (Bearer)"
    }
  ]
}
```

The live `platforms` array contains all six rows in the table, in that order.

## Preview

`POST /import/preview` returns the resolved shop and per-resource counts. Counts cover `resources` from that platform. A count is a number, or `null` when the source did not return one.

**platform** `string`

Platform id from the request.

**platformName** `string`

Platform display name.

**shopId** `string`

Shop id resolved by the source adapter.

**shopName** `string | null`

Shop name from the source, or `null`.

**resources** `string[]`

Resource ids that platform can supply.

**counts** `object`

Map of those resource ids to a number or `null`.

```json theme={null}
{
  "platform": "sellauth",
  "platformName": "SellAuth",
  "shopId": "shop_123",
  "shopName": "Acme",
  "resources": ["products", "orders", "reviews", "categories", "groups", "coupons"],
  "counts": {
    "products": 12,
    "orders": null,
    "reviews": 0,
    "categories": 3,
    "groups": 1,
    "coupons": 2
  }
}
```

## Import job

`POST /import` returns `{ "jobId": "<uuid>" }` with status `201`. `GET /import/jobs/{jobId}` returns the job. `jobId` is a UUID. The job is visible only to the user who created it, and only for the store in the path.

**id** `string`

Job UUID. Same value as `jobId` from the start response.

**storeId** `string`

Destination store record id.

**userId** `string`

User id of the API key that started the job.

**platform** `string`

Platform id.

**status** `string`

`queued`, `running`, `completed`, or `failed`. The job is stored as `queued`, then moved to `running`, then to `completed` or `failed`.

**error** `string | null`

Present when the run fails. The message is the thrown error message, or `Migration failed`.

**createdAt** `number`

Unix time in milliseconds.

**updatedAt** `number`

Unix time in milliseconds.

**progress** `object[]`

One row per resource that was both requested and supported by the platform. Order is `categories`, `groups`, `products`, `orders`, `reviews`, `coupons`.

**progress row**

**resource** `string`

Resource id.

**imported** `number`

Rows written.

**failed** `number`

Rows that threw while writing.

**total** `number | null`

`null` for jobs started by this API.

**done** `boolean`

`true` after that resource finishes.

```json theme={null}
{
  "id": "6f1c0c3e-3e4a-4f0a-9c1a-0a0a0a0a0a0a",
  "storeId": "STORE_ID",
  "userId": "USER_ID",
  "platform": "sellauth",
  "status": "queued",
  "createdAt": 1761364800000,
  "updatedAt": 1761364800000,
  "progress": [
    {
      "resource": "categories",
      "imported": 0,
      "failed": 0,
      "total": null,
      "done": false
    }
  ]
}
```

`POST /import` does not wait for the run. Poll `GET /import/jobs/{jobId}`.

## List platforms

```http theme={null}
GET /api/merchant/v1/stores/{storeId}/import/platforms
```

Scope: `import:read`. No body and no query parameters.

**storeId** `string` *required*

Store record id.

```bash theme={null}
curl https://dash.joinmarkt.com/api/merchant/v1/stores/STORE_ID/import/platforms \
  -H "Authorization: Bearer mk_live_<your_key>"
```

## Preview import

```http theme={null}
POST /api/merchant/v1/stores/{storeId}/import/preview
```

Scope: `import:write`. JSON object.

**storeId** `string` *required*

Store record id.

**platform** `string` *required*

One of `sellauth`, `sellhub`, `shoppex`, `komerza`, `sellapp`, `shoppy`.

**apiKey** `string` *required*

Source platform credential, 8–2048 characters.

**shopId** `string | null`

Source shop identifier, trimmed, max 200 characters. Optional. See `requiresShopId` and `shopIdLabel` on the platform.

```json theme={null}
{
  "platform": "sellauth",
  "apiKey": "source-platform-key",
  "shopId": "shop_123"
}
```

```bash theme={null}
curl -X POST https://dash.joinmarkt.com/api/merchant/v1/stores/STORE_ID/import/preview \
  -H "Authorization: Bearer mk_live_<your_key>" \
  -H "Content-Type: application/json" \
  -d "{\"platform\":\"sellauth\",\"apiKey\":\"source-platform-key\",\"shopId\":\"shop_123\"}"
```

Validation errors are `400`. `error` is the Zod issues joined with `; `, with the field path prefixed (`platform: ...`).

## Start import

```http theme={null}
POST /api/merchant/v1/stores/{storeId}/import
```

Scope: `import:write`. Status `201`. Body matches preview, plus `resources`.

**storeId** `string` *required*

Store record id.

**platform** `string` *required*

Platform id. Same enum as preview.

**apiKey** `string` *required*

Source platform credential, 8–2048 characters.

**shopId** `string | null`

Source shop identifier, trimmed, max 200 characters. Optional.

**resources** `string[]` *required*

One to six of `products`, `orders`, `reviews`, `categories`, `groups`, `coupons`. Ids the platform does not support are dropped. If none remain, the status is `400` and `error` is `No importable resources selected`.

```json theme={null}
{
  "platform": "sellauth",
  "apiKey": "source-platform-key",
  "shopId": "shop_123",
  "resources": ["categories", "products"]
}
```

```bash theme={null}
curl -X POST https://dash.joinmarkt.com/api/merchant/v1/stores/STORE_ID/import \
  -H "Authorization: Bearer mk_live_<your_key>" \
  -H "Content-Type: application/json" \
  -d "{\"platform\":\"sellauth\",\"apiKey\":\"source-platform-key\",\"shopId\":\"shop_123\",\"resources\":[\"categories\",\"products\"]}"
```

```json theme={null}
{ "jobId": "6f1c0c3e-3e4a-4f0a-9c1a-0a0a0a0a0a0a" }
```

## Get import job

```http theme={null}
GET /api/merchant/v1/stores/{storeId}/import/jobs/{jobId}
```

Scope: `import:read`. No body and no query parameters.

**storeId** `string` *required*

Store record id. A job stored for another store returns `404`.

**jobId** `string` *required*

Job UUID from the start response.

```bash theme={null}
curl https://dash.joinmarkt.com/api/merchant/v1/stores/STORE_ID/import/jobs/JOB_ID \
  -H "Authorization: Bearer mk_live_<your_key>"
```

`404` with `error` `Migration job not found` when the id is unknown, belongs to another user, or belongs to another store.

## Errors

Failed responses use `{ "success": false, "error": "...", "code": "..." }`. A `500` also includes `errorId`.

| Status | Code | When |
| - | - | - |
| 400 | `BAD_REQUEST` | Invalid JSON, body is not an object, Zod validation, or `No importable resources selected`. An unknown platform id fails Zod (`platform: ...`). It does not return `Unknown platform`. |
| 400 | `migration_shop_required` | SellAuth or Komerza could not resolve a shop. `error` is `SellAuth Shop ID is required (no shop found for this API key)` or `Komerza Store ID is required (no store found for this API key)`. |
| 400 | `migration_auth_failed` | SellApp's credential probe failed. `error` starts with `SellApp auth failed`. |
| 400 | `migration_upstream_error` | The source responded `401` or `403`. Those are mapped to HTTP `400`. `error` is the source `message`, a string `error`, or `<label> returned <status>`. The JSON also includes numeric `status` (the source HTTP status). |
| 401 | `UNAUTHORIZED` | Missing, invalid, or revoked token. `error` is `Unauthorized`. Expired keys use `API key expired`. |
| 403 | `FORBIDDEN` | Key cannot access this store, or the scope / `manageProducts` is missing. `error` is `Forbidden`. |
| 404 | `NOT_FOUND` | `Migration job not found` |
| 429 | `RATE_LIMITED` | More than 120 requests in 60 seconds for this client IP, on the shared `merchant-api` bucket. The limit is checked before the key. `error` is `Too many requests`. |
| 500 | `INTERNAL_ERROR` | Unexpected failure. `error` is `Failed to list import platforms`, `Failed to preview import`, `Failed to start import`, or `Failed to load import job`. |
| 502 | `migration_upstream_error` | The source responded with any other HTTP error. Same JSON shape as the `400` upstream row, including numeric `status`. |
| 502 | `SERVICE_UNAVAILABLE` | The source request failed before an HTTP response, or Shoppy returned a body whose `status` is `false` or `"error"`. No extra `status` field. `error` is `<label> request failed: ...`, `Shoppy <path> request failed: ...`, or `Shoppy <path> returned error status`. |

A thrown error that includes a string `code` uses that value. Other failures use the code for the HTTP status. Upstream `401` and `403` are returned as HTTP `400`. Other upstream HTTP errors are returned as HTTP `502`.

## Related

* [API keys](https://docs.joinmarkt.com/developers/api-keys.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.