Sable DesignsDevelopersopenapi.json

API and MCP

Make pictures and videos in Sable from Claude, ChatGPT, your own app or anything that speaks MCP or HTTP. Version 1.1.0.

Overview

Sable Designs makes pictures and videos. Other software can use it in two ways:

  • AI assistants such as Claude, ChatGPT, Codex and Cursor connect to the MCP server at https://sabledesigns.red/mcp and use Sable as a set of tools.
  • Your own app or script calls the REST API at https://sabledesigns.red/api/v1.

Both act as one Sable account, spend that account's credits and follow the same rules as the app. The machine-readable description is at https://sabledesigns.red/api/v1/openapi.json (OpenAPI 3.1).

Connect an assistant

Add a connector (some apps call it an MCP server) with this address. When the assistant asks, sign in to Sable, choose what it may do, set a monthly limit if you want one, type your password and press Allow.

https://sabledesigns.red/mcp
AssistantHow
Claude (claude.ai and the Claude app)Settings, Connectors, Add custom connector. Paste the address, then press Connect.
ChatGPTIn settings, under Apps and Connectors, turn on developer mode, then create a connector with the address and choose OAuth.
Claude Codeclaude mcp add --transport http sable https://sabledesigns.red/mcp then type /mcp and choose Authenticate.
Codexcodex mcp add sable --url https://sabledesigns.red/mcp then codex mcp login sable.
CursorAdd sable to .cursor/mcp.json with the url. Cursor asks you to sign in.
Anything elseAny MCP client that supports Streamable HTTP and OAuth. Clients without OAuth can send a key as a Bearer header instead.

Each connected app shows on your Account page under API and AI assistants, where you can change its limit or disconnect it. You are emailed whenever one is connected.

Quickstart

1. Make a key on the Account page, under API and AI assistants. It is shown once. 2. Price a request, 3. start it, 4. wait for the result, 5. download it.

export SABLE_API_KEY=sbl_...

curl https://sabledesigns.red/api/v1/models?kind=image -H "Authorization: Bearer $SABLE_API_KEY"

curl https://sabledesigns.red/api/v1/quote -H "Authorization: Bearer $SABLE_API_KEY" -H "Content-Type: application/json" \
  -d '{"workflow": "api_image", "params": {"prompt": "a red apple on a wooden table", "model": "bd_45"}}'

curl https://sabledesigns.red/api/v1/generate -H "Authorization: Bearer $SABLE_API_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: apple-1" \
  -d '{"workflow": "api_image", "params": {"prompt": "a red apple on a wooden table", "model": "bd_45"}}'

curl "https://sabledesigns.red/api/v1/jobs/JOB_ID?wait=50" -H "Authorization: Bearer $SABLE_API_KEY"

curl -o apple.jpg https://sabledesigns.red/api/v1/outputs/FILE_NAME -H "Authorization: Bearer $SABLE_API_KEY"

The same in Python and JavaScript:

import httpx

sable = httpx.Client(base_url="https://sabledesigns.red/api/v1", timeout=90, headers={
    "Authorization": "Bearer sbl_...", "User-Agent": "my-app/1.0"})
job = sable.post("/generate", headers={"Idempotency-Key": "apple-1"}, json={
    "workflow": "api_image",
    "params": {"prompt": "a red apple on a wooden table", "model": "bd_45"}}).json()
while job["status"] not in ("done", "failed", "blocked", "cancelled", "stopped"):
    job = sable.get(f"/jobs/{job['id']}", params={"wait": 50}).json()
for out in job["outputs"]:
    open(out["name"], "wb").write(sable.get(out["download_url"]).content)
const sable = (path, init = {}) => fetch('https://sabledesigns.red/api/v1' + path, { ...init, headers: {
  Authorization: 'Bearer ' + process.env.SABLE_API_KEY, 'Content-Type': 'application/json',
  ...init.headers } }).then((r) => r.json());

let job = await sable('/generate', { method: 'POST', headers: { 'Idempotency-Key': 'apple-1' },
  body: JSON.stringify({ workflow: 'api_image',
    params: { prompt: 'a red apple on a wooden table', model: 'bd_45' } }) });
while (!['done', 'failed', 'blocked', 'cancelled', 'stopped'].includes(job.status)) {
  job = await sable(`/jobs/${job.id}?wait=50`);
}
console.log(job.outputs);

Tool keys, scenes and settings come from GET /tools; model keys for the Image and Video Generator come from GET /models. Those two tools need params.model.

Authentication

API keys

Send Authorization: Bearer sbl_... on every request. Only the account owner can make keys, with the account password; you are emailed about each one. A key is shown once and we keep only a fingerprint of it. You can have 5 at a time, give each a monthly credit limit, make one that can only look, and revoke any at once. A key works on /api/v1 and /mcp only: it cannot change the account, pay, invite people or make more keys.

OAuth 2.1 (sign in)

Assistants connect by signing in. Everything is discovered from the server:

WhatWhere
Protected resource metadata (RFC 9728)https://sabledesigns.red/.well-known/oauth-protected-resource/mcp
Authorization server metadata (RFC 8414)https://sabledesigns.red/.well-known/oauth-authorization-server
Client registrationClient ID metadata documents (an https client_id), or dynamic registration at /oauth/register (RFC 7591)
Authorization/oauth/authorize, code flow, PKCE with S256 required, resource (RFC 8707) supported, iss returned (RFC 9207)
Tokens/oauth/token. Access tokens last 60 minutes; refresh tokens 90 days and are replaced at every use. Reusing an old refresh token ends the whole connection.
Revocation (RFC 7009)/oauth/revoke. Revoking a refresh token disconnects the app.

A token asked for with resource=https://sabledesigns.red/mcp works only there. Scopes: read (see tools, characters, prices, balance and results) and generate (make pictures and videos, which spends credits). The approval page asks for the account password and a monthly limit, and up to 10 apps can be connected at once.

Making requests

  • Send and receive JSON. Times are Unix seconds.
  • Send a User-Agent naming your app. Python's built-in client is refused by our network without one.
  • Every answer carries X-Request-Id. Quote it if you contact support.
  • The API answers browsers from any site (CORS), but keys are secrets: keep them on your server.

Jobs and results

POST /generate answers at once with a job. Its status moves from queued to running and ends as one of:

StatusMeaning
doneFinished. outputs lists the results.
failedIt did not work. error says why, and the credits come back.
blockedRefused or held by the content rules.
cancelled, stoppedStopped before it finished.

Poll GET /jobs/{id}?wait=50: it answers as soon as the job finishes, or after 50 seconds. Or pass webhook_url and be called. GET /jobs lists jobs newest first; pass next_cursor as after for the next page. POST /jobs/{id}/cancel stops one.

Each output has a download_url (needs your key) and, unless it is explicit, an open_url that works without a key for one hour. Every file has its metadata removed before you get it.

Uploads

To edit or animate a picture, upload it first and pass the ref you get back as params.image (or params.video).

EndpointSend
POST /uploadsmultipart form, field file
POST /uploads/base64{"filename", "data_base64"}, up to 30 MB
POST /uploads/url{"url"}: a public https address, up to 60 MB
POST /uploads/chatgpt{"openaiFileIdRefs": [...]}, as ChatGPT sends files a user attached

Every upload is checked exactly like one made in the app.

Characters

Pass one of the account's characters by name as character (next to workflow) and her face is kept, the same as tagging her in the app. GET /characters lists them.

Webhooks

Add webhook_url (a public https address) to a generate request and it is called once the job finishes, with the same job object GET /jobs/{id} returns. Events: job.cancelled, job.completed, job.failed.

{
  "type": "job.completed",
  "timestamp": "2026-09-28T14:03:12Z",
  "data": { "id": "...", "object": "job", "status": "done", "outputs": [ ... ] }
}

Deliveries are signed to the Standard Webhooks spec, so their libraries work unchanged. The secret (whsec_...) is shown when you make the key, and under Secret on the Account page. Check it before trusting a delivery:

import base64, hashlib, hmac, time

def verify(secret, headers, body):
    msg_id, ts = headers["webhook-id"], headers["webhook-timestamp"]
    if abs(time.time() - int(ts)) > 300:
        raise ValueError("too old")
    key = base64.b64decode(secret.split("_", 1)[1])
    mac = hmac.new(key, f"{msg_id}.{ts}.".encode() + body, hashlib.sha256).digest()
    expected = base64.b64encode(mac).decode()
    for sig in headers["webhook-signature"].split():
        version, _, value = sig.partition(",")
        if version == "v1" and hmac.compare_digest(value, expected):
            return
    raise ValueError("bad signature")
import crypto from 'node:crypto';

export function verify(secret, headers, rawBody) {
  const id = headers['webhook-id'], ts = headers['webhook-timestamp'];
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) throw new Error('too old');
  const key = Buffer.from(secret.slice(6), 'base64');
  const expected = crypto.createHmac('sha256', key).update(`${id}.${ts}.${rawBody}`).digest('base64');
  const ok = headers['webhook-signature'].split(' ').some((s) => {
    const [version, sig] = s.split(',');
    return version === 'v1' && sig.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
  });
  if (!ok) throw new Error('bad signature');
}

Answer with any 2xx within 10 seconds. Otherwise it is tried again after 30 seconds, 2 minutes, 10 minutes, 30 minutes, 1 hour, 3 hours, 6 hours, 8 times in all, then given up. Answer 410 Gone to stop at once. webhook-id is the same on every try, so you can ignore repeats.

Idempotency

Send an Idempotency-Key header (any unique string up to 255 characters) on POST /generate and the upload calls. If the network drops and you retry with the same key and the same body, you get the first answer back (marked Idempotent-Replayed: true) and nothing runs or is charged twice. The same key with a different body is refused. Keys are remembered for 24 hours, and only a request that succeeded is remembered, so a failed one can be retried.

Rate limits

Each key or connection may make 120 requests a minute. Every answer says where you stand:

HeaderMeaning
RateLimit-LimitRequests allowed per minute
RateLimit-RemainingRequests left in the current minute
RateLimit-ResetSeconds until the minute resets
Retry-AfterOn a 429, seconds to wait

Separately, an account runs a few generations at once, the same as in the app, and each key or connection can have a monthly credit limit.

Errors

Errors from /api/v1 look like this, with the HTTP status set to match:

{
  "error": {
    "type": "billing_error",
    "code": "key_limit_reached",
    "message": "This key's monthly limit is 500 credits and 480 are used...",
    "status": 402,
    "request_id": "req_1a2b3c4d5e6f7a8b"
  }
}

Act on code; message is written for people.

codeHTTPMeaning
missing_api_key401No key or token was sent. Send Authorization: Bearer followed by your key.
invalid_api_key401The key or token is wrong, revoked or expired. Make a new key, or sign in again.
unauthorized401Not signed in.
insufficient_scope403This key or connection may only look. Use one that is allowed to make pictures.
age_verification_required403The account has not finished the age check. Finish it in Sable.
forbidden403Not allowed for this account.
invalid_request400Something in the request is missing or wrong. The message says what.
invalid_url400The address is not a public https address.
fetch_failed400The address could not be downloaded.
invalid_webhook_url400webhook_url must be a public https address.
insufficient_credits402Not enough credits for this request. Top up in Sable.
key_limit_reached402This key or connection has reached its monthly limit. Raise it on the Account page.
not_found404No such job, file or route on this account.
unknown_tool404No tool with that key. listTools shows them.
unknown_character404No character with that name on this account. listCharacters shows them.
method_not_allowed405That HTTP method is not used here.
tool_unavailable409The tool is retired, coming soon or switched off right now.
conflict409The request conflicts with the current state.
idempotency_in_progress409A request with the same Idempotency-Key is still running. Retry shortly.
too_large413The file is over the size limit.
idempotency_key_reused422That Idempotency-Key was already used for a different request.
content_policy_violation422Refused by Sable's content rules. It is recorded.
unprocessable422The request could not be processed as sent.
rate_limited429Too many requests. Wait for the seconds in Retry-After.
server_error500Something failed on our side. Retry; if it keeps happening, contact support.

On the MCP server, a problem with a tool call comes back as a tool result with isError: true, so the assistant can read it and fix its request. Protocol errors are JSON-RPC errors:

CodeNameMeaning
-32700Parse errorThe body is not valid JSON.
-32600Invalid requestNot a JSON-RPC 2.0 request.
-32601Method not foundUnknown method. With protocol 2026-07-28 the HTTP status is 404.
-32602Invalid paramsUnknown tool or prompt, or (from 2026-07-28) a resource that does not exist.
-32002Resource not foundA resource that does not exist or may not be shared (protocols before 2026-07-28).
-32001AuthenticationNo valid key or token. The HTTP status is 401, with WWW-Authenticate.
-32020Header mismatchMCP-Protocol-Version, Mcp-Method or Mcp-Name do not match the body (2026-07-28).
-32022Unsupported protocol versiondata.supported lists the versions this server speaks.

Content rules

Everything made through the API follows the same rules as the app: the same content filter, the same age check on uploads, and requests involving minors are refused and recorded. Explicit results can be made by an account that has passed the age check, but they are never shown inside another company's assistant: no preview, no open link and no resource, only a download_url that needs your key. The assistant you connect has its own rules as well, and may decline some requests itself.

MCP reference

Endpoint https://sabledesigns.red/mcp, Streamable HTTP. Protocol versions: 2026-07-28 (stateless, with server/discover) and, with the initialize handshake, 2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05. Answers are JSON, or an SSE stream of progress notifications ending in the result when the request carries a progressToken. There is no GET stream and there are no sessions.

Tools

ToolWhat it doesAccess
sable_list_toolsWhat Sable can make (pictures, edits, videos, voice) with each tool's inputs, scenes and settings. Call this first.looks only
sable_list_modelsThe models the Image or Video Generator can use.looks only
sable_list_charactersThe account's own characters. Pass one by name as `character` to keep her face.looks only
sable_get_priceWhat a request will cost in credits, before anything is spent.looks only
sable_generateStart a generation. SPENDS the account's credits, so price it and confirm with the user first. wait_seconds waits for the result in this call; if it is not done by then, call sable_get_job with the id (and wait_seconds) until status is done.spends credits
sable_get_jobA job's status and results, with download links, and a small preview of a finished picture when include_image is true. Explicit results are never previewed.looks only
sable_cancel_jobStop a job that is still running. Unused credits come back as they do in the app.needs generate
sable_recent_jobsThe account's jobs, newest first. Pass next_cursor as cursor for the next page.looks only
sable_uploadUpload a picture or a video to edit, animate or use as a reference, as base64 or from a public https url. Returns a ref: pass it as params.image for a picture, params.video for a video. Every upload is checked like one made in the app.needs generate
sable_get_balanceCredits left on the account, and this key's monthly limit.looks only

Every tool declares an input and an output schema, and results carry structuredContent. A tool can wait up to 50 seconds for a result, under the 60 second timeout most clients use; call sable_get_job again to keep waiting.

Resources

Non-explicit results are resources at sable://outputs/{name}: listed by resources/list, read by resources/read, and linked from tool results.

Prompts

PromptAbout
make_pictureMake one picture from a description, optionally of one of your characters.
character_photoshootA small set of pictures of one of your characters in one setting.

REST reference

Base URL https://sabledesigns.red/api/v1. The same as openapi.json, which you can import into ChatGPT custom GPT Actions, Postman or any OpenAPI tool.

GET /api/v1/me

Credit balance and this key's monthly limit

Answers with object, or an error.

operationId getAccount

GET /api/v1/tools

What Sable can make, with each tool's inputs, scenes and settings

Answers with object, or an error.

operationId listTools

GET /api/v1/models

Models for the Image and Video Generator

ParameterInTypeRequiredAbout
kindquerystring (image, video)

Answers with object, or an error.

operationId listModels

GET /api/v1/characters

The account's characters

Answers with object, or an error.

operationId listCharacters

POST /api/v1/quote

What a request will cost, in credits. Spends nothing.

Body

FieldTypeRequiredAbout
workflowstringyesA tool key from listTools, e.g. api_image
characterstringOptional: one of the account's characters, by name
paramsobjectprompt, scene, width, height, batch, image or video (a ref from an upload) and the tool's settings. api_image and api_video need model, a key from listModels.
webhook_urlstringOptional https address called when the job finishes, signed to the Standard Webhooks spec

Answers with Quote, or an error.

operationId getPrice

POST /api/v1/generate

Start a picture or video. Spends credits. Returns the job at once. Poll getJob (with wait) until status is done, or pass webhook_url.

Spends credits or changes a job. Assistants ask you first.

ParameterInTypeRequiredAbout
Idempotency-KeyheaderstringAny unique string. Retrying with the same key never runs or charges twice.

Body

FieldTypeRequiredAbout
workflowstringyesA tool key from listTools, e.g. api_image
characterstringOptional: one of the account's characters, by name
paramsobjectprompt, scene, width, height, batch, image or video (a ref from an upload) and the tool's settings. api_image and api_video need model, a key from listModels.
webhook_urlstringOptional https address called when the job finishes, signed to the Standard Webhooks spec

Answers with Job, or an error.

operationId generate

GET /api/v1/jobs

Recent jobs, newest first

ParameterInTypeRequiredAbout
limitqueryinteger
afterquerystringnext_cursor from the previous page

Answers with JobList, or an error.

operationId listJobs

GET /api/v1/jobs/{jid}

A job's status and results

ParameterInTypeRequiredAbout
jidpathstringyes
waitqueryintegerSeconds to wait for it to finish before answering

Answers with Job, or an error.

operationId getJob

POST /api/v1/jobs/{jid}/cancel

Stop a job that is still running

Spends credits or changes a job. Assistants ask you first.

ParameterInTypeRequiredAbout
jidpathstringyes

Answers with Job, or an error.

operationId cancelJob

POST /api/v1/uploads/base64

Upload a picture or video as base64. Returns a ref for params.image or params.video

ParameterInTypeRequiredAbout
Idempotency-KeyheaderstringAny unique string. Retrying with the same key never runs or charges twice.

Body

FieldTypeRequiredAbout
filenamestring
data_base64stringyes

Answers with Upload, or an error.

operationId uploadFile

POST /api/v1/uploads/url

Upload a picture or video from a public https address

ParameterInTypeRequiredAbout
Idempotency-KeyheaderstringAny unique string. Retrying with the same key never runs or charges twice.

Body

FieldTypeRequiredAbout
urlstringyes
filenamestring

Answers with Upload, or an error.

operationId uploadFromUrl

POST /api/v1/uploads/chatgpt

Upload pictures the user attached in ChatGPT. Returns refs

Body

FieldTypeRequiredAbout
openaiFileIdRefsarray of stringyes

Answers with object, or an error.

operationId uploadChatFiles

Changelog

DateVersionWhat changed
2026-09-281.1Sign in with OAuth 2.1, so assistants connect by address alone. Webhooks, Idempotency-Key, cursor pagination, cancel, uploads by URL and from ChatGPT, keys that can only look, request ids and rate limit headers. MCP protocol 2026-07-28 alongside the earlier versions, output schemas, resources and prompts.
2026-09-281.0First release: API keys, the REST API and the MCP server.