# Create a sync job

`POST 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.create`
- Demo key: not accepted
- Web page: https://stylor.ai/guides/rest/v1/create-sync-job

Enqueues a sync for the source right now, regardless of its `syncPeriod`.
The request has no body.

The job is created as `pending` and picked up by a background worker shortly
after. Poll **Retrieve sync job** for progress, or list the source's sync logs
once the job reports a `logId`.

The source must be `active`. `inactive` sources are rejected with `409` — update
the source first. The response is `200`, not `201`.

## One job at a time

If the source already has a `pending` or `processing` job, no new job is
created; the response returns that job's `jobId` and `status` with
`existing: true` and a `message`. This is the same rule the scheduler
follows, so a manual request never stacks up behind itself.

## Job lifecycle

`pending` → `processing` → `completed` or `failed`.

- Workers claim the highest-`priority`, then oldest, `pending` job and move it to
  `processing`, stamping `worker.workerId`, `worker.lockedAt` and
  `worker.startedAt`.
- While processing, the worker creates a sync log (`status: running`), fetches the
  whole catalogue, then diffs it against the dataset's ingest queue: new products
  are queued as `added`, changed products as `updated`, products missing from the
  store as `archived`. Counts describe what was queued for processing, not items
  that are already searchable.
- On success the job becomes `completed` with `result.stats`,
  `result.completedAt` and `logId`, and the source's `sync.previous` is set.
  On any error it becomes `failed` with `result.error` and, if a log was started,
  `logId`. A job that fails before a log exists (source or dataset gone, dataset
  archived) has no `logId`.
- A `processing` job whose worker stops heart-beating for 20 minutes is returned
  to `pending` and claimed again. `retries.count` is informational and is not
  incremented by this.
- Cancelling a `pending` job marks it `failed` with
  `result.error: "Job cancelled by user"`.
- `completed` and `failed` jobs are deleted 7 days after `result.completedAt`.

## Example request

```bash
curl -X POST "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 to sync.

## Responses

### 200

A job is queued — either newly created, or the one that was already in flight.

Content type `application/json`:

- `success` (boolean, required): Always `true` on a 2xx response.
- `jobId` (string (uuid), required): The job to poll.
- `status` (string, required): `pending` for a new job. `processing` only when an existing in-flight job was returned. [one of `"pending"`, `"processing"`]
- `createdAt` (string (date-time)): When the job was created. Only present when a new job was created.
- `existing` (boolean): `true` when an already queued or running job was returned instead of creating a new one. Absent otherwise.
- `message` (string): `A sync job for this source is already queued or in progress`. Only present alongside `existing: true`.

Example:

```json
{
  "success": true,
  "jobId": "c2e4a6b8-1d3f-4a5c-9e7b-6d8f0a2c4e6b",
  "status": "pending",
  "createdAt": "2026-09-12T09:39:58.110Z"
}
```

## 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: <sourceId> | No source with that `sourceId` belongs to the organization that owns the key, or the source belongs to a different `datasetId` than the path. The message ends with the id you sent. |
| 409 | Data source is not active | The source's `status` is `inactive`. |

### 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. |
