---
name: migrate-public-api-v1-to-v2
description: >
  Migrate an existing regional legacy /api integration to the Public
  API V2 contract. Use when code calls /api/uploads, /api/prompt, /api/process,
  /api/downloadImage, /api/getTotalAPI_points, or
  /api/points-deduction-logs and must move to /v2 files, relief jobs,
  artifacts, credits, and usage without inventing mappings for unsupported
  legacy behavior.
---

# Regional Public API: V1 → V2 Migration Guide

Use this Skill to migrate an **existing legacy V1 API integration**. Public API
V2 is a separate regional API surface; it does not rename, redirect, wrap, or
change the old `/api/*` routes.

This guide is self-contained and authoritative for migration behavior. It is
aligned with the regional Public API V2 contract as of 2026-08. Do not invent
endpoints, parameters, prices, status values, or legacy-to-V2 mappings that are
not listed here.

This Skill is intentionally brand-neutral. Select exactly one deployment
region before changing code. Use `cjzmai.com` only for CN and `wesculpt.ai`
only for Global; do not copy a hostname, product label, credential, or resource
identifier across regions.

If a project already calls the regional `/v2` API, stop: it is not a V1
migration. New V2 integrations should use the current V2 developer
documentation directly.

## 0. The big picture

V1 and V2 are different contracts, not two path prefixes for the same API.
The migration is a small workflow rewrite:

1. **Authentication becomes strict Bearer authentication.** V1 accepted
   several legacy API-key header/query forms. V2 accepts only
   `Authorization: Bearer <api-key>`.
2. **An image URL is no longer a relief-job input.** Upload the image with
   `POST /v2/files`, read the returned file `id`, and submit it as
   `source.file_id`.
3. **Creation, polling, and output retrieval are separate resources.** Create
   with `POST /v2/jobs`, poll `GET /v2/jobs/{job_id}`, then list outputs with
   `GET /v2/jobs/{job_id}/artifacts`.
4. **Responses use normal HTTP semantics and resource JSON.** Do not keep
   parsing the legacy nested `data.data`, `data.status`, `taskId`, or
   top-level `success` envelope.
5. **Billing is explicit.** A successful upload costs 1 API point. An accepted
   relief job costs 30 points for `standard` or 50 for `pro`. Upload and
   generation are separate charges.
6. **Safe retries are explicit.** Charged POST operations accept an optional
   `Idempotency-Key`. Without one, every repeated submission is a new billable
   request.
7. **Only the relief workflow has a confirmed V1 migration path.** AI Design,
   3D model generation, and model conversion are additive V2 capabilities,
   not replacements for arbitrary legacy `/api/prompt` task types.

### Migration procedure — follow in order

1. Search the codebase for all V1 paths and legacy response fields listed in
   §2 and §13.
2. Prove whether the integration belongs to CN or Global using deployment
   configuration, the existing production hostname, or explicit user input.
3. Inventory every V1 request field, response field, persistence field, retry,
   timeout, and UI state that the application actually uses.
4. Classify every `/api/prompt` call as relief or non-relief. Stop on
   non-relief calls unless the user separately chooses a V2 capability.
5. Rewrite the request flow using §3–§6.
6. Rewrite response, state, artifact, credit, and usage parsing using §7–§9.
7. Apply the regional and security rules in §10.
8. Run the static and mocked checks in §13. Do not run billable live operations
   without explicit user authorization.

## 1. Scope and stop conditions

### This Skill migrates

- legacy image upload used by a relief workflow;
- legacy relief task creation;
- legacy relief task polling;
- legacy watermark-free result lookup;
- legacy API-point balance lookup;
- legacy API-point deduction-log pagination.

### Stop and tell the user when

- the target region cannot be proved;
- `/api/prompt` creates something other than a relief job;
- a non-empty legacy free-form prompt materially changes the result;
- the old integration depends on multiple outputs from one submission;
- the requested legacy style has no value in the current public V2 style list;
- the application depends on `/api/history/paginated` or another unlisted
  `/api/*` route;
- the application needs a V1 response field or side effect with no mapping in
  this guide;
- migration would expose the API key in browser code;
- a paid live test is required but the user has not authorized it.

Do not interpret `/api/prompt_v2` or `/api/process_v2` as Public API V2. They
remain legacy website-style `/api/*` routes and are outside the `/v2` contract.

## 2. What to find in an existing project

Search source, configuration, tests, fixtures, documentation, saved records,
and retry workers for these V1 paths:

```text
/api/uploads
/api/prompt
/api/process
/api/downloadImage
/api/getTotalAPI_points
/api/points-deduction-logs
/api/history/paginated
```

Also search for these common V1 request/response names:

```text
image
modelWeight
reliefDownloadFormat
isRemoveBackground
image_num
taskId
image_id
data.data
data.status
success
active
completed
queued
textinfo
ids
limit
offset
```

Do not blindly delete every match. Record what the application uses and map it
by customer-visible meaning.

## 3. Regional base URLs, authentication, and envelopes

### 3.1 Select exactly one region

| Region | V1 hostname signal | Public API V2 base URL |
|---|---|---|
| China mainland (CN) | `https://www.cjzmai.com/api/...` | `https://www.cjzmai.com/v2` |
| International (Global) | `https://www.wesculpt.ai/api/...` | `https://www.wesculpt.ai/v2` |

Never choose a region from the developer machine's location, language, locale,
IP address, or a failed request. Use deployment truth or ask the user.

An API key, account, point balance, uploaded file, job, queue record, artifact,
and usage ledger belong to one region. Never retry a CN request against Global
or a Global request against CN. A regional failure is not permission to fall
back across regions.

### 3.2 Authentication

| Concern | V1 | Public API V2 |
|---|---|---|
| Header | Raw key or several legacy header forms were tolerated | `Authorization: Bearer <api-key>` only |
| Query-string key | Some V1 clients used `api_key` | Not accepted |
| Browser use | Historically possible in some clients | Do not expose the API key; call V2 from a trusted server |

Use one server-side environment variable for the key and one server-side
configuration value for the selected regional base URL. Never commit the key,
print it, place it in a URL, or send it to browser JavaScript.

### 3.3 Response model

V1 mixed application status inside HTTP 200 responses, for example:

```json
{
  "data": {
    "data": "...",
    "status": "Finish",
    "taskId": "...",
    "code": 200
  },
  "success": true,
  "message": "success"
}
```

V2 returns resource JSON on success and an error object on failure:

```json
{
  "error": {
    "type": "invalid_request_error",
    "code": "invalid_parameter",
    "message": "Human-readable explanation",
    "request_id": "..."
  }
}
```

Treat every non-2xx HTTP response as failure. Do not decide success only from a
JSON `success`, `status`, or `code` field.

## 4. Complete V1 → V2 endpoint map

| Legacy V1 | Public API V2 | Required migration |
|---|---|---|
| `POST /api/uploads` | `POST /v2/files` | Keep multipart field `file`; read response `id`, not the uploaded URL. |
| `POST /api/prompt` | `POST /v2/jobs` | Relief only. Replace URL input with `source.file_id` and translate public behavior fields. |
| `POST /api/process` | `GET /v2/jobs/{job_id}` | Move the id from JSON body to the URL path; parse V2 job status. |
| `POST /api/downloadImage` | `GET /v2/jobs/{job_id}/artifacts` | Replace `image_id` lookup with owned job artifacts; select by artifact `type`. |
| `POST /api/getTotalAPI_points` | `GET /v2/account/credits` | Change method to GET; read `total`, `used`, and `remaining`. |
| `GET /api/points-deduction-logs?limit=&offset=` | `GET /v2/account/usage?limit=&before=` | Replace offset/page navigation with the `next_before` cursor. |
| `GET /api/history/paginated` | No V2 equivalent | Keep on V1 temporarily or request a product decision. |
| Any other `/api/*` route | No confirmed mapping | Do not migrate by analogy. |

Public API V2 also provides `GET /v2/files/{file_id}` and
`GET /v2/capabilities`; these are useful V2 resources but are not direct V1
endpoint replacements.

## 5. Hard differences — the things that break if unfixed

### 5.1 Upload output: URL → owned file resource

V1 upload clients commonly read an image URL from nested response data and
pass that URL to task creation. V2 returns a file resource:

```json
{
  "id": "file_03f6d6b8f7db4cc7ad7305f605c07a40",
  "object": "file",
  "purpose": "relief_input",
  "filename": "input.png",
  "mime_type": "image/png",
  "bytes": 842311,
  "width": 2048,
  "height": 2048,
  "url": "https://...",
  "created_at": "2026-08-05T00:00:00Z"
}
```

Capture `id` and submit it as `source.file_id`. Do not pass the returned `url`
to `POST /v2/jobs`, reconstruct storage paths, or treat the file id as portable
between regions/accounts.

V2 upload accepts PNG, JPEG, WebP, BMP, or GIF, up to 10 MiB and 40
megapixels. The normalized output's longest edge is at most 4096 pixels.

### 5.2 Task id: nested `taskId` → resource `id`

V1 creation returned a task id inside the legacy envelope. V2 relief creation
returns a job resource and the identifier is its top-level `id`:

```json
{
  "id": "api_...",
  "object": "relief_job",
  "status": "queued",
  "tier": "standard",
  "relief_type": "shallow",
  "style": "general",
  "background_mode": "remove",
  "output_format": "tif",
  "points": 30,
  "progress": 0,
  "artifacts": []
}
```

Persist the V2 `id` as a job id. Do not keep a task-id prefix parser or infer
product behavior from the id string.

### 5.3 Polling: POST body → GET path

V1 polling used `POST /api/process` with `{"taskId":"..."}` and exposed
several booleans. V2 uses:

```http
GET /v2/jobs/{job_id}
```

V2 status values are:

```text
queued | running | finalizing | succeeded | failed
```

- `queued`, `running`, and `finalizing` are non-terminal.
- `succeeded` and `failed` are terminal.
- Do not translate `finalizing` to success; keep polling.
- Use `progress` for progress UI and tolerate `null` for relief jobs.
- `queue_position` may be `null` and must not control correctness.
- On failure, read `error.code` and `error.message`.
- Use bounded polling with a timeout and cancellation signal. A 2–5 second
  interval is reasonable unless the product specifies another interval.

The server finalizes completed jobs and refunds failed jobs even if the client
stops polling. Client polling is observation, not the trigger for settlement.

### 5.4 Result lookup: image ids → artifacts

V1 completion could return preview/result data and separate image ids, followed
by `POST /api/downloadImage`. V2 exposes owned artifact resources:

```http
GET /v2/jobs/{job_id}/artifacts
```

```json
{
  "object": "list",
  "job_id": "api_...",
  "job_status": "succeeded",
  "data": [
    {
      "id": "artifact_...",
      "object": "artifact",
      "type": "tif",
      "url": "https://..."
    }
  ],
  "has_more": false
}
```

Select an artifact by its `type`, not by filename or URL suffix. Supported
artifact types include `png`, `tif`, `vsm`, `exr`, `svg`, `glb`, `gltf`,
`obj`, `fbx`, `usdz`, `stl`, `3mf`, and `preview`; the actual list depends on
the job. Do not reconstruct or rewrite signed storage URLs.

### 5.5 Balance: one nested number → explicit totals

V1 balance lookup returned one nested point value. V2 returns:

```json
{
  "object": "credit_balance",
  "unit": "api_point",
  "total": 1000,
  "used": 320,
  "remaining": 680
}
```

Use `remaining` for an available-balance display. Preserve `total` and `used`
if the product shows package totals or consumption.

### 5.6 Usage pagination: offset → cursor

V1 used `limit` and `offset` plus page counts. V2 returns a cursor list:

```json
{
  "object": "list",
  "data": [
    {
      "id": "...",
      "object": "usage_event",
      "resource_id": "api_...",
      "points": 30,
      "type": "charge",
      "description": "...",
      "created_at": "2026-08-05T00:00:00Z"
    }
  ],
  "has_more": true,
  "next_before": 12345
}
```

Request the first page without `before`. If `has_more` is true, send the
returned `next_before` as the next request's `before`. Do not calculate an
offset. Charges have positive `points`; refunds have negative `points`.

### 5.7 Idempotency is optional, but omission is billable

Charged V2 POST routes accept `Idempotency-Key`:

- `POST /v2/files`
- `POST /v2/jobs`
- `POST /v2/design/jobs`
- `POST /v2/model/jobs`
- `POST /v2/model/conversions`

For exact retries, generate a stable operation key before the first request,
persist it with the pending operation, and reuse it only with the identical
normalized request.

- Same key + same request: return the original resource without a second
  charge.
- Same key + different request: HTTP 409 `idempotency_conflict`.
- Same key while the original is executing: HTTP 409
  `idempotency_in_progress`; retry later with backoff.
- No key: every repeated POST is a new operation and may charge again.

An upload key and a relief-job key represent different operations and must be
different. Do not reuse one key across endpoints, files, users, regions, or
changed parameters.

## 6. Relief request migration

### 6.1 Public V2 request

```json
{
  "source": { "file_id": "file_..." },
  "tier": "standard",
  "relief_type": "shallow",
  "style": "general",
  "background_mode": "remove",
  "output_format": "tif",
  "model_weight": 0.5
}
```

Only `source.file_id` is required. Omitted optional fields use the defaults
shown above.

### 6.2 Map by visible behavior, not internal V1 values

| V1 behavior or field | V2 field | Migration rule |
|---|---|---|
| Uploaded image URL in `image` | `source.file_id` | Re-upload through `/v2/files`; never pass the URL directly. |
| Customer selected basic service | `tier: "standard"` | Costs 30 API points. |
| Customer selected advanced service | `tier: "pro"` | Costs 50 API points. |
| Smaller/finer expected height difference | `relief_type: "shallow"` | Default; no price change. |
| Stronger volumetric expected height difference | `relief_type: "deep"` | No price change. |
| General/default style | `style: "general"` | Recommended safe default. |
| Supported specialized public style | `style` | Use the exact current public value in §15. |
| Result visibly removes or blackens background | `background_mode: "remove"` | Map the observed behavior, not the old boolean name. |
| Result visibly preserves background | `background_mode: "keep"` | Map the observed behavior, not the old boolean name. |
| `reliefDownloadFormat: "PNG"` | `output_format: "png"` | Lowercase V2 enum. |
| Legacy TIF output intent | `output_format: "tif"` | V2 default and recommended production depth format. |
| `reliefDownloadFormat: "VSM"` | `output_format: "vsm"` | V2 generates VSM from the no-watermark TIF with default export settings. |
| `modelWeight` | `model_weight` | Number from 0 through 1; default 0.5. |
| `image_num: 1` or omitted | one `POST /v2/jobs` | Direct mapping. |
| `image_num > 1` | multiple V2 jobs | No one-request equivalent; confirm the behavior and price with the user. |
| Non-empty legacy free-form prompt | no relief field | Stop if the application depends on it. Do not silently discard it. |
| Non-relief legacy task type | no automatic mapping | Stop and ask whether to remove it or adopt a V2-only capability. |
| Any unknown legacy field | no automatic mapping | Prove it is unused before removing it. |

Do not mechanically translate old service-level, depth, background, or style
implementation values. Determine the customer-visible outcome and choose the
documented V2 field. This avoids carrying historical naming inversions and
private routing identifiers into a public integration.

### 6.3 Multiple legacy outputs

Public V2 creates one relief job per request. If V1 requested multiple outputs:

1. ask whether the product still needs multiple outputs;
2. reuse the same owned `file_id` if the input is unchanged;
3. create one `/v2/jobs` request per desired output;
4. use a distinct idempotency key per job;
5. disclose that each accepted job is separately charged 30 or 50 points.

Do not emulate multiple outputs by retrying the same request without a key.

## 7. Job and artifact handling

### 7.1 Job fields

| V2 field | Meaning | Client rule |
|---|---|---|
| `id` | Stable job id | Persist and use in polling/artifact paths. |
| `object` | Job kind | Do not infer kind from the id. |
| `status` | Current lifecycle state | Handle all five values. |
| `progress` | 0–100 or nullable | Display only; status decides termination. |
| `queue_position` | Optional queue position | Informational; tolerate null. |
| `points` | Price recorded for the job | Do not recalculate from private rules. |
| `artifacts` | Output resources if present | After success, use the artifact list endpoint as the explicit retrieval path. |
| `error.code` | Machine-readable failure | Log without secrets and branch when useful. |
| `error.message` | Human-readable failure | Surface an appropriate message to the user. |
| `created_at` / `completed_at` | ISO-8601 timestamps | Parse as date-time strings, not unix integers. |

### 7.2 Bounded polling pattern

```javascript
async function waitForJob(baseUrl, apiKey, jobId, {
  intervalMs = 3000,
  timeoutMs = 30 * 60 * 1000,
  signal,
} = {}) {
  const deadline = Date.now() + timeoutMs;

  while (Date.now() < deadline) {
    if (signal?.aborted) throw new DOMException('Aborted', 'AbortError');

    const response = await fetch(`${baseUrl}/jobs/${encodeURIComponent(jobId)}`, {
      headers: { Authorization: `Bearer ${apiKey}` },
      signal,
    });
    const body = await response.json();
    if (!response.ok) throw new Error(body?.error?.message || `HTTP ${response.status}`);

    if (body.status === 'succeeded') return body;
    if (body.status === 'failed') {
      throw new Error(`${body.error?.code || 'job_failed'}: ${body.error?.message || 'Job failed'}`);
    }
    if (!['queued', 'running', 'finalizing'].includes(body.status)) {
      throw new Error(`Unknown job status: ${body.status}`);
    }

    await new Promise((resolve) => setTimeout(resolve, intervalMs));
  }

  throw new Error(`Timed out waiting for job ${jobId}`);
}
```

## 8. Billing, refunds, and price-preserving migration

| V2 operation | Charge | When charged | Failure behavior |
|---|---:|---|---|
| Successful file upload | 1 point | After validation and storage succeed | Validation is free; later storage/transaction failure is refunded. |
| Relief `tier=standard` | 30 points | When the validated job is accepted for queueing | Queue rejection or terminal failure refunds once. |
| Relief `tier=pro` | 50 points | When the validated job is accepted for queueing | Queue rejection or terminal failure refunds once. |
| AI Design `1k` / `2k` / `4k` | 15 / 20 / 30 points | When accepted | Terminal failure refunds once. |
| 3D model `basic` / `advanced` | 50 / 80 points | When accepted | Terminal failure refunds once. |
| 3D texture | +10 points | Added to accepted model job | Refunded with failed job. |
| 3D PBR | +10 points | Added to accepted model job; requires texture | Refunded with failed job. |
| Model conversion | 10 points | When accepted | Terminal failure refunds once. |

A new upload followed by one relief job normally costs 31 points for
`standard` or 51 for `pro`. Reusing an existing owned `file_id` does not repeat
the upload charge.

Only `tier` changes the relief generation price. `relief_type`, `style`,
`background_mode`, `output_format`, and `model_weight` do not. API prices are
fixed list prices; website membership, enterprise, and distributor discounts
do not apply.

Do not preserve V1 discount assumptions, download charges, or implicit retry
behavior. Use V2's returned `points` and account usage ledger as the public
billing truth.

## 9. Error handling and retries

### 9.1 HTTP statuses

| HTTP | Meaning | Retry? |
|---:|---|---|
| `400` | Malformed or invalid request | No; fix the request. |
| `401` | Missing, invalid, or expired Bearer key | No; fix authentication and confirm region. |
| `402` | Insufficient API points; no charge made | No automatic retry. |
| `404` | Owned file/job/resource not found | No; confirm id, account, and region. |
| `409` | Idempotency conflict or operation still in progress | Retry only `idempotency_in_progress`; never mutate a conflicting request. |
| `413` | Upload too large | No; resize or choose a valid file. |
| `422` | File/request validation failed | No; fix input. |
| `429` | Rate or concurrency limit | Yes, with bounded exponential backoff and jitter. |
| `5xx` | Temporary service failure | Retry with bounded exponential backoff using the same idempotency key for charged POSTs. |

Always parse `error.code`, `error.message`, and `error.request_id` when present.
Log the request id for support correlation, but never log API keys, request
authorization headers, or signed artifact URLs.

### 9.2 Retry rules

- GET requests may be retried with bounded backoff.
- A charged POST may be retried only with its original idempotency key and
  identical body/file.
- Do not generate a new key merely because a request timed out; that can create
  and charge a second operation.
- Do not retry validation, authentication, insufficient-credit, ownership, or
  moderation failures automatically.
- Stop after a finite attempt/time budget and report the last HTTP status,
  public error code, and request id.

## 10. Regional, security, and compatibility rules

1. Keep one explicit configured region per deployment.
2. Never fall back between CN and Global for API, keys, files, jobs, artifacts,
   point balances, usage, queues, databases, or storage.
3. Keep the API key on a trusted server. Browser and mobile clients should call
   the application's own authenticated backend, which then calls the selected
   regional Public API.
4. Do not place credentials in query strings, source, fixtures, screenshots,
   logs, or committed `.env` files.
5. Do not rewrite artifact hosts or construct object-storage URLs.
6. Do not change unrelated website `/api/*` behavior during a Public API V2
   migration.
7. A temporary V1 rollback path is allowed only when the user requests a staged
   rollout. It must be an explicit regional switch, never a silent fallback.
8. Keep V1 and V2 persistence fields distinguishable during a staged rollout;
   do not pass a V1 task id to V2 or treat a V2 file id as a V1 URL.

## 11. Worked migration examples

Examples use region-neutral environment variables. Set them to the exact CN or
Global hostname selected in §3.1. Never swap only because one region returns
an error.

### 11.1 curl: complete relief workflow

**Before — representative V1 flow:**

```bash
# Set LEGACY_API_BASE to the selected region's origin, without `/api`.

# Upload returned a nested image URL.
curl -X POST "$LEGACY_API_BASE/api/uploads" \
  -H 'Authorization: <api-key>' \
  -F 'file=@input.png'

# Creation sent that URL and legacy behavior fields.
curl -X POST "$LEGACY_API_BASE/api/prompt" \
  -H 'Authorization: <api-key>' \
  -H 'Content-Type: application/json' \
  -d '{
    "image": "https://legacy-upload.example/input.png",
    "modelWeight": 0.5,
    "reliefDownloadFormat": "PNG",
    "isRemoveBackground": false
  }'

# Polling posted the task id in JSON.
curl -X POST "$LEGACY_API_BASE/api/process" \
  -H 'Authorization: <api-key>' \
  -H 'Content-Type: application/json' \
  -d '{"taskId":"api_legacy_task_id"}'
```

The exact legacy service/depth/style values differ among historical clients.
Inspect the application and translate visible behavior with §6; do not copy
private or unknown values into V2.

**After — Public API V2:**

```bash
# Set PUBLIC_API_V2_BASE and PUBLIC_API_KEY in the trusted server environment.

# 1. Upload. This costs 1 point after success.
curl -X POST "$PUBLIC_API_V2_BASE/files" \
  -H "Authorization: Bearer $PUBLIC_API_KEY" \
  -H 'Idempotency-Key: upload-<stable-operation-id>' \
  -F 'file=@input.png'
# Read top-level .id as FILE_ID.

# 2. Create one relief job. This costs 30 points for standard.
curl -X POST "$PUBLIC_API_V2_BASE/jobs" \
  -H "Authorization: Bearer $PUBLIC_API_KEY" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: relief-<stable-operation-id>' \
  -d '{
    "source": {"file_id": "file_03f6d6b8f7db4cc7ad7305f605c07a40"},
    "tier": "standard",
    "relief_type": "shallow",
    "style": "general",
    "background_mode": "remove",
    "output_format": "tif",
    "model_weight": 0.5
  }'
# Read top-level .id as JOB_ID.

# 3. Poll until succeeded or failed.
curl "$PUBLIC_API_V2_BASE/jobs/$JOB_ID" \
  -H "Authorization: Bearer $PUBLIC_API_KEY"

# 4. After success, list owned output artifacts.
curl "$PUBLIC_API_V2_BASE/jobs/$JOB_ID/artifacts" \
  -H "Authorization: Bearer $PUBLIC_API_KEY"
```

### 11.2 Python: upload, create, poll, and retrieve

```python
import os
import time
import uuid

import requests

BASE = os.environ["PUBLIC_API_V2_BASE"]  # exact CN or Global base, including /v2
key_from_environment = os.environ["PUBLIC_API_KEY"]
HEADERS = {"Authorization": f"Bearer {key_from_environment}"}


def checked(response):
    body = response.json()
    if not response.ok:
        error = body.get("error", {})
        raise RuntimeError(
            f"HTTP {response.status_code} "
            f"{error.get('code', 'api_error')}: {error.get('message', 'Request failed')} "
            f"request_id={error.get('request_id')}"
        )
    return body


def migrate_relief(image_path):
    upload_key = f"upload-{uuid.uuid4()}"
    with open(image_path, "rb") as image:
        uploaded = checked(requests.post(
            f"{BASE}/files",
            headers={**HEADERS, "Idempotency-Key": upload_key},
            files={"file": image},
            timeout=120,
        ))
    file_id = uploaded["id"]

    job_key = f"relief-{uuid.uuid4()}"
    job = checked(requests.post(
        f"{BASE}/jobs",
        headers={
            **HEADERS,
            "Content-Type": "application/json",
            "Idempotency-Key": job_key,
        },
        json={
            "source": {"file_id": file_id},
            "tier": "standard",
            "relief_type": "shallow",
            "style": "general",
            "background_mode": "remove",
            "output_format": "tif",
            "model_weight": 0.5,
        },
        timeout=30,
    ))
    job_id = job["id"]

    deadline = time.monotonic() + 30 * 60
    while time.monotonic() < deadline:
        detail = checked(requests.get(
            f"{BASE}/jobs/{job_id}", headers=HEADERS, timeout=30
        ))
        status = detail["status"]
        if status == "succeeded":
            break
        if status == "failed":
            error = detail.get("error") or {}
            raise RuntimeError(
                f"{error.get('code', 'job_failed')}: {error.get('message', 'Job failed')}"
            )
        if status not in {"queued", "running", "finalizing"}:
            raise RuntimeError(f"Unknown job status: {status}")
        time.sleep(3)
    else:
        raise TimeoutError(job_id)

    artifacts = checked(requests.get(
        f"{BASE}/jobs/{job_id}/artifacts", headers=HEADERS, timeout=30
    ))
    tif = next((item for item in artifacts["data"] if item["type"] == "tif"), None)
    if not tif:
        raise RuntimeError("Succeeded job has no TIF artifact")
    return {"file_id": file_id, "job_id": job_id, "tif_url": tif["url"]}
```

Persist `upload_key` and `job_key` before their requests if the application
must survive a process crash and safely retry. The example keeps them local for
clarity.

### 11.3 TypeScript: trusted server wrapper

```typescript
type ApiErrorBody = {
  error?: { code?: string; message?: string; request_id?: string | null };
};

async function parseResponse<T>(response: Response): Promise<T> {
  const body = (await response.json()) as T & ApiErrorBody;
  if (!response.ok) {
    const error = body.error;
    throw new Error(
      `HTTP ${response.status} ${error?.code ?? 'api_error'}: ` +
      `${error?.message ?? 'Request failed'} request_id=${error?.request_id ?? 'none'}`,
    );
  }
  return body;
}

export async function createReliefJob({
  baseUrl,
  apiKey,
  fileId,
  idempotencyKey,
}: {
  baseUrl: 'https://www.wesculpt.ai/v2' | 'https://www.cjzmai.com/v2';
  apiKey: string;
  fileId: string;
  idempotencyKey: string;
}) {
  return parseResponse<{ id: string; status: string; points: number }>(
    await fetch(`${baseUrl}/jobs`, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${apiKey}`,
        'Content-Type': 'application/json',
        'Idempotency-Key': idempotencyKey,
      },
      body: JSON.stringify({
        source: { file_id: fileId },
        tier: 'standard',
        relief_type: 'shallow',
        style: 'general',
        background_mode: 'remove',
        output_format: 'tif',
        model_weight: 0.5,
      }),
    }),
  );
}
```

This function belongs in server-only code. Do not call it with a regional API
key embedded in a browser bundle.

### 11.4 Credit and cursor-usage migration

```python
credits = checked(requests.get(f"{BASE}/account/credits", headers=HEADERS))
remaining = credits["remaining"]

events = []
before = None
while True:
    params = {"limit": 100}
    if before is not None:
        params["before"] = before
    page = checked(requests.get(
        f"{BASE}/account/usage", headers=HEADERS, params=params, timeout=30
    ))
    events.extend(page["data"])
    if not page["has_more"]:
        break
    before = page["next_before"]
    if before is None:
        raise RuntimeError("Usage response has_more=true without next_before")
```

## 12. V2-only capabilities — do not fabricate V1 mappings

The following capabilities are available to a client already choosing to adopt
Public API V2. They are not automatic replacements for unrelated V1 task
types.

### 12.1 AI Design

`POST /v2/design/jobs`

| Field | Rule | Price effect |
|---|---|---:|
| `mode` | Required: `custom` or a stable public scene id from `/v2/capabilities`. | None by itself. |
| `prompt` | Required only for `custom`; preset modes use their official scene instruction. | None. |
| `source.file_id` | Required for preset modes; owned uploaded file. | Upload charged separately. |
| `second_source.file_id` | Optional for supported two-image modes; material modes ignore it. | Upload charged separately. |
| `resolution` | `1k` default, `2k`, or `4k`. | 15 / 20 / 30 points. |
| `aspect_ratio` | `auto` default or a documented ratio; `four_view_sheet` uses `1:1`. | None. |

Use `GET /v2/capabilities` for the current stable scene ids. Do not copy
website prompt text, guess modes, or enable automatic scene selection.

### 12.2 3D model generation

`POST /v2/model/jobs`

| `type` | Required input |
|---|---|
| `text_to_model` | `prompt` up to 2000 characters |
| `image_to_model` | `source.file_id` |
| `multiview_to_model` | `views.front.file_id` plus at least one of left/back/right |

`quality=basic` costs 50 points and `quality=advanced` costs 80. `texture=true`
adds 10. `pbr=true` requires texture and adds another 10.

### 12.3 Model conversion

`POST /v2/model/conversions` accepts:

```json
{
  "source": { "job_id": "succeeded-model-job-id" },
  "format": "obj"
}
```

Supported formats are `glb`, `obj`, `fbx`, `usdz`, `stl`, and `3mf`. An
accepted conversion costs 10 points. If the client only needs the generated
GLB, retrieve the original model artifact instead of creating a conversion.

All V2 job types use the same job polling and artifact endpoints.

## 13. Validation checklist

### 13.1 Static discovery after migration

Search the final code and explain every remaining hit:

```text
/api/uploads
/api/prompt
/api/process
/api/downloadImage
/api/getTotalAPI_points
/api/points-deduction-logs
/api/history/paginated
api_key=
taskId
image_id
modelWeight
reliefDownloadFormat
isRemoveBackground
image_num
data.data
data.status
active
completed
queued
textinfo
offset
```

A hit may be intentionally retained V1 code, a migration fixture, or unrelated
application data. It is not automatically an error, but it must be classified.

### 13.2 Required mocked/automated coverage

- correct fixed CN or Global base URL with no cross-region fallback;
- Bearer header present and API key absent from browser bundles/logs;
- valid upload and top-level file `id` parsing;
- upload validation failure with no job submission;
- relief request defaults and each translated public field;
- `queued`, `running`, `finalizing`, `succeeded`, and `failed` states;
- nullable progress and queue position;
- artifact selection by `type` without URL reconstruction;
- credits parsing using `total`, `used`, and `remaining`;
- cursor usage pagination using `next_before`;
- exact idempotent replay returns the original resource;
- changed request with the same key produces 409 conflict;
- timeout/5xx charged-POST retry reuses the same key and body;
- 400, 401, 402, 404, 409, 413, 422, 429, and 5xx handling;
- V1 calls with no V2 equivalent remain explicit rather than silently mapped;
- multi-output migration does not create accidental duplicate charges.

### 13.3 Live verification boundary

`GET /v2/account/credits` is non-billable and may be used as an authenticated
regional check only when the user authorizes access to that environment.

Do not use upload as a free authentication probe: a successful V2 upload costs
1 point. Do not create a generation or conversion task merely to prove the
migration.

If the user explicitly authorizes a paid smoke test:

1. confirm the target region and expected charge;
2. use one small supported image;
3. use `tier=standard` unless another behavior is specifically required;
4. create and persist new upload/job idempotency keys;
5. poll with a bound;
6. verify the requested artifact type;
7. read usage to confirm charge/refund behavior;
8. report points consumed without printing credentials or signed URLs.

## 14. Features with no confirmed V2 equivalent

| V1 behavior | V2 status | Required decision |
|---|---|---|
| `/api/history/paginated` | No endpoint | Keep V1 temporarily or redesign the product history source. |
| Arbitrary non-relief `/api/prompt` operations | No automatic mapping | Evaluate a documented V2-only capability or remove the feature. |
| Free-form prompt affecting legacy relief | No relief request field | Stop and confirm acceptable behavior. |
| Multiple outputs in one relief submission | One job per V2 request | Confirm count and separately billed jobs. |
| Old unsupported/private style value | No automatic mapping | Choose a current public style or use `general` with user approval. |
| Legacy preview/base64 response dependency | Artifact URLs | Rewrite the consumer or retain an explicit compatibility layer in the client. |
| Page-number/offset usage navigation | Cursor only | Rewrite UI/data loader around `next_before`. |
| Any unlisted `/api/*` route | Unknown | Do not infer from a similar name. |

## 15. Public relief parameter appendix

| Field | Required | Default | Allowed values / constraint | Price effect |
|---|---:|---|---|---|
| `source.file_id` | yes | none | Owned `file_...` from `/v2/files` | None at job submission |
| `tier` | no | `standard` | `standard`, `pro` | 30 or 50 points |
| `relief_type` | no | `shallow` | `shallow`, `deep` | None |
| `style` | no | `general` | Current public list below | None |
| `background_mode` | no | `remove` | `remove`, `keep` | None |
| `output_format` | no | `tif` | `png`, `tif`, `vsm` | None |
| `model_weight` | no | `0.5` | number from 0 through 1 | None |

### Choosing shallow or deep

- `shallow` is intended for smaller, finer height differences, commonly about
  0.5–2 mm in product guidance. It is usually suitable for coins, badges,
  fridge magnets, thin plaques, fine text, jewelry details, and bracelet
  interiors.
- `deep` is intended for stronger volume, commonly about 2–8 mm. It is usually
  suitable for people, faces, animals, sculptures, large decorative work, and
  bracelet exteriors.
- Choose from the expected physical result. A line-art source is not
  automatically shallow and a photograph is not automatically deep.

### Choosing background and format

- `remove`: only the main subject should become relief; a simple source
  background gives the cleanest separation.
- `keep`: the scene, mural, landscape, architecture, or background pattern must
  participate in the height map.
- `tif`: recommended for downstream depth/relief and production workflows.
- `png`: convenient for previews and common image tools.
- `vsm`: generated from the no-watermark TIF with default export settings; it
  does not reproduce manual web-editor adjustments.
- Start `model_weight` at 0.5. Lower values generally retain more source
  structure; higher values strengthen generation influence. Higher is not
  automatically better.

### Current public style values

```text
general
JewelryMoldsUniversal
JewelryMoldsCommemorativeCoin
JewelryMoldsBracelet
HotStampingBulgingUniversal
HotStampingThickCharacters
HotStampingThinCharacters
EmbroideryTexture
StoneCarvingUniversal
StoneCarvingLongChart
StoneCarvingYangHua
WoodCarvingUniversal
WoodCarvingLongChart
WoodCarvingYangHua
JadeMoldsUniversal
JadeMoldsPendant
JadeMoldsBrand
CopperAluminumUniversal
CopperAluminumLighter
CopperAluminumEngraving
```

Use `general` whenever the intended specialized style cannot be proved. Never
guess a value from implementation source, historical examples, or an old
unpublished identifier.

## 16. Migration completion report

When finished, report:

- selected region and exact V2 base URL;
- V1 call sites discovered;
- endpoints and behaviors migrated;
- endpoints intentionally retained on V1 and why;
- fields that required a user decision;
- request, response, status, artifact, billing, and pagination changes;
- idempotency and retry behavior;
- automated/static verification run;
- whether any live check was authorized, whether it was billable, and its
  result without credentials or signed URLs;
- rollback switch, only if the user requested one.

Do not declare the migration complete while an unexplained V1 call, legacy
response parser, cross-region fallback, unbounded poller, exposed API key, or
unsafe charged retry remains.
