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

# API Reference

> Every endpoint, parameter, request body, and response for the Zupertry v1 API.

# API Reference

Base URL: `https://app.zupertry.com`

All `/v1/*` endpoints are authenticated with an API key:

```
Authorization: Bearer zt_live_sk_YOUR_KEY
```

Use a **test key** (`zt_test_sk_...`) for development — jobs are processed with a mock output and no credits are consumed.

***

## POST /v1/tryon

Submit a virtual try-on job. Returns immediately with a job ID. The job is processed asynchronously.

**Request body**

| Field               | Type     | Required | Description                                                                   |
| ------------------- | -------- | -------- | ----------------------------------------------------------------------------- |
| `model_image_url`   | `string` | yes      | Publicly accessible URL of the model photo (JPEG/PNG/WebP, max 10 MB)         |
| `garment_image_url` | `string` | yes      | Publicly accessible URL of the garment photo (JPEG/PNG/WebP, max 10 MB)       |
| `workspace_id`      | `string` | no       | Workspace to associate the job with. Defaults to your first active workspace. |
| `webhook_url`       | `string` | no       | URL to receive the job result. Overrides the org-level default webhook URL.   |

**Response 202**

```json theme={null}
{
  "job_id": "job_abc123",
  "status": "pending",
  "created_at": "2026-03-19T10:00:00.000Z",
  "mode": "live"
}
```

| Field        | Type     | Description                         |
| ------------ | -------- | ----------------------------------- |
| `job_id`     | `string` | Use this to poll `/v1/jobs/{jobId}` |
| `status`     | `string` | Always `pending` on submit          |
| `created_at` | `string` | ISO 8601 UTC timestamp              |
| `mode`       | `string` | `live` or `test`                    |

**Errors**

| Status | `error.code`           | When                                                     |
| ------ | ---------------------- | -------------------------------------------------------- |
| 401    | `invalid_api_key`      | Key not found                                            |
| 401    | `api_key_revoked`      | Key has been revoked                                     |
| 402    | `insufficient_credits` | Credit balance is 0 (live mode only)                     |
| 403    | `account_suspended`    | Organisation is suspended                                |
| 404    | `workspace_not_found`  | `workspace_id` not found or not in this org              |
| 422    | `validation_error`     | Missing fields, invalid URL, or unsupported image format |
| 429    | `rate_limit_exceeded`  | Too many requests                                        |
| 500    | `internal_error`       | Server error                                             |

**Example**

```bash theme={null}
curl -X POST https://app.zupertry.com/v1/tryon \
  -H "Authorization: Bearer zt_live_sk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model_image_url": "https://cdn.example.com/model.jpg",
    "garment_image_url": "https://cdn.example.com/shirt.jpg",
    "webhook_url": "https://your-app.com/webhooks/zupertry"
  }'
```

***

## GET /v1/jobs/{jobId}

Get a single job by ID.

**Path parameters**

| Param   | Type     | Description                               |
| ------- | -------- | ----------------------------------------- |
| `jobId` | `string` | The job ID returned from `POST /v1/tryon` |

**Response 200**

```json theme={null}
{
  "job_id": "job_abc123",
  "status": "completed",
  "workspace_id": "ws_xyz",
  "mode": "live",
  "output_url": "https://storage.googleapis.com/zupertry-outputs/...",
  "credits_consumed": 1,
  "webhook_url": "https://your-app.com/webhooks/zupertry",
  "webhook_status": "delivered",
  "created_at": "2026-03-19T10:00:00.000Z",
  "completed_at": "2026-03-19T10:00:05.231Z",
  "duration_ms": 5231
}
```

| Field              | Type             | Description                                                                       |
| ------------------ | ---------------- | --------------------------------------------------------------------------------- |
| `job_id`           | `string`         |                                                                                   |
| `status`           | `string`         | `pending` \| `processing` \| `completed` \| `failed` \| `cancelled` \| `refunded` |
| `workspace_id`     | `string`         |                                                                                   |
| `mode`             | `string`         | `live` or `test`                                                                  |
| `output_url`       | `string \| null` | Signed URL valid for 1 hour. `null` if not completed.                             |
| `credits_consumed` | `number \| null` | `null` until job completes                                                        |
| `error_code`       | `string \| null` | Set on failure                                                                    |
| `webhook_url`      | `string \| null` | Webhook URL for this job                                                          |
| `webhook_status`   | `string \| null` | `pending` \| `delivered` \| `failed` \| `retrying` \| `null`                      |
| `created_at`       | `string`         | ISO 8601 UTC                                                                      |
| `completed_at`     | `string \| null` |                                                                                   |
| `duration_ms`      | `number \| null` | Processing time in milliseconds                                                   |

**Errors**

| Status | `error.code`        | When                           |
| ------ | ------------------- | ------------------------------ |
| 401    | `invalid_api_key`   | Key not found                  |
| 401    | `api_key_revoked`   | Key revoked                    |
| 403    | `account_suspended` | Org suspended                  |
| 403    | `forbidden`         | Job belongs to a different org |
| 404    | `job_not_found`     | Job ID does not exist          |

**Example**

```bash theme={null}
curl https://app.zupertry.com/v1/jobs/job_abc123 \
  -H "Authorization: Bearer zt_live_sk_YOUR_KEY"
```

***

## GET /v1/jobs

List jobs for your account, newest first.

**Query parameters**

| Param          | Type      | Default | Description                                                                               |
| -------------- | --------- | ------- | ----------------------------------------------------------------------------------------- |
| `workspace_id` | `string`  | —       | Filter to one workspace                                                                   |
| `status`       | `string`  | —       | Filter by status: `pending`, `processing`, `completed`, `failed`, `cancelled`, `refunded` |
| `limit`        | `integer` | `20`    | Page size, max `100`                                                                      |
| `cursor`       | `string`  | —       | Pagination cursor from previous response                                                  |

**Response 200**

```json theme={null}
{
  "jobs": [...],
  "next_cursor": "eyJpZCI6ImpvYl94eXoiLCJ0IjoiMjAyNi0wMy0xOVQxMDowMDowMC4wMDBaIn0=",
  "has_more": true
}
```

| Field         | Type             | Description                                                             |
| ------------- | ---------------- | ----------------------------------------------------------------------- |
| `jobs`        | `array`          | Array of job objects (same shape as single job, without webhook fields) |
| `next_cursor` | `string \| null` | Pass as `cursor` param for the next page. `null` if no more results.    |
| `has_more`    | `boolean`        | Whether more results exist                                              |

**Example**

```bash theme={null}
# First page
curl "https://app.zupertry.com/v1/jobs?limit=50&status=completed" \
  -H "Authorization: Bearer zt_live_sk_YOUR_KEY"

# Next page
curl "https://app.zupertry.com/v1/jobs?limit=50&cursor=eyJpZCI6ImpvYl94eXoifQ==" \
  -H "Authorization: Bearer zt_live_sk_YOUR_KEY"
```

***

## DELETE /v1/jobs/{jobId}

Cancel a pending job and refund the reserved credit.

Only jobs with `status: pending` can be cancelled. Once a job moves to `processing`, it cannot be stopped.

**Path parameters**

| Param   | Type     | Description |
| ------- | -------- | ----------- |
| `jobId` | `string` | Job ID      |

**Response 200**

```json theme={null}
{
  "job_id": "job_abc123",
  "status": "refunded",
  "refunded_credits": 1
}
```

**Errors**

| Status | `error.code`          | When                           |
| ------ | --------------------- | ------------------------------ |
| 401    | `invalid_api_key`     | Key not found                  |
| 401    | `api_key_revoked`     | Key revoked                    |
| 403    | `forbidden`           | Job belongs to a different org |
| 404    | `job_not_found`       | Job ID does not exist          |
| 409    | `job_not_cancellable` | Job status is not `pending`    |

**Example**

```bash theme={null}
curl -X DELETE https://app.zupertry.com/v1/jobs/job_abc123 \
  -H "Authorization: Bearer zt_live_sk_YOUR_KEY"
```

***

## GET /v1/workspaces

List all active workspaces for your account.

**Response 200**

```json theme={null}
{
  "workspaces": [
    {
      "id": "ws_xyz",
      "name": "Default",
      "status": "active",
      "created_at": "2026-03-19T10:00:00.000Z"
    }
  ]
}
```

| Field        | Type     | Description                                |
| ------------ | -------- | ------------------------------------------ |
| `id`         | `string` | Use as `workspace_id` when submitting jobs |
| `name`       | `string` | Display name                               |
| `status`     | `string` | `active` or `archived`                     |
| `created_at` | `string` | ISO 8601 UTC                               |

**Example**

```bash theme={null}
curl https://app.zupertry.com/v1/workspaces \
  -H "Authorization: Bearer zt_live_sk_YOUR_KEY"
```

***

## GET /v1/account

Return account info: org name, plan, credit balance, and region.

**Response 200**

```json theme={null}
{
  "org_id": "org_def456",
  "name": "Acme Corp",
  "plan": "growth",
  "region_code": "IN",
  "currency_code": "INR",
  "credits": 347,
  "auto_reload_enabled": true
}
```

| Field                 | Type      | Description                                      |
| --------------------- | --------- | ------------------------------------------------ |
| `org_id`              | `string`  |                                                  |
| `name`                | `string`  | Organisation name                                |
| `plan`                | `string`  | `starter` \| `growth` \| `scale` \| `enterprise` |
| `region_code`         | `string`  | `IN`, `US`, `EU`, or `GLOBAL`                    |
| `currency_code`       | `string`  | `INR`, `USD`, or `EUR`                           |
| `credits`             | `number`  | Current credit balance                           |
| `auto_reload_enabled` | `boolean` |                                                  |

**Example**

```bash theme={null}
curl https://app.zupertry.com/v1/account \
  -H "Authorization: Bearer zt_live_sk_YOUR_KEY"
```

***

## Authentication

All `/v1/*` endpoints use API key authentication:

```
Authorization: Bearer <api_key>
```

| Key prefix    | Mode | Credits  | Vertex AI   |
| ------------- | ---- | -------- | ----------- |
| `zt_live_sk_` | Live | Consumed | Real        |
| `zt_test_sk_` | Test | Free     | Mock output |

Manage your API keys in the console under **Settings → API Keys**.

## Rate limits

Default rate limit: **60 requests per minute** per organisation.

If exceeded, the API returns:

```json theme={null}
{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Too many requests. Check the Retry-After header."
  }
}
```

Check the `Retry-After` response header for the number of seconds to wait. Contact sales for higher limits.

## Pagination

List endpoints use cursor-based pagination. The cursor is opaque — do not decode or construct it manually.

```javascript theme={null}
async function getAllJobs(apiKey) {
  const jobs = [];
  let cursor = null;

  do {
    const url = new URL('https://app.zupertry.com/v1/jobs');
    url.searchParams.set('limit', '100');
    if (cursor) url.searchParams.set('cursor', cursor);

    const res = await fetch(url, {
      headers: { Authorization: `Bearer ${apiKey}` },
    });
    const data = await res.json();

    jobs.push(...data.jobs);
    cursor = data.next_cursor;
  } while (cursor);

  return jobs;
}
```
