Submit design extraction
curl --request POST \
--url https://api.example.com/v1/design/extract \
--header 'Content-Type: application/json' \
--data '
{
"url": "<string>",
"endUserId": "<string>",
"extractors": [
"<string>"
],
"enableIpRotation": true
}
'import requests
url = "https://api.example.com/v1/design/extract"
payload = {
"url": "<string>",
"endUserId": "<string>",
"extractors": ["<string>"],
"enableIpRotation": True
}
headers = {"Content-Type": "application/json"}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
url: '<string>',
endUserId: '<string>',
extractors: ['<string>'],
enableIpRotation: true
})
};
fetch('https://api.example.com/v1/design/extract', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.example.com/v1/design/extract",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'url' => '<string>',
'endUserId' => '<string>',
'extractors' => [
'<string>'
],
'enableIpRotation' => true
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.example.com/v1/design/extract"
payload := strings.NewReader("{\n \"url\": \"<string>\",\n \"endUserId\": \"<string>\",\n \"extractors\": [\n \"<string>\"\n ],\n \"enableIpRotation\": true\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.example.com/v1/design/extract")
.header("Content-Type", "application/json")
.body("{\n \"url\": \"<string>\",\n \"endUserId\": \"<string>\",\n \"extractors\": [\n \"<string>\"\n ],\n \"enableIpRotation\": true\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.example.com/v1/design/extract")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"url\": \"<string>\",\n \"endUserId\": \"<string>\",\n \"extractors\": [\n \"<string>\"\n ],\n \"enableIpRotation\": true\n}"
response = http.request(request)
puts response.read_body{
"taskId": "<string>",
"sessionId": "<string>",
"status": "<string>",
"extractors": [
"<string>"
],
"enableIpRotation": true,
"createdAt": {}
}Design extraction
Submit design extraction
Run the design extractors against a URL. Returns immediately with a taskId; poll GET /v1/tasks/ for the result.
POST
/
v1
/
design
/
extract
Submit design extraction
curl --request POST \
--url https://api.example.com/v1/design/extract \
--header 'Content-Type: application/json' \
--data '
{
"url": "<string>",
"endUserId": "<string>",
"extractors": [
"<string>"
],
"enableIpRotation": true
}
'import requests
url = "https://api.example.com/v1/design/extract"
payload = {
"url": "<string>",
"endUserId": "<string>",
"extractors": ["<string>"],
"enableIpRotation": True
}
headers = {"Content-Type": "application/json"}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
url: '<string>',
endUserId: '<string>',
extractors: ['<string>'],
enableIpRotation: true
})
};
fetch('https://api.example.com/v1/design/extract', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.example.com/v1/design/extract",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'url' => '<string>',
'endUserId' => '<string>',
'extractors' => [
'<string>'
],
'enableIpRotation' => true
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.example.com/v1/design/extract"
payload := strings.NewReader("{\n \"url\": \"<string>\",\n \"endUserId\": \"<string>\",\n \"extractors\": [\n \"<string>\"\n ],\n \"enableIpRotation\": true\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.example.com/v1/design/extract")
.header("Content-Type", "application/json")
.body("{\n \"url\": \"<string>\",\n \"endUserId\": \"<string>\",\n \"extractors\": [\n \"<string>\"\n ],\n \"enableIpRotation\": true\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.example.com/v1/design/extract")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"url\": \"<string>\",\n \"endUserId\": \"<string>\",\n \"extractors\": [\n \"<string>\"\n ],\n \"enableIpRotation\": true\n}"
response = http.request(request)
puts response.read_body{
"taskId": "<string>",
"sessionId": "<string>",
"status": "<string>",
"extractors": [
"<string>"
],
"enableIpRotation": true,
"createdAt": {}
}For an overview of what each extractor returns, see Design extraction.
Response —
Submit response —
See Design extraction for what each field means and the URL TTL caveat.
Request body
string
required
The page to extract from. Must be a valid
http:// or https:// URL.string
required
Opaque identifier for the user this extraction runs on behalf of. See End users. Must be ≤ 256 characters.
string[]
Subset of extractors to run. Valid values:
"images", "fonts", "colors", "icons", "tokens", "logo". Omit (or pass an empty array) to run all six. Unknown names are silently dropped; if the resulting list is empty the request returns 400.boolean
Optional. Defaults to
false. When true, the browser session is launched through the residential proxy/IP rotation pool. Use this for sites that rate-limit, geo-vary, or block normal cloud traffic.Response — 202 Accepted
string
Identifier for the extraction. Poll
GET /v1/tasks/{taskId} until status === "completed".string
Always a freshly-minted session — design extractions are single-turn and don’t chain like agent tasks.
string
Always
"pending" on submission.string[]
The extractors that will actually run (echoed back so callers know exactly what to expect on the result).
boolean
Echoes whether this extraction was submitted with IP rotation enabled.
string (ISO 8601)
Submission timestamp.
Submit + wait (recommended)
Both SDKs wrap submit + poll into a singleclient.design.run(...) call. Most integrators should use this — it returns the completed task with design.results populated and never needs you to think about the task lifecycle.
from stablebrowse import Stablebrowse
client = Stablebrowse() # reads STABLEBROWSE_API_KEY
task = client.design.run(
url="https://www.figma.com/",
end_user_id="alice",
extractors=["colors", "fonts", "logo"],
enable_ip_rotation=True,
)
colors = task.design["results"]["colors"]["colors"]
fonts = task.design["results"]["fonts"]["fonts"]
print("Primary:", next(c for c in colors if c["role"] == "primary")["hex"])
print("Body font:", next(f for f in fonts if f["usage"] == "body")["family"])
import { Stablebrowse } from "@stablebrowse/client";
const client = new Stablebrowse();
const task = await client.design.run({
url: "https://www.figma.com/",
endUserId: "alice",
extractors: ["colors", "fonts", "logo"],
enableIpRotation: true,
});
const colors = task.design!.results.colors as { colors: Array<{ hex: string; role: string }> };
const fonts = task.design!.results.fonts as { fonts: Array<{ family: string; usage: string }> };
console.log("Primary:", colors.colors.find(c => c.role === "primary")?.hex);
console.log("Body font:", fonts.fonts.find(f => f.usage === "body")?.family);
# 1. Submit
TASK_ID=$(curl -s "$API_BASE/design/extract" -X POST \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://www.figma.com/",
"endUserId": "alice",
"extractors": ["colors", "fonts", "logo"],
"enableIpRotation": true
}' | jq -r .taskId)
# 2. Poll until terminal
while :; do
TASK=$(curl -s "$API_BASE/tasks/$TASK_ID" -H "Authorization: Bearer $API_KEY")
STATUS=$(echo "$TASK" | jq -r .status)
[[ "$STATUS" == "completed" || "$STATUS" == "failed" ]] && break
sleep 2
done
echo "$TASK" | jq .design.results.colors.colors[0]
task.design carries the full result shape (response schema).
Async primitives
If you need to queue extractions and poll on your own schedule (cron jobs, batch processors), the underlying primitives are available:submission = client.design.submit(
url="https://www.figma.com/",
end_user_id="alice",
extractors=["colors", "fonts"],
enable_ip_rotation=True,
)
# ... later ...
task = client.tasks.get(submission.task_id)
if task.is_terminal and task.status == "completed":
print(task.design["results"]["colors"])
const submission = await client.design.submit({
url: "https://www.figma.com/",
endUserId: "alice",
extractors: ["colors", "fonts"],
enableIpRotation: true,
});
// ... later ...
const task = await client.tasks.get(submission.taskId);
if (task.status === "completed") console.log(task.design?.results.colors);
client.tasks.get(taskId) is the same call you’d use for an agent task — design tasks ride the same task record and the design field is populated automatically.
Submit response — 202 Accepted
{
"taskId": "7f2a1b8c-...",
"sessionId": "a31d5e9f-...",
"status": "pending",
"extractors": ["colors", "fonts", "logo"],
"enableIpRotation": true,
"createdAt": "2026-05-04T22:10:18Z"
}
Get-task response (completed)
{
"taskId": "7f2a1b8c-...",
"status": "completed",
"design": {
"url": "https://www.figma.com/",
"extractors": ["colors", "fonts", "logo"],
"durationMs": 8420,
"results": {
"colors": { "colors": [ { "hex": "#635BFF", "rgb": "rgb(99,91,255)", "count": 142, "role": "primary" } ], "contrastIssues": [] },
"fonts": { "fonts": [ { "family": "Sohne", "usage": "body", "weights": [400, 500, 700], "source": "self-hosted", "faceUrl": "https://...s3..." } ] },
"logo": { "logo": { "found": true, "src": "https://...s3...", "type": "svg", "width": 120, "height": 32 } }
}
},
"createdAt": "2026-05-04T22:10:18Z",
"updatedAt": "2026-05-04T22:10:27Z"
}
Typical extractions finish in 5–15 seconds. Recommended poll interval if you’re driving the loop yourself: 2 seconds.
Result schema
Thedesign.results object contains one key per requested extractor. Top-level shape per extractor:
| Extractor | Result key | Top-level fields |
|---|---|---|
images | images.images[] | src, type, naturalWidth, naturalHeight, alt, svgSource?, downloadUrl?, originalSrc?, s3Key? |
fonts | fonts.fonts[] | family, usage, count, weights[], source, faceUrl?, downloadUrl?, originalFaceUrl?, s3Key? |
colors | colors.colors[], colors.contrastIssues[] | colors: hex, rgb, count, role (one of primary, background, text, error, success, warning, neutral — see Color roles). issues: fg, bg, ratio, passesAA, passesAAA |
icons | icons.icons[] | hash, source?, size, style, count, signedUrl?, downloadUrl?, s3Key? |
tokens | tokens.tokens | dtcg, spacing[], radii[], shadows[], gradients[], motionDurationsMs[], cssVariables[] |
logo | logo.logo | found, src?, type?, width?, height?, svgSource?, alt?, downloadUrl?, originalSrc?, s3Key? |
Errors
| Code | Meaning |
|---|---|
400 | url missing or not http(s); endUserId missing or > 256 chars; invalid extractors (no valid names); invalid JSON body |
401 | Missing Authorization header |
403 | Revoked API key |
429 | Monthly task quota exceeded; see response body for the limit |
500 | Worker enqueue failed (transient; safe to retry) |
