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

# Jobs API

Create store analysis jobs, check status, poll for completion, and stream real-time progress over WebSocket.

## Overview

A **job** is one store analysis run. Creating a job kicks off the full scraping and analysis pipeline (sitemap → audit → product indexing → PageSpeed → export). This section covers job creation, lookup, status polling, retry, and the real-time WebSocket progress stream.

Base URL: `https://shopsniffer.com/api`

## `POST /jobs` Stable

Create a store analysis job. Starts the full pipeline.

**Auth:** API key, Better Auth session, **or** x402 payment.

**Create job**

```bash
curl -X POST https://shopsniffer.com/api/jobs \
  -H "X-API-Key: ss_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "domain": "allbirds.com",
    "webhook_url": "https://your-server.com/hooks/shopsniffer",
    "notify_email": "you@example.com"
  }'
```

### Request body

Shopify store domain, with or without protocol (e.g. `allbirds.com` or `https://allbirds.com`). Must be a public Shopify store with an accessible sitemap.

URL to POST a `job.completed` event to when the job finishes. Single-attempt delivery — see [webhooks](/webhooks).

Email address to notify on completion. Single-attempt delivery via the Workers email binding.

Job type. Currently only `full` is supported.

Indexer strategy. Reserved for internal use.

**Success**

```json
{
  "job": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "domain": "allbirds.com",
    "slug": "allbirds-com",
    "status": "pending",
    "created_at": "2026-04-12T10:30:00Z",
    "user_id": "usr_better_auth_id"
  }
}
```

### Response fields

**job.id**

UUIDv4 job identifier. Use this for all subsequent status, report, and download calls.

**job.slug**

URL-safe slug derived from the domain (dots replaced with hyphens).

**job.status**

Initial status is always `pending`. Transitions: `pending` → `processing` → `completed` | `errored`.

**job.user_id**

Better Auth user ID if authenticated via session or API key. Null for anonymous x402 payments.

### Error responses

```json title="400 Bad Request"
{ "error": "Invalid domain format. Please enter a valid domain like 'store.com'." }
```

```json title="400 Bad Request"
{ "error": "This domain does not appear to be a Shopify store." }
```

```json title="400 Bad Request"
{ "error": "This Shopify store is password-protected and cannot be scanned." }
```

```json title="401 Unauthorized"
{ "error": "Unauthorized" }
```

```json title="402 Payment Required (x402)"
{
  "error": "Payment Required",
  "x402": {
    "version": "1",
    "accepts": [{
      "scheme": "exact",
      "network": "base",
      "maxAmountRequired": "9990000",
      "resource": "https://shopsniffer.com/api/jobs",
      "payTo": "0x767131d92c41D56546eC72fecD0F1d63900fa9D9",
      "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
    }]
  }
}
```

See [x402 payment](/x402-payment) for handling the 402 flow.

---

## `GET /jobs` Stable

Look up a job by ID or by most recent for a domain. No authentication required.

Job UUID. Mutually exclusive with `domain`.

Store domain. Returns the most recent job for that domain. Mutually exclusive with `id`.

**By ID**

```bash
curl "https://shopsniffer.com/api/jobs?id=a1b2c3d4-e5f6-7890-abcd-ef1234567890"
```

**Completed job**

```json
{
  "job": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "domain": "allbirds.com",
    "slug": "allbirds-com",
    "status": "completed",
    "created_at": "2026-04-12T10:30:00Z",
    "completed_at": "2026-04-12T10:33:42Z",
    "product_count": 156,
    "collection_count": 24,
    "page_count": 18,
    "shop_meta": {
      "shop_name": "Allbirds",
      "currency": "USD",
      "theme_name": "Custom Theme",
      "theme_id": 123456789,
      "detected_apps": ["Klaviyo", "Judge.me", "Afterpay"]
    },
    "insights": {
      "top_vendors": [{ "name": "Allbirds", "count": 156 }],
      "price_range": { "min": 28, "max": 160, "avg": 98.5 },
      "product_types": [{ "type": "Shoes", "count": 82 }]
    },
    "downloads": {
      "csv": "products.csv",
      "productsJson": "products.json",
      "collectionsJson": "collections.json",
      "pagesJson": "pages.json"
    }
  }
}
```

Errors:

```json title="400 Bad Request"
{ "error": "Provide id or domain parameter" }
```

```json title="404 Not Found"
{ "error": "Job not found" }
```

---

## `GET /jobs/:id/status` Stable

Lightweight status check for polling. Returns only status and progress counts to minimize bandwidth. **No authentication required.**

Job UUID.

**Poll status**

```bash
curl https://shopsniffer.com/api/jobs/a1b2c3d4-e5f6-7890-abcd-ef1234567890/status
```

**Processing**

```json
{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "status": "processing",
  "domain": "allbirds.com",
  "product_count": 89,
  "collection_count": 24,
  "page_count": 18,
  "created_at": "2026-04-12T10:30:00Z",
  "updated_at": "2026-04-12T10:31:47Z"
}
```

For real-time progress updates, use the [WebSocket endpoint](#get-ws-jobid) instead of polling — no rate limits, push-based updates, lower latency.

---

## `POST /jobs/:id/retry` Stable

Retry a failed or completed job. Creates a new job with a new UUID; the original is left unchanged. **Auth:** Better Auth session.

UUID of the job to retry.

**Retry created**

```json
{
  "job": {
    "id": "new-retry-uuid",
    "domain": "allbirds.com",
    "slug": "allbirds-com",
    "status": "pending",
    "created_at": "2026-04-12T11:00:00Z"
  }
}
```

Errors: `400` if the job isn't in a retryable state, `401` if unauthenticated, `404` if the job doesn't exist.

---

## `GET /ws/:jobId` WebSocket

Real-time job progress stream via WebSocket. Push-based: no polling, sub-second latency, backed by a Cloudflare Durable Object that tracks per-step progress.

**Protocol:** WebSocket upgrade. Send `Upgrade: websocket` header.

**Auth:** None — the endpoint is keyed by job ID.

**Browser**

```typescript
const ws = new WebSocket(`wss://shopsniffer.com/api/ws/${jobId}`);

ws.addEventListener("message", (event) => {
  const msg = JSON.parse(event.data);
  // { type: "status", status: "processing", step: "indexing-products", progress: 0.42 }
  console.log(msg);
});

ws.addEventListener("close", () => {
  console.log("Job complete or connection closed");
});
```

### Message shape

Messages are JSON with a `type` discriminator:

```json
{ "type": "status", "status": "processing", "step": "indexing-products", "progress": 0.42 }
```

```json
{ "type": "step", "step": "pagespeed", "completed": true }
```

```json
{ "type": "complete", "job_id": "a1b2c3d4-…", "report_url": "https://shopsniffer.com/report/a1b2c3d4-…" }
```

The connection closes after a `complete` or `error` message.

---

## Status lifecycle

**pending**

Job accepted, waiting for the ScrapeShopWorkflow to pick it up (typically &lt;1s).
  **processing**

Workflow is actively scraping, auditing, or generating exports. `product_count` / `collection_count` / `page_count` update as items are indexed.
  **completed**

All steps finished, downloads are available, webhook and email notifications have been dispatched.
  **errored**

A workflow step failed after retries. The job is kept in the database for inspection. Use `POST /jobs/:id/retry` to create a new attempt.

## Next steps

**[Reports & downloads](/api/reports)**

Fetch the full report and download exported files.
  **[Webhooks](/webhooks)**

Get notified on completion instead of polling.
  **[Rate limits](/rate-limits)**

Limits on job creation and polling.
  **[x402 payment](/x402-payment)**

Pay per job without an account.