1. [Home](https://svgmill.com/)
2. API docs

# API docs

Send an image, get a vector file back. One endpoint does the work; the rest manage keys, credits and billing.

## Quickstart

Create an API key on your [account page](https://svgmill.com/account/#keys); new accounts start with 5 free credits. Then post an image to `/v1/vectorize`. The response body is the file.

```
curl https://svgmill.com/v1/vectorize \
  -H "Authorization: Bearer $SVGMILL_KEY" \
  -F image=@logo.png \
  -F format=svg \
  -D headers.txt -o logo.svg

# Same trace as a DXF, free: use the token from headers.txt
curl https://svgmill.com/v1/download \
  -H "Authorization: Bearer $SVGMILL_KEY" \
  -H "Content-Type: application/json" \
  -d '{"token":"img_…","format":"dxf"}' -o logo.dxf
```

```
import os, requests

key = os.environ["SVGMILL_KEY"]
with open("logo.png", "rb") as f:
    r = requests.post(
        "https://svgmill.com/v1/vectorize",
        headers={"Authorization": f"Bearer {key}"},
        files={"image": f},
        data={"format": "svg", "colors": "8"},
    )
r.raise_for_status()
open("logo.svg", "wb").write(r.content)
print(r.headers["X-Image-Token"], r.headers["X-Credits-Remaining"])
```

```
import { readFile, writeFile } from "node:fs/promises";

const form = new FormData();
form.append("image", new Blob([await readFile("logo.png")]), "logo.png");
form.append("format", "svg");

const res = await fetch("https://svgmill.com/v1/vectorize", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.SVGMILL_KEY}` },
  body: form,
});
if (!res.ok) throw new Error((await res.json()).error.message);
await writeFile("logo.svg", Buffer.from(await res.arrayBuffer()));
console.log(res.headers.get("x-image-token"), res.headers.get("x-credits-remaining"));
```

Every successful conversion returns three headers: `X-Image-Token` (use it to download other formats for free), `X-Credits-Charged` and `X-Credits-Remaining`.

## Authentication

Send your key as a Bearer token: `Authorization: Bearer svgmill_live_…`. Live keys spend credits; test keys (`svgmill_test_…`) never do. Keys are shown once when you create them and stored only as a hash, so keep them somewhere safe and revoke any you think have leaked.

The website uses a browser session instead of a key. Both spend from the same balance.

## Options and formats

| Field | Values | What it does |
| --- | --- | --- |
| `image` | file | PNG, JPG, WebP, GIF (first frame), BMP or TIFF, up to 30 MB and 16 megapixels. Or send `image_url` or `image_base64` in a JSON body. |
| `format` | `svg` `pdf` `eps` `dxf` `png` | The file you get back. Default `svg`. |
| `preset` | `logo` `bw` | Color logo and art (default), or one-color black and white. |
| `colors` | 2–64 | Caps the palette. Fewer colors give simpler shapes. |
| `mode` | `production` `preview` `test` | Production costs 1 credit. Preview is free and returns a watermarked PNG. Test needs a test key. |
| `token` | string | In a JSON body, re-traces an earlier image with new options instead of uploading it again. |

## Test mode

Build your integration without spending credits. Create a test key on your account page and call the API as usual: conversions are free and the result carries a “SVGMILL TEST” watermark. Test mode returns SVG, or PNG with `format=png`. Downloads need a live key.

Without any key, `mode=preview` returns a free watermarked PNG and a token you can download later for 1 credit. Anonymous previews are limited to 20 an hour per IP address.

## Errors

Errors always have the same shape, and a request that ends in an error uses no credit.

```
{ "error": { "code": "insufficient_credits", "message": "You have no credits left. Buy a pack or a plan to continue." } }
```

| Code | Status | Meaning |
| --- | --- | --- |
| `invalid_request` | 400 | A field is missing or out of range. The message says which. |
| `unauthorized` | 401 | No key or session, or the key was revoked. |
| `insufficient_credits` | 402 | No credits left. |
| `forbidden` | 403 | The key can’t do this, for example test mode with a live key. |
| `account_frozen` | 403 | The plan is frozen after a failed payment and there are no pack credits. |
| `not_found` | 404 | Unknown token, or its files are past the 30 days we keep them. |
| `decode` | 422 | The file isn’t a readable image. |
| `too_large` | 422 | Over 30 MB or 16 megapixels. |
| `rate_limited` | 429 | Too many requests. Wait for `Retry-After` seconds. |
| `internal` | 500 | Something failed on our side. Retry with the same `Idempotency-Key`. |
| `timeout` | 504 | The trace ran past its time limit. Try a smaller image or fewer colors. |
| `out_of_memory` | 507 | The image needs more memory than one job allows. Try a smaller image. |
| `busy` | 503 | Every converter is working on other images. Retry after the `Retry-After` seconds (5). Not charged. |

## Rate limits

Each API key can make 60 requests a minute. Every response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset` (Unix seconds). A 429 adds `Retry-After`.

## Idempotency

Every POST accepts an `Idempotency-Key` header. Repeat a request with the same key within 24 hours and you get the stored response instead of a second charge. Use it whenever you retry after a timeout or network error. It works for multipart uploads (`-F image=@…`) as well as JSON: a retry matches on its fields and file contents, not on the multipart boundary.

## Endpoint reference

Base URL `https://svgmill.com`. Generated from [openapi.json](https://svgmill.com/openapi.json) (OpenAPI 3.1.0).

## Vectorize

### POST `/v1/vectorize` Optional: API key or browser session

**Convert an image to a vector.** `production` (default) costs 1 credit and returns the file. `preview` is free, works anonymously and returns a watermarked PNG whose token can be downloaded later for 1 credit. `test` is free, needs a test key and returns a watermarked result. A failed trace is never charged. Send a JSON `token` to re-trace an earlier image with new options (1 credit).

#### Multipart body

| Field | Type | Notes |
| --- | --- | --- |
| `image` required | file |   |
| `mode` | `production` \| `preview` \| `test` | Default `production`. |
| `preset` | `logo` \| `bw` | Default `logo`. |
| `colors` | integer | 2–64. |
| `format` | `svg` \| `pdf` \| `eps` \| `dxf` \| `png` | Default `svg`. |

#### JSON body

| Field | Type | Notes |
| --- | --- | --- |
| `image_url` | string (uri) |   |
| `image_base64` | string | Raw base64 or a data: URL. |
| `token` | string | Re-trace an earlier image. |
| `mode` | `production` \| `preview` \| `test` | Default `production`. |
| `preset` | `logo` \| `bw` | Default `logo`. |
| `colors` | integer | 2–64. |
| `format` | `svg` \| `pdf` \| `eps` \| `dxf` \| `png` | Default `svg`. |

#### Responses

- `200` The vector file (or the preview PNG). See X-Image-Token, X-Credits-Charged, X-Credits-Remaining.
- `402` No credits
- `403` Frozen plan and no pack credits, or test mode without a test key
- `422` Not a readable image, or too large
- `429` Rate limited

Accepts an `Idempotency-Key` header.

### POST `/v1/download` API key or browser session

**Download a format of an earlier trace.** The first paid download of a preview token costs 1 credit. Every other format, and any later download of a paid token, is free.

#### JSON body

| Field | Type | Notes |
| --- | --- | --- |
| `token` required | string |   |
| `format` | `svg` \| `pdf` \| `eps` \| `dxf` \| `png` | Default `svg`. |

#### Responses

- `200` The file
- `401` Not signed in
- `402` No credits
- `404` Unknown or expired token
- `429` Rate limited

Accepts an `Idempotency-Key` header.

### GET `/v1/conversions` API key or browser session

**Conversion history.** Newest first. Files are removed after 30 days; rows stay.

#### Parameters

| Name | In | Notes |
| --- | --- | --- |
| `cursor` | query | `next_cursor` from the previous page. |
| `limit` | query | 1–100, default 50. |

#### Responses

- `200` A page of conversions
- `401` Not signed in
- `429` Rate limited

## Account

### POST `/v1/register` No auth

**Create an account.** Sends a verification link. A known email gets a sign-in link instead. Always 202, so the response does not reveal which emails exist. Disposable domains get 422.

#### JSON body

| Field | Type | Notes |
| --- | --- | --- |
| `email` required | string (email) |   |
| `return_to` | string | Same-origin page to return to after the link (web app). |

#### Responses

- `202` Link sent
- `422` Disposable email domain
- `429` Rate limited

Accepts an `Idempotency-Key` header.

### GET `/v1/verify` No auth

**Open an email link.** Verifies the email, grants 5 signup credits on the first verification only, and starts a session. With a `return_to` from registration it redirects there (web app); otherwise it shows the first live API key once (JSON with `Accept: application/json`).

#### Parameters

| Name | In | Notes |
| --- | --- | --- |
| `token` required | query | The token from the email link. |

#### Responses

- `200` Key page (HTML) or JSON
- `302` Redirect to return_to (web app)
- `400` Expired or used link
- `429` Rate limited

### POST `/v1/login` No auth

**Send a sign-in link.** Same flow as register. Existing accounts get no credits.

#### JSON body

| Field | Type | Notes |
| --- | --- | --- |
| `email` required | string (email) |   |
| `return_to` | string | Same-origin page to return to after the link (web app). |

#### Responses

- `202` Link sent
- `429` Rate limited

Accepts an `Idempotency-Key` header.

### POST `/v1/logout` browser session

**End the browser session.**

#### Responses

- `200` Signed out
- `401` Not signed in
- `429` Rate limited

Accepts an `Idempotency-Key` header.

### GET `/v1/account` API key or browser session

**Balance, grants, plan and usage.**

#### Responses

- `200` Account
- `401` Not signed in
- `429` Rate limited

### DELETE `/v1/account` API key or browser session

**Delete the account.** Deletes the user, keys, sessions and stored files, and cancels any plan. Ledger rows are anonymised.

#### Responses

- `200` Deleted
- `401` Not signed in
- `429` Rate limited

## Keys

### GET `/v1/keys` API key or browser session

**List API keys.**

#### Responses

- `200` Active keys
- `401` Not signed in
- `429` Rate limited

### POST `/v1/keys` API key or browser session

**Create an API key.** The secret is returned once and stored only as a SHA-256 hash.

#### JSON body

| Field | Type | Notes |
| --- | --- | --- |
| `label` | string |   |
| `test` | boolean | Default `false`. |

#### Responses

- `201` The new key, with its secret
- `401` Not signed in
- `429` Rate limited

Accepts an `Idempotency-Key` header.

### DELETE `/v1/keys/{id}` API key or browser session

**Revoke an API key.**

#### Parameters

| Name | In | Notes |
| --- | --- | --- |
| `id` required | path |   |

#### Responses

- `200` Revoked
- `401` Not signed in
- `429` Rate limited

## Billing

### GET `/v1/plans` No auth

**Packs and plans.**

#### Responses

- `200` The catalogue
- `429` Rate limited

### POST `/v1/checkout` API key or browser session

**Start a Stripe Checkout.** Packs use a one-off payment, plans a subscription. Tax is calculated by Stripe.

#### JSON body

| Field | Type | Notes |
| --- | --- | --- |
| `item` required | string |   |

#### Responses

- `200` Checkout URL
- `401` Not signed in
- `409` Already on a plan (use /v1/subscription)
- `429` Rate limited

Accepts an `Idempotency-Key` header.

### GET `/v1/purchases` API key or browser session

**Packs bought and invoices paid.**

#### Responses

- `200` Purchases
- `401` Not signed in
- `429` Rate limited

### GET `/v1/subscription` API key or browser session

**Current plan.**

#### Responses

- `200` Plan or null
- `401` Not signed in
- `429` Rate limited

### POST `/v1/subscription` API key or browser session

**Cancel, resume, change or reactivate the plan.** `cancel` takes effect at the period end. `change {item}`: upgrades are immediate, billed pro rata, and the extra credits for the rest of this month are granted now; downgrades take effect at the period end. `reactivate` works only from `inactive`: it pays the outstanding invoice first, then returns a Checkout URL for a new subscription.

#### JSON body

| Field | Type | Notes |
| --- | --- | --- |
| `action` required | `cancel` \| `resume` \| `change` \| `reactivate` |   |
| `item` | string |   |

#### Responses

- `200` Updated plan, or a URL to continue
- `401` Not signed in
- `402` Upgrade payment failed
- `429` Rate limited

Accepts an `Idempotency-Key` header.

### POST `/v1/billing-portal` API key or browser session

**Stripe Customer Portal link.**

#### Responses

- `200` Portal URL
- `401` Not signed in
- `429` Rate limited

Accepts an `Idempotency-Key` header.

---
Source: https://svgmill.com/docs/
