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.

  1. 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"
    
  2. Send an image Post the raw image bytes as the request body and pass options in the query string:
    curl -sS "https://shapesoda.com/v1/vectorize?colors=auto" \
      -H "Authorization: Bearer $SHAPESODA_KEY" \
      --data-binary @logo.png \
      -D - -o logo.svg
    
    Or send a multipart/form-data form with the file in the image field 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
    
  3. Read the response On success you get 200 with 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_4f1c2a9e0b7d4c3a8e21
    
    HeaderMeaning
    X-Credits-ChargedCredits this request used: 1 for a successful vectorization (whatever the number of formats), 0 in test mode and for free repeats. Failed requests are not charged.
    X-Modelive, or test in 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. GET and DELETE /v1/results/{id} take the result token, not the API key. Send it as Authorization: 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/plans is 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-Type header is not used.
  • Multipart — Content-Type: multipart/form-data with the file in the field image; parameters can be form fields or query parameters (form fields win). Any other file field is rejected.
  • By URL — image_url with 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.

ParameterValuesDescription
Basics
presetweb print cut laser embroideryReady-made settings for the job; default web. See Presets.
formatsvg pdf eps dxf pngOutput format, or a comma-separated list (svg,pdf) for 1 credit. Default: the preset’s format. See Output formats.
image_typeauto artwork artwork_sharp photoKind 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.
qualityauto high medium lowQuality of the input. Use low for blurry, noisy or heavily compressed JPEG images.
detailauto low medium highLevel of detail in the output. Applies to photos.
modelive test previewtest — free, watermarked result for building an integration (Test mode). preview — free PNG preview up to 1 MP, vectors unlocked later for 1 credit (Preview).
retainfalse truetrue — also store a single-format result for 24 hours. See Result storage.
retention_days1 – 30Keep the stored result this many days instead of 24 hours; free. See Retention.
image_urlURLA public image address instead of a file. See Image URL (rolling out).
Processing
colorsauto unlimited 2 – 32Number 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_colors2 – 256Maximum number of colors; the engine may use fewer. Not combined with colors or palette.
palette2–256 × #rrggbbFixed colors, comma-separated; the result uses only these. In the query string write # as %23. Not combined with colors or max_colors.
palette_tolerance0 – 100With 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_area0 – 10000Smallest shape kept, in px²; smaller specks are dropped. Overrides the preset’s value, also for photos.
max_pixels10000 – 4200000Downscale the input to at most this many pixels before tracing: faster, fewer nodes. The factor comes in X-Input-Scale.
curve_tolerance0.01 – 10Curve 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.
smoothness1 – 10Contour smoothing: higher is smoother with fewer nodes.
cornerssharp smoothKeep corners sharp or round them. Default from the automatic analysis.
denoise0 – 3Noise reduction before tracing (JPEG artifacts, grain): 0 off, 3 strongest.
Drawing
drawfill stroke stroke_edgesfill — 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.
layersstacked cutouts cutouts_groupedShapes 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_bynone color layerGroups 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
curvesall lines lines_arcsAllowed 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_width0.01 – 100Stroke width; needs draw=stroke or stroke_edges. In output units, or in source pixels with stroke_scaling=scale. Default: a hairline.
stroke_color#rrggbbStroke color instead of each shape’s color. Same requirement as stroke_width.
stroke_scalingfixed scalefixed (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_filleroff onon — 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_width0 – 10Width of the underlay outline in source pixels (default 1). Needs gap_filler=on.
Size
width, heightnumber > 0Output 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.
unitpx pt mm cm inUnit of width / height; default px.
aspectfit fill stretchWith 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_y0 – 1Placement of the drawing on the sheet with aspect=fit or fill; default 0.5 (centered).
scale>0 – 100Output size as a multiple of the source size. Not combined with width / height.
dpi1 – 2400Pixels per inch of PNG output when the size is in pt, mm, cm or in; default 300.
Format-specific
svg_version1.1 1.0 tiny1.2SVG flavor. tiny1.2 is the SVG Tiny profile for plotters and embedded viewers: no opacity, no gradient transforms.
svg_fixed_sizetrue falsetrue (default) — width and height on the root element; false — only a viewBox, the SVG scales to its container.
adobe_compatfalse truetrue — SVG friendly to Adobe Illustrator: size always set, stroke widths in drawing coordinates.
dxf_curveslines arcs splinesDXF curves as polylines (default), polylines with arcs, or SPLINE entities. curves=lines or lines_arcs wins.
png_antialiason offAnti-aliasing in PNG; default on.

Options are checked against each other; a conflict returns 400 bad_request:

  • palette cannot be combined with colors or max_colors, nor max_colors with colors. palette_tolerance needs palette.
  • stroke_width, stroke_color, stroke_scaling need draw=stroke or stroke_edges (also when the preset sets it, e.g. laser). gap_filler=on needs draw=fill; gap_filler_width needs gap_filler=on.
  • unit needs width or height. scale cannot be combined with width / height. aspect, align_x, align_y need both width and height (the alignment has no effect with aspect=stretch). dpi needs a size in pt, mm, cm or in.
  • retention_days works with mode=live only and not with retain=false. mode=preview returns png only.

Input limits

LimitValueError
FormatsPNG, JPEG, WebP, BMP (uncompressed, 24/32-bit), GIF (first frame), TIFF (first page)415 unsupported_format
File size10 MB413 too_large
ResolutionUp to 40 megapixels. Images larger than 4.2 MP or 4096 px on a side are downscaled automatically; the factor comes in X-Input-Scale422 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 OK with the file itself as the body and the format’s Content-Type (see Output formats). SVG is served inline, other formats as an attachment vector.<format>. Nothing is stored unless you pass retain=true or retention_days.
  • Several formats, or Accept: application/json — JSON with a download link per format; the result is stored for 24 hours. See Result storage.
HeaderMeaning
X-Credits-Charged, X-Credits-RemainingWhat the request cost and what is left (file responses; JSON has the same in fields).
X-Modelive, test or preview.
X-Input-ScaleOnly 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-ExpiresOnly when the result is stored (retain=true, retention_days, mode=preview): the result, its secret token and when it is deleted.
X-Request-IdUnique 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.

PresetDefault formatWhat it doesUse for
webSVGAutomatic colors, shapes stacked on top of each other.Websites, apps, Figma
printPDFShapes cut out of each other, no overlaps.Print shops, posters, documents
cutSVG8 colors; shapes cut out and grouped by color — one group per vinyl color.Vinyl cutters, stickers, Cricut, Silhouette
laserSVG2 colors, outlines only (draw=stroke).Laser cutting and engraving; add format=dxf for CAD software
embroiderySVG12 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.

FormatContent-TypeWith width / heightGood for
svgimage/svg+xmlwidth / height attributes in the unit, same viewBoxWeb, Figma, Illustrator, Cricut Design Space
pdfapplication/pdfPage size (MediaBox)Print, documents, Illustrator
epsapplication/postscriptBoundingBoxPrint RIPs, CorelDRAW, older design software
dxfapplication/dxfCoordinates in the unit and $INSUNITSCAD, laser and CNC software (curves become polylines)
pngimage/pngPixels: mm and in at 300 DPI, at most 4.2 MPA 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: 0 and X-Mode: test.
  • Presets, formats and options work as in live mode, with two exceptions: retention_days and image_url are rejected with 400 bad_request (test with a file and without retention_days).
  • Test results can be stored (several formats, retain=true or Accept: 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_limit with Retry-After until 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: 0 and X-Mode: preview. Only format=png; not with retention_days or image_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_limit with Retry-After until 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 with Accept: application/json).
  • Upgrade: GET /v1/results/{id}?format=svg (or pdf, 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 add upgrade=true: ?format=png&upgrade=true. Plain ?format=png returns the free preview again.
  • After the upgrade the result is a normal live result (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 get 402 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:

RepeatFree forLimit
Exactly the same request: same image bytes, same parameters24 hoursNone — safe retries
The same image with other options or formats30 days after it was paid for20 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 send application/json, */* still get the file;
  • you pass retain=true or retention_days — you still get the file, and the link comes in the X-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"
  }
}
FieldMeaning
result_id, result_urlThe stored result and its address.
result_tokenSecret token for downloading; shown once. It is also the decryption key — we can’t recover it.
filesA download link per requested format.
expires_atWhen the result is deleted (24 hours, or retention_days days).
width, height, input_scalePixel size of the vectorized image and the downscale factor (1 if not downscaled).
credits_charged, credits_remaining, modeThe 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_days after that many days (see expires_at). DELETE removes it right away and returns 204.
  • 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 after Retry-After or ask for one format), and retain=true or retention_days return the file without the X-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 with retain=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 with retention_days. If storage is still full, you get 503 storage_full (not charged).
  • Extra formats built from a stored result are free for the whole period. DELETE removes 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
  • http or https, port 80 or 443, no user name or password, up to 2048 characters — otherwise 400 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 gives 422 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
  }
}
FieldMeaning
availableImages you can process right now: quota_left + balance, capped by what is left of your spending limit.
balancePurchased credits, shared with the website. Includes subscription_balance.
subscription_balanceCredits 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_leftMonthly included credits, if your plan has any (they are used first). 0 for pay-as-you-go accounts.
spent_monthCredits used this calendar month (UTC).
spend_limitYour 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"
  }
}
HTTPCodeMeaningWhat to do
400bad_requestUnknown 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.
400emptyThe request has no image.Send the file as the body or in the image field.
400url_invalidimage_url is not an http(s) URL on port 80 or 443, has credentials or is longer than 2048 characters.Fix the URL.
401unauthorizedMissing, malformed or revoked API key.Check the Authorization: Bearer header; create a new key if needed.
402no_creditsNo credits left on the account (also when unlocking a preview).Buy a credit pack. Don’t retry.
402spend_limitYour monthly spending limit is reached.Raise or remove the limit in the account menu, or wait for the next month.
404not_foundUnknown path, or an unknown, expired or wrong-token upload/result link.Check the URL.
405method_not_allowedWrong HTTP method for this path.Use the method from this reference.
413too_largeThe file (or the image at image_url) is larger than 10 MB.Downscale or recompress the image.
415unsupported_formatNot a PNG, JPEG, WebP, uncompressed BMP, GIF or TIFF file (detected by content).Convert the image to PNG first.
422invalid_imageThe file is damaged, truncated or can’t be decoded.Re-export the image.
422too_many_pixelsMore than 40 MP, or the image needs too much memory.Downscale the image.
422url_blockedimage_url points to a private, local or internal address.Use a public URL. Don’t retry.
422url_unreachableThe image at image_url could not be downloaded.Check the URL, or upload the file.
429rate_limitedToo many requests for this key (or too many failed sign-ins from your IP).Wait for Retry-After seconds, then retry.
429daily_limitThe key hit its daily cap.Wait until 00:00 UTC, or use another key.
429preview_limitThe account made 200 previews (mode=preview) today.Wait until 00:00 UTC (Retry-After), or use mode=live.
429test_limitThe daily test-mode limit of the key or account is reached.Wait until 00:00 UTC (Retry-After), or use mode=live.
500internalSomething went wrong on our side. You are not charged.Retry once after a short pause; if it persists, email us the request_id.
503busyAll workers are busy and the queue is full.Retry after Retry-After seconds (usually 5).
503timeoutProcessing took longer than 60 seconds. You are not charged.Try a smaller image or fewer colors.
503storage_fullResult storage is full. You are not charged.Retry after Retry-After seconds, or ask for a single format.
503url_unavailableInput by URL is off or temporarily unavailable.Upload the file instead.
504url_timeoutDownloading 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

LimitDefaultError
Requests per key60 per minute, with bursts of up to 10 at once429 rate_limited + Retry-After
Concurrent requests per account5 at once (being processed or queued); send up to 5 in parallel429 rate_limited + Retry-After: 2, not charged
Images per key per day5,000, free repeats with other options included (resets at 00:00 UTC)429 daily_limit + Retry-After
Failed authentication per IP30 per minute429 rate_limited + Retry-After
Previews (mode=preview)200 per account per day429 preview_limit + Retry-After
Test-mode calls100 per key and 100 per account per day429 test_limit + Retry-After
Free repeats of a paid image with other options20 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_limited and busy (after the Retry-After header), and internal once with a short backoff. Other 4xx errors will fail again.
  • Retry-After is a whole number of seconds, never an HTTP date. It comes with rate_limited (the time until a request is allowed again), busy and timeout (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_limited does not use up a token, so waiting for Retry-After is 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 200 and Content-Type is what you asked for (image/svg+xml, application/pdf, …, or application/json for several formats). Errors are JSON — never save them as an .svg.
  • Honor Retry-After (seconds); retry only rate_limited, busy and, 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 Authorization header rather than in ?token=, and keep result links out of logs and analytics.
  • Development: use mode=test while building (free, watermarked, 100 calls a day); switch to live when you ship.
  • Support: log X-Request-Id with 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 with retention_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"), plus preview_file() and upgrade().
  • JavaScript / TypeScript: npm install @shapesoda/sdk (npm). new Shapesoda(key).vectorizeFile("logo.png", { formats: ["svg","pdf"], preset: "print" }), plus previewFile() and upgrade(). Works with import and require, Node 18+.
  • Go, PHP, Ruby, Java, C#: generated clients are in the clients/sdk folder 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:

  1. create_upload returns upload_id and a secret upload_url.
  2. 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.
  3. vectorize with upload_id The result has a link per requested format in files (valid 24 hours; the token is in the link), download_url for the first one, the SVG inline when it is under 60 KB, the image size, credits_charged and credits_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.