The same engine, server-side.
Turn any image into clean, editable vector art — real paths, one fill per colour region. The API runs the same WebAssembly engine as the in-browser converter, on our machine instead of yours, so the SVG you get back is the SVG the app would have produced.
curl -X POST "https://vectortrace.app/api/v1/vectorize?format=svg" \ -F image=@logo.png \ -o logo.svg
One request, one file back. No key needed for the anonymous limits below.
Authentication
Anonymous requests work, with tight limits. A key lifts them and is sent as a bearer token. A key we do not recognise is refused with 401 rather than quietly downgraded to the anonymous tier — a silent downgrade turns a typo into a rate-limit error an hour later.
Authorization: Bearer $VECTORTRACE_KEY
Week 1 keys come from an allowlist in the deployment's environment. Self-service keys arrive with metered billing.
POST/api/v1/vectorize
Vectorize one raster image. Send it as a multipart file part or as base64 inside a JSON body. The response is the vector file itself, or the document as JSON when you ask for it.
PNG · JPG · JPEG · WebP · BMP · GIF
Request
| Part | Where | Value |
|---|---|---|
| image | multipart/form-data | The raster file. |
| image | application/json body | The same file, base64 encoded, with or without a data: prefix. Use one of the two. |
| options | JSON body, or form field | Any subset of the options below. Fields you leave out take the named preset's values. |
| format | query string | What to export. The query wins over the body. |
| Accept | request header | application/json returns the document and its stats instead of the file. |
Examples
curl -X POST "https://vectortrace.app/api/v1/vectorize?format=svg" \
-H "Authorization: Bearer $VECTORTRACE_KEY" \
-F "image=@logo.png" \
-F 'options={"preset":"logo","colors":8}' \
-o logo.svgimport { readFile, writeFile } from 'node:fs/promises'
const form = new FormData()
form.append('image', new Blob([await readFile('logo.png')]), 'logo.png')
form.append('options', JSON.stringify({ preset: 'logo', colors: 8 }))
const response = await fetch('https://vectortrace.app/api/v1/vectorize?format=svg', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.VECTORTRACE_KEY}` },
body: form,
})
if (!response.ok) throw new Error(await response.text())
await writeFile('logo.svg', Buffer.from(await response.arrayBuffer()))import json, os, requests
with open("logo.png", "rb") as image:
response = requests.post(
"https://vectortrace.app/api/v1/vectorize",
params={"format": "svg"},
headers={"Authorization": f"Bearer {os.environ['VECTORTRACE_KEY']}"},
files={"image": image},
data={"options": json.dumps({"preset": "logo", "colors": 8})},
)
response.raise_for_status()
open("logo.svg", "wb").write(response.content)JSON body
POST /api/v1/vectorize?format=svg
Content-Type: application/json
Accept: application/json
{
"image": "iVBORw0KGgoAAAANSUhEUgAA…",
"options": { "preset": "logo", "colors": 8 }
}Options
Every field is optional. A preset fills in the rest; a field you set wins over the preset. These are the same controls as the converter's inspector, and the same table the Rust engine reads.
| Field | Values | Default | Notes |
|---|---|---|---|
| preset | logo · line-art · photo · embroidery · vinyl · laser | logo | Named option sets. Same table as the app and as the Rust engine. |
| mode | color · binary | color | Binary traces a single ink; color clusters a palette first. |
| colors | 2 – 64 | 16 | Palette size after clustering. Ignored in binary mode. |
| filterSpeckle | pixels of area | 4 | Regions smaller than this are dropped. |
| cornerThreshold | 0 – 180 degrees | 60 | Turns sharper than this stay hard corners rather than curves. |
| pathPrecision | 0 – 4 | 2 | Decimals written into the path data. |
| curveFitting | pixel · polygon · spline | spline | Pixel keeps the staircase, polygon fits lines, spline fits cubics. |
| hierarchical | stacked · cutout | stacked | Stacked paints regions over each other; cutout makes every region disjoint. |
| spliceThreshold | 0 – 180 degrees | 45 | Angle at which a fitted curve is split in two. |
| lengthThreshold | pixels | 4 | Shortest polygon edge kept before simplification. |
| maxIterations | 1 – 100 | 10 | Curve-fit refinement budget per subpath. |
Responses and errors
Success
| Status | Content-Type | Body |
|---|---|---|
| 200 | image/svg+xml | The exported SVG file, inline, with a server-derived filename. |
| 200 | application/pdf | The exported PDF file, inline, with a server-derived filename. |
| 200 | image/png | The exported PNG file, inline, with a server-derived filename. |
| 200 | application/json | The vector document and its stats, with Accept: application/json. |
{
"document": {
"version": 1, "width": 512, "height": 512, "unit": "px",
"palette": ["#101114", "#e8491d"],
"elements": [{ "kind": "path", "fill": 0, "stroke": null, "subpaths": [ … ] }]
},
"stats": { "paths": 12, "nodes": 184, "colors": 2, "ms": 41, "estimatedSvgBytes": 3210 }
}Every answer carries the rate-limit headers, refused or not. X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset
Error envelope
One envelope, always. Branch on the code, never on the prose — the code is stable and the message is not.
{ "error": { "code": "IMAGE_TOO_LARGE", "message": "…" } }| Status | Code | When |
|---|---|---|
| 400 | DECODE_FAILED | The bytes are a format we accept but could not be decoded. |
| 400 | VALIDATION_ERROR | The body or a field did not match the contract. |
| 401 | UNAUTHORIZED | An Authorization header we could not read, or a key we do not know. |
| 413 | IMAGE_TOO_LARGE | The image is over this caller class's pixel limit. |
| 413 | PAYLOAD_TOO_LARGE | The body is over this caller class's byte limit. |
| 415 | UNSUPPORTED_FORMAT | The requested output format has no exporter in this build. |
| 415 | UNSUPPORTED_INPUT_FORMAT | The bytes are not one of the accepted rasters. |
| 429 | RATE_LIMITED | Hourly budget spent. Retry-After says when to come back. |
| 500 | ENGINE_ERROR | The tracer ran and failed on this image. |
| 500 | INTERNAL_ERROR | Our fault. Retry once, then tell us. |
Rate limits
| Tier | Max body | Max pixels | Requests | Counted by |
|---|---|---|---|---|
| Anonymous | 2 MB | 1 MP | 20 / hour | Client IP |
| Keyed | 20 MB | 16 MP | 600 / hour | Key handle |
An image over the pixel limit is refused, not downscaled. The converter in your browser reduces oversized artwork because the alternative is a quarter-gigabyte allocation in your own tab; here the ceiling is a budget, and returning coordinates that do not map onto the artwork you sent would be worse than an error.
OpenAPI
The reference above is generated from the same zod schemas the server validates with, so there is no hand-written spec to fall out of date. Point a client generator at it.
GET https://vectortrace.app/api/openapi.json
llms.txt
A plain-text summary of the product, the pages and this endpoint, written for language models and agents. The full version adds the page index and the preset table.
GET https://vectortrace.app/llms.txt GET https://vectortrace.app/llms-full.txt
CLI
week 10The engine as a command. Same presets, same output, no server in the loop.
npx @vectortrace/cli in.png -o out.svg --preset laser
MCP
week 10An MCP server exposing the engine to agents over stdio or streamable HTTP. Three tools:
| Tool | Does |
|---|---|
| vectorize_image | Traces an image and returns a document handle plus stats. |
| list_presets | Returns the preset table with every option value. |
| export_document | Serializes a handle to one of the export formats. |