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
Sign in at shapesoda.com with your email or Google, open the account menu and choose API keys → 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.Terminalexport 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:
Raw bodycurl -sS "https://shapesoda.com/v1/vectorize?colors=auto" \ -H "Authorization: Bearer $SHAPESODA_KEY" \ --data-binary @logo.png \ -D - -o logo.svgOr send a
multipart/form-dataform with the file in theimagefield and options as regular fields:Multipartcurl -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:Response headersHTTP/1.1 200 OK Content-Type: image/svg+xml; charset=utf-8 Content-Disposition: inline; filename="vector.svg" X-Credits-Charged: 1 X-Credits-Remaining: 99 X-Request-Id: req_4f1c2a9e0b7d4c3a8e21Header Meaning X-Credits-ChargedCredits this request used. Always 1for a successful vectorization; failed requests are not charged.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#
Every API request must carry your key as a bearer token:
Authorization: Bearer ssk_…
- Create, list and revoke keys in the account menu on shapesoda.com (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 keys on your server. The API does not send CORS headers, so it is not meant to be called from a web page; never ship a key in browser or mobile app code.
- 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, 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"))
}
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#
Converts one raster image into SVG. 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 two 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.
The format is detected from the file content (its signature), not from the file name or Content-Type.
Parameters#
All parameters are optional and default to auto. Values are case-sensitive; unknown parameters or values return 400 bad_request.
| Parameter | Values | Description |
|---|---|---|
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. |
colors | auto unlimited 2–32 | Number of colors in the result. An integer caps the palette (2 gives a two-color result); unlimited keeps every color the engine finds. |
detail | auto low medium high | Level of detail in the output. Applies to photos. |
Input limits#
| Limit | Value | Error |
|---|---|---|
| Formats | PNG, JPEG, WebP (still images; animated or multi-page files are rejected) | 415 unsupported_format |
| File size | 10 MB | 413 too_large |
| Resolution | 4.2 megapixels, and at most 4096 px on each side | 422 too_many_pixels |
Larger images do not produce better vectors: downscale to about 2000 px on the long side for the fastest results.
Response#
200 OK with the SVG document as the body, Content-Type: image/svg+xml; charset=utf-8, and the headers X-Credits-Charged, X-Credits-Remaining and X-Request-Id described in the Quickstart. On any error the body is JSON — see Errors.
Account and credits#
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. They never expire and are shared with the website. |
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. |
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, malformed multipart form, or a file in a field other than image. | 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. |
| 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. | 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 expired or invalid 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 is larger than 10 MB. | Downscale or recompress the image. |
| 415 | unsupported_format | Not a PNG, JPEG or WebP file (detected by content). | Convert the image to PNG first. |
| 422 | invalid_image | The file is damaged, truncated, animated or can’t be decoded. | Re-export the image. |
| 422 | too_many_pixels | More than 4.2 MP or a side longer than 4096 px. | Downscale the image. |
| 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. |
| 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. |
Credits are reserved when processing starts and refunded automatically if it fails, so you only pay for SVGs 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 |
| Images per key per day | 5,000 (resets at 00:00 UTC) | 429 daily_limit + Retry-After |
| Failed authentication per IP | 30 per minute | 429 rate_limited + Retry-After |
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 backoff. Other 4xx errors will fail again. - 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. A different image or different parameters is a new request. - Use a generous client timeout — at least 90–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) {
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);
}
throw new Error(`${error.code}: ${error.message} (${error.request_id})`);
}
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 not stored. REST requests are processed in memory in an isolated worker process and discarded; images are never written to disk or a database and never used to train models. MCP uploads live in memory for at most 10 minutes, MCP results for at most 1 hour. See 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.
- 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.
OpenAPI spec#
A machine-readable OpenAPI 3.1 description of the API — import it into Postman, Insomnia or Bruno, or generate a client:
npx @openapitools/openapi-generator-cli generate \
-i https://shapesoda.com/openapi.json -g typescript-fetch -o ./shapesoda-client
MCP for AI agents#
The Model Context Protocol lets AI agents call external tools. Shapesoda’s MCP server gives your agent three tools to turn images into SVG, using the same API key, credits and limits as the REST API.
Claude Code#
claude mcp add --transport http shapesoda https://shapesoda.com/mcp \
--header "Authorization: Bearer $SHAPESODA_KEY"
Cursor#
Add the server to ~/.cursor/mcp.json (or .cursor/mcp.json in a project). 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 with custom headers work the same way: URL https://shapesoda.com/mcp, header Authorization: Bearer <key>. The server is stateless, answers with JSON (no SSE stream) and supports protocol versions 2025-11-25, 2025-06-18 and 2025-03-26.
ChatGPT and Claude.ai connectors (sign-in with OAuth instead of a key) are coming soon.
Tools#
create_uploadCreates a one-time upload link for a local file (valid 10 minutes, up to 10 MB) and returns the curl command to upload it.
vectorizeConverts an uploaded file (upload_id) or a small image_base64 into SVG. Takes the same options as the REST API. Uses 1 credit.
get_usageShows 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 or WebP file and needs no other auth. - vectorize with upload_id
The result contains the SVG inline (when it is under 60 KB), plus a
download_urlvalid for 1 hour, the image size andcredits_remaining.
For small images (up to 2 MB) the agent can skip the upload and pass the file directly as image_base64.
Pricing#
Prepaid credit packs. One credit is one successfully vectorized image, through the API, MCP or the website.
- Credits never expire
- No subscription
- Shared balance with the website
- Failed requests are free
Online payments are being connected. Enterprise or 100k+ images? Email [email protected] for volume pricing and higher limits.