Shapesoda
Vectorize images: PNG/JPG/WebP to clean SVG, PDF, EPS or DXF, with a before/after preview and color controls. Works without an account.
Hosted MCP Server
npx add-mcp 'https://shapesoda.com/mcp'Installs into Claude Code, Codex, Cursor and more
Documentation
Quickstart
Three steps from zero to your first SVG. The base URL is https://shapesoda.com; all requests go over HTTPS.
- Get an API key
Open API keys in your account (sign in with your email or Google if asked) and choose New key. The key starts with
ssk_and is shown only once — copy it into your secret store. New accounts get a free credit to try things out.export SHAPESODA_KEY="ssk_your_key_here" - Send an image
Post the raw image bytes as the request body and pass options in the query string:
Or send acurl -sS "https://shapesoda.com/v1/vectorize?colors=auto" \ -H "Authorization: Bearer $SHAPESODA_KEY" \ --data-binary @logo.png \ -D - -o logo.svgmultipart/form-dataform with the file in theimagefield and options as regular fields:curl -sS https://shapesoda.com/v1/vectorize \ -H "Authorization: Bearer $SHAPESODA_KEY" \ -F [email protected] \ -F colors=8 \ -F image_type=artwork \ -D - -o logo.svg - Read the response
On success you get
200with the SVG as the body (image/svg+xml).-D -prints the headers, which tell you what the request cost:HTTP/1.1 200 OK Content-Type: image/svg+xml; charset=utf-8 Content-Disposition: inline; filename="vector.svg" X-Credits-Charged: 1 X-Mode: live X-Credits-Remaining: 99 X-Request-Id: req_4f1c2a9e0b7d4c3a8e21Header Meaning X-Credits-ChargedCredits this request used: 1for a successful vectorization (whatever the number of formats),0in test mode and for free repeats. Failed requests are not charged.X-Modelive, ortestin test mode.X-Credits-RemainingImages you can still process right now (balance plus any included credits, capped by your spending limit). X-Request-IdUnique ID of the request. It is also returned on errors — include it when you contact support.
Authentication
Account and vectorization requests (POST /v1/vectorize, GET /v1/account, /mcp) must carry your API key as a bearer token:
Authorization: Bearer ssk_…
- Create, list and revoke keys on the API keys page of your account (up to 10 active keys per account). Revoking takes effect immediately.
- A missing, malformed or revoked key returns
401 unauthorized. Repeated failed attempts from one IP address are rate limited. - Keep the key secret and on your server only. The API does not send CORS headers, so it is not meant to be called from a web page: never call the API from a browser or a mobile app with your secret key, and never ship it in front-end code or commit it. If a key leaks, revoke it.
- Stored results use a different credential.
GETandDELETE /v1/results/{id}take the result token, not the API key. Send it asAuthorization: Bearer <result_token>(recommended).?token=in the URL is also accepted, but a token in a URL can leak into access logs, browser history and analytics — don’t use it where URLs are logged or shared. See Result storage. - One-time upload links of the MCP upload flow carry their own secret and need no extra authentication.
GET /v1/plansis public. - All keys of an account share one credit balance — the same balance you use on the website.
Code samples
Minimal, dependency-light examples: vectorize logo.png into logo.svg, get several formats with a preset, surface API errors, print the remaining credits. Your language choice is remembered across the page.
Vectorize an image
# --fail-with-body: exit non-zero on HTTP errors (the JSON error is written to logo.svg)
curl -sS --fail-with-body "https://shapesoda.com/v1/vectorize?colors=auto&image_type=artwork" \
-H "Authorization: Bearer $SHAPESODA_KEY" \
--data-binary @logo.png \
-D headers.txt -o logo.svg
grep -i x-credits-remaining headers.txt
// vectorize.mjs — Node.js 18+ (built-in fetch), no dependencies
import { readFile, writeFile } from "node:fs/promises";
const res = await fetch("https://shapesoda.com/v1/vectorize?colors=auto", {
method: "POST",
headers: { Authorization: \`Bearer ${process.env.SHAPESODA_KEY}\` },
body: await readFile("logo.png"),
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(\`${res.status} ${error.code}: ${error.message} (${error.request_id})\`);
}
await writeFile("logo.svg", await res.text());
console.log("Credits left:", res.headers.get("X-Credits-Remaining"));
# vectorize.py — pip install requests
import os
import requests
with open("logo.png", "rb") as f:
r = requests.post(
"https://shapesoda.com/v1/vectorize",
headers={"Authorization": f"Bearer {os.environ['SHAPESODA_KEY']}"},
files={"image": f},
data={"colors": "auto"},
timeout=120,
)
if r.status_code != 200:
e = r.json()["error"]
raise SystemExit(f"{r.status_code} {e['code']}: {e['message']} ({e['request_id']})")
with open("logo.svg", "w", encoding="utf-8") as out:
out.write(r.text)
print("Credits left:", r.headers["X-Credits-Remaining"])
<?php
// vectorize.php — PHP 8 with the curl extension
$remaining = null;
$ch = curl_init("https://shapesoda.com/v1/vectorize");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("SHAPESODA_KEY")],
CURLOPT_POSTFIELDS => ["image" => new CURLFile("logo.png"), "colors" => "auto"],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 120,
CURLOPT_HEADERFUNCTION => function ($ch, $line) use (&$remaining) {
if (stripos($line, "X-Credits-Remaining:") === 0) $remaining = trim(substr($line, 20));
return strlen($line);
},
]);
$body = curl_exec($ch);
if ($body === false) {
fwrite(STDERR, "Network error: " . curl_error($ch) . PHP_EOL);
exit(1);
}
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($status !== 200) {
$e = json_decode($body, true)["error"];
fwrite(STDERR, "$status {$e['code']}: {$e['message']} ({$e['request_id']})" . PHP_EOL);
exit(1);
}
file_put_contents("logo.svg", $body);
echo "Credits left: $remaining", PHP_EOL;
// main.go — standard library only: go run main.go
package main
import (
"bytes"
"fmt"
"io"
"log"
"net/http"
"os"
"time"
)
func main() {
img, err := os.ReadFile("logo.png")
if err != nil {
log.Fatal(err)
}
req, err := http.NewRequest("POST", "https://shapesoda.com/v1/vectorize?colors=auto", bytes.NewReader(img))
if err != nil {
log.Fatal(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("SHAPESODA_KEY"))
client := &http.Client{Timeout: 120 * time.Second}
resp, err := client.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
if resp.StatusCode != http.StatusOK {
log.Fatalf("%d: %s", resp.StatusCode, body) // {"error":{"code","message","request_id"}}
}
if err := os.WriteFile("logo.svg", body, 0o644); err != nil {
log.Fatal(err)
}
fmt.Println("Credits left:", resp.Header.Get("X-Credits-Remaining"))
}
Vectorize with a preset
The print preset: a PDF and an SVG 210 mm wide in one call, for 1 credit. Several formats come back as JSON, and each file is downloaded by its link with the result token.
# preset=print, two formats in one call (1 credit) → JSON with a link per format (needs jq)
curl -sS --fail-with-body "https://shapesoda.com/v1/vectorize?preset=print&format=pdf,svg&width=210&unit=mm" \
-H "Authorization: Bearer $SHAPESODA_KEY" \
--data-binary @logo.png -o result.json
# download each file with the result token (not the API key)
TOKEN=$(jq -r .result_token result.json)
for f in pdf svg; do
curl -sS --fail-with-body "$(jq -r .files.$f result.json)" \
-H "Authorization: Bearer $TOKEN" -o "logo.$f"
done
// preset.mjs — Node.js 18+ (built-in fetch), no dependencies
import { readFile, writeFile } from "node:fs/promises";
const url = "https://shapesoda.com/v1/vectorize?preset=print&format=pdf,svg&width=210&unit=mm";
const res = await fetch(url, {
method: "POST",
headers: { Authorization: \`Bearer ${process.env.SHAPESODA_KEY}\` },
body: await readFile("logo.png"),
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(\`${res.status} ${error.code}: ${error.message} (${error.request_id})\`);
}
// several formats → JSON with a link per format, stored for 24 hours
const result = await res.json();
for (const [format, link] of Object.entries(result.files)) {
const file = await fetch(link, { headers: { Authorization: \`Bearer ${result.result_token}\` } });
if (!file.ok) throw new Error(\`download ${format}: ${file.status}\`);
await writeFile(\`logo.${format}\`, Buffer.from(await file.arrayBuffer()));
}
console.log("Credits left:", result.credits_remaining);
# preset.py — pip install requests
import os
import requests
with open("logo.png", "rb") as f:
r = requests.post(
"https://shapesoda.com/v1/vectorize",
headers={"Authorization": f"Bearer {os.environ['SHAPESODA_KEY']}"},
files={"image": f},
data={"preset": "print", "format": "pdf,svg", "width": "210", "unit": "mm"},
timeout=120,
)
if r.status_code != 200:
e = r.json()["error"]
raise SystemExit(f"{r.status_code} {e['code']}: {e['message']} ({e['request_id']})")
# several formats → JSON with a link per format, stored for 24 hours
result = r.json()
for fmt, link in result["files"].items():
file = requests.get(link, headers={"Authorization": f"Bearer {result['result_token']}"}, timeout=120)
file.raise_for_status()
with open(f"logo.{fmt}", "wb") as out:
out.write(file.content)
print("Credits left:", result["credits_remaining"])
<?php
// preset.php — PHP 8 with the curl extension
function http(string $url, array $opts): array {
$ch = curl_init($url);
curl_setopt_array($ch, $opts + [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 120]);
$body = curl_exec($ch);
if ($body === false) throw new RuntimeException(curl_error($ch));
return [curl_getinfo($ch, CURLINFO_RESPONSE_CODE), $body];
}
[$status, $body] = http("https://shapesoda.com/v1/vectorize", [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("SHAPESODA_KEY")],
CURLOPT_POSTFIELDS => ["image" => new CURLFile("logo.png"), "preset" => "print", "format" => "pdf,svg", "width" => "210", "unit" => "mm"],
]);
if ($status !== 200) {
$e = json_decode($body, true)["error"];
fwrite(STDERR, "$status {$e['code']}: {$e['message']} ({$e['request_id']})" . PHP_EOL);
exit(1);
}
// several formats → JSON with a link per format, stored for 24 hours
$result = json_decode($body, true);
foreach ($result["files"] as $format => $link) {
[$status, $file] = http($link, [CURLOPT_HTTPHEADER => ["Authorization: Bearer " . $result["result_token"]]]);
if ($status !== 200) exit("download $format: $status" . PHP_EOL);
file_put_contents("logo.$format", $file);
}
echo "Credits left: {$result['credits_remaining']}", PHP_EOL;
// main.go — standard library only: go run main.go
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"time"
)
var client = &http.Client{Timeout: 120 * time.Second}
// do sends the request with a bearer token and returns the body of a 200 response.
func do(req *http.Request, token string) []byte {
req.Header.Set("Authorization", "Bearer "+token)
resp, err := client.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
if resp.StatusCode != http.StatusOK {
log.Fatalf("%d: %s", resp.StatusCode, body) // {"error":{"code","message","request_id"}}
}
return body
}
func main() {
img, err := os.ReadFile("logo.png")
if err != nil {
log.Fatal(err)
}
url := "https://shapesoda.com/v1/vectorize?preset=print&format=pdf,svg&width=210&unit=mm"
req, err := http.NewRequest("POST", url, bytes.NewReader(img))
if err != nil {
log.Fatal(err)
}
// several formats → JSON with a link per format, stored for 24 hours
var result struct {
Token string \`json:"result_token"\`
Files map[string]string \`json:"files"\`
CreditsRemaining int \`json:"credits_remaining"\`
}
if err := json.Unmarshal(do(req, os.Getenv("SHAPESODA_KEY")), &result); err != nil {
log.Fatal(err)
}
for format, link := range result.Files {
get, err := http.NewRequest("GET", link, nil)
if err != nil {
log.Fatal(err)
}
if err := os.WriteFile("logo."+format, do(get, result.Token), 0o644); err != nil {
log.Fatal(err)
}
}
fmt.Println("Credits left:", result.CreditsRemaining)
}
Check your balance
curl -sS https://shapesoda.com/v1/account \
-H "Authorization: Bearer $SHAPESODA_KEY"
const res = await fetch("https://shapesoda.com/v1/account", {
headers: { Authorization: \`Bearer ${process.env.SHAPESODA_KEY}\` },
});
const { credits } = await res.json();
console.log(\`${credits.available} images available\`);
import os
import requests
r = requests.get(
"https://shapesoda.com/v1/account",
headers={"Authorization": f"Bearer {os.environ['SHAPESODA_KEY']}"},
timeout=30,
)
r.raise_for_status()
print(r.json()["credits"]["available"], "images available")
<?php
$ch = curl_init("https://shapesoda.com/v1/account");
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("SHAPESODA_KEY")],
CURLOPT_RETURNTRANSFER => true,
]);
$data = json_decode(curl_exec($ch), true);
echo $data["credits"]["available"], " images available", PHP_EOL;
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
)
func main() {
req, _ := http.NewRequest("GET", "https://shapesoda.com/v1/account", nil)
req.Header.Set("Authorization", "Bearer "+os.Getenv("SHAPESODA_KEY"))
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
var out struct {
Credits struct {
Available int \`json:"available"\`
} \`json:"credits"\`
}
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
log.Fatal(err)
}
fmt.Println(out.Credits.Available, "images available")
}
Vectorize an image
POST/v1/vectorize1 credit per successful image
Converts one raster image into SVG, PDF, EPS, DXF or PNG — one format or several for 1 credit. The request is synchronous: the response arrives when the vector is ready, usually within a few seconds (up to about a minute for the largest images).
Request body
Send the image in one of three ways:
- Raw bytes — the file itself is the body; parameters go in the query string. The
Content-Typeheader is not used. - Multipart —
Content-Type: multipart/form-datawith the file in the fieldimage; parameters can be form fields or query parameters (form fields win). Any other file field is rejected. - By URL —
image_urlwith an empty body (rolling out).
The format is detected from the file content (its signature), not from the file name or Content-Type.
Parameters
All parameters are optional. Values are case-sensitive; unknown parameters or values and conflicting options (see Combinations) return 400 bad_request with the reason.
Fine-tuning vs presets. A preset only sets defaults. Every parameter below is applied on top of it and always wins: preset=cut&max_colors=4&stroke_width=0.2 is the cut preset with 4 colors. Without a preset, web is the starting point.
| Parameter | Values | Description |
|---|---|---|
| Basics | ||
preset | web print cut laser embroidery | Ready-made settings for the job; default web. See Presets. |
format | svg pdf eps dxf png | Output format, or a comma-separated list (svg,pdf) for 1 credit. Default: the preset’s format. See Output formats. |
image_type | auto artwork artwork_sharp photo | Kind of image. artwork — logos, icons, illustrations with smooth (anti-aliased) edges; artwork_sharp — hard pixel edges without anti-aliasing (pixel art, screenshots); photo — photographs and complex images. |
quality | auto high medium low | Quality of the input. Use low for blurry, noisy or heavily compressed JPEG images. |
detail | auto low medium high | Level of detail in the output. Applies to photos. |
mode | live test preview | test — free, watermarked result for building an integration (Test mode). preview — free PNG preview up to 1 MP, vectors unlocked later for 1 credit (Preview). |
retain | false true | true — also store a single-format result for 24 hours. See Result storage. |
retention_days | 1 – 30 | Keep the stored result this many days instead of 24 hours; free. See Retention. |
image_url | URL | A public image address instead of a file. See Image URL (rolling out). |
| Processing | ||
colors | auto unlimited 2 – 32 | Number of colors (the original option). An integer caps the palette (2 gives a two-color result); unlimited keeps every color the engine finds. Default auto. For a finer cap use max_colors. |
max_colors | 2 – 256 | Maximum number of colors; the engine may use fewer. Not combined with colors or palette. |
palette | 2–256 × #rrggbb | Fixed colors, comma-separated; the result uses only these. In the query string write # as %23. Not combined with colors or max_colors. |
palette_tolerance | 0 – 100 | With palette: how far (CIE76 ΔE) an image color may be from a palette color to snap to it; colors farther away keep their own value. |
min_area | 0 – 10000 | Smallest shape kept, in px²; smaller specks are dropped. Overrides the preset’s value, also for photos. |
max_pixels | 10000 – 4200000 | Downscale the input to at most this many pixels before tracing: faster, fewer nodes. The factor comes in X-Input-Scale. |
curve_tolerance | 0.01 – 10 | Curve fitting tolerance in source pixels (default about 0.5). Lower follows the contour closer with more nodes; higher gives fewer nodes. Also sets smoothness unless you pass it. |
smoothness | 1 – 10 | Contour smoothing: higher is smoother with fewer nodes. |
corners | sharp smooth | Keep corners sharp or round them. Default from the automatic analysis. |
denoise | 0 – 3 | Noise reduction before tracing (JPEG artifacts, grain): 0 off, 3 strongest. |
| Drawing | ||
draw | fill stroke stroke_edges | fill — filled shapes; stroke — outline of every shape, no fill (laser, engraving); stroke_edges — only the borders between shapes, each one once, so a shared edge is not cut twice. Default from the preset. |
layers | stacked cutouts cutouts_grouped | Shapes stacked on top of each other, cut out of each other (no overlaps), or cut out and grouped by color. Default from the preset. |
group_by | none color layer | Groups in the output: color — one group per color (shapes are cut out of each other); layer — by nesting depth (layer_0 is the background). SVG , PDF layers, EPS blocks, DXF layers. By default SVG groups by color only for cutouts_grouped, DXF always uses color layers, PDF and EPS have no groups. |
| Curves & stroke | ||
curves | all lines lines_arcs | Allowed segments: all — curves and lines; lines — straight segments only (for machines without curves); lines_arcs — DXF gets lines and arcs, other formats the same as lines. |
stroke_width | 0.01 – 100 | Stroke width; needs draw=stroke or stroke_edges. In output units, or in source pixels with stroke_scaling=scale. Default: a hairline. |
stroke_color | #rrggbb | Stroke color instead of each shape’s color. Same requirement as stroke_width. |
stroke_scaling | fixed scale | fixed (default) — the width stays in output units when the drawing is resized; scale — the width is in source pixels and scales with the drawing. |
| Gap filler | ||
gap_filler | off on | on — an underlay under the fills: each shape outlined in its own color, so no light gaps show between neighbours. Needs draw=fill. Default off. |
gap_filler_width | 0 – 10 | Width of the underlay outline in source pixels (default 1). Needs gap_filler=on. |
| Size | ||
width, height | number > 0 | Output size in unit. One of them — the other follows the aspect ratio; both — a sheet of that size, the drawing is placed by aspect. Up to 10 m. |
unit | px pt mm cm in | Unit of width / height; default px. |
aspect | fit fill stretch | With both width and height: fit (default) — the whole drawing inside the sheet; fill — covers the sheet, the excess is cropped; stretch — scaled per axis to the exact size. |
align_x, align_y | 0 – 1 | Placement of the drawing on the sheet with aspect=fit or fill; default 0.5 (centered). |
scale | >0 – 100 | Output size as a multiple of the source size. Not combined with width / height. |
dpi | 1 – 2400 | Pixels per inch of PNG output when the size is in pt, mm, cm or in; default 300. |
| Format-specific | ||
svg_version | 1.1 1.0 tiny1.2 | SVG flavor. tiny1.2 is the SVG Tiny profile for plotters and embedded viewers: no opacity, no gradient transforms. |
svg_fixed_size | true false | true (default) — width and height on the root element; false — only a viewBox, the SVG scales to its container. |
adobe_compat | false true | true — SVG friendly to Adobe Illustrator: size always set, stroke widths in drawing coordinates. |
dxf_curves | lines arcs splines | DXF curves as polylines (default), polylines with arcs, or SPLINE entities. curves=lines or lines_arcs wins. |
png_antialias | on off | Anti-aliasing in PNG; default on. |
Options are checked against each other; a conflict returns 400 bad_request:
palettecannot be combined withcolorsormax_colors, normax_colorswithcolors.palette_toleranceneedspalette.stroke_width,stroke_color,stroke_scalingneeddraw=strokeorstroke_edges(also when the preset sets it, e.g.laser).gap_filler=onneedsdraw=fill;gap_filler_widthneedsgap_filler=on.unitneedswidthorheight.scalecannot be combined withwidth/height.aspect,align_x,align_yneed bothwidthandheight(the alignment has no effect withaspect=stretch).dpineeds a size inpt,mm,cmorin.retention_daysworks withmode=liveonly and not withretain=false.mode=previewreturnspngonly.
Input limits
| Limit | Value | Error |
|---|---|---|
| Formats | PNG, JPEG, WebP, BMP (uncompressed, 24/32-bit), GIF (first frame), TIFF (first page) | 415 unsupported_format |
| File size | 10 MB | 413 too_large |
| Resolution | Up to 40 megapixels. Images larger than 4.2 MP or 4096 px on a side are downscaled automatically; the factor comes in X-Input-Scale | 422 too_many_pixels |
Downscaling doesn’t change the physical size you ask for with width / height. Larger images don’t produce better vectors: about 2000 px on the long side gives the fastest results.
Response
What comes back depends on the formats you ask for:
- One format (the default) —
200 OKwith the file itself as the body and the format’sContent-Type(see Output formats). SVG is servedinline, other formats as an attachmentvector.<format>. Nothing is stored unless you passretain=trueorretention_days. - Several formats, or
Accept: application/json— JSON with a download link per format; the result is stored for 24 hours. See Result storage.
| Header | Meaning |
|---|---|
X-Credits-Charged, X-Credits-Remaining | What the request cost and what is left (file responses; JSON has the same in fields). |
X-Mode | live, test or preview. |
X-Input-Scale | Only when the input was downscaled: the factor applied to each side, e.g. 0.5. |
X-Result-Id, X-Result-Url, X-Result-Token, X-Result-Expires | Only when the result is stored (retain=true, retention_days, mode=preview): the result, its secret token and when it is deleted. |
X-Request-Id | Unique ID of the request. |
On any error the body is JSON — see Errors.
Presets
A preset is a ready-made set of options for the job. Pass preset and adjust it with explicit options if needed — they always win over the preset. Without preset you get web: exactly the SVG the API has always returned.
| Preset | Default format | What it does | Use for |
|---|---|---|---|
web | SVG | Automatic colors, shapes stacked on top of each other. | Websites, apps, Figma |
print | Shapes cut out of each other, no overlaps. | Print shops, posters, documents | |
cut | SVG | 8 colors; shapes cut out and grouped by color — one group per vinyl color. | Vinyl cutters, stickers, Cricut, Silhouette |
laser | SVG | 2 colors, outlines only (draw=stroke). | Laser cutting and engraving; add format=dxf for CAD software |
embroidery | SVG | 12 colors, shapes cut out of each other, tiny specks dropped. | Embroidery digitizing |
Terminal · one call per preset
API="https://shapesoda.com/v1/vectorize"
AUTH="Authorization: Bearer $SHAPESODA_KEY"
# web (default): SVG for sites and apps, stacked shapes — the classic result
curl -sS "$API?preset=web" -H "$AUTH" --data-binary @logo.png -o logo.svg
# print: PDF, shapes cut out of each other; here 210 mm wide (A4)
curl -sS "$API?preset=print&width=210&unit=mm" -H "$AUTH" --data-binary @poster.png -o poster.pdf
# cut: SVG, 8 colors cut out and grouped by color — vinyl, stickers, Cricut
curl -sS "$API?preset=cut&width=4&unit=in" -H "$AUTH" --data-binary @sticker.png -o sticker.svg
# laser: 2-color outlines; DXF 100 mm wide for the laser software
curl -sS "$API?preset=laser&format=dxf&width=100&unit=mm" -H "$AUTH" --data-binary @sign.png -o sign.dxf
# embroidery: 12 colors, no tiny shapes; a fixed thread palette instead of 12 colors
curl -sS "$API?preset=embroidery&palette=%23000000,%23ffffff,%23d62828,%23003049" \
-H "$AUTH" --data-binary @patch.png -o patch.svg
The settings behind print, cut, laser and embroidery may be tuned over time; web stays as it is. The same call in five languages: Vectorize with a preset.
Output formats
Choose the format with format. One value returns the file itself; a comma-separated list (svg,pdf,png) returns JSON with a download link per format — all of them for 1 credit.
| Format | Content-Type | With width / height | Good for |
|---|---|---|---|
svg | image/svg+xml | width / height attributes in the unit, same viewBox | Web, Figma, Illustrator, Cricut Design Space |
pdf | application/pdf | Page size (MediaBox) | Print, documents, Illustrator |
eps | application/postscript | BoundingBox | Print RIPs, CorelDRAW, older design software |
dxf | application/dxf | Coordinates in the unit and $INSUNITS | CAD, laser and CNC software (curves become polylines) |
png | image/png | Pixels: mm and in at 300 DPI, at most 4.2 MP | A clean re-render of the vector at any size |
Terminal · one call per format
API="https://shapesoda.com/v1/vectorize"
AUTH="Authorization: Bearer $SHAPESODA_KEY"
curl -sS "$API?format=svg" -H "$AUTH" --data-binary @logo.png -o logo.svg # image/svg+xml
curl -sS "$API?format=pdf" -H "$AUTH" --data-binary @logo.png -o logo.pdf # application/pdf
curl -sS "$API?format=eps" -H "$AUTH" --data-binary @logo.png -o logo.eps # application/postscript
curl -sS "$API?format=dxf" -H "$AUTH" --data-binary @logo.png -o logo.dxf # application/dxf
curl -sS "$API?format=png&width=2000" -H "$AUTH" --data-binary @logo.png -o logo.png # image/png
# several at once for 1 credit → JSON with links (see Result storage)
curl -sS "$API?format=svg,pdf,eps,dxf,png" -H "$AUTH" --data-binary @logo.png
Physical size: set width or height together with unit — px (default), pt, mm, cm or in. With one side the other follows the aspect ratio; with both you get a sheet of exactly that size and aspect (fit, fill, stretch) with align_x / align_y decide how the drawing sits on it. The maximum is 10 m. Without a size, the output has the pixel size of the input (after any downscale), multiplied by scale if given. PNG gets pixels: physical units at dpi (default 300).
Test mode
Build and test your integration for free with mode=test. The response has the same format, headers and structure as a live one, but the result carries a watermark: a dense pattern woven into the shapes themselves, so it can’t be removed by deleting a layer.
- No credits or subscription needed:
X-Credits-Charged: 0andX-Mode: test. - Presets, formats and options work as in live mode, with two exceptions:
retention_daysandimage_urlare rejected with400 bad_request(test with a file and withoutretention_days). - Test results can be stored (several formats,
retain=trueorAccept: application/json), but only for 1 hour instead of 24, and they stay watermarked in every format. They count toward the account’s storage and are evicted first when space runs out. - Limit: 100 test calls per key and 100 per account per UTC day, then
429 test_limitwithRetry-Afteruntil 00:00 UTC. Test calls don’t count against the daily image limit.
curl -sS "https://shapesoda.com/v1/vectorize?mode=test&preset=cut" \
-H "Authorization: Bearer $SHAPESODA_KEY" \
--data-binary @logo.png \
-D - -o test.svg
HTTP/1.1 200 OK
Content-Type: image/svg+xml; charset=utf-8
X-Credits-Charged: 0
X-Mode: test
X-Credits-Remaining: 99
X-Request-Id: req_4f1c2a9e0b7d4c3a8e21
Preview
Show your users a result before they pay. mode=preview is free and returns a raster PNG of up to 1 megapixel (larger images are downscaled) — no vectors. When the user is happy, unlock the vectors from the same link for 1 credit, without uploading the image again.
- Free, no watermark:
X-Credits-Charged: 0andX-Mode: preview. Onlyformat=png; not withretention_daysorimage_url. All other options apply, so the preview shows the real colors and shapes. - Limit: 200 previews per account per UTC day, then
429 preview_limitwithRetry-Afteruntil 00:00 UTC. Previews don’t count against the daily image limit or the test-mode limit. - A preview is always stored for 24 hours. The link comes in the
X-Result-*headers (or as JSON withAccept: application/json). - Upgrade:
GET /v1/results/{id}?format=svg(orpdf,eps,dxf) with the result token. It charges 1 credit once (0 if the same image was already paid for in the last 30 days, see Free repeats). For the full-size PNG instead of the 1 MP preview addupgrade=true:?format=png&upgrade=true. Plain?format=pngreturns the free preview again. - After the upgrade the result is a normal
liveresult (X-Mode: live): every other format is free and the retention rules of Result storage apply. If the build fails, the credit is returned and the result stays a preview. Without credits you get402 no_credits.
# 1. free preview: the PNG is the body, the link to the result is in the headers
curl -sS "https://shapesoda.com/v1/vectorize?mode=preview&preset=cut" \
-H "Authorization: Bearer $SHAPESODA_KEY" \
--data-binary @logo.png -D headers.txt -o preview.png
RES=$(grep -i '^x-result-url:' headers.txt | cut -d' ' -f2 | tr -d '\r')
TOKEN=$(grep -i '^x-result-token:' headers.txt | cut -d' ' -f2 | tr -d '\r')
# 2. the user is happy: unlock the SVG for 1 credit (the result token, not the API key)
curl -sS "$RES?format=svg" -H "Authorization: Bearer $TOKEN" -o logo.svg
# 3. now every other format is free
curl -sS "$RES?format=pdf" -H "Authorization: Bearer $TOKEN" -o logo.pdf
curl -sS "$RES?format=png&upgrade=true" -H "Authorization: Bearer $TOKEN" -o logo-full.png
HTTP/1.1 200 OK
Content-Type: image/png
X-Credits-Charged: 0
X-Mode: preview
X-Result-Id: res_9b2e41c07f3a58d61e0c4b7a
X-Result-Url: https://shapesoda.com/v1/results/res_9b2e41c07f3a58d61e0c4b7a
X-Result-Token: q3v6X0cQ1l2mYp8rT4wZkH9sNfB7aJdE5uGiLoVxCyA
X-Result-Expires: 2026-09-30T12:00:00.000Z
Free repeats
You pay for an image once. Repeats are free in two cases, and the response says X-Credits-Charged: 0:
| Repeat | Free for | Limit |
|---|---|---|
| Exactly the same request: same image bytes, same parameters | 24 hours | None — safe retries |
| The same image with other options or formats | 30 days after it was paid for | 20 per image per day; beyond that a repeat costs 1 credit |
- The image is recognized by a SHA-256 hash of its bytes, the same one the website uses: an image paid for with a credit on shapesoda.com can be redone through the API for free, and the other way round. A resized or re-saved file is a new image.
- Repeats with other options count toward the daily image limit of the key.
- Test-mode calls and images made with the website’s unlimited subscription don’t start the 30-day window.
- Previews (
mode=preview) are free and don’t start the 30-day window; unlocking the vectors from a preview is an ordinary paid call (1 credit, or a free repeat if the image was paid for in the last 30 days).
Result storage
GET/v1/results/{id}free · result token
DELETE/v1/results/{id}result token
By default nothing is stored: a single-format request returns the file and forgets the image. A result is kept for 24 hours only when you need it later:
- you ask for several formats, or send
Accept: application/json— exactly that value; clients that sendapplication/json, */*still get the file; - you pass
retain=trueorretention_days— you still get the file, and the link comes in theX-Result-*headers; - you use
mode=preview(always stored, see Preview); - the call comes through MCP.
# several formats → JSON (or add -H "Accept: application/json" for a single format)
curl -sS "https://shapesoda.com/v1/vectorize?format=svg,pdf" \
-H "Authorization: Bearer $SHAPESODA_KEY" \
--data-binary @logo.png
{
"result_id": "res_9b2e41c07f3a58d61e0c4b7a",
"result_url": "https://shapesoda.com/v1/results/res_9b2e41c07f3a58d61e0c4b7a",
"result_token": "q3v6X0cQ1l2mYp8rT4wZkH9sNfB7aJdE5uGiLoVxCyA",
"width": 1200,
"height": 800,
"input_scale": 1,
"credits_charged": 1,
"credits_remaining": 99,
"mode": "live",
"expires_at": "2026-09-30T12:00:00.000Z",
"files": {
"svg": "https://shapesoda.com/v1/results/res_9b2e41c07f3a58d61e0c4b7a?format=svg",
"pdf": "https://shapesoda.com/v1/results/res_9b2e41c07f3a58d61e0c4b7a?format=pdf"
}
}
| Field | Meaning |
|---|---|
result_id, result_url | The stored result and its address. |
result_token | Secret token for downloading; shown once. It is also the decryption key — we can’t recover it. |
files | A download link per requested format. |
expires_at | When the result is deleted (24 hours, or retention_days days). |
width, height, input_scale | Pixel size of the vectorized image and the downscale factor (1 if not downscaled). |
credits_charged, credits_remaining, mode | The same as the X-Credits-* and X-Mode headers of a file response. |
Download with the result token, not your API key: Authorization: Bearer <result_token> (recommended). ?token=<result_token> is also accepted, but a token in a URL can leak into access logs, browser history and analytics. Anyone who has the token can download the result, so treat the link like the file itself. Without format you get the first format of the original request: the first one in your format list, in the order you wrote it (duplicates are ignored), or the preset’s format if you didn’t pass format. The files object of the JSON response lists the formats in the same order. A format you didn’t ask for is built once from the stored source with exactly the same preset, options and mode — free and without a new upload. Building is limited by the request rate of the account that owns the result (60 per minute, 429 rate_limited with Retry-After); formats already stored are not.
RES="https://shapesoda.com/v1/results/res_9b2e41c07f3a58d61e0c4b7a"
TOKEN="q3v6X0cQ1l2mYp8rT4wZkH9sNfB7aJdE5uGiLoVxCyA" # result_token, not the API key
curl -sS "$RES?format=pdf" -H "Authorization: Bearer $TOKEN" -o logo.pdf
# a format you did not ask for is built from the stored source, free of charge
curl -sS "$RES?format=dxf" -H "Authorization: Bearer $TOKEN" -o logo.dxf
# the same with the token in the link (less safe: URLs end up in logs)
curl -sS "$RES?format=eps&token=$TOKEN" -o logo.eps
# delete now instead of waiting 24 hours
curl -sS -X DELETE "$RES" -H "Authorization: Bearer $TOKEN"
With retain=true (or retention_days) and one format, the file comes as usual plus the link:
HTTP/1.1 200 OK
Content-Type: image/svg+xml; charset=utf-8
X-Credits-Charged: 1
X-Mode: live
X-Result-Id: res_9b2e41c07f3a58d61e0c4b7a
X-Result-Url: https://shapesoda.com/v1/results/res_9b2e41c07f3a58d61e0c4b7a
X-Result-Token: q3v6X0cQ1l2mYp8rT4wZkH9sNfB7aJdE5uGiLoVxCyA
X-Result-Expires: 2026-09-30T12:00:00.000Z
- Encrypted. The source image and every format are encrypted with AES-256-GCM under a key derived from the token. We keep only a hash of the token, so a stored result can’t be read without the link.
- Deleted after 24 hours (previews too), test-mode results after 1 hour, results with
retention_daysafter that many days (seeexpires_at).DELETEremoves it right away and returns204. - An unknown id, a wrong token and an expired result all return
404 not_found. - If storage is full, JSON requests get
503 storage_full(not charged; retry afterRetry-Afteror ask for one format), andretain=trueorretention_daysreturn the file without theX-Result-*headers.
Retention
By default a stored result lives 24 hours. If your users come back for their files, ask for longer with retention_days — 1 to 30 days, free of charge. It stores the result even for a single format (as retain=true does) and returns the link in the X-Result-* headers; expires_at / X-Result-Expires show the deadline.
- Only for
mode=live, and not together withretain=false. Test results live 1 hour, previews 24 hours. - Stored results count toward the storage quota of the account. When space runs out, the oldest results are evicted first in this order: test results, previews, live results without
retention_days, and last of all results withretention_days. If storage is still full, you get503 storage_full(not charged). - Extra formats built from a stored result are free for the whole period.
DELETEremoves a result at any time.
Terminal · keep for 30 days
curl -sS "https://shapesoda.com/v1/vectorize?retention_days=30" \
-H "Authorization: Bearer $SHAPESODA_KEY" \
--data-binary @logo.png -D - -o logo.svg # X-Result-Expires: in 30 days
Image URLRolling out
Rolling out. Input by URL is being switched on gradually. Until it is on, a request with image_url gets 503 url_unavailable — upload the file instead.
Instead of a file, pass the address of a public image in image_url (query parameter or form field) and leave the body empty. Everything else works as with a file.
curl -sS https://shapesoda.com/v1/vectorize \
-H "Authorization: Bearer $SHAPESODA_KEY" \
-F image_url=https://example.com/logo.png \
-F preset=cut \
-o logo.svg
httporhttps, port 80 or 443, no user name or password, up to 2048 characters — otherwise400 url_invalid.- The image is downloaded through an isolated egress proxy: private, loopback, link-local and cloud-metadata addresses are blocked, after redirects too (
422 url_blocked). - At most 3 redirects, 10 seconds (
504 url_timeout) and 10 MB (413 too_large). A server that doesn’t answer or returns an error gives422 url_unreachable. - Send either a file or
image_url, not both. Not available in test mode.
Account and credits
GET/v1/accountfree
Returns the account the key belongs to and its credits. Credits are counted per calendar month in UTC.
{
"account": { "id": "acc_3f9c1e7a2b4d6e80", "name": "[email protected]" },
"credits": {
"available": 97,
"balance": 97,
"quota": 0,
"quota_used": 0,
"quota_left": 0,
"spent_month": 3,
"spend_limit": null
}
}
| Field | Meaning |
|---|---|
available | Images you can process right now: quota_left + balance, capped by what is left of your spending limit. |
balance | Purchased credits, shared with the website. Includes subscription_balance. |
subscription_balance | Credits from your API plan (used first). Unused ones roll over up to 5× your monthly amount and expire when the plan ends. |
quota, quota_used, quota_left | Monthly included credits, if your plan has any (they are used first). 0 for pay-as-you-go accounts. |
spent_month | Credits used this calendar month (UTC). |
spend_limit | Your own monthly cap in credits, or null for no cap. Set it in the account menu. |
Price list
GET/v1/planspublic · no key
The same prices as in Pricing, as JSON, without authentication. It returns api_plans (monthly credit plans with credits, price and per_image), subscriptions (website plans), credits_never_expire, redownload_free_days, result_retention_hours, signup_credits, enterprise_contact and payments_enabled / billing (checkout data when payments are on for the visitor).
Errors
Errors use standard HTTP status codes and a JSON body with a stable, machine-readable code. Branch on code, not on message: the message is a human-readable hint and may change or be localized.
{
"error": {
"code": "too_many_pixels",
"message": "Human-readable description",
"request_id": "req_4f1c2a9e0b7d4c3a8e21"
}
}
| HTTP | Code | Meaning | What to do |
|---|---|---|---|
| 400 | bad_request | Unknown parameter or value, conflicting options, malformed multipart form, a file in a field other than image, or both a file and image_url. | Fix the request. Don’t retry as is. |
| 400 | empty | The request has no image. | Send the file as the body or in the image field. |
| 400 | url_invalid | image_url is not an http(s) URL on port 80 or 443, has credentials or is longer than 2048 characters. | Fix the URL. |
| 401 | unauthorized | Missing, malformed or revoked API key. | Check the Authorization: Bearer header; create a new key if needed. |
| 402 | no_credits | No credits left on the account (also when unlocking a preview). | Buy a credit pack. Don’t retry. |
| 402 | spend_limit | Your monthly spending limit is reached. | Raise or remove the limit in the account menu, or wait for the next month. |
| 404 | not_found | Unknown path, or an unknown, expired or wrong-token upload/result link. | Check the URL. |
| 405 | method_not_allowed | Wrong HTTP method for this path. | Use the method from this reference. |
| 413 | too_large | The file (or the image at image_url) is larger than 10 MB. | Downscale or recompress the image. |
| 415 | unsupported_format | Not a PNG, JPEG, WebP, uncompressed BMP, GIF or TIFF file (detected by content). | Convert the image to PNG first. |
| 422 | invalid_image | The file is damaged, truncated or can’t be decoded. | Re-export the image. |
| 422 | too_many_pixels | More than 40 MP, or the image needs too much memory. | Downscale the image. |
| 422 | url_blocked | image_url points to a private, local or internal address. | Use a public URL. Don’t retry. |
| 422 | url_unreachable | The image at image_url could not be downloaded. | Check the URL, or upload the file. |
| 429 | rate_limited | Too many requests for this key (or too many failed sign-ins from your IP). | Wait for Retry-After seconds, then retry. |
| 429 | daily_limit | The key hit its daily cap. | Wait until 00:00 UTC, or use another key. |
| 429 | preview_limit | The account made 200 previews (mode=preview) today. | Wait until 00:00 UTC (Retry-After), or use mode=live. |
| 429 | test_limit | The daily test-mode limit of the key or account is reached. | Wait until 00:00 UTC (Retry-After), or use mode=live. |
| 500 | internal | Something went wrong on our side. You are not charged. | Retry once after a short pause; if it persists, email us the request_id. |
| 503 | busy | All workers are busy and the queue is full. | Retry after Retry-After seconds (usually 5). |
| 503 | timeout | Processing took longer than 60 seconds. You are not charged. | Try a smaller image or fewer colors. |
| 503 | storage_full | Result storage is full. You are not charged. | Retry after Retry-After seconds, or ask for a single format. |
| 503 | url_unavailable | Input by URL is off or temporarily unavailable. | Upload the file instead. |
| 504 | url_timeout | Downloading image_url took longer than 10 seconds. You are not charged. | Upload the file instead. |
Credits are reserved when processing starts and refunded automatically if it fails, so you only pay for results you receive.
Rate limits
| Limit | Default | Error |
|---|---|---|
| Requests per key | 60 per minute, with bursts of up to 10 at once | 429 rate_limited + Retry-After |
| Concurrent requests per account | 5 at once (being processed or queued); send up to 5 in parallel | 429 rate_limited + Retry-After: 2, not charged |
| Images per key per day | 5,000, free repeats with other options included (resets at 00:00 UTC) | 429 daily_limit + Retry-After |
| Failed authentication per IP | 30 per minute | 429 rate_limited + Retry-After |
Previews (mode=preview) | 200 per account per day | 429 preview_limit + Retry-After |
| Test-mode calls | 100 per key and 100 per account per day | 429 test_limit + Retry-After |
| Free repeats of a paid image with other options | 20 per image per day, then 1 credit each | — |
The per-key budget is shared between the REST API and the MCP server. GET /v1/account does not count against it. Need more? Email [email protected].
Retries and idempotency
- Retry only
rate_limitedandbusy(after theRetry-Afterheader), andinternalonce with a short backoff. Other 4xx errors will fail again. Retry-Afteris a whole number of seconds, never an HTTP date. It comes withrate_limited(the time until a request is allowed again),busyandtimeout(5),storage_full(60) and the daily limits (the seconds left until 00:00 UTC).- A rejected request costs nothing against the limit. The per-minute limit is a token bucket: a request answered with
429 rate_limiteddoes not use up a token, so waiting forRetry-Afteris enough. - Retries are free: repeating the same request (same image bytes and the same parameters) within 24 hours is not charged again — the response has
X-Credits-Charged: 0. The same image with other options is free for 30 days too, within a daily limit — see Free repeats. - Use a generous client timeout — at least 120 seconds — so a slow request isn’t abandoned after the image has already been processed and charged.
- For batches, run a few requests in parallel (2–4) rather than dozens: requests beyond the burst are rejected with
429.
async function vectorize(image, attempt = 0, retriedInternal = false) {
const res = await fetch("https://shapesoda.com/v1/vectorize", {
method: "POST",
headers: { Authorization: \`Bearer ${process.env.SHAPESODA_KEY}\` },
body: image,
signal: AbortSignal.timeout(120_000),
});
if (res.ok) return res.text();
const { error } = await res.json();
if (["rate_limited", "busy"].includes(error.code) && attempt < 3) {
const wait = Number(res.headers.get("Retry-After") ?? 2 ** attempt);
await new Promise((r) => setTimeout(r, wait * 1000));
return vectorize(image, attempt + 1, retriedInternal);
}
if (error.code === "internal" && !retriedInternal) { // one retry, short backoff
await new Promise((r) => setTimeout(r, 1000));
return vectorize(image, attempt, true);
}
throw new Error(\`${error.code}: ${error.message} (${error.request_id})\`);
}
Production checklist
- Timeout: set the client timeout to at least 120 seconds.
- Check before saving: write the body to a file only when the status is
200andContent-Typeis what you asked for (image/svg+xml,application/pdf, …, orapplication/jsonfor several formats). Errors are JSON — never save them as an.svg. - Honor
Retry-After(seconds); retry onlyrate_limited,busyand, once,internal. See Retries. - Keep the API key secret and on your server only: not in a browser, a mobile app or a repository. Revoke a leaked key.
- Result tokens: send them in the
Authorizationheader rather than in?token=, and keep result links out of logs and analytics. - Development: use
mode=testwhile building (free, watermarked, 100 calls a day); switch tolivewhen you ship. - Support: log
X-Request-Idwith every failure.
Security and privacy
- Keys are hashed. We store only a keyed hash (HMAC-SHA-256) and a short prefix of each key, so the key is shown once and can’t be recovered — only revoked and replaced.
- You set the spending limit. A monthly cap in the account menu applies to every key, the MCP server and the website: neither an agent nor a leaked key can spend more than you allow.
- Images are stored only when you need them. A single-format request is processed in memory in an isolated worker process and discarded — nothing is written to disk or a database. Several formats,
retain=true,retention_days, previews and MCP results are kept for 24 hours (up to 30 days withretention_days), encrypted with a key that only the holder of the result token has, and then deleted; anyone with the link and token can download them. MCP uploads live in memory for at most 10 minutes. Images are never used to train models. See Result storage and the Privacy Policy. - SVG output is sanitized. Every result is checked against a strict allowlist (paths, groups and gradients with plain color values) before it is returned — no scripts, event handlers, external references or embedded content. PDF, EPS and DXF are generated from the vector geometry and colors only.
- Inputs are verified. Formats are detected by content, resolution is checked before decoding (decompression-bomb protection), and each job runs in its own process with a time limit. Images by URL are downloaded in a separate process through an egress proxy that blocks internal addresses.
SDKs and CLI
Official client libraries wrap the same API with typed options, automatic retries after 429 / 503 and result downloads. They are generated from the OpenAPI spec, so every parameter on this page is available.
- Python:
pip install shapesoda(PyPI).Shapesoda(key).vectorize_file("logo.png", formats=["svg","pdf"], preset="print"), pluspreview_file()andupgrade(). - JavaScript / TypeScript:
npm install @shapesoda/sdk(npm).new Shapesoda(key).vectorizeFile("logo.png", { formats: ["svg","pdf"], preset: "print" }), pluspreviewFile()andupgrade(). Works withimportandrequire, Node 18+. - Go, PHP, Ruby, Java, C#: generated clients are in the
clients/sdkfolder of the repository; publishing to pkg.go.dev, Packagist, RubyGems, Maven Central and NuGet is in progress. Until then, generate one from the spec (see below) or use the plain HTTP samples above. - CLI:
npx shapesoda ./in ./out --preset cut --format svg,dxf(npm, Node 18+). Folder in, folder out, retries and skip-already-done built in;--preview,--test,shapesoda account.
OpenAPI spec
A machine-readable OpenAPI 3.1 description of the API — import it into Postman, Insomnia or Bruno, or generate a client:
GET https://shapesoda.com/openapi.json
npx @openapitools/openapi-generator-cli generate \
-i https://shapesoda.com/openapi.json -g typescript-fetch -o ./shapesoda-client
MCP for AI apps and agents
The Model Context Protocol lets AI apps and agents call external tools. Shapesoda’s MCP server turns images into SVG, PDF, EPS, DXF or PNG right in ChatGPT, Claude, Cursor, Claude Code and any other MCP client. A Shapesoda account connects with a sign-in (OAuth); agents and scripts can still use an API key with the same credits and limits as the REST API.
POSThttps://shapesoda.com/mcpStreamable HTTP · OAuth or Bearer key
Free: 3 vectorizations a day without an account, 10 a day with a connected account (OAuth or key), then credits. REST and the website are not affected. A personal plan from the website is unlimited in chats when the account is connected with a sign-in; calls with an API key use credits. Changing the settings of the same image in the Shapesoda window is free for 24 hours, so one image is charged once. Test mode, previews, retention_days and vectorize_url need an account. Limits reset at 00:00 UTC; get_usage and free_remaining_today in every result show what is left. Nothing is sold inside chats: prices and checkout are on the website.
Connect in ChatGPT, Claude, Cursor or Claude Code
Server URL: https://shapesoda.com/mcp. When the free images run out, the chat offers to connect your Shapesoda account: sign in with Google or an email code and press Allow. Connected apps are listed in your account, where you can disconnect them.
ChatGPT
Find Shapesoda in the ChatGPT apps directory, or add the server URL as a custom app in developer mode. Attach an image and write @Shapesoda or “make this an SVG”: ChatGPT asks once to share the file (the image input of vectorize), and the Shapesoda window shows a before/after slider, settings, downloads in every format and Copy SVG.
Claude
In claude.ai or Claude Desktop open Settings → Connectors and add Shapesoda from the directory, or “Add custom connector” with the server URL. Claude does not pass attached files to apps, so ask it to vectorize an image: it calls open_editor, and you pick the file in the Shapesoda window.
Claude Code
Add the server, then run /mcp, choose shapesoda and Authenticate to connect your account in the browser:
claude mcp add --transport http shapesoda https://shapesoda.com/mcp
Or use an API key instead of the sign-in:
claude mcp add --transport http shapesoda https://shapesoda.com/mcp \
--header "Authorization: Bearer $SHAPESODA_KEY"
Cursor
Install the Shapesoda plugin from the Cursor marketplace, or add the server to ~/.cursor/mcp.json (or .cursor/mcp.json in a project). Cursor offers to connect the account on first use.
{
"mcpServers": {
"shapesoda": { "url": "https://shapesoda.com/mcp" }
}
}
With an API key, Cursor reads ${env:SHAPESODA_KEY} from your environment, so the key stays out of the file:
{
"mcpServers": {
"shapesoda": {
"url": "https://shapesoda.com/mcp",
"headers": { "Authorization": "Bearer ${env:SHAPESODA_KEY}" }
}
}
}
Other clients that support remote (Streamable HTTP) MCP servers work the same way: URL https://shapesoda.com/mcp, OAuth 2.1 sign-in (discovered from /.well-known/oauth-protected-resource) or the header Authorization: Bearer <key>. The server answers with JSON (no SSE stream) and supports protocol versions 2025-11-25, 2025-06-18 and 2025-03-26.
Tools
vectorize
Converts an image into SVG, PDF, EPS, DXF or PNG. The image comes in exactly one way: image (a file attached to the ChatGPT message), upload_id (from create_upload) or a small image_base64. Takes the same options as the REST API — preset, format as a list, draw, layers, palette, transparency, width / height / unit, mode=test, mode=preview (free PNG preview; open its link with format=svg instead of format=png to unlock the vectors for 1 credit), retention_days and the rest — see Parameters. Returns a download link per format, valid 24 hours, and shows the Shapesoda window in chats that support it. Uses 1 credit for all formats.
open_editor
Opens the Shapesoda window without an image: the user picks the file there. For Claude, which does not pass attachments to apps. Uses no credits.
create_upload
Creates a one-time upload link for a local file (valid 10 minutes, up to 10 MB) and returns the curl command to upload it. For agents with a terminal.
revectorize
Used only by the Shapesoda window (hidden from the model): vectorizes the same image again with other settings, free for 24 hours after the first vectorization, up to 30 changes a day.
vectorize_url
Downloads a public image by URL and vectorizes it like vectorize. Listed only when input by URL is enabled (rolling out); no test mode.
get_usage
Shows free vectorizations left today, credits available, balance, monthly usage and your spending limit. Free.
How an agent sends a file
Agents usually can’t attach binary files to a tool call, so large images go through a short-lived upload link:
- create_upload
returns
upload_idand a secretupload_url. - Upload with curl
The agent runs the returned command:
curl -sS -X PUT --data-binary @image.png "<upload_url>". The link accepts one PNG, JPEG, WebP, BMP, GIF or TIFF file and needs no other auth. - vectorize with upload_id
The result has a link per requested format in
files(valid 24 hours; the token is in the link),download_urlfor the first one, the SVG inline when it is under 60 KB, the image size,credits_chargedandcredits_remaining.
For small images (up to 2 MB) the agent can skip the upload and pass the file directly as image_base64.
The upload link is served by PUT /v1/uploads/{upload_id}. It is part of the MCP upload flow and is listed in the OpenAPI spec for completeness; you don’t call it yourself in a REST integration.
Pricing
Monthly subscription with the number of credits you choose. One credit is one successfully vectorized image, in every format you request in that call, through the API, MCP or the website. Cancel anytime.
- Unused credits roll over (up to 5 months’ worth)
- Cancel anytime; credits last until the plan ends
- Shared balance with the website
- Failed requests are free
50
credits / month
$3.99 / month
$0.08 per image
200
credits / month
$9.99 / month
$0.05 per image
5,000
credits / month
$74.99 / month
$0.015 per image
20,000
credits / month
$199.99 / month
$0.01 per image
100,000
credits / month
$499.99 / month
$0.005 per image
Enterprise or more than 100k images a month? Email [email protected] for volume pricing and higher limits.