# List sync jobs

`GET https://stylor.ai/api/v1/datasets/{datasetId}/sources/{sourceId}/sync/jobs`

- Authentication: private API key, sent as `Authorization: Bearer sgpt-sk-…::…`
- Rate-limit resource: `api.v1.datasets.sources.syncJobs.list`
- Demo key: not accepted
- Web page: https://stylor.ai/guides/rest/v1/list-sync-jobs

Returns the sync jobs queued or run for a source, newest first, optionally
filtered by `status`.

Finished jobs are short-lived: a `completed` or `failed` job is purged **7 days
after it finishes**. Sync logs are kept, so use **List sync logs** for long-term
history and this endpoint to watch the queue.

Only the first page is reachable from this endpoint: `pagination.page` is
always `1`; raise `limit` (up to 100) to see more jobs.

An unknown `datasetId` or `sourceId`, or a source that belongs to a different
dataset, returns `404`. A source with no jobs returns an empty list.

## Example request

```bash
curl -X GET "https://stylor.ai/api/v1/datasets/YOUR_DATASET_ID/sources/YOUR_SOURCE_ID/sync/jobs" \
  -H "Authorization: Bearer $STYLOR_API_KEY"
```

## Path parameters

- `datasetId` (string (uuid), required): The dataset the source belongs to.
- `sourceId` (string (uuid), required): The source whose jobs to list.

## Query parameters

- `status` (string): Return only jobs in this state. Omit or leave empty for all states. [one of `"pending"`, `"processing"`, `"completed"`, `"failed"`]
- `limit` (integer): Jobs to return. Must be an integer from 1 to 100; non-numeric and out-of-range values are rejected with `422` rather than clamped. [default `20`; minimum 1; maximum 100]

## Responses

### 200

The most recent jobs for the source.

Content type `application/json`:

- `success` (boolean, required): Always `true` on a 2xx response.
- `jobs` (array<SyncJob>, required): Jobs newest first. Each includes `priority`.
  - `jobId` (string (uuid), required): Unique identifier of the job. [example `"c2e4a6b8-1d3f-4a5c-9e7b-6d8f0a2c4e6b"`]
  - `sourceId` (string (uuid), required): The source being synced. [example `"8a4d2f1e-3c5b-4e6a-9d7f-2b1c3d4e5f6a"`]
  - `datasetId` (string (uuid), required): The dataset the source feeds. [example `"3f1c2a9e-6b7d-4c1e-9a2b-1d4e5f6a7b8c"`]
  - `status` (string, required): `pending` — waiting for a worker. `processing` — a worker holds the job. `completed` — finished; see `result.stats`. `failed` — errored or cancelled; see `result.error`. [one of `"pending"`, `"processing"`, `"completed"`, `"failed"`]
  - `priority` (integer): Workers claim higher values first, then older jobs. Jobs created through the API always have `0`. **Only present on list responses.** [default `0`; example `0`]
  - `logId` (string (uuid)): The sync log for this run. Set when the job finishes; absent while `pending`, and on jobs that were cancelled or failed before a log was started. [example `"d9f1b3a5-7c2e-4d6f-8a1b-3c5e7f9a1b2d"`]
  - `result` (object, required): Outcome of the run. `stats` is present from creation (all zeros until completion).
    - `stats` (SyncStats, required): Counters for one sync run. `retrieved` is how many products the store returned; the other four describe what the run did with them in the dataset's ingest queue. Items are processed asynchronously afterwards, so `added` and `updated` count queued changes rather than items that are already searchable.
      - `retrieved` (integer, required): Products fetched from the store (capped at 25,000 per run). [minimum 0; example `1284`]
      - `added` (integer, required): Products not previously in the dataset that were queued for import. [minimum 0; example `12`]
      - `updated` (integer, required): Existing products whose store data changed and were queued for re-processing. [minimum 0; example `37`]
      - `archived` (integer, required): Dataset items whose SKU no longer appears in the store, marked archived. [minimum 0; example `3`]
      - `failed` (integer, required): Products that could not be queued. Details are in the log's `error`-level entries. [minimum 0; example `0`]
    - `error` (string): Why the job failed. `Job cancelled by user` for cancelled jobs. Absent on other statuses. [example `"Shopify API access forbidden (403)"`]
    - `completedAt` (string (date-time)): When the job reached `completed` or `failed`. Absent before then. Purge timer runs from this time. [example `"2026-09-11T03:00:12.418Z"`]
  - `worker` (object): Lock information. Absent until a worker claims the job; `workerId` and `lockedAt` are cleared when it finishes, `startedAt` remains.
    - `workerId` (string): Identifier of the worker holding the job. Present only while `processing`. [example `"v1-sync-9c0f1a2b-3d4e-4f5a-8b6c-7d8e9f0a1b2c"`]
    - `lockedAt` (string (date-time)): Last heartbeat from the worker. A lock older than 20 minutes is released and the job returns to `pending`. [example `"2026-09-12T09:42:11.204Z"`]
    - `startedAt` (string (date-time)): When the worker began this run. [example `"2026-09-12T09:40:03.917Z"`]
  - `retries` (object, required): Retry bookkeeping. Informational only; stale-lock re-queues do not increment `count`.
    - `count` (integer, required): Recorded retry count. [default `0`; example `0`]
    - `maxRetries` (integer, required): Configured retry ceiling. [default `3`; example `3`]
    - `lastRetryAt` (string (date-time)): When the job was last retried. Absent if never retried.
  - `createdAt` (string (date-time), required): When the job was enqueued. [example `"2026-09-12T09:39:58.110Z"`]
  - `updatedAt` (string (date-time), required): When the job last changed, including worker heartbeats. [example `"2026-09-12T09:42:11.204Z"`]
- `pagination` (SyncJobPagination, required): Page metadata returned by **List sync jobs**. Differs from the shared `Pagination` shape.
  - `page` (integer, required): The page that was returned. Currently always `1`. [example `1`]
  - `limit` (integer, required): The `limit` that was applied. [example `20`]
  - `total` (integer, required): Total jobs matching the filter, across all pages. [example `2`]
  - `totalPages` (integer, required): `total` divided by `limit`, rounded up. [example `1`]

Example:

```json
{
  "success": true,
  "jobs": [
    {
      "jobId": "c2e4a6b8-1d3f-4a5c-9e7b-6d8f0a2c4e6b",
      "sourceId": "8a4d2f1e-3c5b-4e6a-9d7f-2b1c3d4e5f6a",
      "datasetId": "3f1c2a9e-6b7d-4c1e-9a2b-1d4e5f6a7b8c",
      "status": "processing",
      "priority": 0,
      "result": {
        "stats": {
          "retrieved": 0,
          "added": 0,
          "updated": 0,
          "archived": 0,
          "failed": 0
        }
      },
      "worker": {
        "workerId": "v1-sync-9c0f1a2b-3d4e-4f5a-8b6c-7d8e9f0a1b2c",
        "lockedAt": "2026-09-12T09:42:11.204Z",
        "startedAt": "2026-09-12T09:40:03.917Z"
      },
      "retries": {
        "count": 0,
        "maxRetries": 3
      },
      "createdAt": "2026-09-12T09:39:58.110Z",
      "updatedAt": "2026-09-12T09:42:11.204Z"
    },
    {
      "jobId": "7e5c3a1f-9b8d-4c2e-a6f4-1d3b5c7e9a0f",
      "sourceId": "8a4d2f1e-3c5b-4e6a-9d7f-2b1c3d4e5f6a",
      "datasetId": "3f1c2a9e-6b7d-4c1e-9a2b-1d4e5f6a7b8c",
      "status": "completed",
      "priority": 0,
      "logId": "d9f1b3a5-7c2e-4d6f-8a1b-3c5e7f9a1b2d",
      "result": {
        "stats": {
          "retrieved": 1284,
          "added": 12,
          "updated": 37,
          "archived": 3,
          "failed": 0
        },
        "completedAt": "2026-09-11T03:00:12.418Z"
      },
      "worker": {
        "startedAt": "2026-09-11T02:58:40.005Z"
      },
      "retries": {
        "count": 0,
        "maxRetries": 3
      },
      "createdAt": "2026-09-11T02:58:31.772Z",
      "updatedAt": "2026-09-11T03:00:12.419Z"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 2,
    "totalPages": 1
  }
}
```

## Errors

Every error is a JSON object with an `error` string. Match on the status code; the message is written for people.

| Status | Message | When |
| --- | --- | --- |
| 404 | Data source not found or access denied | No source with that `sourceId` belongs to the organization that owns the key, or the source belongs to a different `datasetId` than the path. Checked after parameter validation. |
| 422 | Limit must be between 1 and 100 | `limit` was supplied and is non-numeric, below 1 or above 100, and it is the only invalid parameter. |
| 422 | Invalid status. Must be one of: pending, processing, completed, failed | `status` was supplied and is not one of the four job states, and it is the only invalid parameter. |
| 422 | Validation failed. | Both `limit` and `status` are invalid. `details` lists one `{ path, message }` entry per invalid parameter, using the messages above. A single-parameter failure also carries `details`.Body: `{"error":"Validation failed.","details":[{"path":"limit","message":"Limit must be between 1 and 100"},{"path":"status","message":"Invalid status. Must be one of: pending, processing, completed, failed"}]}` |

### Shared authentication, quota and rate-limit errors

| Status | Message | When |
| --- | --- | --- |
| 401 | API key missing. | The `Authorization` header is absent, is not `Bearer <key>`, or the key does not have three dash-separated segments. |
| 401 | Authentication failed: public key is invalid or not recognized. | A public-key endpoint was called with a key that does not exist. |
| 401 | Authentication failed: private key provided instead of a public key. | A public-key endpoint was called with an `sgpt-sk-…` key. |
| 401 | Authentication failed: token is not recognized. | A private-key endpoint was called with a key whose lookup segment does not exist. |
| 401 | Authentication failed: private key is invalid. | A private-key endpoint was called with a key whose secret does not match. |
| 401 | Authentication failed: public key provided instead of a private key. | A private-key endpoint was called with an `sgpt-pk-…` key. |
| 401 | Authentication failed: bearer token format is invalid. | A private key was sent without its `::` lookup segment. |
| 401 | Authentication failed: API key could not be read. Regenerate the key. | The key's embedded organization data could not be decrypted or parsed. |
| 403 | Demo mode is not available for private-key routes. Demos are only supported with public API keys. | The demo key was sent to a private-key endpoint. |
| 403 | Demo mode is not supported on this API route. Check the documentation for available demo endpoints. | The demo key was sent to a public-key endpoint that opts out of demo mode. |
| 403 | No active subscription for this organization. | The organization that owns the key has no active subscription. |
| 403 | No quota found for organization "<organizationId>". | The organization has no quota record for the current billing period. |
| 403 | No entitlement for "<resource>". | The plan's quota template has no entry for this endpoint. |
| 403 | Access denied to "<resource>". | The plan's quota template turns this endpoint off. |
| 429 | Monthly quota exceeded for "<resource>" (<quota>/<quota> used). | The billing-period quota for this endpoint is exhausted. `Retry-After` gives the seconds until the period resets. |
| 429 | RPM limit exceeded for "<resource>" (<used>/<limit> used). | More than the allowed requests per minute were sent. `Retry-After` is 60. |
| 500 | Internal server error. | An unexpected failure on the server. The message is always this string; details are logged, never returned. Quota consumed by the request is refunded, as it is for every 5xx response. |
