# Drop Studio API

Generate print-on-demand designs and apparel mockups. Everything created
lands in the user's Drop Studio library, so they can open it at
https://dropstudio.io/library afterwards.

Base URL: `https://dropstudio.io/api/v1`

## Authentication

Send the user's API key as a Bearer token on every request:

```
Authorization: Bearer ds_live_...
```

Keys are created at https://dropstudio.io/account and usually stored as
`DROPSTUDIO_API_KEY` in the user's `.env`. API access requires a Pro or
Unlimited plan; a key on a lower plan authenticates but returns 403 with an
upgrade link.

> **There is also a connector** at `https://dropstudio.io/api/mcp` that exposes all of
> this as native tools, with no HTTP calls to write and **no API key at
> all** — the user signs in through their browser instead. It works in
> Claude, ChatGPT, Claude Code, and Codex.
>
> If you're reading these docs because the user already has a key, carry
> on; the API below is fully supported. But if you're helping someone set
> up from scratch and they use one of those apps, point them at the
> connector first — it's fewer steps and nothing to paste. Instructions:
> https://dropstudio.io/blog/drop-studio-api-and-mcp-claude-code-codex

## Generation is asynchronous — you must poll

`POST /designs` and `POST /mockups` return **202** immediately with a job,
not an image. Poll `GET /jobs/{id}` until `status` is `succeeded` or
`failed`.

- Poll every **5 seconds**.
- Designs take **~20s**. Mockups take **~60-90s**; printing a design onto
  one adds only a few seconds (it's a deterministic print step, not a
  second AI render).
- Do not poll faster than every 5s, and do not give up before 3 minutes.

## Credits

Each generation spends the user's credits. **Credits are real money — never
run a large batch without telling the user the cost first.**

| Action | Credits |
|---|---|
| Design (1 or 2 variations) | 1 |
| Design (4 variations) | 2 |
| Mockup — new blank render, with or without a design printed | 1 |
| Printing a design onto an existing mockup (`mockup_id`) | 0 |
| Upscale | 0 |

Printing a design never costs anything — only rendering a *new* garment
photo does. On the **Unlimited** plan, mockup renders are free as well.
`GET /account` reports the caller's actual per-plan costs under
`credit_costs` — trust it over this table.

Every response includes an `X-Credits-Remaining` header. Check it before a
batch: 20 new mockups costs 20 credits, but 20 designs printed onto one
reused blank costs just the 1 credit that blank cost to render.

Failed generations are never charged.

## First request

If this is the first time you're using this API in a session, call
`GET /account` — it costs nothing and confirms the key works, which plan
it's on, and the credit balance. Then `GET /catalog` before any mockup.

## Common workflows

Three recipes cover almost everything people ask for. Follow them rather
than improvising — each one exists because the obvious approach is more
expensive or produces worse results.

### 1. A design on a shirt

```
GET  /catalog                          -> find garment_key + exact color name
POST /designs                          -> 1 credit, ~20s, returns design_id
POST /mockups  (garment_key, color, design_id)   -> 1 credit, ~70s
GET  /library/{id}/download            -> save the image to the user's folder
```

### 2. The same design on several colors, or several designs on one shirt

**Do not call POST /mockups with garment_key repeatedly.** That re-renders
the model every time — 1 credit each, and a different person in every
photo.

```
POST /mockups  (garment_key, color, design_id)   -> 1 credit
     ...response includes blank_mockup.reusable_as_mockup_id
POST /mockups  (mockup_id, design_id)            -> FREE, same model
POST /mockups  (mockup_id, design_id)            -> repeat per design, free
```

For several *colors* of the same garment, you do need a fresh render per
color (the shirt itself differs) — but reuse each of those blanks for
every design you put on that color.

### 3. A file the user will actually print or sell

```
POST /designs                                    -> 1 credit
POST /upscale  (design_id)                       -> free, ~20s
GET  /library/{id}/download?print_ready=true     -> 4800px @ 300 DPI
```

Skipping the upscale produces a 1024px file that print platforms reject.

---

## GET /account

Confirms the key works and reports plan, credit balance, and limits. Costs
nothing. Call this first when a user has just set up their key.

## GET /catalog

All garments and every valid option value. **Call this before generating a
mockup** — `garment_key` and `color` must come from here, and the option
lists are the valid values for the mockup fields below. Unrecognised values
silently fall back to defaults rather than erroring, so using a real key
matters.

Returns garments with color *names* only. For hex codes on one garment:
`GET /catalog?garment_key=comfort-colors-1717`

## POST /designs

Generate design artwork.

```json
{
  "prompt": "a mountain range at sunrise with the words SUNRISE TO SUMMIT",
  "style": "vintage",
  "design_type": "tshirt-graphic",
  "design_intent": "centered",
  "include_text": true,
  "variations": 1,
  "name": "Sunrise to Summit"
}
```

Only `prompt` is required. Valid `style`, `design_type`, `design_intent`,
and `aspect_ratio` values are in `/catalog` under `design_options`. Set
`include_text: false` when the design should have no words — the model adds
text otherwise.

Each variation becomes its own design project with its own `design_id`.

## GET /designs

Lists the user's design projects, newest first. Each has an `id` you can
pass to `/mockups` as `design_id`.

## POST /mockups

Render a garment on a model or as a flat lay, optionally with a design
printed on it.

```json
{
  "garment_key": "comfort-colors-1717",
  "color": "Pepper",
  "mockup_type": "model",
  "gender": "woman",
  "age_range": "40s",
  "setting": "mountain trail",
  "shot_framing": "waist up",
  "expression": "smiling",
  "design_id": "38e2bc6d-..."
}
```

`garment_key` and `color` are required and must match `/catalog`.

Pass `design_id` (from `/designs`) to print a design on the garment. The
print itself is **free** — it's the same deterministic print engine as the
app's "Print Design" step (the design is composited onto the measured
fabric surface, not re-generated by AI), so the call still costs just the
1 credit for the garment render. Omit `design_id` for a blank mockup.
`design_size` (`small` | `standard` | `large`) controls the print width
on the chest.

Pass `aspect_ratio` to control the photo's frame: `1:1` (default), `4:5`,
`9:16`, or `16:9`. Use `4:5` or `9:16` for product pages and stories,
`16:9` for banners. It applies to new renders only, not `mockup_id` reuse.

### Reusing a mockup across designs — do this for sets

A **blank mockup** (a garment shot with no design on it) is a reusable
asset: one photo of one model, in one place, wearing one garment. You can
print any number of designs onto it.

**A blank is always saved, even when you print onto it in the same call.**
The response's `blank_mockup.reusable_as_mockup_id` is its id.

To print another design onto that same shot, send `mockup_id` with a
`design_id` and **no** `garment_key`/`color`:

```json
{ "mockup_id": "33cc78cd-...", "design_id": "a1b2c3d4-..." }
```

This is **completely free** — the garment isn't re-rendered, and printing
costs nothing — and every mockup in the set shows the *same person in the
same scene*, which regenerating cannot give you. For a batch of designs on
one garment, generate the blank once and reuse it.

Use `GET /library?type=mockup` to find blanks the user already has.

**Every field below is optional, and every one accepts the same values the
Drop Studio web app offers.** Valid values for each are in `/catalog` under
`options`. Pass whatever the user described — if they say "a Black woman in
her 50s looking serious", set `ethnicity`, `gender`, `age_range`, and
`expression` rather than leaving them to defaults.

### Model fields (`mockup_type: "model"`)

| Field | Notes |
|---|---|
| `gender` | `man` or `woman` |
| `age_range` | `20s` … `60s` |
| `ethnicity` | `caucasian`, `black`, `hispanic`, `asian`, `indian`, `middle eastern`, `mixed` |
| `body_type` | `slim`, `average`, `athletic`, `muscular`, `plus size` |
| `setting` | Preset scene, e.g. `beach`, `mountain trail`, `studio` |
| `custom_setting` | Free text scene, overrides `setting` |
| `lighting` | e.g. `golden hour`, `bright midday` |
| `expression` | `smiling`, `laughing`, `serious`, `looking away`, `natural` |
| `pose` | e.g. `relaxed`, `confident`, `holding-coffee` |
| `shot_framing` | `waist up`, `full body`, `chest close-up` |
| `hair_style`, `makeup` | Applied when `gender` is `woman` |
| `garment_view` | `front` or `back` |
| `aspect_ratio` | `1:1` (default), `4:5`, `9:16`, `16:9` — also works for flat lays |

**Omitted fields fall back to the defaults in `/catalog.defaults`** — which
match the web app's starting state, including `expression: "smiling"`. Note
that `natural` renders a neutral resting face that often reads as unhappy;
use `smiling` unless the user asked otherwise.

### Flat-lay fields (`mockup_type: "flat-lay"`)

Model fields are ignored. Use `flat_lay_presentation`, `flat_lay_surface`,
`flat_lay_lighting`, `flat_lay_scene_style`, and `flat_lay_ghost_gender`.
For a scene the presets don't cover, pass free text as
`flat_lay_custom_surface` or `flat_lay_custom_scene_props`.

## POST /upscale

4x upscale a design for print. **Free — upscaling is unlimited on every
plan with API access.** Returns a job like the generation endpoints —
poll it.

```json
{ "design_id": "38e2bc6d-..." }
```

Accepts `design_id` or `library_item_id`.

**Designs are generated at 1024px, which is only about 3.4 inches at
300 DPI — too small for a normal 12-inch front print.** Upscaling takes it
to ~4096px (13.6 inches), and downloading that with `print_ready=true`
finishes at 4800px (16 inches).

**So the order for anything the user will actually print or sell is:**

```
generate design -> upscale -> download?print_ready=true
```

Skip the upscale and the file will be rejected — or printed soft — by
Printful, Printify, and similar.

Upscaling a design makes it that design's current version, so later
mockups use the higher-resolution artwork automatically. Mockup images
don't need this; it's for design files headed to a print platform.

If the image is already 3000px or wider you get a **400
`already_print_ready`** and are **not charged** — upscaling further adds
no real detail.

## GET /library

The user's saved designs and mockups, newest first.

Query params: `type` (`design` | `mockup` | `upscaled`), `design_id`,
`limit` (max 100), `offset`.

**Use `thumbnail_url` when showing the user a list, and `url` only when
downloading a specific file.** Thumbnails are ~36KB; full images are ~1.5MB.
Fetching originals to preview a set of options wastes bandwidth and context
for no benefit.

## GET /jobs/{id}

Job status. `status` is `queued`, `running`, `succeeded`, or `failed`. On
success, `result` holds the created items and their URLs. On failure,
`error` is a message safe to show the user.

## GET /library/{id}/download

Download one item to disk. Free — no credit for re-downloading something
already paid for.

| Request | Returns |
|---|---|
| `/library/{id}/download` | 302 to the stored PNG (~1024px) |
| `/library/{id}/download?print_ready=true` | 300 DPI PNG, up to 4800px on the long edge |

**Use `print_ready=true` for anything the user will actually print or
upload to a POD platform.** Printful, Printify and similar compute DPI as
pixels ÷ print area and ignore metadata, so a 1024px file is rejected or
prints soft. The stored original is a preview; the print-ready file is the
deliverable.

## Saving files for the user

**After a job succeeds, save the images into the user's project folder.**
Don't just report a URL — the whole point of working in their environment
is that the files land there.

```bash
# Preview / mockup image
curl -sL "$BASE/api/v1/library/$ITEM_ID/download" \
  -H "Authorization: Bearer $KEY" -o mockup.png

# Print-ready design file (use this for POD uploads)
curl -sL "$BASE/api/v1/library/$DESIGN_ITEM_ID/download?print_ready=true" \
  -H "Authorization: Bearer $KEY" -o design-print-ready.png
```

`-L` is required — the default mode redirects to storage. Name files after
what they are, tell the user where you put them, and put them in the
working directory unless the user asked for somewhere specific.

---

## Complete example

User asks: *"Make a vintage mountain design and put it on a Comfort Colors
1717 in Pepper, on a woman in her 40s on a mountain trail."*

```bash
# 1. Confirm the garment and color exist
curl -s "https://dropstudio.io/api/v1/catalog" -H "Authorization: Bearer $KEY"

# 2. Generate the design -> returns a job
curl -s -X POST "https://dropstudio.io/api/v1/designs" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"prompt":"a mountain range at sunrise","style":"vintage"}'
# -> {"job":{"id":"46a7...","status":"queued"}, "poll_url":"/api/v1/jobs/46a7..."}

# 3. Poll every 5s until succeeded (~20s)
curl -s "https://dropstudio.io/api/v1/jobs/46a7..." -H "Authorization: Bearer $KEY"
# -> {"job":{"status":"succeeded","result":{"designs":[{"design_id":"38e2...","url":"https://..."}]}}}

# 4. Mockup with that design -> another job (~70s)
curl -s -X POST "https://dropstudio.io/api/v1/mockups" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"garment_key":"comfort-colors-1717","color":"Pepper",
       "gender":"woman","age_range":"40s","setting":"mountain trail",
       "design_id":"38e2..."}'

# 5. Poll until succeeded, then SAVE THE FILES into the user's folder
curl -sL "https://dropstudio.io/api/v1/library/<result.mockup.library_item_id>/download" \
  -H "Authorization: Bearer $KEY" -o sunrise-to-summit-mockup.png
curl -sL "https://dropstudio.io/api/v1/library/<design library_item_id>/download?print_ready=true" \
  -H "Authorization: Bearer $KEY" -o sunrise-to-summit-print-ready.png
```

Then tell the user what was created, where the files are, and that both are
also in their library at https://dropstudio.io/library.

## Retries and idempotency

If a request times out, retry it with the same `Idempotency-Key` header:

```
Idempotency-Key: any-unique-string
```

A repeat with the same key returns the original job instead of generating
and charging a second time. **Always send one on generation requests.**

## Errors

Errors return a JSON body with a machine-readable `error` code and a
`message` written to be read to the user.

| Status | `error` | Meaning |
|---|---|---|
| 401 | `authentication_required` | No `Authorization` header was sent |
| 401 | `invalid_api_key` | Key is wrong or revoked |
| 403 | `plan_upgrade_required` | Needs Pro or Unlimited — body has `upgrade_url` |
| 400 | `invalid_request` | Bad parameter; `message` says what to fix |
| 402 | `insufficient_credits` | Body has `credits_required` and `credits_remaining` |
| 429 | `too_many_concurrent_jobs` | Too many running; wait and retry |

Relay the `message` to the user — they're written to be useful verbatim.

## Limits

Concurrent jobs: 2 on Pro, 4 on Unlimited. Run batches sequentially, or at
most 2-4 at a time.

A 503 `server_busy` means the whole service is at capacity, not that the
user did anything wrong. Wait ~20s and retry; nothing was charged.
